From c3372e8076caab9f24b1dc48bb323e9281f72c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 7 Dec 2020 15:31:39 +0100 Subject: [PATCH] MOBILE-3608 blocks: Add sitehome and dashboard blocks --- src/core/features/courses/courses.module.ts | 2 +- .../courses/pages/dashboard/dashboard.html | 19 ++- .../pages/dashboard/dashboard.module.ts | 2 + .../courses/pages/dashboard/dashboard.ts | 90 ++++++++++- .../features/courses/services/dashboard.ts | 140 ++++++++++++++++++ .../services/handlers/dashboard-home.ts | 8 +- ...{my-courses.home.ts => my-courses-home.ts} | 9 +- .../features/sitehome/pages/index/index.ts | 13 +- 8 files changed, 264 insertions(+), 19 deletions(-) create mode 100644 src/core/features/courses/services/dashboard.ts rename src/core/features/courses/services/handlers/{my-courses.home.ts => my-courses-home.ts} (84%) diff --git a/src/core/features/courses/courses.module.ts b/src/core/features/courses/courses.module.ts index 3f5014a11..e47bfb9f7 100644 --- a/src/core/features/courses/courses.module.ts +++ b/src/core/features/courses/courses.module.ts @@ -19,7 +19,7 @@ import { CoreMainMenuHomeRoutingModule } from '@features/mainmenu/pages/home/hom import { CoreMainMenuHomeDelegate } from '@features/mainmenu/services/home-delegate'; import { CoreDashboardHomeHandler, CoreDashboardHomeHandlerService } from './services/handlers/dashboard-home'; -import { CoreCoursesMyCoursesHomeHandler, CoreCoursesMyCoursesHomeHandlerService } from './services/handlers/my-courses.home'; +import { CoreCoursesMyCoursesHomeHandler, CoreCoursesMyCoursesHomeHandlerService } from './services/handlers/my-courses-home'; const mainMenuHomeChildrenRoutes: Routes = [ { diff --git a/src/core/features/courses/pages/dashboard/dashboard.html b/src/core/features/courses/pages/dashboard/dashboard.html index 53ab4c47d..5e0488613 100644 --- a/src/core/features/courses/pages/dashboard/dashboard.html +++ b/src/core/features/courses/pages/dashboard/dashboard.html @@ -12,8 +12,19 @@ - - -
Dashboard
-
+ + + + + + + + + + + + + +
diff --git a/src/core/features/courses/pages/dashboard/dashboard.module.ts b/src/core/features/courses/pages/dashboard/dashboard.module.ts index 5ba48f8f2..40924f020 100644 --- a/src/core/features/courses/pages/dashboard/dashboard.module.ts +++ b/src/core/features/courses/pages/dashboard/dashboard.module.ts @@ -20,6 +20,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreBlockComponentsModule } from '@features/block/components/components.module'; import { CoreCoursesDashboardPage } from './dashboard'; @@ -38,6 +39,7 @@ const routes: Routes = [ TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, + CoreBlockComponentsModule, ], declarations: [ CoreCoursesDashboardPage, diff --git a/src/core/features/courses/pages/dashboard/dashboard.ts b/src/core/features/courses/pages/dashboard/dashboard.ts index 997f5e5ac..cc93164a6 100644 --- a/src/core/features/courses/pages/dashboard/dashboard.ts +++ b/src/core/features/courses/pages/dashboard/dashboard.ts @@ -12,12 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { NavController } from '@ionic/angular'; +import { Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core'; +import { IonRefresher, NavController } from '@ionic/angular'; import { CoreCourses, CoreCoursesProvider } from '../../services/courses'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; +import { CoreCoursesDashboard } from '@features/courses/services/dashboard'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { CoreBlockComponent } from '@features/block/components/block/block'; /** * Page that displays the dashboard page. @@ -29,16 +33,20 @@ import { CoreSites } from '@services/sites'; }) export class CoreCoursesDashboardPage implements OnInit, OnDestroy { + @ViewChildren(CoreBlockComponent) blocksComponents?: QueryList; + + searchEnabled = false; downloadEnabled = false; downloadCourseEnabled = false; downloadCoursesEnabled = false; downloadEnabledIcon = 'far-square'; + userId?: number; + blocks: Partial[] = []; + loaded = false; protected updateSiteObserver?: CoreEventObserver; - siteName = 'Hello world'; - constructor( protected navCtrl: NavController, ) { } @@ -59,8 +67,82 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { this.switchDownload(this.downloadEnabled && this.downloadCourseEnabled && this.downloadCoursesEnabled); }, CoreSites.instance.getCurrentSiteId()); + + this.loadContent(); } + /** + * Convenience function to fetch the dashboard data. + * + * @return Promise resolved when done. + */ + protected async loadContent(): Promise { + const available = await CoreCoursesDashboard.instance.isAvailable(); + + if (available) { + this.userId = CoreSites.instance.getCurrentSiteUserId(); + + try { + this.blocks = await CoreCoursesDashboard.instance.getDashboardBlocks(); + } catch (error) { + CoreDomUtils.instance.showErrorModal(error); + + // Cannot get the blocks, just show dashboard if needed. + this.loadFallbackBlocks(); + } + } else if (!CoreCoursesDashboard.instance.isDisabledInSite()) { + // Not available, but not disabled either. Use fallback. + this.loadFallbackBlocks(); + } else { + // Disabled. + this.blocks = []; + } + + // this.dashboardEnabled = this.blockDelegate.hasSupportedBlock(this.blocks); + this.loaded = true; + } + + /** + * Load fallback blocks to shown before 3.6 when dashboard blocks are not supported. + */ + protected loadFallbackBlocks(): void { + this.blocks = [ + { + name: 'myoverview', + visible: true, + }, + { + name: 'timeline', + visible: true, + }, + ]; + } + + /** + * Refresh the dashboard data. + * + * @param refresher Refresher. + */ + refreshDashboard(refresher: CustomEvent): void { + const promises: Promise[] = []; + + promises.push(CoreCoursesDashboard.instance.invalidateDashboardBlocks()); + + // Invalidate the blocks. + this.blocksComponents?.forEach((blockComponent) => { + promises.push(blockComponent.invalidate().catch(() => { + // Ignore errors. + })); + }); + + Promise.all(promises).finally(() => { + this.loadContent().finally(() => { + refresher?.detail.complete(); + }); + }); + } + + /** * Toggle download enabled. */ diff --git a/src/core/features/courses/services/dashboard.ts b/src/core/features/courses/services/dashboard.ts new file mode 100644 index 000000000..fbf19c3d7 --- /dev/null +++ b/src/core/features/courses/services/dashboard.ts @@ -0,0 +1,140 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreSites } from '@services/sites'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { CoreStatusWithWarningsWSResponse } from '@services/ws'; +import { makeSingleton } from '@singletons'; + +const ROOT_CACHE_KEY = 'CoreCoursesDashboard:'; + +/** + * Service that provides some features regarding course overview. + */ +@Injectable({ providedIn: 'root' }) +export class CoreCoursesDashboardProvider { + + /** + * Get cache key for dashboard blocks WS calls. + * + * @param userId User ID. Default, 0 means current user. + * @return Cache key. + */ + protected getDashboardBlocksCacheKey(userId: number = 0): string { + return ROOT_CACHE_KEY + 'blocks:' + userId; + } + + /** + * Get dashboard blocks. + * + * @param userId User ID. Default, current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the list of blocks. + * @since 3.6 + */ + async getDashboardBlocks(userId?: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + const params: CoreBlockGetDashboardBlocksWSParams = { + returncontents: true, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getDashboardBlocksCacheKey(userId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; + if (userId) { + params.userid = userId; + } + const result = await site.read('core_block_get_dashboard_blocks', params, preSets); + + return result.blocks || []; + } + + /** + * Invalidates dashboard blocks WS call. + * + * @param userId User ID. Default, current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateDashboardBlocks(userId?: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return await site.invalidateWsCacheForKey(this.getDashboardBlocksCacheKey(userId)); + } + + /** + * Returns whether or not block based Dashboard is available for a certain site. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with true if available, resolved with false or rejected otherwise. + * @since 3.6 + */ + async isAvailable(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + // First check if it's disabled. + if (this.isDisabledInSite(site)) { + return false; + } + + return site.wsAvailable('core_block_get_dashboard_blocks'); + } + + /** + * Check if Site Home is disabled in a certain site. + * + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + async isDisabled(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return this.isDisabledInSite(site); + } + + /** + * Check if Site Home is disabled in a certain site. + * + * @param site Site. If not defined, use current site. + * @return Whether it's disabled. + */ + isDisabledInSite(site?: CoreSite): boolean { + site = site || CoreSites.instance.getCurrentSite(); + + return !!site?.isFeatureDisabled('CoreMainMenuDelegate_CoreCoursesDashboard'); + } + +} + +export class CoreCoursesDashboard extends makeSingleton(CoreCoursesDashboardProvider) {} + + +/** + * Params of core_block_get_dashboard_blocks WS. + */ +type CoreBlockGetDashboardBlocksWSParams = { + userid?: number; // User id (optional), default is current user. + returncontents?: boolean; // Whether to return the block contents. +}; + +/** + * Data returned by core_block_get_dashboard_blocks WS. + */ +type CoreBlockGetDashboardBlocksWSResponse = { + blocks: CoreCourseBlock[]; // List of blocks in the course. + warnings?: CoreStatusWithWarningsWSResponse[]; +}; diff --git a/src/core/features/courses/services/handlers/dashboard-home.ts b/src/core/features/courses/services/handlers/dashboard-home.ts index 0f2ce7e4e..e3a3edb1c 100644 --- a/src/core/features/courses/services/handlers/dashboard-home.ts +++ b/src/core/features/courses/services/handlers/dashboard-home.ts @@ -13,8 +13,10 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; import { CoreMainMenuHomeHandler, CoreMainMenuHomeHandlerToDisplay } from '@features/mainmenu/services/home-delegate'; import { makeSingleton } from '@singletons'; +import { CoreCoursesDashboard } from '../dashboard'; /** * Handler to add dashboard into home page. @@ -42,10 +44,10 @@ export class CoreDashboardHomeHandlerService implements CoreMainMenuHomeHandler * @param siteId Site ID. If not defined, current site. * @return Whether or not the handler is enabled on a site level. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars async isEnabledForSite(siteId?: string): Promise { - // @todo return this.blockDelegate.hasSupportedBlock(this.blocks); - return true; + const blocks = await CoreCoursesDashboard.instance.getDashboardBlocks(undefined, siteId); + + return CoreBlockDelegate.instance.hasSupportedBlock(blocks); } /** diff --git a/src/core/features/courses/services/handlers/my-courses.home.ts b/src/core/features/courses/services/handlers/my-courses-home.ts similarity index 84% rename from src/core/features/courses/services/handlers/my-courses.home.ts rename to src/core/features/courses/services/handlers/my-courses-home.ts index 49477e125..43796c803 100644 --- a/src/core/features/courses/services/handlers/my-courses.home.ts +++ b/src/core/features/courses/services/handlers/my-courses-home.ts @@ -13,8 +13,11 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; import { CoreMainMenuHomeHandler, CoreMainMenuHomeHandlerToDisplay } from '@features/mainmenu/services/home-delegate'; +import { CoreSiteHome } from '@features/sitehome/services/sitehome'; import { makeSingleton } from '@singletons'; +import { CoreCoursesDashboard } from '../dashboard'; /** * Handler to add my courses into home page. @@ -42,10 +45,10 @@ export class CoreCoursesMyCoursesHomeHandlerService implements CoreMainMenuHomeH * @param siteId Site ID. If not defined, current site. * @return Whether or not the handler is enabled on a site level. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars async isEnabledForSite(siteId?: string): Promise { - // @todo return !this.blockDelegate.hasSupportedBlock(this.blocks) && !CoreSiteHome.instance.isAvailable(siteId); - return true; + const blocks = await CoreCoursesDashboard.instance.getDashboardBlocks(undefined, siteId); + + return !CoreBlockDelegate.instance.hasSupportedBlock(blocks)&& !CoreSiteHome.instance.isAvailable(siteId); } /** diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index bf62e8373..6ad4cc726 100644 --- a/src/core/features/sitehome/pages/index/index.ts +++ b/src/core/features/sitehome/pages/index/index.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { IonRefresher, NavController } from '@ionic/angular'; @@ -24,6 +24,7 @@ import { CoreSiteHome } from '@features/sitehome/services/sitehome'; import { CoreCourses, CoreCoursesProvider } from '@features//courses/services/courses'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks'; /** * Page that displays site home index. @@ -34,7 +35,7 @@ import { CoreCourseHelper } from '@features/course/services/course-helper'; }) export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { - // @todo @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent: CoreBlockCourseBlocksComponent; + @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent?: CoreBlockCourseBlocksComponent; dataLoaded = false; section?: CoreCourseSection & { @@ -158,13 +159,17 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { // @todo promises.push(this.prefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId)); } - // @todo promises.push(this.courseBlocksComponent.invalidateBlocks()); + if (this.courseBlocksComponent) { + promises.push(this.courseBlocksComponent.invalidateBlocks()); + } Promise.all(promises).finally(async () => { const p2: Promise[] = []; p2.push(this.loadContent()); - // @todo p2.push(this.courseBlocksComponent.loadContent()); + if (this.courseBlocksComponent) { + p2.push(this.courseBlocksComponent.loadContent()); + } await Promise.all(p2).finally(() => { refresher?.detail.complete();