From 3d795ea39f472538e529b7e30efc3f7859397c42 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 19 Jan 2018 09:00:59 +0100 Subject: [PATCH 1/9] MOBILE-2309 sitehome: Implement provider and register handler --- src/app/app.module.ts | 2 + src/components/tabs/tabs.html | 26 +++-- src/components/tabs/tabs.ts | 63 ++++++++--- src/core/courses/courses.module.ts | 2 +- .../pages/my-overview/my-overview.html | 7 +- .../courses/pages/my-overview/my-overview.ts | 18 ++- .../{handlers.ts => mainmenu-handler.ts} | 2 +- src/core/login/providers/helper.ts | 82 -------------- src/core/mainmenu/pages/menu/menu.html | 2 +- src/core/mainmenu/pages/menu/menu.ts | 18 ++- src/core/mainmenu/providers/delegate.ts | 20 +++- src/core/sitehome/lang/en.json | 4 + .../sitehome/providers/mainmenu-handler.ts | 62 +++++++++++ src/core/sitehome/providers/sitehome.ts | 104 ++++++++++++++++++ src/core/sitehome/sitehome.module.ts | 34 ++++++ 15 files changed, 325 insertions(+), 121 deletions(-) rename src/core/courses/providers/{handlers.ts => mainmenu-handler.ts} (97%) create mode 100644 src/core/sitehome/lang/en.json create mode 100644 src/core/sitehome/providers/mainmenu-handler.ts create mode 100644 src/core/sitehome/providers/sitehome.ts create mode 100644 src/core/sitehome/sitehome.module.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 12c7bfd48..0ea058335 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -57,6 +57,7 @@ import { CoreCoursesModule } from '../core/courses/courses.module'; import { CoreFileUploaderModule } from '../core/fileuploader/fileuploader.module'; import { CoreSharedFilesModule } from '../core/sharedfiles/sharedfiles.module'; import { CoreCourseModule } from '../core/course/course.module'; +import { CoreSiteHomeModule } from '../core/sitehome/sitehome.module'; // Addon modules. import { AddonCalendarModule } from '../addon/calendar/calendar.module'; @@ -92,6 +93,7 @@ export function createTranslateLoader(http: HttpClient) { CoreFileUploaderModule, CoreSharedFilesModule, CoreCourseModule, + CoreSiteHomeModule, AddonCalendarModule ], bootstrap: [IonicApp], diff --git a/src/components/tabs/tabs.html b/src/components/tabs/tabs.html index 9bac58750..25ef4700e 100644 --- a/src/components/tabs/tabs.html +++ b/src/components/tabs/tabs.html @@ -1,12 +1,14 @@ -
- - - - {{ tab.title }} - {{tab.badge}} - - -
-
- -
\ No newline at end of file + + +
+ +
+
\ No newline at end of file diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 93ab502fa..54c23b204 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, EventEmitter, OnInit, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; +import { Component, Input, Output, EventEmitter, OnInit, OnChanges, AfterViewInit, ViewChild, ElementRef, + SimpleChange } from '@angular/core'; import { CoreTabComponent } from './tab'; /** @@ -35,14 +36,17 @@ import { CoreTabComponent } from './tab'; selector: 'core-tabs', templateUrl: 'tabs.html' }) -export class CoreTabsComponent implements OnInit, AfterViewInit { +export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { @Input() selectedIndex?: number = 0; // Index of the tab to select. + @Input() hideUntil: boolean; // Determine when should the contents be shown. @Output() ionChange: EventEmitter = new EventEmitter(); // Emitted when the tab changes. @ViewChild('originalTabs') originalTabsRef: ElementRef; tabs: CoreTabComponent[] = []; // List of tabs. selected: number; // Selected tab number. protected originalTabsContainer: HTMLElement; // The container of the original tabs. It will include each tab's content. + protected initialized = false; + protected afterViewInitTriggered = false; constructor() {} @@ -57,22 +61,24 @@ export class CoreTabsComponent implements OnInit, AfterViewInit { * View has been initialized. */ ngAfterViewInit() { - let selectedIndex = this.selectedIndex || 0, - selectedTab = this.tabs[selectedIndex]; - - if (!selectedTab.enabled || !selectedTab.show) { - // The tab is not enabled or not shown. Get the first tab that is enabled. - selectedTab = this.tabs.find((tab, index) => { - if (tab.enabled && tab.show) { - selectedIndex = index; - return true; - } - return false; - }); + this.afterViewInitTriggered = true; + if (!this.initialized && this.hideUntil) { + // Tabs should be shown, initialize them. + this.initializeTabs(); } + } - if (selectedTab) { - this.selectTab(selectedIndex); + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}) { + // We need to wait for ngAfterViewInit because we need core-tab components to be executed. + if (!this.initialized && this.hideUntil && this.afterViewInitTriggered) { + // Tabs should be shown, initialize them. + // Use a setTimeout so child core-tab update their inputs before initializing the tabs. + setTimeout(() => { + this.initializeTabs(); + }); } } @@ -114,6 +120,31 @@ export class CoreTabsComponent implements OnInit, AfterViewInit { return this.tabs[this.selected]; } + /** + * Initialize the tabs, determining the first tab to be shown. + */ + protected initializeTabs() : void { + let selectedIndex = this.selectedIndex || 0, + selectedTab = this.tabs[selectedIndex]; + + if (!selectedTab.enabled || !selectedTab.show) { + // The tab is not enabled or not shown. Get the first tab that is enabled. + selectedTab = this.tabs.find((tab, index) => { + if (tab.enabled && tab.show) { + selectedIndex = index; + return true; + } + return false; + }); + } + + if (selectedTab) { + this.selectTab(selectedIndex); + } + + this.initialized = true; + } + /** * Remove a tab from the list of tabs. * diff --git a/src/core/courses/courses.module.ts b/src/core/courses/courses.module.ts index 39d01fba1..8dfd4bd1f 100644 --- a/src/core/courses/courses.module.ts +++ b/src/core/courses/courses.module.ts @@ -14,7 +14,7 @@ import { NgModule } from '@angular/core'; import { CoreCoursesProvider } from './providers/courses'; -import { CoreCoursesMainMenuHandler } from './providers/handlers'; +import { CoreCoursesMainMenuHandler } from './providers/mainmenu-handler'; import { CoreCoursesMyOverviewProvider } from './providers/my-overview'; import { CoreCoursesDelegate } from './providers/delegate'; import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; diff --git a/src/core/courses/pages/my-overview/my-overview.html b/src/core/courses/pages/my-overview/my-overview.html index 4744a669e..7980e8c02 100644 --- a/src/core/courses/pages/my-overview/my-overview.html +++ b/src/core/courses/pages/my-overview/my-overview.html @@ -17,7 +17,12 @@ - + + + + + +
diff --git a/src/core/courses/pages/my-overview/my-overview.ts b/src/core/courses/pages/my-overview/my-overview.ts index fb5183d40..f9e698ba4 100644 --- a/src/core/courses/pages/my-overview/my-overview.ts +++ b/src/core/courses/pages/my-overview/my-overview.ts @@ -14,10 +14,12 @@ import { Component, OnDestroy } from '@angular/core'; import { IonicPage, NavController } from 'ionic-angular'; +import { CoreSitesProvider } from '../../../../providers/sites'; import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; import { CoreCoursesProvider } from '../../providers/courses'; import { CoreCoursesMyOverviewProvider } from '../../providers/my-overview'; import { CoreCourseHelperProvider } from '../../../course/providers/helper'; +import { CoreSiteHomeProvider } from '../../../sitehome/providers/sitehome'; import * as moment from 'moment'; /** @@ -29,6 +31,9 @@ import * as moment from 'moment'; templateUrl: 'my-overview.html', }) export class CoreCoursesMyOverviewPage implements OnDestroy { + firstSelectedTab: number; + siteHomeEnabled: boolean; + tabsReady: boolean = false; tabShown = 'courses'; timeline = { sort: 'sortbydates', @@ -64,13 +69,24 @@ export class CoreCoursesMyOverviewPage implements OnDestroy { constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider, private domUtils: CoreDomUtilsProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider, - private courseHelper: CoreCourseHelperProvider) {} + private courseHelper: CoreCourseHelperProvider, private sitesProvider: CoreSitesProvider, + private siteHomeProvider: CoreSiteHomeProvider) {} /** * View loaded. */ ionViewDidLoad() { this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); + + // Decide which tab to load first. + this.siteHomeProvider.isAvailable().then((enabled) => { + let site = this.sitesProvider.getCurrentSite(), + displaySiteHome = site.getInfo() && site.getInfo().userhomepage === 0; + + this.siteHomeEnabled = enabled; + this.firstSelectedTab = displaySiteHome ? 0 : 2; + this.tabsReady = true; + }); } /** diff --git a/src/core/courses/providers/handlers.ts b/src/core/courses/providers/mainmenu-handler.ts similarity index 97% rename from src/core/courses/providers/handlers.ts rename to src/core/courses/providers/mainmenu-handler.ts index f09078432..3dad056d8 100644 --- a/src/core/courses/providers/handlers.ts +++ b/src/core/courses/providers/mainmenu-handler.ts @@ -18,7 +18,7 @@ import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/pro import { CoreCoursesMyOverviewProvider } from '../providers/my-overview'; /** - * Handler to inject an option into main menu. + * Handler to add My Courses or My Overview into main menu. */ @Injectable() export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler { diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 77faf9dbc..ba8dd8080 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -404,49 +404,6 @@ export class CoreLoginHelperProvider { */ goToSiteInitialPage() : Promise { return this.appProvider.getRootNavController().setRoot('CoreMainMenuPage'); - // return this.isMyOverviewEnabled().then((myOverview) => { - // let myCourses = !myOverview && this.isMyCoursesEnabled(), - // site = this.sitesProvider.getCurrentSite(), - // promise; - - // if (!site) { - // return Promise.reject(null); - // } - - // // Check if frontpage is needed to be shown. (If configured or if any of the other avalaible). - // if ((site.getInfo() && site.getInfo().userhomepage === 0) || (!myCourses && !myOverview)) { - // promise = this.isFrontpageEnabled(); - // } else { - // promise = Promise.resolve(false); - // } - - // return promise.then((frontpage) => { - // // Check avalaibility in priority order. - // let pageName, - // params; - - // // @todo Use real pages names when they are implemented. - // if (frontpage) { - // pageName = 'Frontpage'; - // } else if (myOverview) { - // pageName = 'MyOverview'; - // } else if (myCourses) { - // pageName = 'MyCourses'; - // } else { - // // Anything else available, go to the user profile. - // pageName = 'User'; - // params = { - // userId: site.getUserId() - // }; - // } - - // if (setRoot) { - // return navCtrl.setRoot(pageName, params, {animate: false}); - // } else { - // return navCtrl.push(pageName, params); - // } - // }); - // }); } /** @@ -547,45 +504,6 @@ export class CoreLoginHelperProvider { return !!CoreConfigConstants.siteurl; } - /** - * Check if the app is configured to use a fixed URL (only 1). - * - * @return {Promise} Promise resolved with boolean: whether there is 1 fixed URL. - */ - protected isFrontpageEnabled() : Promise { - // var $mmaFrontpage = $mmAddonManager.get('$mmaFrontpage'); - // if ($mmaFrontpage && !$mmaFrontpage.isDisabledInSite()) { - // return $mmaFrontpage.isFrontpageAvailable().then(() => { - // return true; - // }).catch(() => { - // return false; - // }); - // } - // @todo: Implement it when front page is implemented. - return Promise.resolve(false); - } - - /** - * Check if My Courses is enabled. - * - * @return {boolean} Whether My Courses is enabled. - */ - protected isMyCoursesEnabled() : boolean { - // @todo: Implement it when My Courses is implemented. - return false; - // return !$mmCourses.isMyCoursesDisabledInSite(); - } - - /** - * Check if My Overview is enabled. - * - * @return {Promise} Promise resolved with boolean: whether My Overview is enabled. - */ - protected isMyOverviewEnabled() : Promise { - // @todo: Implement it when My Overview is implemented. - return Promise.resolve(false); - } - /** * Check if current site is logged out, triggering mmCoreEventSessionExpired if it is. * diff --git a/src/core/mainmenu/pages/menu/menu.html b/src/core/mainmenu/pages/menu/menu.html index 4e56c064d..241edcbf3 100644 --- a/src/core/mainmenu/pages/menu/menu.html +++ b/src/core/mainmenu/pages/menu/menu.html @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/src/core/mainmenu/pages/menu/menu.ts b/src/core/mainmenu/pages/menu/menu.ts index d319918f1..372b7dd5f 100644 --- a/src/core/mainmenu/pages/menu/menu.ts +++ b/src/core/mainmenu/pages/menu/menu.ts @@ -54,6 +54,7 @@ export class CoreMainMenuPage implements OnDestroy { loaded: boolean; redirectPage: string; redirectParams: any; + initialTab: number; protected subscription; protected moreTabData = { @@ -79,25 +80,36 @@ export class CoreMainMenuPage implements OnDestroy { return; } + let site = this.sitesProvider.getCurrentSite(), + displaySiteHome = site.getInfo() && site.getInfo().userhomepage === 0; + this.subscription = this.menuDelegate.getHandlers().subscribe((handlers) => { handlers = handlers.slice(0, CoreMainMenuProvider.NUM_MAIN_HANDLERS); // Get main handlers. // Check if handlers are already in tabs. Add the ones that aren't. // @todo: https://github.com/ionic-team/ionic/issues/13633 - for (let i in handlers) { + for (let i = 0; i < handlers.length; i++) { let handler = handlers[i], - found = false; + found = false, + shouldSelect = (displaySiteHome && handler.name == 'CoreSiteHome') || + (!displaySiteHome && handler.name == 'CoreCourses'); - for (let j in this.tabs) { + for (let j = 0; j < this.tabs.length; j++) { let tab = this.tabs[j]; if (tab.title == handler.title && tab.icon == handler.icon) { found = true; + if (shouldSelect) { + this.initialTab = j; + } break; } } if (!found) { this.tabs.push(handler); + if (shouldSelect) { + this.initialTab = this.tabs.length; + } } } diff --git a/src/core/mainmenu/providers/delegate.ts b/src/core/mainmenu/providers/delegate.ts index 773615f58..a6edd6ee6 100644 --- a/src/core/mainmenu/providers/delegate.ts +++ b/src/core/mainmenu/providers/delegate.ts @@ -79,6 +79,18 @@ export interface CoreMainMenuHandlerData { class?: string; }; +/** + * Data returned by the delegate for each handler. + */ +export interface CoreMainMenuHandlerToDisplay extends CoreMainMenuHandlerData { + + /** + * Name of the handler. + * @type {string} + */ + name?: string; +}; + /** * Service to interact with plugins to be shown in the main menu. Provides functions to register a plugin * and notify an update in the data. @@ -90,7 +102,7 @@ export class CoreMainMenuDelegate { protected enabledHandlers: {[s: string]: CoreMainMenuHandler} = {}; protected loaded = false; protected lastUpdateHandlersStart: number; - protected siteHandlers: Subject = new BehaviorSubject([]); + protected siteHandlers: Subject = new BehaviorSubject([]); constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) { this.logger = logger.getInstance('CoreMainMenuDelegate'); @@ -123,7 +135,7 @@ export class CoreMainMenuDelegate { * * @return {Subject} An observable that will receive the handlers. */ - getHandlers() : Subject { + getHandlers() : Subject { return this.siteHandlers; } @@ -223,7 +235,9 @@ export class CoreMainMenuDelegate { for (let name in this.enabledHandlers) { let handler = this.enabledHandlers[name], - data = handler.getDisplayData(); + data: CoreMainMenuHandlerToDisplay = handler.getDisplayData(); + + data.name = handler.name; handlersData.push({ data: data, diff --git a/src/core/sitehome/lang/en.json b/src/core/sitehome/lang/en.json new file mode 100644 index 000000000..639239276 --- /dev/null +++ b/src/core/sitehome/lang/en.json @@ -0,0 +1,4 @@ +{ + "sitehome": "Site home", + "sitenews": "Site announcements" +} \ No newline at end of file diff --git a/src/core/sitehome/providers/mainmenu-handler.ts b/src/core/sitehome/providers/mainmenu-handler.ts new file mode 100644 index 000000000..88d8c28a3 --- /dev/null +++ b/src/core/sitehome/providers/mainmenu-handler.ts @@ -0,0 +1,62 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSiteHomeProvider } from './sitehome'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/providers/delegate'; +import { CoreCoursesMyOverviewProvider } from '../../courses/providers/my-overview'; + +/** + * Handler to add Site Home into main menu. + */ +@Injectable() +export class CoreSiteHomeMainMenuHandler implements CoreMainMenuHandler { + name = 'CoreSiteHome'; + priority = 1000; + isOverviewEnabled: boolean; + + constructor(private siteHomeProvider: CoreSiteHomeProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider) {} + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean|Promise { + // Check if my overview is enabled. + return this.myOverviewProvider.isEnabled().then((enabled) => { + if (enabled) { + // My overview is enabled, Site Home will be inside the overview page. + return false; + } + + // My overview not enabled, check if site home is enabled. + return this.siteHomeProvider.isAvailable(); + }); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreMainMenuHandlerData} Data needed to render the handler. + */ + getDisplayData(): CoreMainMenuHandlerData { + return { + icon: 'home', + title: 'core.sitehome.sitehome', + page: 'CoreSiteHomeIndexPage', + class: 'core-sitehome-handler' + }; + } +} diff --git a/src/core/sitehome/providers/sitehome.ts b/src/core/sitehome/providers/sitehome.ts new file mode 100644 index 000000000..4fffeba3b --- /dev/null +++ b/src/core/sitehome/providers/sitehome.ts @@ -0,0 +1,104 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreLoggerProvider } from '../../../providers/logger'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreSite } from '../../../classes/site'; +import { CoreCourseProvider } from '../../course/providers/course'; + +/** + * Service that provides some features regarding site home. + */ +@Injectable() +export class CoreSiteHomeProvider { + protected logger; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider) { + this.logger = logger.getInstance('CoreSiteHomeProvider'); + } + + /** + * Returns whether or not the frontpage is available for the current site. + * + * @param {string} [siteId] The site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: whether it's available. + */ + isAvailable(siteId?: string) : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + // First check if it's disabled. + if (this.isDisabledInSite(site)) { + return false; + } + + // Use a WS call to check if there's content in the site home. + const siteHomeId = site.getSiteHomeId(), + preSets = {emergencyCache: false}; + + this.logger.debug('Using WS call to check if site home is available.'); + + return this.courseProvider.getSections(siteHomeId, false, true, preSets, site.id).then((sections) : any => { + if (!sections || !sections.length) { + return Promise.reject(null); + } + + for (let i = 0; i < sections.length; i++) { + let section = sections[i]; + if (section.summary || (section.modules && section.modules.length)) { + // It has content, return true. + return true; + } + } + + return Promise.reject(null); + }).catch(() => { + const config = site.getStoredConfig(); + if (config && config.frontpageloggedin) { + const items = config.frontpageloggedin.split(','); + if (items.length > 0) { + // It's enabled. + return true; + } + } + + return false; + }); + }).catch(() => { + return false; + }); + } + + /** + * Check if Site Home is disabled in a certain site. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + isDisabled(siteId?: string) : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isDisabledInSite(site); + }); + } + + /** + * Check if Site Home is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + isDisabledInSite(site: CoreSite) : boolean { + site = site || this.sitesProvider.getCurrentSite(); + return site.isFeatureDisabled('$mmSideMenuDelegate_mmaFrontpage'); + } +} diff --git a/src/core/sitehome/sitehome.module.ts b/src/core/sitehome/sitehome.module.ts new file mode 100644 index 000000000..fc41faded --- /dev/null +++ b/src/core/sitehome/sitehome.module.ts @@ -0,0 +1,34 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { CoreSiteHomeProvider } from './providers/sitehome'; +import { CoreSiteHomeMainMenuHandler } from './providers/mainmenu-handler'; +import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; + +@NgModule({ + declarations: [], + imports: [ + ], + providers: [ + CoreSiteHomeProvider, + CoreSiteHomeMainMenuHandler + ], + exports: [] +}) +export class CoreSiteHomeModule { + constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreSiteHomeMainMenuHandler) { + mainMenuDelegate.registerHandler(mainMenuHandler); + } +} From 58a872f99292428138b03a369fab18610945b83e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 19 Jan 2018 12:07:05 +0100 Subject: [PATCH 2/9] MOBILE-2309 sitehome: Implement index page and component --- src/components/tabs/tabs.html | 2 +- src/components/tabs/tabs.scss | 5 + .../pages/my-overview/my-overview.html | 136 +++++++++-------- .../pages/my-overview/my-overview.module.ts | 2 + .../courses/pages/my-overview/my-overview.ts | 5 +- .../sitehome/components/components.module.ts | 40 +++++ src/core/sitehome/components/index/index.html | 29 ++++ src/core/sitehome/components/index/index.ts | 141 ++++++++++++++++++ src/core/sitehome/pages/index/index.html | 6 + src/core/sitehome/pages/index/index.module.ts | 31 ++++ src/core/sitehome/pages/index/index.ts | 33 ++++ 11 files changed, 363 insertions(+), 67 deletions(-) create mode 100644 src/core/sitehome/components/components.module.ts create mode 100644 src/core/sitehome/components/index/index.html create mode 100644 src/core/sitehome/components/index/index.ts create mode 100644 src/core/sitehome/pages/index/index.html create mode 100644 src/core/sitehome/pages/index/index.module.ts create mode 100644 src/core/sitehome/pages/index/index.ts diff --git a/src/components/tabs/tabs.html b/src/components/tabs/tabs.html index 25ef4700e..22fdd6113 100644 --- a/src/components/tabs/tabs.html +++ b/src/components/tabs/tabs.html @@ -8,7 +8,7 @@
-
+
\ No newline at end of file diff --git a/src/components/tabs/tabs.scss b/src/components/tabs/tabs.scss index b7daa7804..4eb747c02 100644 --- a/src/components/tabs/tabs.scss +++ b/src/components/tabs/tabs.scss @@ -22,8 +22,13 @@ core-tabs { } } + .core-tabs-content-container { + height: 100%; + } + core-tab { display: none; + height: 100%; &.selected { display: block; diff --git a/src/core/courses/pages/my-overview/my-overview.html b/src/core/courses/pages/my-overview/my-overview.html index 7980e8c02..b2667fcba 100644 --- a/src/core/courses/pages/my-overview/my-overview.html +++ b/src/core/courses/pages/my-overview/my-overview.html @@ -13,82 +13,90 @@ - - - - - + -
- - {{ 'core.courses.sortbydates' | translate }} - {{ 'core.courses.sortbycourses' | translate }} - -
- - - - - - - - - - - - - - - + + + + + +
+ + {{ 'core.courses.sortbydates' | translate }} + {{ 'core.courses.sortbycourses' | translate }} + +
+ + + + + + + + + + + + + + + +
- - -
- - {{ 'core.courses.inprogress' | translate }} - {{ 'core.courses.future' | translate }} - {{ 'core.courses.past' | translate }} - - -
- - - {{prefetchCoursesData[courses.selected].badge}} -
-
- -
- - - - -
- -
- - - - - - - + + + + - - - -
-
+ + +
+ + {{ 'core.courses.inprogress' | translate }} + {{ 'core.courses.future' | translate }} + {{ 'core.courses.past' | translate }} + + +
+ + + {{prefetchCoursesData[courses.selected].badge}} +
+
+ +
+ + + + +
+ +
+ + + + + + + + + + + +
+
+
diff --git a/src/core/courses/pages/my-overview/my-overview.module.ts b/src/core/courses/pages/my-overview/my-overview.module.ts index 0259d3bb8..041267ef4 100644 --- a/src/core/courses/pages/my-overview/my-overview.module.ts +++ b/src/core/courses/pages/my-overview/my-overview.module.ts @@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreCoursesMyOverviewPage } from './my-overview'; import { CoreComponentsModule } from '../../../../components/components.module'; import { CoreCoursesComponentsModule } from '../../components/components.module'; +import { CoreSiteHomeComponentsModule } from '../../../sitehome/components/components.module'; @NgModule({ declarations: [ @@ -26,6 +27,7 @@ import { CoreCoursesComponentsModule } from '../../components/components.module' imports: [ CoreComponentsModule, CoreCoursesComponentsModule, + CoreSiteHomeComponentsModule, IonicPageModule.forChild(CoreCoursesMyOverviewPage), TranslateModule.forChild() ], diff --git a/src/core/courses/pages/my-overview/my-overview.ts b/src/core/courses/pages/my-overview/my-overview.ts index f9e698ba4..15231e6f7 100644 --- a/src/core/courses/pages/my-overview/my-overview.ts +++ b/src/core/courses/pages/my-overview/my-overview.ts @@ -17,6 +17,7 @@ import { IonicPage, NavController } from 'ionic-angular'; import { CoreSitesProvider } from '../../../../providers/sites'; import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; import { CoreCoursesProvider } from '../../providers/courses'; +import { CoreCoursesDelegate } from '../../providers/delegate'; import { CoreCoursesMyOverviewProvider } from '../../providers/my-overview'; import { CoreCourseHelperProvider } from '../../../course/providers/helper'; import { CoreSiteHomeProvider } from '../../../sitehome/providers/sitehome'; @@ -70,7 +71,7 @@ export class CoreCoursesMyOverviewPage implements OnDestroy { constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider, private domUtils: CoreDomUtilsProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider, private courseHelper: CoreCourseHelperProvider, private sitesProvider: CoreSitesProvider, - private siteHomeProvider: CoreSiteHomeProvider) {} + private siteHomeProvider: CoreSiteHomeProvider, private coursesDelegate: CoreCoursesDelegate) {} /** * View loaded. @@ -238,7 +239,7 @@ export class CoreCoursesMyOverviewPage implements OnDestroy { } promises.push(this.coursesProvider.invalidateUserCourses()); - // promises.push(this.coursesDelegate.clearAndInvalidateCoursesOptions()); + promises.push(this.coursesDelegate.clearAndInvalidateCoursesOptions()); return Promise.all(promises).finally(() => { switch (this.tabShown) { diff --git a/src/core/sitehome/components/components.module.ts b/src/core/sitehome/components/components.module.ts new file mode 100644 index 000000000..558bc272d --- /dev/null +++ b/src/core/sitehome/components/components.module.ts @@ -0,0 +1,40 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '../../../components/components.module'; +import { CoreDirectivesModule } from '../../../directives/directives.module'; +import { CoreCourseComponentsModule } from '../../course/components/components.module'; +import { CoreSiteHomeIndexComponent } from './index/index'; + +@NgModule({ + declarations: [ + CoreSiteHomeIndexComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCourseComponentsModule + ], + exports: [ + CoreSiteHomeIndexComponent + ] +}) +export class CoreSiteHomeComponentsModule {} diff --git a/src/core/sitehome/components/index/index.html b/src/core/sitehome/components/index/index.html new file mode 100644 index 000000000..742d1f24d --- /dev/null +++ b/src/core/sitehome/components/index/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/core/sitehome/components/index/index.ts b/src/core/sitehome/components/index/index.ts new file mode 100644 index 000000000..f15d090bb --- /dev/null +++ b/src/core/sitehome/components/index/index.ts @@ -0,0 +1,141 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component, OnInit, Input } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreCourseProvider } from '../../../course/providers/course'; +import { CoreCourseHelperProvider } from '../../../course/providers/helper'; +import { CoreCourseModulePrefetchDelegate } from '../../../course/providers/module-prefetch-delegate'; + +/** + * Component that displays site home index. + */ +@Component({ + selector: 'core-sitehome-index', + templateUrl: 'index.html', +}) +export class CoreSiteHomeIndexComponent implements OnInit { + @Input() moduleId?: number; + + dataLoaded: boolean; + section: any; + block: any; + hasContent: boolean; + items: any[] = []; + siteHomeId: number; + + protected sectionsLoaded: any[]; + + constructor(private domUtils: CoreDomUtilsProvider, private sitesProvider: CoreSitesProvider, + private courseProvider: CoreCourseProvider, private courseHelper: CoreCourseHelperProvider, + private prefetchDelegate: CoreCourseModulePrefetchDelegate) { + this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); + } + + /** + * Component being initialized. + */ + ngOnInit() { + this.loadContent().finally(() => { + this.dataLoaded = true; + }); + } + + /** + * Refresh the data. + * + * @param {any} refresher Refresher. + */ + doRefresh(refresher: any) { + const promises = []; + + promises.push(this.courseProvider.invalidateSections(this.siteHomeId)); + promises.push(this.sitesProvider.getCurrentSite().invalidateConfig()); + + if (this.sectionsLoaded) { + // Invalidate modules prefetch data. + const modules = this.courseProvider.getSectionsModules(this.sectionsLoaded); + promises.push(this.prefetchDelegate.invalidateModules(modules, this.siteHomeId)); + } + + Promise.all(promises).finally(() => { + this.loadContent().finally(() => { + refresher.complete(); + }); + }); + } + + /** + * Convenience function to fetch the data. + */ + protected loadContent() { + this.hasContent = false; + + let config = this.sitesProvider.getCurrentSite().getStoredConfig() || {numsections: 1}; + + if (config.frontpageloggedin) { + // Items with index 1 and 3 were removed on 2.5 and not being supported in the app. + let frontpageItems = [ + 'mma-frontpage-item-news', // News items. + false, + 'mma-frontpage-item-categories', // List of categories. + false, + 'mma-frontpage-item-categories', // Combo list. + 'mma-frontpage-item-enrolled-course-list', // Enrolled courses. + 'mma-frontpage-item-all-course-list', // List of courses. + 'mma-frontpage-item-course-search' // Course search box. + ], + items = config.frontpageloggedin.split(','); + + this.items = []; + + items.forEach((itemNumber) => { + // Get the frontpage item directive to render itself. + const item = frontpageItems[parseInt(itemNumber, 10)]; + if (!item || this.items.indexOf(item) >= 0) { + return; + } + + this.hasContent = true; + this.items.push(item); + }); + + } + + return this.courseProvider.getSections(this.siteHomeId, false, true).then((sections) => { + this.sectionsLoaded = Array.from(sections); + + // Check "Include a topic section" setting from numsections. + this.section = config.numsections && sections.length > 0 ? sections.pop() : false; + if (this.section) { + this.section.hasContent = this.courseHelper.sectionHasContent(this.section); + } + + this.block = sections.length > 0 ? sections.pop() : false; + if (this.block) { + this.block.hasContent = this.courseHelper.sectionHasContent(this.block); + } + + this.hasContent = this.courseHelper.addHandlerDataForModules(this.sectionsLoaded, this.siteHomeId, this.moduleId) || + this.hasContent; + + // Add log in Moodle. + this.courseProvider.logView(this.siteHomeId); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true); + }); + } +} diff --git a/src/core/sitehome/pages/index/index.html b/src/core/sitehome/pages/index/index.html new file mode 100644 index 000000000..e5d6b3299 --- /dev/null +++ b/src/core/sitehome/pages/index/index.html @@ -0,0 +1,6 @@ + + + {{ 'core.sitehome.sitehome' | translate }} + + + diff --git a/src/core/sitehome/pages/index/index.module.ts b/src/core/sitehome/pages/index/index.module.ts new file mode 100644 index 000000000..ce04e8590 --- /dev/null +++ b/src/core/sitehome/pages/index/index.module.ts @@ -0,0 +1,31 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreSiteHomeIndexPage } from './index'; +import { CoreSiteHomeComponentsModule } from '../../components/components.module'; + +@NgModule({ + declarations: [ + CoreSiteHomeIndexPage, + ], + imports: [ + CoreSiteHomeComponentsModule, + IonicPageModule.forChild(CoreSiteHomeIndexPage), + TranslateModule.forChild() + ] +}) +export class CoreSiteHomeIndexPageModule {} diff --git a/src/core/sitehome/pages/index/index.ts b/src/core/sitehome/pages/index/index.ts new file mode 100644 index 000000000..58ad7dcdd --- /dev/null +++ b/src/core/sitehome/pages/index/index.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; + +/** + * Page that displays site home index. + */ +@IonicPage({segment: 'core-sitehome-index'}) +@Component({ + selector: 'page-core-sitehome-index', + templateUrl: 'index.html', +}) +export class CoreSiteHomeIndexPage { + + moduleId: number; + + constructor(navParams: NavParams) { + this.moduleId = navParams.get('moduleId'); + } +} From 8a3f7053e51a25f3b10a6552fc1cf0c0799b7631 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 19 Jan 2018 13:44:22 +0100 Subject: [PATCH 3/9] MOBILE-2309 sitehome: Implement and apply items --- .../all-course-list/all-course-list.html | 4 ++ .../all-course-list/all-course-list.ts | 32 +++++++++ .../components/categories/categories.html | 4 ++ .../components/categories/categories.ts | 32 +++++++++ .../sitehome/components/components.module.ts | 19 +++++- .../course-search/course-search.html | 4 ++ .../components/course-search/course-search.ts | 32 +++++++++ .../enrolled-course-list.html | 4 ++ .../enrolled-course-list.ts | 43 ++++++++++++ src/core/sitehome/components/index/index.html | 14 +++- src/core/sitehome/components/index/index.ts | 25 ++++--- src/core/sitehome/components/news/news.html | 1 + src/core/sitehome/components/news/news.ts | 67 +++++++++++++++++++ 13 files changed, 267 insertions(+), 14 deletions(-) create mode 100644 src/core/sitehome/components/all-course-list/all-course-list.html create mode 100644 src/core/sitehome/components/all-course-list/all-course-list.ts create mode 100644 src/core/sitehome/components/categories/categories.html create mode 100644 src/core/sitehome/components/categories/categories.ts create mode 100644 src/core/sitehome/components/course-search/course-search.html create mode 100644 src/core/sitehome/components/course-search/course-search.ts create mode 100644 src/core/sitehome/components/enrolled-course-list/enrolled-course-list.html create mode 100644 src/core/sitehome/components/enrolled-course-list/enrolled-course-list.ts create mode 100644 src/core/sitehome/components/news/news.html create mode 100644 src/core/sitehome/components/news/news.ts diff --git a/src/core/sitehome/components/all-course-list/all-course-list.html b/src/core/sitehome/components/all-course-list/all-course-list.html new file mode 100644 index 000000000..9aa7dd581 --- /dev/null +++ b/src/core/sitehome/components/all-course-list/all-course-list.html @@ -0,0 +1,4 @@ + + +

{{ 'core.courses.availablecourses' | translate}}

+
diff --git a/src/core/sitehome/components/all-course-list/all-course-list.ts b/src/core/sitehome/components/all-course-list/all-course-list.ts new file mode 100644 index 000000000..71ea79d22 --- /dev/null +++ b/src/core/sitehome/components/all-course-list/all-course-list.ts @@ -0,0 +1,32 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component, Input } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { CoreCoursesProvider } from '../../../courses/providers/courses'; + +/** + * Component to open the page to view the list of all courses. + */ +@Component({ + selector: 'core-sitehome-all-course-list', + templateUrl: 'all-course-list.html', +}) +export class CoreSiteHomeAllCourseListComponent { + show: boolean; + + constructor(coursesProvider: CoreCoursesProvider) { + this.show = coursesProvider.isGetCoursesByFieldAvailable(); + } +} diff --git a/src/core/sitehome/components/categories/categories.html b/src/core/sitehome/components/categories/categories.html new file mode 100644 index 000000000..9ecbba6da --- /dev/null +++ b/src/core/sitehome/components/categories/categories.html @@ -0,0 +1,4 @@ + + +

{{ 'core.courses.categories' | translate}}

+
diff --git a/src/core/sitehome/components/categories/categories.ts b/src/core/sitehome/components/categories/categories.ts new file mode 100644 index 000000000..6cfa1b25d --- /dev/null +++ b/src/core/sitehome/components/categories/categories.ts @@ -0,0 +1,32 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component, Input } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { CoreCoursesProvider } from '../../../courses/providers/courses'; + +/** + * Component to open the page to view the list of categories. + */ +@Component({ + selector: 'core-sitehome-categories', + templateUrl: 'categories.html', +}) +export class CoreSiteHomeCategoriesComponent { + show: boolean; + + constructor(coursesProvider: CoreCoursesProvider) { + this.show = coursesProvider.isGetCoursesByFieldAvailable(); + } +} diff --git a/src/core/sitehome/components/components.module.ts b/src/core/sitehome/components/components.module.ts index 558bc272d..044a2fa01 100644 --- a/src/core/sitehome/components/components.module.ts +++ b/src/core/sitehome/components/components.module.ts @@ -20,10 +20,20 @@ import { CoreComponentsModule } from '../../../components/components.module'; import { CoreDirectivesModule } from '../../../directives/directives.module'; import { CoreCourseComponentsModule } from '../../course/components/components.module'; import { CoreSiteHomeIndexComponent } from './index/index'; +import { CoreSiteHomeAllCourseListComponent } from './all-course-list/all-course-list'; +import { CoreSiteHomeCategoriesComponent } from './categories/categories'; +import { CoreSiteHomeCourseSearchComponent } from './course-search/course-search'; +import { CoreSiteHomeEnrolledCourseListComponent } from './enrolled-course-list/enrolled-course-list'; +import { CoreSiteHomeNewsComponent } from './news/news'; @NgModule({ declarations: [ - CoreSiteHomeIndexComponent + CoreSiteHomeIndexComponent, + CoreSiteHomeAllCourseListComponent, + CoreSiteHomeCategoriesComponent, + CoreSiteHomeCourseSearchComponent, + CoreSiteHomeEnrolledCourseListComponent, + CoreSiteHomeNewsComponent ], imports: [ CommonModule, @@ -34,7 +44,12 @@ import { CoreSiteHomeIndexComponent } from './index/index'; CoreCourseComponentsModule ], exports: [ - CoreSiteHomeIndexComponent + CoreSiteHomeIndexComponent, + CoreSiteHomeAllCourseListComponent, + CoreSiteHomeCategoriesComponent, + CoreSiteHomeCourseSearchComponent, + CoreSiteHomeEnrolledCourseListComponent, + CoreSiteHomeNewsComponent ] }) export class CoreSiteHomeComponentsModule {} diff --git a/src/core/sitehome/components/course-search/course-search.html b/src/core/sitehome/components/course-search/course-search.html new file mode 100644 index 000000000..e67ae19ee --- /dev/null +++ b/src/core/sitehome/components/course-search/course-search.html @@ -0,0 +1,4 @@ + + +

{{ 'core.courses.searchcourses' | translate}}

+
diff --git a/src/core/sitehome/components/course-search/course-search.ts b/src/core/sitehome/components/course-search/course-search.ts new file mode 100644 index 000000000..8a74bfad5 --- /dev/null +++ b/src/core/sitehome/components/course-search/course-search.ts @@ -0,0 +1,32 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component, Input } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { CoreCoursesProvider } from '../../../courses/providers/courses'; + +/** + * Component to open the page to search courses. + */ +@Component({ + selector: 'core-sitehome-course-search', + templateUrl: 'course-search.html', +}) +export class CoreSiteHomeCourseSearchComponent { + show: boolean; + + constructor(coursesProvider: CoreCoursesProvider) { + this.show = !coursesProvider.isSearchCoursesDisabledInSite(); + } +} diff --git a/src/core/sitehome/components/enrolled-course-list/enrolled-course-list.html b/src/core/sitehome/components/enrolled-course-list/enrolled-course-list.html new file mode 100644 index 000000000..88c5c83c2 --- /dev/null +++ b/src/core/sitehome/components/enrolled-course-list/enrolled-course-list.html @@ -0,0 +1,4 @@ + + +

{{ 'core.courses.mycourses' | translate}}

+
diff --git a/src/core/sitehome/components/enrolled-course-list/enrolled-course-list.ts b/src/core/sitehome/components/enrolled-course-list/enrolled-course-list.ts new file mode 100644 index 000000000..ab05eeb73 --- /dev/null +++ b/src/core/sitehome/components/enrolled-course-list/enrolled-course-list.ts @@ -0,0 +1,43 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component, Input, OnInit } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { CoreCoursesProvider } from '../../../courses/providers/courses'; + +/** + * Component to open the page to view the list of courses the user is enrolled in. + */ +@Component({ + selector: 'core-sitehome-enrolled-course-list', + templateUrl: 'enrolled-course-list.html', +}) +export class CoreSiteHomeEnrolledCourseListComponent implements OnInit { + show: boolean; + + constructor(private coursesProvider: CoreCoursesProvider) {} + + /** + * Component being initialized. + */ + ngOnInit() { + if (this.coursesProvider.isMyCoursesDisabledInSite()) { + this.show = false; + } else { + return this.coursesProvider.getUserCourses().then((courses) => { + this.show = courses.length > 0; + }); + } + } +} diff --git a/src/core/sitehome/components/index/index.html b/src/core/sitehome/components/index/index.html index 742d1f24d..0c7c4ed3f 100644 --- a/src/core/sitehome/components/index/index.html +++ b/src/core/sitehome/components/index/index.html @@ -5,6 +5,7 @@ + @@ -13,9 +14,18 @@ - - + + + + + + + + + + + diff --git a/src/core/sitehome/components/index/index.ts b/src/core/sitehome/components/index/index.ts index f15d090bb..2f677e15c 100644 --- a/src/core/sitehome/components/index/index.ts +++ b/src/core/sitehome/components/index/index.ts @@ -60,10 +60,16 @@ export class CoreSiteHomeIndexComponent implements OnInit { * @param {any} refresher Refresher. */ doRefresh(refresher: any) { - const promises = []; + const promises = [], + currentSite = this.sitesProvider.getCurrentSite(); promises.push(this.courseProvider.invalidateSections(this.siteHomeId)); - promises.push(this.sitesProvider.getCurrentSite().invalidateConfig()); + promises.push(currentSite.invalidateConfig().then(() => { + // Config invalidated, fetch it again. + return currentSite.getConfig().then((config) => { + currentSite.setConfig(config); + }); + })); if (this.sectionsLoaded) { // Invalidate modules prefetch data. @@ -89,21 +95,21 @@ export class CoreSiteHomeIndexComponent implements OnInit { if (config.frontpageloggedin) { // Items with index 1 and 3 were removed on 2.5 and not being supported in the app. let frontpageItems = [ - 'mma-frontpage-item-news', // News items. + 'news', // News items. false, - 'mma-frontpage-item-categories', // List of categories. + 'categories', // List of categories. false, - 'mma-frontpage-item-categories', // Combo list. - 'mma-frontpage-item-enrolled-course-list', // Enrolled courses. - 'mma-frontpage-item-all-course-list', // List of courses. - 'mma-frontpage-item-course-search' // Course search box. + 'categories', // Combo list. + 'enrolled-course-list', // Enrolled courses. + 'all-course-list', // List of courses. + 'course-search' // Course search box. ], items = config.frontpageloggedin.split(','); this.items = []; items.forEach((itemNumber) => { - // Get the frontpage item directive to render itself. + // Get the frontpage item "name". const item = frontpageItems[parseInt(itemNumber, 10)]; if (!item || this.items.indexOf(item) >= 0) { return; @@ -112,7 +118,6 @@ export class CoreSiteHomeIndexComponent implements OnInit { this.hasContent = true; this.items.push(item); }); - } return this.courseProvider.getSections(this.siteHomeId, false, true).then((sections) => { diff --git a/src/core/sitehome/components/news/news.html b/src/core/sitehome/components/news/news.html new file mode 100644 index 000000000..1469eb616 --- /dev/null +++ b/src/core/sitehome/components/news/news.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/core/sitehome/components/news/news.ts b/src/core/sitehome/components/news/news.ts new file mode 100644 index 000000000..32519d767 --- /dev/null +++ b/src/core/sitehome/components/news/news.ts @@ -0,0 +1,67 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component, OnInit, Input } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { CoreSitesProvider } from '../../../../providers/sites'; + +/** + * Component that displays site home news. + */ +@Component({ + selector: 'core-sitehome-news', + templateUrl: 'news.html', +}) +export class CoreSiteHomeNewsComponent implements OnInit { + module: any; + show: boolean; + siteHomeId: number; + + constructor(private sitesProvider: CoreSitesProvider) { + this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); + } + + /** + * Component being initialized. + */ + ngOnInit() { + // Get number of news items to show. + const newsItems = this.sitesProvider.getCurrentSite().getStoredConfig('newsitems') || 0; + if (!newsItems) { + return; + } + + // @todo: Implement it once forum is supported. + // $mmaModForum = $mmAddonManager.get('$mmaModForum'); + // if ($mmaModForum) { + // return $mmaModForum.getCourseForums(courseId).then(function(forums) { + // for (var x in forums) { + // if (forums[x].type == 'news') { + // return forums[x]; + // } + // } + // }).then(function(forum) { + // if (forum) { + // return $mmCourse.getModuleBasicInfo(forum.cmid).then(function(module) { + // scope.show = true; + // scope.module = module; + // scope.module._controller = + // $mmCourseDelegate.getContentHandlerControllerFor(module.modname, module, courseId, + // module.section); + // }); + // } + // }); + // } + } +} From cb561cc4a389421d781d3929165b3c31d31a2fa9 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 19 Jan 2018 15:41:14 +0100 Subject: [PATCH 4/9] MOBILE-2309 core: Don't initialize core-tab content until it's selected --- src/components/loading/loading.ts | 8 + src/components/tabs/tab.ts | 31 +++- src/components/tabs/tabs.ts | 13 +- .../pages/my-overview/my-overview.html | 145 +++++++++--------- 4 files changed, 118 insertions(+), 79 deletions(-) diff --git a/src/components/loading/loading.ts b/src/components/loading/loading.ts index c881ad67c..60a74be91 100644 --- a/src/components/loading/loading.ts +++ b/src/components/loading/loading.ts @@ -26,6 +26,14 @@ import { TranslateService } from '@ngx-translate/core'; * * * + * + * IMPORTANT: Due to how ng-content works in Angular, the content of core-loading will be executed as soon as your view + * is loaded, even if the content hidden. So if you have the following code: + * + * + * The component "my-component" will be initialized immediately, even if dataLoaded is false, but it will be hidden. If you want + * your component to be initialized only if dataLoaded is true, then you should use ngIf: + * */ @Component({ selector: 'core-loading', diff --git a/src/components/tabs/tab.ts b/src/components/tabs/tab.ts index 16a9a297e..f1aaaa36a 100644 --- a/src/components/tabs/tab.ts +++ b/src/components/tabs/tab.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, OnInit, OnDestroy, ElementRef, EventEmitter } from '@angular/core'; +import { Component, Input, Output, OnInit, OnDestroy, ElementRef, EventEmitter, ContentChild, TemplateRef } from '@angular/core'; import { CoreTabsComponent } from './tabs'; /** @@ -20,17 +20,23 @@ import { CoreTabsComponent } from './tabs'; * * You must provide either a title or an icon for the tab. * + * The tab content MUST be surrounded by ng-template. This component uses ngTemplateOutlet instead of ng-content because the + * latter executes all the code immediately. This means that all the tabs would be initialized as soon as your view is + * loaded, leading to performance issues. + * * Example usage: * * * - * + * + * + * * * */ @Component({ selector: 'core-tab', - template: '' + template: '' }) export class CoreTabComponent implements OnInit, OnDestroy { @Input() title?: string; // The tab title. @@ -42,7 +48,10 @@ export class CoreTabComponent implements OnInit, OnDestroy { @Input() id?: string; // An ID to identify the tab. @Output() ionSelect: EventEmitter = new EventEmitter(); + @ContentChild(TemplateRef) template: TemplateRef // Template defined by the content. + element: HTMLElement; // The core-tab element. + loaded = false; constructor(private tabs: CoreTabsComponent, element: ElementRef) { this.element = element.nativeElement; @@ -61,4 +70,20 @@ export class CoreTabComponent implements OnInit, OnDestroy { ngOnDestroy() { this.tabs.removeTab(this); } + + /** + * Select tab. + */ + selectTab() { + this.element.classList.add('selected'); + this.loaded = true; + this.ionSelect.emit(this); + } + + /** + * Unselect tab. + */ + unselectTab() { + this.element.classList.remove('selected'); + } } diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 54c23b204..3edfe1706 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -26,7 +26,9 @@ import { CoreTabComponent } from './tab'; * * * - * + * + * + * * * * @@ -171,7 +173,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { index = 0; } - const currenTab = this.getSelected(), + const currentTab = this.getSelected(), newTab = this.tabs[index]; if (!newTab.enabled || !newTab.show) { @@ -179,14 +181,13 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { return; } - if (currenTab) { + if (currentTab) { // Unselect previous selected tab. - currenTab.element.classList.remove('selected'); + currentTab.unselectTab(); } this.selected = index; - newTab.element.classList.add('selected'); - newTab.ionSelect.emit(newTab); + newTab.selectTab(); this.ionChange.emit(newTab); } diff --git a/src/core/courses/pages/my-overview/my-overview.html b/src/core/courses/pages/my-overview/my-overview.html index b2667fcba..8cdaddcbf 100644 --- a/src/core/courses/pages/my-overview/my-overview.html +++ b/src/core/courses/pages/my-overview/my-overview.html @@ -16,88 +16,93 @@ - + + + - - - - + + + + + -
- - {{ 'core.courses.sortbydates' | translate }} - {{ 'core.courses.sortbycourses' | translate }} - -
- - - - - - - - - - - - - - - -
+
+ + {{ 'core.courses.sortbydates' | translate }} + {{ 'core.courses.sortbycourses' | translate }} + +
+ + + + + + + + + + + + + + + +
+
- - - - + + + + + - - -
- - {{ 'core.courses.inprogress' | translate }} - {{ 'core.courses.future' | translate }} - {{ 'core.courses.past' | translate }} - - -
- - - {{prefetchCoursesData[courses.selected].badge}} + + +
+ + {{ 'core.courses.inprogress' | translate }} + {{ 'core.courses.future' | translate }} + {{ 'core.courses.past' | translate }} + + +
+ + + {{prefetchCoursesData[courses.selected].badge}} +
-
- -
- - - - -
- -
- - - - - - - + +
+ + + + +
+ +
+ + + + + + + - - - -
- - + + + +
+ + + - From 1cfd38229fe3fe28e19b76fa9087daaae0ceb097 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 22 Jan 2018 10:28:27 +0100 Subject: [PATCH 5/9] MOBILE-2309 contentlinks: Implement delegate and helper --- src/core/contentlinks/contentlinks.module.ts | 29 ++ src/core/contentlinks/lang/en.json | 7 + .../pages/choose-site/choose-site.html | 24 ++ .../pages/choose-site/choose-site.module.ts | 33 ++ .../pages/choose-site/choose-site.ts | 95 ++++++ src/core/contentlinks/providers/delegate.ts | 310 ++++++++++++++++++ src/core/contentlinks/providers/helper.ts | 239 ++++++++++++++ src/core/login/providers/helper.ts | 2 +- 8 files changed, 738 insertions(+), 1 deletion(-) create mode 100644 src/core/contentlinks/contentlinks.module.ts create mode 100644 src/core/contentlinks/lang/en.json create mode 100644 src/core/contentlinks/pages/choose-site/choose-site.html create mode 100644 src/core/contentlinks/pages/choose-site/choose-site.module.ts create mode 100644 src/core/contentlinks/pages/choose-site/choose-site.ts create mode 100644 src/core/contentlinks/providers/delegate.ts create mode 100644 src/core/contentlinks/providers/helper.ts diff --git a/src/core/contentlinks/contentlinks.module.ts b/src/core/contentlinks/contentlinks.module.ts new file mode 100644 index 000000000..05c3ea838 --- /dev/null +++ b/src/core/contentlinks/contentlinks.module.ts @@ -0,0 +1,29 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { CoreContentLinksDelegate } from './providers/delegate'; +import { CoreContentLinksHelperProvider } from './providers/helper'; + +@NgModule({ + declarations: [], + imports: [ + ], + providers: [ + CoreContentLinksDelegate, + CoreContentLinksHelperProvider + ], + exports: [] +}) +export class CoreContentLinksModule {} diff --git a/src/core/contentlinks/lang/en.json b/src/core/contentlinks/lang/en.json new file mode 100644 index 000000000..833ba3e3a --- /dev/null +++ b/src/core/contentlinks/lang/en.json @@ -0,0 +1,7 @@ +{ + "chooseaccount": "Choose account", + "chooseaccounttoopenlink": "Choose an account to open the link with.", + "confirmurlothersite": "This link belongs to another site. Do you want to open it?", + "errornoactions": "Couldn't find an action to perform with this link.", + "errornosites": "Couldn't find any site to handle this link." +} \ No newline at end of file diff --git a/src/core/contentlinks/pages/choose-site/choose-site.html b/src/core/contentlinks/pages/choose-site/choose-site.html new file mode 100644 index 000000000..49823f017 --- /dev/null +++ b/src/core/contentlinks/pages/choose-site/choose-site.html @@ -0,0 +1,24 @@ + + + {{ 'core.contentlinks.chooseaccount' | translate }} + + + + + + +

{{ 'core.contentlinks.chooseaccounttoopenlink' | translate }}

+

{{ url }}

+
+ + +

{{site.fullName}}

+

+

{{site.siteUrl}}

+
+ + + +
+
+
diff --git a/src/core/contentlinks/pages/choose-site/choose-site.module.ts b/src/core/contentlinks/pages/choose-site/choose-site.module.ts new file mode 100644 index 000000000..abd8544de --- /dev/null +++ b/src/core/contentlinks/pages/choose-site/choose-site.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { CoreContentLinksChooseSitePage } from './choose-site'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreDirectivesModule } from '../../../../directives/directives.module'; + +@NgModule({ + declarations: [ + CoreContentLinksChooseSitePage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(CoreContentLinksChooseSitePage), + TranslateModule.forChild() + ] +}) +export class CoreContentLinksChooseSitePageModule {} diff --git a/src/core/contentlinks/pages/choose-site/choose-site.ts b/src/core/contentlinks/pages/choose-site/choose-site.ts new file mode 100644 index 000000000..2f5ed59c6 --- /dev/null +++ b/src/core/contentlinks/pages/choose-site/choose-site.ts @@ -0,0 +1,95 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component, OnInit } from '@angular/core'; +import { IonicPage, NavController, NavParams } from 'ionic-angular'; +import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreContentLinksDelegate, CoreContentLinksAction } from '../../providers/delegate'; +import { CoreContentLinksHelperProvider } from '../../providers/helper'; + +/** + * Page to display the list of sites to choose one to perform a content link action. + */ +@IonicPage({segment: 'core-content-links-choose-site'}) +@Component({ + selector: 'page-core-content-links-choose-site', + templateUrl: 'choose-site.html', +}) +export class CoreContentLinksChooseSitePage implements OnInit { + + url: string; + sites: any[]; + loaded: boolean; + protected action: CoreContentLinksAction; + + constructor(private navCtrl: NavController, navParams: NavParams, private contentLinksDelegate: CoreContentLinksDelegate, + private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, + private contentLinksHelper: CoreContentLinksHelperProvider) { + this.url = navParams.get('url'); + } + + /** + * Component being initialized. + */ + ngOnInit() { + if (!this.url) { + return this.leaveView(); + } + + // Get the action to perform. + this.contentLinksDelegate.getActionsFor(this.url).then((actions) => { + this.action = this.contentLinksHelper.getFirstValidAction(actions); + if (!this.action) { + return Promise.reject(null); + } + + // Get the sites that can perform the action. + return this.sitesProvider.getSites(this.action.sites).then((sites) => { + this.sites = sites; + }); + }).catch(() => { + this.domUtils.showErrorModal('core.contentlinks.errornosites', true); + this.leaveView(); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Cancel. + */ + cancel() : void { + this.leaveView(); + } + + /** + * Perform the action on a certain site. + * + * @param {string} siteId Site ID. + */ + siteClicked(siteId: string) : void { + this.action.action(siteId); + } + + /** + * Cancel and leave the view. + */ + protected leaveView() { + this.sitesProvider.logout().finally(() => { + this.navCtrl.setRoot('CoreLoginSitesPage'); + }); + } + +} \ No newline at end of file diff --git a/src/core/contentlinks/providers/delegate.ts b/src/core/contentlinks/providers/delegate.ts new file mode 100644 index 000000000..d355f96ee --- /dev/null +++ b/src/core/contentlinks/providers/delegate.ts @@ -0,0 +1,310 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreLoggerProvider } from '../../../providers/logger'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; +import { CoreUtilsProvider } from '../../../providers/utils/utils'; + +/** + * Interface that all handlers must implement. + */ +export interface CoreContentLinksHandler { + /** + * A name to identify the handler. + * @type {string} + */ + name: string; + + /** + * Handler's priority. The highest priority is treated first. + * @type {number} + */ + priority?: number; + + /** + * Whether the isEnabled function should be called for all the users in a site. It should be true only if the isEnabled call + * can return different values for different users in same site. + * @type {boolean} + */ + checkAllUsers?: boolean; + + /** + * Name of the feature this handler is related to. + * It will be used to check if the feature is disabled (@see CoreSite.isFeatureDisabled). + * @type {string} + */ + featureName?: string; + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number) : + CoreContentLinksAction[]|Promise; + + /** + * Check if a URL is handled by this handler. + * + * @param {string} url The URL to check. + * @return {boolean} Whether the URL is handled by this handler + */ + handles(url: string) : boolean; + + + /** + * If the URL is handled by this handler, return the site URL. + * + * @param {string} url The URL to check. + * @return {string} Site URL if it is handled, undefined otherwise. + */ + getSiteUrl(url: string) : string; + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled?(siteId: string, url: string, params: any, courseId?: number) : boolean|Promise; +}; + +/** + * Action to perform when a link is clicked. + */ +export interface CoreContentLinksAction { + /** + * A message to identify the action. Default: 'core.view'. + * @type {string} + */ + message?: string; + + /** + * Name of the icon of the action. Default: 'eye'. + * @type {string} + */ + icon?: string; + + /** + * IDs of the sites that support the action. + * @type {string[]} + */ + sites?: string[]; + + /** + * Action to perform when the link is clicked. + * + * @param {string} siteId The site ID. + */ + action(siteId: string) : void; +}; + +/** + * Actions and priority for a handler and URL. + */ +export interface CoreContentLinksHandlerActions { + /** + * Handler's priority. + * @type {number} + */ + priority: number; + + /** + * List of actions. + * @type {CoreContentLinksAction[]} + */ + actions: CoreContentLinksAction[]; +}; + +/** + * Delegate to register handlers to handle links. + */ +@Injectable() +export class CoreContentLinksDelegate { + protected logger; + protected handlers: {[s: string]: CoreContentLinksHandler} = {}; // All registered handlers. + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider, + private utils: CoreUtilsProvider) { + this.logger = logger.getInstance('CoreContentLinksDelegate'); + } + + /** + * Get the list of possible actions to do for a URL. + * + * @param {string} url URL to handle. + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @param {string} [username] Username to use to filter sites. + * @return {Promise} Promise resolved with the actions. + */ + getActionsFor(url: string, courseId?: number, username?: string) : Promise { + if (!url) { + return Promise.resolve([]); + } + + // Get the list of sites the URL belongs to. + return this.sitesProvider.getSiteIdsFromUrl(url, true, username).then((siteIds) => { + const linkActions: CoreContentLinksHandlerActions[] = [], + promises = [], + params = this.urlUtils.extractUrlParams(url); + + for (let name in this.handlers) { + const handler = this.handlers[name], + checkAll = handler.checkAllUsers, + isEnabledFn = this.isHandlerEnabled.bind(this, handler, url, params, courseId); + + if (!handler.handles(url)) { + // Invalid handler or it doesn't handle the URL. Stop. + continue; + } + + // Filter the site IDs using the isEnabled function. + promises.push(this.utils.filterEnabledSites(siteIds, isEnabledFn, checkAll).then((siteIds) => { + if (!siteIds.length) { + // No sites supported, no actions. + return; + } + + return Promise.resolve(handler.getActions(siteIds, url, params, courseId)).then((actions) => { + if (actions && actions.length) { + // Set default values if any value isn't supplied. + actions.forEach((action) => { + action.message = action.message || 'core.view'; + action.icon = action.icon || 'eye'; + action.sites = action.sites || siteIds; + }); + + // Add them to the list. + linkActions.push({ + priority: handler.priority, + actions: actions + }); + } + }); + })); + } + + return this.utils.allPromises(promises).catch(() => { + // Ignore errors. + }).then(() => { + // Sort link actions by priority. + return this.sortActionsByPriority(linkActions); + }); + }); + } + + /** + * Get the site URL if the URL is supported by any handler. + * + * @param {string} url URL to handle. + * @return {string} Site URL if the URL is supported by any handler, undefined otherwise. + */ + getSiteUrl(url: string) : string { + if (!url) { + return; + } + + // Check if any handler supports this URL. + for (let name in this.handlers) { + const handler = this.handlers[name], + siteUrl = handler.getSiteUrl(url); + + if (siteUrl) { + return siteUrl; + } + } + } + + /** + * Check if a handler is enabled for a certain site and URL. + * + * @param {CoreContentLinksHandler} handler Handler to check. + * @param {string} url The URL to check. + * @param {any} params The params of the URL + * @param {number} courseId Course ID the URL belongs to (can be undefined). + * @param {string} siteId The site ID to check. + * @return {Promise} Promise resolved with boolean: whether the handler is enabled. + */ + protected isHandlerEnabled(handler: CoreContentLinksHandler, url: string, params: any, courseId: number, siteId: string) + : Promise { + let promise; + + if (handler.featureName) { + // Check if the feature is disabled. + promise = this.sitesProvider.isFeatureDisabled(handler.featureName, siteId); + } else { + promise = Promise.resolve(false); + } + + return promise.then((disabled) => { + if (disabled) { + return false; + } + + if (!handler.isEnabled) { + // isEnabled function not provided, assume it's enabled. + return true; + } + + return handler.isEnabled(siteId, url, params, courseId); + }); + } + + /** + * Register a handler. + * + * @param {CoreContentLinksHandler} handler The handler to register. + * @return {boolean} True if registered successfully, false otherwise. + */ + registerHandler(handler: CoreContentLinksHandler) : boolean { + if (typeof this.handlers[handler.name] !== 'undefined') { + this.logger.log(`Addon '${handler.name}' already registered`); + return false; + } + this.logger.log(`Registered addon '${handler.name}'`); + this.handlers[handler.name] = handler; + return true; + } + + /** + * Sort actions by priority. + * + * @param {CoreContentLinksHandlerActions[]} actions Actions to sort. + * @return {CoreContentLinksAction[]} Sorted actions. + */ + protected sortActionsByPriority(actions: CoreContentLinksHandlerActions[]) : CoreContentLinksAction[] { + let sorted: CoreContentLinksAction[] = []; + + // Sort by priority. + actions = actions.sort((a, b) => { + return a.priority >= b.priority ? 1 : -1; + }); + + // Fill result array. + actions.forEach((entry) => { + sorted = sorted.concat(entry.actions); + }); + return sorted; + } +} diff --git a/src/core/contentlinks/providers/helper.ts b/src/core/contentlinks/providers/helper.ts new file mode 100644 index 000000000..2d0fb6853 --- /dev/null +++ b/src/core/contentlinks/providers/helper.ts @@ -0,0 +1,239 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NavController } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '../../../providers/app'; +import { CoreEventsProvider } from '../../../providers/events'; +import { CoreInitDelegate } from '../../../providers/init'; +import { CoreLoggerProvider } from '../../../providers/logger'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; +import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; +import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreContentLinksDelegate, CoreContentLinksAction } from './delegate'; +import { CoreConstants } from '../../constants'; +import { CoreConfigConstants } from '../../../configconstants'; + +/** + * Service that provides some features regarding content links. + */ +@Injectable() +export class CoreContentLinksHelperProvider { + protected logger; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, + private contentLinksDelegate: CoreContentLinksDelegate, private appProvider: CoreAppProvider, + private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private translate: TranslateService, + private initDelegate: CoreInitDelegate, eventsProvider: CoreEventsProvider) { + this.logger = logger.getInstance('CoreContentLinksHelperProvider'); + + // Listen for app launched URLs. If we receive one, check if it's a content link. + eventsProvider.on(CoreEventsProvider.APP_LAUNCHED_URL, this.handleCustomUrl.bind(this)); + } + + /** + * Get the first valid action in a list of actions. + * + * @param {CoreContentLinksAction[]} actions List of actions. + * @return {CoreContentLinksAction} First valid action. Returns undefined if no valid action found. + */ + getFirstValidAction(actions: CoreContentLinksAction[]) : CoreContentLinksAction { + if (actions) { + for (let i = 0; i < actions.length; i++) { + const action = actions[i]; + if (action && action.sites && action.sites.length) { + return action; + } + } + } + } + + /** + * Goes to a certain page in a certain site. If the site is current site it will perform a regular navigation, + * otherwise it will 'redirect' to the other site. + * + * @param {NavController} navCtrl The NavController instance to use. + * @param {string} pageName Name of the page to go. + * @param {any} [pageParams] Params to send to the page. + * @param {string} [siteId] Site ID. If not defined, current site. + */ + goInSite(navCtrl: NavController, pageName: string, pageParams: any, siteId?: string) : void { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + if (siteId == this.sitesProvider.getCurrentSiteId()) { + navCtrl.push(pageName, pageParams); + } else { + this.loginHelper.redirect(pageName, pageParams, siteId); + } + } + + /** + * Go to the page to choose a site. + * + * @param {string} url URL to treat. + */ + goToChooseSite(url: string) : void { + this.appProvider.getRootNavController().setRoot('CoreContentLinksChooseSitePage', {url: url}); + } + + /** + * Handle a URL received by Custom URL Scheme. + * + * @param {string} url URL to handle. + * @return {boolean} True if the URL should be handled by this component, false otherwise. + */ + handleCustomUrl(url: string) : boolean { + const contentLinksScheme = CoreConfigConstants.customurlscheme + '://link'; + if (url.indexOf(contentLinksScheme) == -1) { + return false; + } + + url = decodeURIComponent(url); + + // App opened using custom URL scheme. + this.logger.debug('Treating custom URL scheme: ' + url); + + let modal = this.domUtils.showModalLoading(), + username; + + // Delete the scheme from the URL. + url = url.replace(contentLinksScheme + '=', ''); + + // Detect if there's a user specified. + username = this.urlUtils.getUsernameFromUrl(url); + if (username) { + url = url.replace(username + '@', ''); // Remove the username from the URL. + } + + // Wait for the app to be ready. + this.initDelegate.ready().then(() => { + // Check if the site is stored. + return this.sitesProvider.getSiteIdsFromUrl(url, false, username); + }).then((siteIds) => { + if (siteIds.length) { + modal.dismiss(); // Dismiss modal so it doesn't collide with confirms. + return this.handleLink(url, username).then((treated) => { + if (!treated) { + this.domUtils.showErrorModal('core.contentlinks.errornoactions', true); + } + }); + } else { + // Get the site URL. + const siteUrl = this.contentLinksDelegate.getSiteUrl(url); + if (!siteUrl) { + this.domUtils.showErrorModal('core.login.invalidsite', true); + return; + } + + // Check that site exists. + return this.sitesProvider.checkSite(siteUrl).then((result) => { + // Site exists. We'll allow to add it. + let promise, + ssoNeeded = this.loginHelper.isSSOLoginNeeded(result.code), + hasRemoteAddonsLoaded = false, + pageName = 'CoreLoginCredentialsPage', + pageParams = { + siteUrl: result.siteUrl, + username: username, + urlToOpen: url, + siteConfig: result.config + }; + + modal.dismiss(); // Dismiss modal so it doesn't collide with confirms. + + if (!this.sitesProvider.isLoggedIn()) { + // Not logged in, no need to confirm. If SSO the confirm will be shown later. + promise = Promise.resolve(); + } else { + // Ask the user before changing site. + const confirmMsg = this.translate.instant('core.contentlinks.confirmurlothersite'); + promise = this.domUtils.showConfirm(confirmMsg).then(() => { + if (!ssoNeeded) { + // hasRemoteAddonsLoaded = $mmAddonManager.hasRemoteAddonsLoaded(); @todo + if (hasRemoteAddonsLoaded) { + // Store the redirect since logout will restart the app. + this.appProvider.storeRedirect(CoreConstants.NO_SITE_ID, pageName, pageParams); + } + + return this.sitesProvider.logout().catch(() => { + // Ignore errors (shouldn't happen). + }); + } + }); + } + + return promise.then(() => { + if (ssoNeeded) { + this.loginHelper.confirmAndOpenBrowserForSSOLogin( + result.siteUrl, result.code, result.service, result.config && result.config.launchurl); + } else if (!hasRemoteAddonsLoaded) { + this.appProvider.getRootNavController().setRoot(pageName, pageParams); + } + }); + + }).catch((error) => { + if (error) { + this.domUtils.showErrorModal(error); + } + }); + } + }).finally(() => { + modal.dismiss(); + }); + + return true; + } + + /** + * Handle a link. + * + * @param {string} url URL to handle. + * @param {string} [username] Username related with the URL. E.g. in 'http://myuser@m.com', url would be 'http://m.com' and + * the username 'myuser'. Don't use it if you don't want to filter by username. + * @return {Promise} Promise resolved with a boolean: true if URL was treated, false otherwise. + */ + handleLink(url: string, username?: string) : Promise { + // Check if the link should be treated by some component/addon. + return this.contentLinksDelegate.getActionsFor(url, undefined, username).then((actions) => { + const action = this.getFirstValidAction(actions); + if (action) { + if (!this.sitesProvider.isLoggedIn()) { + // No current site. Perform the action if only 1 site found, choose the site otherwise. + if (action.sites.length == 1) { + action.action(action.sites[0]); + } else { + this.goToChooseSite(url); + } + } else if (action.sites.length == 1 && action.sites[0] == this.sitesProvider.getCurrentSiteId()) { + // Current site. + action.action(action.sites[0]); + } else { + // Not current site or more than one site. Ask for confirmation. + this.domUtils.showConfirm(this.translate.instant('core.contentlinks.confirmurlothersite')).then(() => { + if (action.sites.length == 1) { + action.action(action.sites[0]); + } else { + this.goToChooseSite(url); + } + }); + } + return true; + } + return false; + }).catch(() => { + return false; + }); + } +} diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index ba8dd8080..28db16e29 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -776,7 +776,7 @@ export class CoreLoginHelperProvider { if (siteId) { this.loadSiteAndPage(page, params, siteId); } else { - this.appProvider.getRootNavController().setRoot('CoreLoginSitesPage') + this.appProvider.getRootNavController().setRoot('CoreLoginSitesPage'); } } } From b3d045754085fc3ffbc8914f52b9c914a63f815f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 23 Jan 2018 09:08:49 +0100 Subject: [PATCH 6/9] MOBILE-2309 contentlinks: Implement base handlers --- src/core/contentlinks/classes/base-handler.ts | 111 ++++++++++++++++++ .../classes/module-grade-handler.ts | 101 ++++++++++++++++ .../classes/module-index-handler.ts | 63 ++++++++++ src/core/contentlinks/providers/delegate.ts | 1 - src/core/course/components/format/format.ts | 25 +++- src/core/course/pages/section/section.html | 2 +- src/core/course/pages/section/section.ts | 19 +-- src/core/course/providers/helper.ts | 95 +++++++++++++-- src/core/course/providers/module-delegate.ts | 9 +- src/core/sitehome/components/index/index.ts | 5 +- src/core/sitehome/pages/index/index.html | 2 +- src/core/sitehome/pages/index/index.ts | 14 ++- 12 files changed, 411 insertions(+), 36 deletions(-) create mode 100644 src/core/contentlinks/classes/base-handler.ts create mode 100644 src/core/contentlinks/classes/module-grade-handler.ts create mode 100644 src/core/contentlinks/classes/module-index-handler.ts diff --git a/src/core/contentlinks/classes/base-handler.ts b/src/core/contentlinks/classes/base-handler.ts new file mode 100644 index 000000000..15b7c156a --- /dev/null +++ b/src/core/contentlinks/classes/base-handler.ts @@ -0,0 +1,111 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreContentLinksHandler, CoreContentLinksAction } from '../providers/delegate'; + +/** + * Base handler to be registered in CoreContentLinksHandler. It is useful to minimize the amount of + * functions that handlers need to implement. + * + * It allows you to specify a "pattern" (RegExp) that will be used to check if the handler handles a URL and to get its site URL. + */ +export class CoreContentLinksHandlerBase implements CoreContentLinksHandler { + /** + * A name to identify the handler. + * @type {string} + */ + name = 'CoreContentLinksHandlerBase'; + + /** + * Handler's priority. The highest priority is treated first. + * @type {number} + */ + priority = 0; + + /** + * Whether the isEnabled function should be called for all the users in a site. It should be true only if the isEnabled call + * can return different values for different users in same site. + * @type {boolean} + */ + checkAllUsers = false; + + /** + * Name of the feature this handler is related to. + * It will be used to check if the feature is disabled (@see CoreSite.isFeatureDisabled). + * @type {string} + */ + featureName = ''; + + /** + * A pattern to use to detect if the handler handles a URL and to get its site URL. Required if "handles" and + * "getSiteUrl" functions aren't overridden. + * @type {RexExp} + */ + pattern?: RegExp; + + constructor() {} + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number) : + CoreContentLinksAction[]|Promise { + return []; + } + + /** + * Check if a URL is handled by this handler. + * + * @param {string} url The URL to check. + * @return {boolean} Whether the URL is handled by this handler + */ + handles(url: string) : boolean { + return this.pattern && url.search(this.pattern) >= 0; + } + + /** + * If the URL is handled by this handler, return the site URL. + * + * @param {string} url The URL to check. + * @return {string} Site URL if it is handled, undefined otherwise. + */ + getSiteUrl(url: string) : string { + if (this.pattern) { + var position = url.search(this.pattern); + if (position > -1) { + return url.substr(0, position); + } + } + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number) : boolean|Promise { + return true; + } +} diff --git a/src/core/contentlinks/classes/module-grade-handler.ts b/src/core/contentlinks/classes/module-grade-handler.ts new file mode 100644 index 000000000..15866a9a5 --- /dev/null +++ b/src/core/contentlinks/classes/module-grade-handler.ts @@ -0,0 +1,101 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreContentLinksAction } from '../providers/delegate'; +import { CoreContentLinksHandlerBase } from './base-handler'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; +import { CoreCourseHelperProvider } from '../../course/providers/helper'; + +/** + * Handler to handle URLs pointing to the grade of a module. + */ +export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerBase { + + /** + * Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled. + * @type {string} + */ + addon: string; + + /** + * Name of the module (assign, book, ...). + * @type {string} + */ + modName: string; + + /** + * Whether the module can be reviewed in the app. If true, the handler needs to implement the goToReview function. + * @type {boolean} + */ + canReview: boolean; + + constructor(protected courseHelper: CoreCourseHelperProvider, protected domUtils: CoreDomUtilsProvider, + protected sitesProvider: CoreSitesProvider) { + super(); + + // Match the grade.php URL with an id param. + this.pattern = new RegExp('\/mod\/' + this.modName + '\/grade\.php.*([\&\?]id=\\d+)'); + this.featureName = '$mmCourseDelegate_' + this.addon; + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number) : + CoreContentLinksAction[]|Promise { + + courseId = courseId || params.courseid || params.cid; + return [{ + action: (siteId) : void => { + // Check if userid is the site's current user. + const modal = this.domUtils.showModalLoading(); + this.sitesProvider.getSite(siteId).then((site) => { + if (!params.userid || params.userid == site.getUserId()) { + // No user specified or current user. Navigate to module. + this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId); + } else if (this.canReview) { + // Use the goToReview function. + this.goToReview(url, params, courseId, siteId); + } else { + // Not current user and cannot review it in the app, open it in browser. + site.openInBrowserWithAutoLogin(url); + } + }).finally(() => { + modal.dismiss(); + }); + } + }]; + } + + /** + * Go to the page to review. + * + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} courseId Course ID related to the URL. + * @param {string} siteId List of sites the URL belongs to. + * @return {Promise} Promise resolved when done. + */ + protected goToReview(url: string, params: any, courseId: number, siteId: string) : Promise { + // This function should be overridden. + return Promise.resolve(); + } +} diff --git a/src/core/contentlinks/classes/module-index-handler.ts b/src/core/contentlinks/classes/module-index-handler.ts new file mode 100644 index 000000000..1d7a5819f --- /dev/null +++ b/src/core/contentlinks/classes/module-index-handler.ts @@ -0,0 +1,63 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreContentLinksAction } from '../providers/delegate'; +import { CoreContentLinksHandlerBase } from './base-handler'; +import { CoreCourseHelperProvider } from '../../course/providers/helper'; + +/** + * Handler to handle URLs pointing to the index of a module. + */ +export class CoreContentLinksModuleIndexHandler extends CoreContentLinksHandlerBase { + + /** + * Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled. + * @type {string} + */ + addon: string; + + /** + * Name of the module (assign, book, ...). + * @type {string} + */ + modName: string; + + constructor(private courseHelper: CoreCourseHelperProvider) { + super(); + + // Match the view.php URL with an id param. + this.pattern = new RegExp('\/mod\/' + this.modName + '\/view\.php.*([\&\?]id=\\d+)'); + this.featureName = '$mmCourseDelegate_' + this.addon; + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number) : + CoreContentLinksAction[]|Promise { + + courseId = courseId || params.courseid || params.cid; + return [{ + action: (siteId) => { + this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId); + } + }]; + } +} diff --git a/src/core/contentlinks/providers/delegate.ts b/src/core/contentlinks/providers/delegate.ts index d355f96ee..b9fbced5d 100644 --- a/src/core/contentlinks/providers/delegate.ts +++ b/src/core/contentlinks/providers/delegate.ts @@ -68,7 +68,6 @@ export interface CoreContentLinksHandler { */ handles(url: string) : boolean; - /** * If the URL is handled by this handler, return the site URL. * diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index f6ff16cdb..fd7fd2d30 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -42,6 +42,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { @Input() course: any; // The course to render. @Input() sections: any[]; // List of course sections. @Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled. + @Input() initialSectionId: number; // The section to load first. @Output() completionChanged?: EventEmitter; // Will emit an event when any module completion changes. // Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf. @@ -142,11 +143,24 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { ngOnChanges(changes: {[name: string]: SimpleChange}) { if (changes.sections && this.sections) { if (!this.selectedSection) { - // There is no selected section yet, calculate which one to get. - this.cfDelegate.getCurrentSection(this.course, this.sections).then((section) => { - this.loaded = true; - this.sectionChanged(section); - }); + // There is no selected section yet, calculate which one to load. + if (this.initialSectionId) { + // We have an input indicating the section ID to load. Search the section. + for (let i = 0; i < this.sections.length; i++) { + let section = this.sections[i]; + if (section.id == this.initialSectionId) { + this.loaded = true; + this.sectionChanged(section); + break; + } + } + } else { + // No section specified, get current section. + this.cfDelegate.getCurrentSection(this.course, this.sections).then((section) => { + this.loaded = true; + this.sectionChanged(section); + }); + } } else { // We have a selected section, but the list has changed. Search the section in the list. let newSection; @@ -214,6 +228,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { // Set the Input data. this.componentInstances[type].course = this.course; this.componentInstances[type].sections = this.sections; + this.componentInstances[type].initialSectionId = this.initialSectionId; this.componentInstances[type].downloadEnabled = this.downloadEnabled; this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed. diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html index 6d64c169e..f81e941c9 100644 --- a/src/core/course/pages/section/section.html +++ b/src/core/course/pages/section/section.html @@ -21,6 +21,6 @@ {{ 'core.course.contents' | translate }} {{ handler.data.title || translate }}
- +
diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 91ad41c80..94b107cfa 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, ViewChild, OnDestroy } from '@angular/core'; -import { IonicPage, NavParams, Content } from 'ionic-angular'; +import { IonicPage, NavParams, Content, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '../../../../providers/events'; import { CoreSitesProvider } from '../../../../providers/sites'; @@ -39,6 +39,7 @@ export class CoreCourseSectionPage implements OnDestroy { title: string; course: any; sections: any[]; + sectionId: number; courseHandlers: CoreCoursesHandlerToDisplay[]; dataLoaded: boolean; downloadEnabled: boolean; @@ -47,18 +48,17 @@ export class CoreCourseSectionPage implements OnDestroy { prefetchCourseIcon: 'spinner' }; - protected moduleId; protected completionObserver; protected courseStatusObserver; protected isDestroyed = false; - constructor(navParams: NavParams, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider, + constructor(private navParams: NavParams, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider, private courseFormatDelegate: CoreCourseFormatDelegate, private coursesDelegate: CoreCoursesDelegate, private translate: TranslateService, private courseHelper: CoreCourseHelperProvider, eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider, private coursesProvider: CoreCoursesProvider, - sitesProvider: CoreSitesProvider) { + sitesProvider: CoreSitesProvider, private navCtrl: NavController) { this.course = navParams.get('course'); - this.moduleId = navParams.get('moduleId'); + this.sectionId = navParams.get('sectionId'); // Get the title to display. We dont't have sections yet. this.title = courseFormatDelegate.getCourseTitle(this.course); @@ -81,9 +81,14 @@ export class CoreCourseSectionPage implements OnDestroy { * View loaded. */ ionViewDidLoad() { + + let module = this.navParams.get('module'); + if (module) { + this.courseHelper.openModule(this.navCtrl, module, this.course.id, this.sectionId); + } + this.loadData().finally(() => { this.dataLoaded = true; - delete this.moduleId; // Only load module automatically the first time. // Determine the course prefetch status. this.determineCoursePrefetchIcon().then(() => { @@ -133,7 +138,7 @@ export class CoreCourseSectionPage implements OnDestroy { promises.push(promise.then((completionStatus) => { // Get all the sections. promises.push(this.courseProvider.getSections(this.course.id, false, true).then((sections) => { - this.courseHelper.addHandlerDataForModules(sections, this.course.id, this.moduleId, completionStatus); + this.courseHelper.addHandlerDataForModules(sections, this.course.id, completionStatus); // Format the name of each section and check if it has content. this.sections = sections.map((section) => { diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 1ba282c95..226ca0d19 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreFilepoolProvider } from '../../../providers/filepool'; import { CoreSitesProvider } from '../../../providers/sites'; @@ -21,10 +22,13 @@ import { CoreTextUtilsProvider } from '../../../providers/utils/text'; import { CoreTimeUtilsProvider } from '../../../providers/utils/time'; import { CoreUtilsProvider } from '../../../providers/utils/utils'; import { CoreCoursesDelegate, CoreCoursesHandlerToDisplay } from '../../courses/providers/delegate'; +import { CoreSiteHomeProvider } from '../../sitehome/providers/sitehome'; import { CoreCourseProvider } from './course'; import { CoreCourseModuleDelegate } from './module-delegate'; import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler } from './module-prefetch-delegate'; +import { CoreLoginHelperProvider } from '../../login/providers/helper'; import { CoreConstants } from '../../constants'; +import { CoreSite } from '../../../classes/site'; import * as moment from 'moment'; /** @@ -109,7 +113,8 @@ export class CoreCourseHelperProvider { private moduleDelegate: CoreCourseModuleDelegate, private prefetchDelegate: CoreCourseModulePrefetchDelegate, private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider, - private utils: CoreUtilsProvider, private translate: TranslateService, private coursesDelegate: CoreCoursesDelegate) {} + private utils: CoreUtilsProvider, private translate: TranslateService, private coursesDelegate: CoreCoursesDelegate, + private loginHelper: CoreLoginHelperProvider, private siteHomeProvider: CoreSiteHomeProvider) {} /** * This function treats every module on the sections provided to load the handler data, treat completion @@ -117,11 +122,10 @@ export class CoreCourseHelperProvider { * * @param {any[]} sections List of sections to treat modules. * @param {number} courseId Course ID of the modules. - * @param {number} [moduleId] Module to navigate to if needed. * @param {any[]} [completionStatus] List of completion status. * @return {boolean} Whether the sections have content. */ - addHandlerDataForModules(sections: any[], courseId: number, moduleId?: number, completionStatus?: any) { + addHandlerDataForModules(sections: any[], courseId: number, completionStatus?: any) { let hasContent = false; sections.forEach((section) => { @@ -139,11 +143,6 @@ export class CoreCourseHelperProvider { module.completionstatus = completionStatus[module.id]; module.completionstatus.courseId = courseId; } - - if (module.id == moduleId) { - // This is the module we're looking for. Open it. - module.handlerData.action(new Event('click'), module, courseId); - } }); }); @@ -578,6 +577,86 @@ export class CoreCourseHelperProvider { return 'Section-' + section.id; } + /** + * Navigate to a module. + * + * @param {number} moduleId Module's ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [courseId] Course ID. If not defined we'll try to retrieve it from the site. + * @param {number} [sectionId] Section the module belongs to. If not defined we'll try to retrieve it from the site. + * @return {Promise} Promise resolved when done. + */ + navigateToModule(moduleId: number, siteId?: string, courseId?: number, sectionId?: number) : Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + let modal = this.domUtils.showModalLoading(), + promise, + site: CoreSite; + + if (courseId && sectionId) { + // No need to retrieve more data. + promise = Promise.resolve(); + } else if (!courseId) { + // We don't have courseId. + promise = this.courseProvider.getModuleBasicInfo(moduleId, siteId).then((module) => { + courseId = module.course; + sectionId = module.section; + }); + } else { + // We don't have sectionId but we have courseId. + promise = this.courseProvider.getModuleSectionId(moduleId, siteId).then((id) => { + sectionId = id; + }); + } + + return promise.then(() => { + // Get the site. + return this.sitesProvider.getSite(siteId); + }).then((s) => { + site = s; + + // Get the module. + return this.courseProvider.getModule(moduleId, courseId, sectionId, false, false, siteId); + }).then((module) => { + const params = { + course: {id: courseId}, + module: module, + sectionId: sectionId + }; + + module.handlerData = this.moduleDelegate.getModuleDataFor(module.modname, module, courseId, sectionId); + + if (courseId == site.getSiteHomeId()) { + // Check if site home is available. + return this.siteHomeProvider.isAvailable().then(() => { + this.loginHelper.redirect('CoreSiteHomeIndexPage', params, siteId); + }); + } else { + this.loginHelper.redirect('CoreCourseSectionPage', params, siteId); + } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); + }).finally(() => { + modal.dismiss(); + }); + } + + /** + * Open a module. + * + * @param {NavController} navCtrl The NavController to use. + * @param {any} module The module to open. + * @param {number} courseId The course ID of the module. + * @param {number} [sectionId] The section ID of the module. + */ + openModule(navCtrl: NavController, module: any, courseId: number, sectionId?: number) : void { + if (!module.handlerData) { + module.handlerData = this.moduleDelegate.getModuleDataFor(module.modname, module, courseId, sectionId); + } + + module.handlerData.action(new Event('click'), navCtrl, module, courseId, {animate: false}); + } + /** * Prefetch all the activities in a course and also the course addons. * diff --git a/src/core/course/providers/module-delegate.ts b/src/core/course/providers/module-delegate.ts index 6dd1a7700..de195f394 100644 --- a/src/core/course/providers/module-delegate.ts +++ b/src/core/course/providers/module-delegate.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { NavController } from 'ionic-angular'; +import { NavController, NavOptions } from 'ionic-angular'; import { CoreEventsProvider } from '../../../providers/events'; import { CoreLoggerProvider } from '../../../providers/logger'; import { CoreSitesProvider } from '../../../providers/sites'; @@ -104,8 +104,9 @@ export interface CoreCourseModuleHandlerData { * @param {NavController} navCtrl NavController instance. * @param {any} module The module object. * @param {number} courseId The course ID. + * @param {NavOptions} [options] Options for the navigation. */ - action?(event: Event, navCtrl: NavController, module: any, courseId: number) : void; + action?(event: Event, navCtrl: NavController, module: any, courseId: number, options?: NavOptions) : void; }; /** @@ -208,11 +209,11 @@ export class CoreCourseModuleDelegate { icon: this.courseProvider.getModuleIconSrc(module.modname), title: module.name, class: 'core-course-default-handler core-course-module-' + module.modname + '-handler', - action: (event: Event, navCtrl: NavController, module: any, courseId: number) => { + action: (event: Event, navCtrl: NavController, module: any, courseId: number, options?: NavOptions) => { event.preventDefault(); event.stopPropagation(); - navCtrl.push('CoreCourseUnsupportedModulePage', {module: module}); + navCtrl.push('CoreCourseUnsupportedModulePage', {module: module}, options); } }; diff --git a/src/core/sitehome/components/index/index.ts b/src/core/sitehome/components/index/index.ts index 2f677e15c..88d052376 100644 --- a/src/core/sitehome/components/index/index.ts +++ b/src/core/sitehome/components/index/index.ts @@ -28,8 +28,6 @@ import { CoreCourseModulePrefetchDelegate } from '../../../course/providers/modu templateUrl: 'index.html', }) export class CoreSiteHomeIndexComponent implements OnInit { - @Input() moduleId?: number; - dataLoaded: boolean; section: any; block: any; @@ -134,8 +132,7 @@ export class CoreSiteHomeIndexComponent implements OnInit { this.block.hasContent = this.courseHelper.sectionHasContent(this.block); } - this.hasContent = this.courseHelper.addHandlerDataForModules(this.sectionsLoaded, this.siteHomeId, this.moduleId) || - this.hasContent; + this.hasContent = this.courseHelper.addHandlerDataForModules(this.sectionsLoaded, this.siteHomeId) || this.hasContent; // Add log in Moodle. this.courseProvider.logView(this.siteHomeId); diff --git a/src/core/sitehome/pages/index/index.html b/src/core/sitehome/pages/index/index.html index e5d6b3299..0c0c35973 100644 --- a/src/core/sitehome/pages/index/index.html +++ b/src/core/sitehome/pages/index/index.html @@ -3,4 +3,4 @@ {{ 'core.sitehome.sitehome' | translate }} - + diff --git a/src/core/sitehome/pages/index/index.ts b/src/core/sitehome/pages/index/index.ts index 58ad7dcdd..1208b8a77 100644 --- a/src/core/sitehome/pages/index/index.ts +++ b/src/core/sitehome/pages/index/index.ts @@ -13,7 +13,9 @@ // limitations under the License. import { Component } from '@angular/core'; -import { IonicPage, NavParams } from 'ionic-angular'; +import { IonicPage, NavParams, NavController } from 'ionic-angular'; +import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreCourseHelperProvider } from '../../../course/providers/helper'; /** * Page that displays site home index. @@ -25,9 +27,11 @@ import { IonicPage, NavParams } from 'ionic-angular'; }) export class CoreSiteHomeIndexPage { - moduleId: number; - - constructor(navParams: NavParams) { - this.moduleId = navParams.get('moduleId'); + constructor(navParams: NavParams, navCtrl: NavController, courseHelper: CoreCourseHelperProvider, + sitesProvider: CoreSitesProvider) { + let module = navParams.get('module'); + if (module) { + courseHelper.openModule(navCtrl, module, sitesProvider.getCurrentSite().getSiteHomeId()); + } } } From 3cb62c748cb17946120f4e48d66b7873a5bf8f84 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 23 Jan 2018 11:26:21 +0100 Subject: [PATCH 7/9] MOBILE-2309 contentlinks: Implement some handlers and handle links --- src/app/app.module.ts | 2 + .../classes/module-grade-handler.ts | 10 +- .../classes/module-index-handler.ts | 2 +- .../pages/choose-site/choose-site.ts | 2 +- src/core/contentlinks/providers/delegate.ts | 4 +- src/core/contentlinks/providers/helper.ts | 9 +- src/core/course/components/format/format.ts | 8 +- src/core/course/pages/section/section.html | 2 +- src/core/course/pages/section/section.ts | 2 + .../overview-events/overview-events.ts | 24 +- src/core/courses/courses.module.ts | 17 +- .../courses/providers/course-link-handler.ts | 267 ++++++++++++++++++ .../providers/courses-index-link-handler.ts | 64 +++++ .../providers/my-overview-link-handler.ts | 52 ++++ .../login/pages/credentials/credentials.ts | 24 +- .../sitehome/providers/index-link-handler.ts | 84 ++++++ src/core/sitehome/sitehome.module.ts | 9 +- src/directives/format-text.ts | 9 +- src/directives/link.ts | 14 +- 19 files changed, 551 insertions(+), 54 deletions(-) create mode 100644 src/core/courses/providers/course-link-handler.ts create mode 100644 src/core/courses/providers/courses-index-link-handler.ts create mode 100644 src/core/courses/providers/my-overview-link-handler.ts create mode 100644 src/core/sitehome/providers/index-link-handler.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0ea058335..83e483cc6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -58,6 +58,7 @@ import { CoreFileUploaderModule } from '../core/fileuploader/fileuploader.module import { CoreSharedFilesModule } from '../core/sharedfiles/sharedfiles.module'; import { CoreCourseModule } from '../core/course/course.module'; import { CoreSiteHomeModule } from '../core/sitehome/sitehome.module'; +import { CoreContentLinksModule } from '../core/contentlinks/contentlinks.module'; // Addon modules. import { AddonCalendarModule } from '../addon/calendar/calendar.module'; @@ -94,6 +95,7 @@ export function createTranslateLoader(http: HttpClient) { CoreSharedFilesModule, CoreCourseModule, CoreSiteHomeModule, + CoreContentLinksModule, AddonCalendarModule ], bootstrap: [IonicApp], diff --git a/src/core/contentlinks/classes/module-grade-handler.ts b/src/core/contentlinks/classes/module-grade-handler.ts index 15866a9a5..a6f8bf3c0 100644 --- a/src/core/contentlinks/classes/module-grade-handler.ts +++ b/src/core/contentlinks/classes/module-grade-handler.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { NavController } from 'ionic-angular'; import { CoreContentLinksAction } from '../providers/delegate'; import { CoreContentLinksHandlerBase } from './base-handler'; import { CoreSitesProvider } from '../../../providers/sites'; @@ -64,7 +65,7 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB courseId = courseId || params.courseid || params.cid; return [{ - action: (siteId) : void => { + action: (siteId, navCtrl?) : void => { // Check if userid is the site's current user. const modal = this.domUtils.showModalLoading(); this.sitesProvider.getSite(siteId).then((site) => { @@ -73,7 +74,7 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId); } else if (this.canReview) { // Use the goToReview function. - this.goToReview(url, params, courseId, siteId); + this.goToReview(url, params, courseId, siteId, navCtrl); } else { // Not current user and cannot review it in the app, open it in browser. site.openInBrowserWithAutoLogin(url); @@ -91,10 +92,11 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB * @param {string} url The URL to treat. * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} * @param {number} courseId Course ID related to the URL. - * @param {string} siteId List of sites the URL belongs to. + * @param {string} siteId Site to use. + * @param {NavController} [navCtrl] Nav Controller to use to navigate. * @return {Promise} Promise resolved when done. */ - protected goToReview(url: string, params: any, courseId: number, siteId: string) : Promise { + protected goToReview(url: string, params: any, courseId: number, siteId: string, navCtrl?: NavController) : Promise { // This function should be overridden. return Promise.resolve(); } diff --git a/src/core/contentlinks/classes/module-index-handler.ts b/src/core/contentlinks/classes/module-index-handler.ts index 1d7a5819f..89c1c4aef 100644 --- a/src/core/contentlinks/classes/module-index-handler.ts +++ b/src/core/contentlinks/classes/module-index-handler.ts @@ -55,7 +55,7 @@ export class CoreContentLinksModuleIndexHandler extends CoreContentLinksHandlerB courseId = courseId || params.courseid || params.cid; return [{ - action: (siteId) => { + action: (siteId, navCtrl?) => { this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId); } }]; diff --git a/src/core/contentlinks/pages/choose-site/choose-site.ts b/src/core/contentlinks/pages/choose-site/choose-site.ts index 2f5ed59c6..7c71471b6 100644 --- a/src/core/contentlinks/pages/choose-site/choose-site.ts +++ b/src/core/contentlinks/pages/choose-site/choose-site.ts @@ -80,7 +80,7 @@ export class CoreContentLinksChooseSitePage implements OnInit { * @param {string} siteId Site ID. */ siteClicked(siteId: string) : void { - this.action.action(siteId); + this.action.action(siteId, this.navCtrl); } /** diff --git a/src/core/contentlinks/providers/delegate.ts b/src/core/contentlinks/providers/delegate.ts index b9fbced5d..6ab53e226 100644 --- a/src/core/contentlinks/providers/delegate.ts +++ b/src/core/contentlinks/providers/delegate.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { NavController } from 'ionic-angular'; import { CoreLoggerProvider } from '../../../providers/logger'; import { CoreSitesProvider } from '../../../providers/sites'; import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; @@ -115,8 +116,9 @@ export interface CoreContentLinksAction { * Action to perform when the link is clicked. * * @param {string} siteId The site ID. + * @param {NavController} [navCtrl] Nav Controller to use to navigate. */ - action(siteId: string) : void; + action(siteId: string, navCtrl?: NavController) : void; }; /** diff --git a/src/core/contentlinks/providers/helper.ts b/src/core/contentlinks/providers/helper.ts index 2d0fb6853..3033cc368 100644 --- a/src/core/contentlinks/providers/helper.ts +++ b/src/core/contentlinks/providers/helper.ts @@ -202,9 +202,10 @@ export class CoreContentLinksHelperProvider { * @param {string} url URL to handle. * @param {string} [username] Username related with the URL. E.g. in 'http://myuser@m.com', url would be 'http://m.com' and * the username 'myuser'. Don't use it if you don't want to filter by username. + * @param {NavController} [navCtrl] Nav Controller to use to navigate. * @return {Promise} Promise resolved with a boolean: true if URL was treated, false otherwise. */ - handleLink(url: string, username?: string) : Promise { + handleLink(url: string, username?: string, navCtrl?: NavController) : Promise { // Check if the link should be treated by some component/addon. return this.contentLinksDelegate.getActionsFor(url, undefined, username).then((actions) => { const action = this.getFirstValidAction(actions); @@ -212,18 +213,18 @@ export class CoreContentLinksHelperProvider { if (!this.sitesProvider.isLoggedIn()) { // No current site. Perform the action if only 1 site found, choose the site otherwise. if (action.sites.length == 1) { - action.action(action.sites[0]); + action.action(action.sites[0], navCtrl); } else { this.goToChooseSite(url); } } else if (action.sites.length == 1 && action.sites[0] == this.sitesProvider.getCurrentSiteId()) { // Current site. - action.action(action.sites[0]); + action.action(action.sites[0], navCtrl); } else { // Not current site or more than one site. Ask for confirmation. this.domUtils.showConfirm(this.translate.instant('core.contentlinks.confirmurlothersite')).then(() => { if (action.sites.length == 1) { - action.action(action.sites[0]); + action.action(action.sites[0], navCtrl); } else { this.goToChooseSite(url); } diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index fd7fd2d30..20d84c694 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -42,7 +42,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { @Input() course: any; // The course to render. @Input() sections: any[]; // List of course sections. @Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled. - @Input() initialSectionId: number; // The section to load first. + @Input() initialSectionId?: number; // The section to load first (by ID). + @Input() initialSectionNumber?: number; // The section to load first (by number). @Output() completionChanged?: EventEmitter; // Will emit an event when any module completion changes. // Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf. @@ -144,11 +145,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { if (changes.sections && this.sections) { if (!this.selectedSection) { // There is no selected section yet, calculate which one to load. - if (this.initialSectionId) { + if (this.initialSectionId || this.initialSectionNumber) { // We have an input indicating the section ID to load. Search the section. for (let i = 0; i < this.sections.length; i++) { let section = this.sections[i]; - if (section.id == this.initialSectionId) { + if (section.id == this.initialSectionId || section.section == this.initialSectionNumber) { this.loaded = true; this.sectionChanged(section); break; @@ -229,6 +230,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.componentInstances[type].course = this.course; this.componentInstances[type].sections = this.sections; this.componentInstances[type].initialSectionId = this.initialSectionId; + this.componentInstances[type].initialSectionNumber = this.initialSectionNumber; this.componentInstances[type].downloadEnabled = this.downloadEnabled; this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed. diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html index f81e941c9..f890b24f3 100644 --- a/src/core/course/pages/section/section.html +++ b/src/core/course/pages/section/section.html @@ -21,6 +21,6 @@ {{ 'core.course.contents' | translate }} {{ handler.data.title || translate }}
- + diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 94b107cfa..b8fb69a58 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -40,6 +40,7 @@ export class CoreCourseSectionPage implements OnDestroy { course: any; sections: any[]; sectionId: number; + sectionNumber: number; courseHandlers: CoreCoursesHandlerToDisplay[]; dataLoaded: boolean; downloadEnabled: boolean; @@ -59,6 +60,7 @@ export class CoreCourseSectionPage implements OnDestroy { sitesProvider: CoreSitesProvider, private navCtrl: NavController) { this.course = navParams.get('course'); this.sectionId = navParams.get('sectionId'); + this.sectionNumber = navParams.get('sectionNumber'); // Get the title to display. We dont't have sections yet. this.title = courseFormatDelegate.getCourseTitle(this.course); diff --git a/src/core/courses/components/overview-events/overview-events.ts b/src/core/courses/components/overview-events/overview-events.ts index df8db57cb..6e67a0149 100644 --- a/src/core/courses/components/overview-events/overview-events.ts +++ b/src/core/courses/components/overview-events/overview-events.ts @@ -13,11 +13,13 @@ // limitations under the License. import { Component, Input, Output, OnChanges, EventEmitter, SimpleChange } from '@angular/core'; +import { NavController } from 'ionic-angular'; import { CoreSitesProvider } from '../../../../providers/sites'; import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; import { CoreUtilsProvider } from '../../../../providers/utils/utils'; import { CoreCourseProvider } from '../../../course/providers/course'; +import { CoreContentLinksHelperProvider } from '../../../contentlinks/providers/helper'; import * as moment from 'moment'; /** @@ -41,9 +43,9 @@ export class CoreCoursesOverviewEventsComponent implements OnChanges { next30Days: any[] = []; future: any[] = []; - constructor(private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider, + constructor(private navCtrl: NavController, private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider, private sitesProvider: CoreSitesProvider, - private courseProvider: CoreCourseProvider) { + private courseProvider: CoreCourseProvider, private contentLinksHelper: CoreContentLinksHelperProvider) { this.loadMore = new EventEmitter(); } @@ -100,9 +102,6 @@ export class CoreCoursesOverviewEventsComponent implements OnChanges { loadMoreEvents() { this.loadingMore = true; this.loadMore.emit(); - // this.loadMore().finally(function() { - // scope.loadingMore = false; - // }); } /** @@ -119,19 +118,14 @@ export class CoreCoursesOverviewEventsComponent implements OnChanges { url = this.textUtils.decodeHTMLEntities(url); let modal = this.domUtils.showModalLoading(); - this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url).finally(() => { + this.contentLinksHelper.handleLink(url, undefined, this.navCtrl).then((treated) => { + if (!treated) { + return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url); + } + }).finally(() => { modal.dismiss(); }); - // @todo - // $mmContentLinksHelper.handleLink(url).then((treated) => { - // if (!treated) { - // return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url); - // } - // }).finally(() => { - // modal.dismiss(); - // }); - return false; } } diff --git a/src/core/courses/courses.module.ts b/src/core/courses/courses.module.ts index 8dfd4bd1f..cd603bae5 100644 --- a/src/core/courses/courses.module.ts +++ b/src/core/courses/courses.module.ts @@ -17,7 +17,11 @@ import { CoreCoursesProvider } from './providers/courses'; import { CoreCoursesMainMenuHandler } from './providers/mainmenu-handler'; import { CoreCoursesMyOverviewProvider } from './providers/my-overview'; import { CoreCoursesDelegate } from './providers/delegate'; +import { CoreCoursesCourseLinkHandler } from './providers/course-link-handler'; +import { CoreCoursesIndexLinkHandler } from './providers/courses-index-link-handler'; +import { CoreCoursesMyOverviewLinkHandler } from './providers/my-overview-link-handler'; import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; +import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate'; @NgModule({ declarations: [], @@ -27,12 +31,21 @@ import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; CoreCoursesProvider, CoreCoursesMainMenuHandler, CoreCoursesMyOverviewProvider, - CoreCoursesDelegate + CoreCoursesDelegate, + CoreCoursesCourseLinkHandler, + CoreCoursesIndexLinkHandler, + CoreCoursesMyOverviewLinkHandler ], exports: [] }) export class CoreCoursesModule { - constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreCoursesMainMenuHandler) { + constructor(mainMenuDelegate: CoreMainMenuDelegate, contentLinksDelegate: CoreContentLinksDelegate, + mainMenuHandler: CoreCoursesMainMenuHandler, courseLinkHandler: CoreCoursesCourseLinkHandler, + indexLinkHandler: CoreCoursesIndexLinkHandler, myOverviewLinkHandler: CoreCoursesMyOverviewLinkHandler) { mainMenuDelegate.registerHandler(mainMenuHandler); + + contentLinksDelegate.registerHandler(courseLinkHandler); + contentLinksDelegate.registerHandler(indexLinkHandler); + contentLinksDelegate.registerHandler(myOverviewLinkHandler); } } diff --git a/src/core/courses/providers/course-link-handler.ts b/src/core/courses/providers/course-link-handler.ts new file mode 100644 index 000000000..fa1fa3235 --- /dev/null +++ b/src/core/courses/providers/course-link-handler.ts @@ -0,0 +1,267 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { TranslateService } from '@ngx-translate/core'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; +import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreCourseProvider } from '../../course/providers/course'; +import { CoreCoursesProvider } from './courses'; + +/** + * Handler to treat links to course view or enrol (except site home). + */ +@Injectable() +export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase { + name = 'CoreCoursesCourseLinkHandler'; + pattern = /((\/enrol\/index\.php)|(\/course\/enrol\.php)|(\/course\/view\.php)).*([\?\&]id=\d+)/; + + protected waitStart = 0; + + constructor(private sitesProvider: CoreSitesProvider, private coursesProvider: CoreCoursesProvider, + private loginHelper: CoreLoginHelperProvider, private domUtils: CoreDomUtilsProvider, + private translate: TranslateService, private courseProvider: CoreCourseProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number) : + CoreContentLinksAction[]|Promise { + courseId = parseInt(params.id, 10); + + let sectionId = params.sectionid ? parseInt(params.sectionid, 10) : null, + sectionNumber = typeof params.section != 'undefined' ? parseInt(params.section, 10) : NaN, + pageParams: any = { + course: {id: courseId}, + sectionId: sectionId || null + }; + + if (!isNaN(sectionNumber)) { + pageParams.sectionNumber = sectionNumber; + } + + return [{ + action: (siteId, navCtrl?) => { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + if (siteId == this.sitesProvider.getCurrentSiteId()) { + this.actionEnrol(courseId, url, pageParams).catch(() => { + // Ignore errors. + }); + } else { + // Use redirect to make the course the new history root (to avoid "loops" in history). + this.loginHelper.redirect('CoreCourseSectionPage', pageParams, siteId); + } + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number) : boolean|Promise { + courseId = parseInt(params.id, 10); + + if (!courseId) { + return false; + } + + // Get the course id of Site Home. + return this.sitesProvider.getSiteHomeId(siteId).then((siteHomeId) => { + return courseId != siteHomeId; + }); + } + + /** + * Action to perform when an enrol link is clicked. + * + * @param {number} courseId Course ID. + * @param {string} url Treated URL. + * @param {any} pageParams Params to send to the new page. + * @return {Promise} Promise resolved when done. + */ + protected actionEnrol(courseId: number, url: string, pageParams: any) : Promise { + let modal = this.domUtils.showModalLoading(), + isEnrolUrl = !!url.match(/(\/enrol\/index\.php)|(\/course\/enrol\.php)/); + + // Check if user is enrolled in the course. + return this.coursesProvider.getUserCourse(courseId).catch(() => { + // User is not enrolled in the course. Check if can self enrol. + return this.canSelfEnrol(courseId).then(() => { + modal.dismiss(); + + // The user can self enrol. If it's not a enrolment URL we'll ask for confirmation. + let promise = isEnrolUrl ? Promise.resolve() : + this.domUtils.showConfirm(this.translate.instant('core.courses.confirmselfenrol')); + + return promise.then(() => { + // Enrol URL or user confirmed. + return this.selfEnrol(courseId).catch((error) => { + if (error) { + this.domUtils.showErrorModal(error); + } + return Promise.reject(null); + }); + }, () => { + // User cancelled. Check if the user can view the course contents (guest access or similar). + return this.courseProvider.getSections(courseId, false, true); + }); + }, (error) => { + // Can't self enrol. Check if the user can view the course contents (guest access or similar). + return this.courseProvider.getSections(courseId, false, true).catch(() => { + // Error. Show error message and allow the user to open the link in browser. + modal.dismiss(); + + if (error) { + error = error.message || error.error || error.content || error.body || error; + } + if (!error) { + error = this.translate.instant('core.courses.notenroled'); + } + + let body = this.translate.instant('core.twoparagraphs', + {p1: error, p2: this.translate.instant('core.confirmopeninbrowser')}); + this.domUtils.showConfirm(body).then(() => { + this.sitesProvider.getCurrentSite().openInBrowserWithAutoLogin(url); + }).catch(() => { + // User cancelled. + }); + return Promise.reject(null); + }); + }); + }).then(() => { + modal.dismiss(); + + // Use redirect to make the course the new history root (to avoid "loops" in history). + this.loginHelper.redirect('CoreCourseSectionPage', pageParams, this.sitesProvider.getCurrentSiteId()); + }); + } + + /** + * Check if a user can be "automatically" self enrolled in a course. + * + * @param {number} courseId Course ID. + * @return {Promise} Promise resolved if user can be enrolled in a course, rejected otherwise. + */ + protected canSelfEnrol(courseId: number) : Promise { + // Check that the course has self enrolment enabled. + return this.coursesProvider.getCourseEnrolmentMethods(courseId).then((methods) => { + let isSelfEnrolEnabled = false, + instances = 0; + + methods.forEach((method) => { + if (method.type == 'self' && method.status) { + isSelfEnrolEnabled = true; + instances++; + } + }); + + if (!isSelfEnrolEnabled || instances != 1) { + // Self enrol not enabled or more than one instance. + return Promise.reject(null); + } + }); + } + + /** + * Try to self enrol a user in a course. + * + * @param {number} courseId Course ID. + * @param {string} [password] Password. + * @return {Promise} Promise resolved when the user is enrolled, rejected otherwise. + */ + protected selfEnrol(courseId: number, password?: string) : Promise { + const modal = this.domUtils.showModalLoading(); + return this.coursesProvider.selfEnrol(courseId, password).then(() => { + // Success self enrolling the user, invalidate the courses list. + return this.coursesProvider.invalidateUserCourses().catch(() => { + // Ignore errors. + }).then(() => { + // Sometimes the list of enrolled courses takes a while to be updated. Wait for it. + return this.waitForEnrolled(courseId, true).finally(() => { + modal.dismiss(); + }); + }); + + }).catch((error) => { + modal.dismiss(); + if (error && error.code === CoreCoursesProvider.ENROL_INVALID_KEY) { + // Invalid password. Allow the user to input password. + let title = this.translate.instant('core.courses.selfenrolment'), + body = ' ', // Empty message. + placeholder = this.translate.instant('core.courses.password'); + + if (typeof password != 'undefined') { + // The user attempted a password. Show an error message. + this.domUtils.showErrorModal(error.message); + } + + return this.domUtils.showPrompt(body, title, placeholder).then((password) => { + return this.selfEnrol(courseId, password); + }); + } else { + return Promise.reject(error); + } + }); + } + + /** + * Wait for the user to be enrolled in a course. + * + * @param {number} courseId The course ID. + * @param {boolean} first If it's the first call (true) or it's a recursive call (false). + * @return {Promise} Promise resolved when enrolled or timeout. + */ + protected waitForEnrolled(courseId: number, first?: boolean) : Promise { + if (first) { + this.waitStart = Date.now(); + } + + // Check if user is enrolled in the course. + return this.coursesProvider.invalidateUserCourses().catch(() => { + // Ignore errors. + }).then(() => { + return this.coursesProvider.getUserCourse(courseId); + }).catch(() => { + // Not enrolled, wait a bit and try again. + if (Date.now() - this.waitStart > 60000) { + // Max time reached, stop. + return; + } + + return new Promise((resolve, reject) => { + setTimeout(() => { + this.waitForEnrolled(courseId).then(resolve); + }, 5000); + }); + }); + } +} diff --git a/src/core/courses/providers/courses-index-link-handler.ts b/src/core/courses/providers/courses-index-link-handler.ts new file mode 100644 index 000000000..051adf797 --- /dev/null +++ b/src/core/courses/providers/courses-index-link-handler.ts @@ -0,0 +1,64 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreCoursesProvider } from './courses'; + +/** + * Handler to treat links to course index (list of courses). + */ +@Injectable() +export class CoreCoursesIndexLinkHandler extends CoreContentLinksHandlerBase { + name = 'CoreCoursesIndexLinkHandler'; + featureName = '$mmSideMenuDelegate_mmCourses'; + pattern = /\/course\/?(index\.php.*)?$/; + + constructor(private coursesProvider: CoreCoursesProvider, private loginHelper: CoreLoginHelperProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number) : + CoreContentLinksAction[]|Promise { + return [{ + action: (siteId, navCtrl?) => { + var page = 'CoreCoursesMyCoursesPage', // By default, go to My Courses. + pageParams: any = {}; + + if (this.coursesProvider.isGetCoursesByFieldAvailable()) { + if (params.categoryid) { + page = 'CoreCoursesCategoriesPage'; + pageParams.categoryId = parseInt(params.categoryid, 10); + } else { + page = 'CoreCoursesAvailableCoursesPage'; + } + } + + // Always use redirect to make it the new history root (to avoid "loops" in history). + this.loginHelper.redirect(page, pageParams, siteId); + } + }]; + } +} diff --git a/src/core/courses/providers/my-overview-link-handler.ts b/src/core/courses/providers/my-overview-link-handler.ts new file mode 100644 index 000000000..28905b628 --- /dev/null +++ b/src/core/courses/providers/my-overview-link-handler.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreCoursesProvider } from './courses'; + +/** + * Handler to treat links to my overview. + */ +@Injectable() +export class CoreCoursesMyOverviewLinkHandler extends CoreContentLinksHandlerBase { + name = 'CoreCoursesMyOverviewLinkHandler'; + featureName = '$mmSideMenuDelegate_mmCourses'; + pattern = /\/my\/?$/; + + constructor(private coursesProvider: CoreCoursesProvider, private loginHelper: CoreLoginHelperProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number) : + CoreContentLinksAction[]|Promise { + return [{ + action: (siteId, navCtrl?) => { + // Always use redirect to make it the new history root (to avoid "loops" in history). + this.loginHelper.redirect('CoreCoursesMyOverviewPage', undefined, siteId); + } + }]; + } +} diff --git a/src/core/login/pages/credentials/credentials.ts b/src/core/login/pages/credentials/credentials.ts index 32c939a05..9ba824b40 100644 --- a/src/core/login/pages/credentials/credentials.ts +++ b/src/core/login/pages/credentials/credentials.ts @@ -21,6 +21,8 @@ import { CoreSitesProvider } from '../../../../providers/sites'; import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; import { CoreUtilsProvider } from '../../../../providers/utils/utils'; import { CoreLoginHelperProvider } from '../../providers/helper'; +import { CoreContentLinksDelegate } from '../../../contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '../../../contentlinks/providers/helper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; /** @@ -52,7 +54,8 @@ export class CoreLoginCredentialsPage { constructor(private navCtrl: NavController, navParams: NavParams, fb: FormBuilder, private appProvider: CoreAppProvider, private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider, - private eventsProvider: CoreEventsProvider) { + private eventsProvider: CoreEventsProvider, private contentLinksDelegate: CoreContentLinksDelegate, + private contentLinksHelper: CoreContentLinksHelperProvider) { this.siteUrl = navParams.get('siteUrl'); this.siteConfig = navParams.get('siteConfig'); @@ -203,16 +206,15 @@ export class CoreLoginCredentialsPage { if (this.urlToOpen) { // There's a content link to open. - // @todo: Implement this once content links delegate is implemented. - // return $mmContentLinksDelegate.getActionsFor(urlToOpen, undefined, username).then((actions) => { - // action = $mmContentLinksHelper.getFirstValidAction(actions); - // if (action && action.sites.length) { - // // Action should only have 1 site because we're filtering by username. - // action.action(action.sites[0]); - // } else { - // return $mmLoginHelper.goToSiteInitialPage(); - // } - // }); + return this.contentLinksDelegate.getActionsFor(this.urlToOpen, undefined, username).then((actions) => { + const action = this.contentLinksHelper.getFirstValidAction(actions); + if (action && action.sites.length) { + // Action should only have 1 site because we're filtering by username. + action.action(action.sites[0]); + } else { + return this.loginHelper.goToSiteInitialPage(); + } + }); } else { return this.loginHelper.goToSiteInitialPage(); } diff --git a/src/core/sitehome/providers/index-link-handler.ts b/src/core/sitehome/providers/index-link-handler.ts new file mode 100644 index 000000000..daacc1542 --- /dev/null +++ b/src/core/sitehome/providers/index-link-handler.ts @@ -0,0 +1,84 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSitesProvider } from '../../../providers/sites'; +import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreSiteHomeProvider } from './sitehome'; + +/** + * Handler to treat links to site home index. + */ +@Injectable() +export class CoreSiteHomeIndexLinkHandler extends CoreContentLinksHandlerBase { + name = 'CoreSiteHomeIndexLinkHandler'; + featureName = '$mmSideMenuDelegate_mmaFrontpage'; + pattern = /\/course\/view\.php.*([\?\&]id=\d+)/; + + constructor(private sitesProvider: CoreSitesProvider, private siteHomeProvider: CoreSiteHomeProvider, + private loginHelper: CoreLoginHelperProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number) : + CoreContentLinksAction[]|Promise { + return [{ + action: (siteId, navCtrl?) => { + // Always use redirect to make it the new history root (to avoid "loops" in history). + this.loginHelper.redirect('CoreSiteHomeIndexPage', undefined, siteId); + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number) : boolean|Promise { + courseId = parseInt(params.id, 10); + if (!courseId) { + return false; + } + + return this.sitesProvider.getSite(siteId).then((site) => { + if (courseId != site.getSiteHomeId()) { + // The course is not site home. + return false; + } + + return this.siteHomeProvider.isAvailable(siteId).then(() => { + return true; + }).catch(() => { + return false; + }); + }); + } +} diff --git a/src/core/sitehome/sitehome.module.ts b/src/core/sitehome/sitehome.module.ts index fc41faded..c498d2b12 100644 --- a/src/core/sitehome/sitehome.module.ts +++ b/src/core/sitehome/sitehome.module.ts @@ -15,7 +15,9 @@ import { NgModule } from '@angular/core'; import { CoreSiteHomeProvider } from './providers/sitehome'; import { CoreSiteHomeMainMenuHandler } from './providers/mainmenu-handler'; +import { CoreSiteHomeIndexLinkHandler } from './providers/index-link-handler'; import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; +import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate'; @NgModule({ declarations: [], @@ -23,12 +25,15 @@ import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; ], providers: [ CoreSiteHomeProvider, - CoreSiteHomeMainMenuHandler + CoreSiteHomeMainMenuHandler, + CoreSiteHomeIndexLinkHandler ], exports: [] }) export class CoreSiteHomeModule { - constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreSiteHomeMainMenuHandler) { + constructor(mainMenuDelegate: CoreMainMenuDelegate, contentLinksDelegate: CoreContentLinksDelegate, + mainMenuHandler: CoreSiteHomeMainMenuHandler, indexLinkHandler: CoreSiteHomeIndexLinkHandler) { mainMenuDelegate.registerHandler(mainMenuHandler); + contentLinksDelegate.registerHandler(indexLinkHandler); } } diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index da089bc25..d150799a9 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core'; -import { Platform } from 'ionic-angular'; +import { Platform, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '../providers/app'; import { CoreFilepoolProvider } from '../providers/filepool'; @@ -26,6 +26,7 @@ import { CoreUtilsProvider } from '../providers/utils/utils'; import { CoreSite } from '../classes/site'; import { CoreLinkDirective } from '../directives/link'; import { CoreExternalContentDirective } from '../directives/external-content'; +import { CoreContentLinksHelperProvider } from '../core/contentlinks/providers/helper'; /** * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective @@ -60,7 +61,8 @@ export class CoreFormatTextDirective implements OnChanges { constructor(element: ElementRef, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, private translate: TranslateService, private platform: Platform, private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private loggerProvider: CoreLoggerProvider, - private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider) { + private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider, + private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController) { this.element = element.nativeElement; this.element.classList.add('opacity-hide'); // Hide contents until they're treated. this.afterRender = new EventEmitter(); @@ -274,7 +276,8 @@ export class CoreFormatTextDirective implements OnChanges { // Important: We need to look for links first because in 'img' we add new links without core-link. anchors.forEach((anchor) => { // Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually. - let linkDir = new CoreLinkDirective(anchor, this.domUtils, this.utils, this.sitesProvider, this.urlUtils); + let linkDir = new CoreLinkDirective(anchor, this.domUtils, this.utils, this.sitesProvider, this.urlUtils, + this.contentLinksHelper, this.navCtrl); linkDir.capture = true; linkDir.ngOnInit(); diff --git a/src/directives/link.ts b/src/directives/link.ts index f95aa2624..7a50b20d8 100644 --- a/src/directives/link.ts +++ b/src/directives/link.ts @@ -13,10 +13,12 @@ // limitations under the License. import { Directive, Input, OnInit, ElementRef } from '@angular/core'; +import { NavController } from 'ionic-angular'; import { CoreSitesProvider } from '../providers/sites'; import { CoreDomUtilsProvider } from '../providers/utils/dom'; import { CoreUrlUtilsProvider } from '../providers/utils/url'; import { CoreUtilsProvider } from '../providers/utils/utils'; +import { CoreContentLinksHelperProvider } from '../core/contentlinks/providers/helper'; import { CoreConfigConstants } from '../configconstants'; /** @@ -36,7 +38,8 @@ export class CoreLinkDirective implements OnInit { protected element: HTMLElement; constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider, - private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider) { + private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider, + private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController) { // This directive can be added dynamically. In that case, the first param is the anchor HTMLElement. this.element = element.nativeElement || element; } @@ -56,12 +59,11 @@ export class CoreLinkDirective implements OnInit { event.stopPropagation(); if (this.utils.isTrueOrOne(this.capture)) { - // @todo: Handle link using content links helper. - // $mmContentLinksHelper.handleLink(href).then((treated) => { - // if (!treated) { + this.contentLinksHelper.handleLink(href, undefined, this.navCtrl).then((treated) => { + if (!treated) { this.navigate(href); - // } - // }); + } + }); } else { this.navigate(href); } From 347a84955183c7044159e48171c62ff92d360668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 24 Jan 2018 11:35:49 +0100 Subject: [PATCH 8/9] MOBILE-2309 sitehome: Style changes --- src/addon/calendar/pages/event/event.html | 2 +- src/addon/calendar/pages/list/list.html | 2 +- src/app/app.ios.scss | 5 ++ src/app/app.md.scss | 5 ++ src/app/app.wp.scss | 5 ++ src/components/loading/loading.scss | 4 +- src/components/tabs/tab.ts | 15 +++++- src/components/tabs/tabs.html | 4 +- src/components/tabs/tabs.scss | 15 +++++- src/components/tabs/tabs.ts | 44 +++++++++++++++- .../courses/pages/categories/categories.html | 1 - .../courses/pages/my-courses/my-courses.html | 18 +++++-- .../pages/my-overview/my-overview.html | 4 +- src/core/sitehome/components/index/index.html | 52 ++++++++++--------- 14 files changed, 134 insertions(+), 42 deletions(-) diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html index b55da9358..598ae4c83 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index f61ed9e87..6eaaeb367 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -16,7 +16,7 @@ - + diff --git a/src/app/app.ios.scss b/src/app/app.ios.scss index 662eb401e..ad9be8fdd 100644 --- a/src/app/app.ios.scss +++ b/src/app/app.ios.scss @@ -16,6 +16,11 @@ color: $toolbar-ios-button-color; } +.item-ios ion-spinner[item-start], +.item-ios ion-spinner[item-end] { + @include margin($item-ios-padding-icon-top, null, $item-ios-padding-icon-bottom, 0); +} + // Highlights inside the input element. @if ($core-text-input-ios-show-highlight) { .card-ios, .list-ios { diff --git a/src/app/app.md.scss b/src/app/app.md.scss index c0814c60b..b79bb1bdf 100644 --- a/src/app/app.md.scss +++ b/src/app/app.md.scss @@ -16,6 +16,11 @@ color: $toolbar-md-button-color; } +.item-md ion-spinner[item-start] + .item-inner, +.item-md ion-spinner[item-start] + .item-input { + @include margin-horizontal($item-md-padding-start + ($item-md-padding-start / 2) - 1, null); +} + // Highlights inside the input element. @if ($core-text-input-md-show-highlight) { .card-md, .list-md { diff --git a/src/app/app.wp.scss b/src/app/app.wp.scss index fd5175023..312358980 100644 --- a/src/app/app.wp.scss +++ b/src/app/app.wp.scss @@ -15,3 +15,8 @@ .bar-buttons core-context-menu .button-clear-wp { color: $toolbar-wp-button-color; } + +.item-wp ion-spinner[item-start] + .item-inner, +.item-wp ion-spinner[item-start] + .item-input { + @include margin-horizontal(($item-wp-padding-start / 2), null); +} diff --git a/src/components/loading/loading.scss b/src/components/loading/loading.scss index 752623059..2fb7b23f1 100644 --- a/src/components/loading/loading.scss +++ b/src/components/loading/loading.scss @@ -15,8 +15,8 @@ core-loading { } } -.scroll-content > .padding > core-loading > .core-loading-container, -ion-content[padding] > .scroll-content > core-loading > .core-loading-container, +.scroll-content > core-loading > .core-loading-container, +ion-content > .scroll-content > core-loading > .core-loading-container, .core-loading-center .core-loading-container { position: absolute; top: 0; diff --git a/src/components/tabs/tab.ts b/src/components/tabs/tab.ts index f1aaaa36a..1435f2247 100644 --- a/src/components/tabs/tab.ts +++ b/src/components/tabs/tab.ts @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, OnInit, OnDestroy, ElementRef, EventEmitter, ContentChild, TemplateRef } from '@angular/core'; +import { Component, Input, Output, OnInit, OnDestroy, ElementRef, EventEmitter, ContentChild, TemplateRef, + ViewChild } from '@angular/core'; import { CoreTabsComponent } from './tabs'; +import { Content } from 'ionic-angular'; /** * A tab to use inside core-tabs. The content of this tab will be displayed when the tab is selected. @@ -49,6 +51,7 @@ export class CoreTabComponent implements OnInit, OnDestroy { @Output() ionSelect: EventEmitter = new EventEmitter(); @ContentChild(TemplateRef) template: TemplateRef // Template defined by the content. + @ContentChild(Content) scroll: Content; element: HTMLElement; // The core-tab element. loaded = false; @@ -76,8 +79,18 @@ export class CoreTabComponent implements OnInit, OnDestroy { */ selectTab() { this.element.classList.add('selected'); + this.loaded = true; this.ionSelect.emit(this); + + // Setup tab scrolling. + setTimeout(() => { + if (this.scroll) { + this.scroll.getScrollElement().onscroll = (e) => { + this.tabs.showHideTabs(e); + } + } + }, 1); } /** diff --git a/src/components/tabs/tabs.html b/src/components/tabs/tabs.html index 22fdd6113..51e2fe3ba 100644 --- a/src/components/tabs/tabs.html +++ b/src/components/tabs/tabs.html @@ -1,5 +1,5 @@ - -
+ +
diff --git a/src/components/tabs/tabs.scss b/src/components/tabs/tabs.scss index 4eb747c02..dd40b2d86 100644 --- a/src/components/tabs/tabs.scss +++ b/src/components/tabs/tabs.scss @@ -1,7 +1,6 @@ core-tabs { .core-tabs-bar { @include position(null, null, 0, 0); - z-index: $z-index-toolbar; display: flex; width: 100%; @@ -21,11 +20,19 @@ core-tabs { } } } - .core-tabs-content-container { height: 100%; } + &.tabs-hidden { + .core-tabs-bar { + display: none !important; + } + .core-tabs-content-container { + padding-bottom: 0 !important; + } + } + core-tab { display: none; height: 100%; @@ -35,3 +42,7 @@ core-tabs { } } } + +.scroll-content.no-scroll { + overflow: hidden !important; +} diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 3edfe1706..2fca81ca0 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -15,6 +15,7 @@ import { Component, Input, Output, EventEmitter, OnInit, OnChanges, AfterViewInit, ViewChild, ElementRef, SimpleChange } from '@angular/core'; import { CoreTabComponent } from './tab'; +import { Content } from 'ionic-angular'; /** * This component displays some tabs that usually share data between them. @@ -43,6 +44,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { @Input() hideUntil: boolean; // Determine when should the contents be shown. @Output() ionChange: EventEmitter = new EventEmitter(); // Emitted when the tab changes. @ViewChild('originalTabs') originalTabsRef: ElementRef; + @ViewChild('topTabs') topTabs: ElementRef; tabs: CoreTabComponent[] = []; // List of tabs. selected: number; // Selected tab number. @@ -50,13 +52,27 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { protected initialized = false; protected afterViewInitTriggered = false; - constructor() {} + protected topTabsElement: HTMLElement; // The container of the original tabs. It will include each tab's content. + protected tabBarHeight; + protected tabBarElement: HTMLElement; // Host element. + protected tabsShown = true; + protected scroll: HTMLElement; // Parent scroll element (if core-tabs is inside a ion-content). + + constructor(element: ElementRef, content: Content) { + this.tabBarElement = element.nativeElement; + setTimeout(() => { + if (content) { + this.scroll = content.getScrollElement(); + } + }, 1); + } /** * Component being initialized. */ ngOnInit() { this.originalTabsContainer = this.originalTabsRef.nativeElement; + this.topTabsElement = this.topTabs.nativeElement; } /** @@ -144,9 +160,35 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { this.selectTab(selectedIndex); } + // Setup tab scrolling. + this.tabBarHeight = this.topTabsElement.offsetHeight; + this.originalTabsContainer.style.paddingBottom = this.tabBarHeight + 'px'; + if (this.scroll) { + this.scroll.classList.add('no-scroll'); + } + this.initialized = true; } + /** + * Show or hide the tabs. This is used when the user is scrolling inside a tab. + * + * @param {any} e Scroll event. + */ + showHideTabs(e: any) : void { + if (e.target.scrollTop < this.tabBarHeight) { + if (!this.tabsShown) { + this.tabBarElement.classList.remove('tabs-hidden'); + this.tabsShown = true; + } + } else { + if (this.tabsShown) { + this.tabBarElement.classList.add('tabs-hidden'); + this.tabsShown = false; + } + } + } + /** * Remove a tab from the list of tabs. * diff --git a/src/core/courses/pages/categories/categories.html b/src/core/courses/pages/categories/categories.html index c22dfe194..64d9a9971 100644 --- a/src/core/courses/pages/categories/categories.html +++ b/src/core/courses/pages/categories/categories.html @@ -23,7 +23,6 @@

{{category.coursecount}} -
diff --git a/src/core/courses/pages/my-courses/my-courses.html b/src/core/courses/pages/my-courses/my-courses.html index b93f51ee9..bd837b0b2 100644 --- a/src/core/courses/pages/my-courses/my-courses.html +++ b/src/core/courses/pages/my-courses/my-courses.html @@ -19,11 +19,19 @@ - - - - - +
+ + + + +
+ + + + + + +

{{ 'core.courses.searchcoursesadvice' | translate }}

diff --git a/src/core/courses/pages/my-overview/my-overview.html b/src/core/courses/pages/my-overview/my-overview.html index 8cdaddcbf..76123c08d 100644 --- a/src/core/courses/pages/my-overview/my-overview.html +++ b/src/core/courses/pages/my-overview/my-overview.html @@ -82,7 +82,7 @@
- +
@@ -105,4 +105,4 @@ - + \ No newline at end of file diff --git a/src/core/sitehome/components/index/index.html b/src/core/sitehome/components/index/index.html index 0c7c4ed3f..23187e37e 100644 --- a/src/core/sitehome/components/index/index.html +++ b/src/core/sitehome/components/index/index.html @@ -5,34 +5,38 @@ - - - - - + + + + + + - - - - - - - - - - - + - - - - - - + + + + + + + + + + + - - + + + + + + + + + + From 3f8dece14e68dbc637fb21c03c058c533fda4e41 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 24 Jan 2018 15:28:47 +0100 Subject: [PATCH 9/9] MOBILE-2309 core: Use popover interface in all selects --- src/addon/calendar/pages/event/event.html | 2 +- src/addon/calendar/pages/settings/settings.html | 2 +- src/components/site-picker/site-picker.html | 2 +- src/core/course/components/format/format.html | 2 +- src/core/courses/pages/my-overview/my-overview.html | 4 ++-- src/core/login/pages/email-signup/email-signup.html | 2 +- src/core/login/pages/site/site.html | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html index 598ae4c83..7e1087d80 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -40,7 +40,7 @@ {{ 'addon.calendar.notifications' | translate }} - + {{ 'core.defaultvalue' | translate :{$a: defaultTimeReadable} }} {{ 'core.settings.disabled' | translate }} {{ 600 | coreDuration }} diff --git a/src/addon/calendar/pages/settings/settings.html b/src/addon/calendar/pages/settings/settings.html index b32c35dcf..3d4557bc1 100644 --- a/src/addon/calendar/pages/settings/settings.html +++ b/src/addon/calendar/pages/settings/settings.html @@ -7,7 +7,7 @@ {{ 'addon.calendar.defaultnotificationtime' | translate }} - + {{ 'core.settings.disabled' | translate }} {{ 600 | coreDuration }} {{ 1800 | coreDuration }} diff --git a/src/components/site-picker/site-picker.html b/src/components/site-picker/site-picker.html index ee5fc727c..c10dbe49f 100644 --- a/src/components/site-picker/site-picker.html +++ b/src/components/site-picker/site-picker.html @@ -1,6 +1,6 @@ {{ 'core.site' | translate }} - + {{ site.fullNameAndSiteName }} diff --git a/src/core/course/components/format/format.html b/src/core/course/components/format/format.html index 60289795a..a0fbe654f 100644 --- a/src/core/course/components/format/format.html +++ b/src/core/course/components/format/format.html @@ -12,7 +12,7 @@
- + {{section.formattedName || section.name}} diff --git a/src/core/courses/pages/my-overview/my-overview.html b/src/core/courses/pages/my-overview/my-overview.html index 76123c08d..2813939ce 100644 --- a/src/core/courses/pages/my-overview/my-overview.html +++ b/src/core/courses/pages/my-overview/my-overview.html @@ -30,7 +30,7 @@
- + {{ 'core.courses.sortbydates' | translate }} {{ 'core.courses.sortbycourses' | translate }} @@ -65,7 +65,7 @@
- + {{ 'core.courses.inprogress' | translate }} {{ 'core.courses.future' | translate }} {{ 'core.courses.past' | translate }} diff --git a/src/core/login/pages/email-signup/email-signup.html b/src/core/login/pages/email-signup/email-signup.html index e284fd771..e90bddbb5 100644 --- a/src/core/login/pages/email-signup/email-signup.html +++ b/src/core/login/pages/email-signup/email-signup.html @@ -69,7 +69,7 @@ {{ 'core.user.country' | translate }} - + {{ 'core.login.selectacountry' | translate }} {{countries[key]}} diff --git a/src/core/login/pages/site/site.html b/src/core/login/pages/site/site.html index 29dcac83f..a23e975ff 100644 --- a/src/core/login/pages/site/site.html +++ b/src/core/login/pages/site/site.html @@ -29,7 +29,7 @@ {{ 'core.login.selectsite' | translate }} - + {{site.name}}