MOBILE-3594 sitehome: Add sitehome tab to home page

main
Pau Ferrer Ocaña 2020-11-23 10:22:47 +01:00
parent cc5378aee7
commit 9b41458104
11 changed files with 687 additions and 4 deletions

View File

@ -20,6 +20,7 @@ import { CoreEmulatorModule } from './emulator/emulator.module';
import { CoreFileUploaderInitModule } from './fileuploader/fileuploader-init.module';
import { CoreLoginModule } from './login/login.module';
import { CoreSettingsInitModule } from './settings/settings-init.module';
import { CoreSiteHomeInitModule } from './sitehome/sitehome-init.module';
@NgModule({
imports: [
@ -29,6 +30,7 @@ import { CoreSettingsInitModule } from './settings/settings-init.module';
CoreCoursesModule,
CoreSettingsInitModule,
CoreFileUploaderInitModule,
CoreSiteHomeInitModule,
],
})
export class CoreFeaturesModule {}

View File

@ -17,8 +17,8 @@
</ion-button>
</ion-item>
<ion-list class="core-search-history" [hidden]="!historyShown">
<ion-item class="ion-text-wrap" *ngFor="let item of history"
(click)="historyClicked($event, item.searchedtext)" class="core-clickable" tabindex="1">
<ion-item button class="ion-text-wrap" *ngFor="let item of history"
(click)="historyClicked($event, item.searchedtext)" tabindex="1" detail>
<ion-icon name="fa-history" slot="start">
</ion-icon>
{{item.searchedtext}}

View File

@ -28,7 +28,7 @@
<ion-item *ngIf="isIOS"
(click)="openHandler('CoreSharedFilesListPage', {manage: true, siteId: siteId, hideSitePicker: true})"
[title]="'core.sharedfiles.sharedfiles' | translate"
[class.core-split-item-selected]="'CoreSharedFilesListPage' == selectedPage" details>
[class.core-split-item-selected]="'CoreSharedFilesListPage' == selectedPage" detail>
<ion-icon name="fas-folder" slot="start"></ion-icon>
<ion-label>
<h2>{{ 'core.sharedfiles.sharedfiles' | translate }}</h2>
@ -37,7 +37,7 @@
</ion-item>
<ion-item *ngFor="let handler of handlers" [ngClass]="['core-settings-handler', handler.class]"
(click)="openHandler(handler.page, handler.params)" [title]="handler.title | translate" details
(click)="openHandler(handler.page, handler.params)" [title]="handler.title | translate" detail
[class.core-split-item-selected]="handler.page == selectedPage">
<ion-icon [name]="handler.icon" slot="start" *ngIf="handler.icon">
</ion-icon>

View File

@ -0,0 +1,4 @@
{
"sitehome": "Site home",
"sitenews": "Site announcements"
}

View File

@ -0,0 +1,85 @@
<ion-content>
<ion-refresher slot="fixed" [disabled]="!dataLoaded"
(ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<!-- @todo <core-block-course-blocks [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled">-->
<core-loading [hideUntil]="dataLoaded">
<ion-list>
<!-- Site home main contents. -->
<!-- @todo <ng-container *ngIf="section && section.hasContent">
<ion-item class="ion-text-wrap" *ngIf="section.summary">
<core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="siteHomeId"></core-format-text>
</ion-item>
<core-course-module *ngFor="let module of section.modules" [module]="module" [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled" [section]="section"></core-course-module>
</ng-container> -->
<!-- Site home items: news, categories, courses, etc. -->
<ng-container *ngIf="items.length > 0">
<ion-item-divider *ngIf="section && section!.hasContent"></ion-item-divider>
<ng-container *ngFor="let item of items">
<ng-container [ngSwitch]="item">
<ng-container *ngSwitchCase="'LIST_OF_COURSE'">
<ng-template *ngTemplateOutlet="allCourseList"></ng-template>
</ng-container>
<ng-container *ngSwitchCase="'LIST_OF_CATEGORIES'">
<ng-template *ngTemplateOutlet="categories"></ng-template>
</ng-container>
<ng-container *ngSwitchCase="'COURSE_SEARCH_BOX'">
<ng-template *ngTemplateOutlet="courseSearch"></ng-template>
</ng-container>
<ng-container *ngSwitchCase="'ENROLLED_COURSES'">
<ng-template *ngTemplateOutlet="enrolledCourseList"></ng-template>
</ng-container>
<ng-container *ngSwitchCase="'NEWS_ITEMS'">
<ng-template *ngTemplateOutlet="news"></ng-template>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
</ion-list>
<core-empty-box *ngIf="!hasContent" icon="qr-scanner" [message]="'core.course.nocontentavailable' | translate"></core-empty-box>
</core-loading>
<!-- @todo </core-block-course-blocks> -->
</ion-content>
<ng-template #allCourseList>
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/courses/all" detail>
<ion-icon name="fas-graduation-cap" fixed-width slot="start"></ion-icon>
<ion-label>
<h2>{{ 'core.courses.availablecourses' | translate}}</h2>
</ion-label>
</ion-item>
</ng-template>
<ng-template #news>
<ion-item>
<ion-label>News (TODO)</ion-label>
</ion-item>
<!-- @todo <core-course-module class="core-sitehome-news" *ngIf="show" [module]="module" [courseId]="siteHomeId"></core-course-module> -->
</ng-template>
<ng-template #categories>
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/courses/categories" detail>
<ion-icon name="folder" slot="start"></ion-icon>
<ion-label>
<h2>{{ 'core.courses.categories' | translate}}</h2>
</ion-label>
</ion-item>
</ng-template>
<ng-template #enrolledCourseList>
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/courses/my" detail>
<ion-icon name="fa-graduation-cap" fixed-width slot="start">
</ion-icon>
<ion-label><h2>{{ 'core.courses.mycourses' | translate}}</h2></ion-label>
</ion-item>
</ng-template>
<ng-template #courseSearch>
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/courses/search" detail>
<ion-icon name="search" slot="start"></ion-icon>
<ion-label><h2>{{ 'core.courses.searchcourses' | translate}}</h2></ion-label>
</ion-item>
</ng-template>

View File

@ -0,0 +1,47 @@
// (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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreComponentsModule } from '@components/components.module';
import { CoreSiteHomeIndexPage } from './index.page';
const routes: Routes = [
{
path: '',
component: CoreSiteHomeIndexPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreDirectivesModule,
CoreComponentsModule,
],
declarations: [
CoreSiteHomeIndexPage,
],
exports: [RouterModule],
})
export class CoreSiteHomeIndexPageModule {}

View File

@ -0,0 +1,155 @@
// (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 { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { IonRefresher } from '@ionic/angular';
import { CoreSite, CoreSiteConfig } from '@classes/site';
import { CoreCourse, CoreCourseSection } from '@features/course/services/course';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreSites } from '@services/sites';
import { CoreSiteHome } from '@features/sitehome/services/sitehome';
// import { CoreCourseHelperProvider } from '@features/course/services/helper';
/**
* Page that displays site home index.
*/
@Component({
selector: 'page-core-sitehome-index',
templateUrl: 'index.html',
})
export class CoreSiteHomeIndexPage implements OnInit {
// @todo @Input() downloadEnabled: boolean;
// @todo @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent: CoreBlockCourseBlocksComponent;
dataLoaded = false;
section?: CoreCourseSection & {
hasContent?: boolean;
};
hasContent = false;
items: string[] = [];
siteHomeId?: number;
currentSite?: CoreSite;
constructor(
protected route: ActivatedRoute,
// @todo private prefetchDelegate: CoreCourseModulePrefetchDelegate,
) {
}
/**
* Page being initialized.
*/
ngOnInit(): void {
const navParams = this.route.snapshot.queryParams;
this.currentSite = CoreSites.instance.getCurrentSite()!;
this.siteHomeId = this.currentSite.getSiteHomeId();
const module = navParams['module'];
if (module) {
// @todo const modParams = navParams.get('modParams');
// courseHelper.openModule(module, this.siteHomeId, undefined, modParams);
}
this.loadContent().finally(() => {
this.dataLoaded = true;
});
}
/**
* Convenience function to fetch the data.
*
* @return Promise resolved when done.
*/
protected async loadContent(): Promise<void> {
this.hasContent = false;
const config = this.currentSite!.getStoredConfig() || { numsections: 1, frontpageloggedin: undefined };
this.items = await CoreSiteHome.instance.getFrontPageItems(config.frontpageloggedin);
this.hasContent = this.items.length > 0;
try {
const sections = await CoreCourse.instance.getSections(this.siteHomeId!, false, true);
// Check "Include a topic section" setting from numsections.
this.section = config.numsections ? sections.find((section) => section.section == 1) : undefined;
if (this.section) {
this.section.hasContent = false;
/* @todo this.section.hasContent = this.courseHelper.sectionHasContent(this.section);
this.hasContent = this.courseHelper.addHandlerDataForModules(
[this.section],
this.siteHomeId,
undefined,
undefined,
true,
) || this.hasContent;*/
}
// Add log in Moodle.
CoreCourse.instance.logView(
this.siteHomeId!,
undefined,
undefined,
this.currentSite!.getInfo()?.sitename,
).catch(() => {
// Ignore errors.
});
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true);
}
}
/**
* Refresh the data.
*
* @param refresher Refresher.
*/
doRefresh(refresher?: CustomEvent<IonRefresher>): void {
const promises: Promise<unknown>[] = [];
promises.push(CoreCourse.instance.invalidateSections(this.siteHomeId!));
promises.push(this.currentSite!.invalidateConfig().then(async () => {
// Config invalidated, fetch it again.
const config: CoreSiteConfig = await this.currentSite!.getConfig();
this.currentSite!.setConfig(config);
return;
}));
if (this.section && this.section.modules) {
// Invalidate modules prefetch data.
// @todo promises.push(this.prefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId));
}
// @todo promises.push(this.courseBlocksComponent.invalidateBlocks());
Promise.all(promises).finally(async () => {
const p2: Promise<unknown>[] = [];
p2.push(this.loadContent());
// @todo p2.push(this.courseBlocksComponent.loadContent());
await Promise.all(p2).finally(() => {
refresher?.detail.complete();
});
});
}
}

View File

@ -0,0 +1,75 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Params } from '@angular/router';
import { CoreSites } from '@services/sites';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks.helper';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks.delegate';
import { CoreSiteHome } from '../sitehome';
/**
* Handler to treat links to site home index.
*/
Injectable();
export class CoreSiteHomeIndexLinkHandler extends CoreContentLinksHandlerBase {
name = 'CoreSiteHomeIndexLinkHandler';
featureName = 'CoreMainMenuDelegate_CoreSiteHome';
pattern = /\/course\/view\.php.*([?&]id=\d+)/;
/**
* Get the list of actions for a link (url).
*
* @param siteIds List of sites the URL belongs to.
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param courseId Course ID related to the URL. Optional but recommended.
* @return List of (or promise resolved with list of) actions.
*/
getActions(): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: (siteId: string): void => {
CoreContentLinksHelper.instance.goInSite('sitehome', [], siteId);
},
}];
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param siteId The site ID.
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param courseId Course ID related to the URL. Optional but recommended.
* @return Whether the handler is enabled for the URL and site.
*/
async isEnabled(siteId: string, url: string, params: Params, courseId?: number): Promise<boolean> {
courseId = parseInt(params.id, 10);
if (!courseId) {
return false;
}
const site = await CoreSites.instance.getSite(siteId);
if (courseId != site.getSiteHomeId()) {
// The course is not site home.
return false;
}
return CoreSiteHome.instance.isAvailable(siteId).then(() => true).catch(() => false);
}
}

View File

@ -0,0 +1,66 @@
// (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 { CoreHomeHandler, CoreHomeHandlerToDisplay } from '@features/mainmenu/services/home.delegate';
import { CoreSiteHome } from '../sitehome';
/**
* Handler to add Home into main menu.
*/
Injectable();
export class CoreSiteHomeHomeHandler implements CoreHomeHandler {
name = 'CoreSiteHomeDashboard';
priority = 1200;
/**
* Check if the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): Promise<boolean> {
return this.isEnabledForSite();
}
/**
* Check if the handler is enabled on a certain site.
*
* @param siteId Site ID. If not defined, current site.
* @return Whether or not the handler is enabled on a site level.
*/
async isEnabledForSite(siteId?: string): Promise<boolean> {
return CoreSiteHome.instance.isAvailable(siteId);
}
/**
* Returns the data needed to render the handler.
*
* @return Data needed to render the handler.
*/
getDisplayData(): CoreHomeHandlerToDisplay {
const site = CoreSites.instance.getCurrentSite();
const displaySiteHome = site?.getInfo() && site?.getInfo()?.userhomepage === 0;
return {
title: 'core.sitehome.sitehome',
page: 'sitehome',
class: 'core-sitehome-dashboard-handler',
icon: 'fas-home',
selectPriority: displaySiteHome ? 1100 : 900,
};
}
}

View File

@ -0,0 +1,196 @@
// (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 { makeSingleton } from '@singletons/core.singletons';
import { CoreCourse, CoreCourseSection } from '../../course/services/course';
import { CoreCourses } from '../../courses/services/courses';
/**
* Items with index 1 and 3 were removed on 2.5 and not being supported in the app.
*/
export enum FrontPageItemNames {
NEWS_ITEMS = 0,
LIST_OF_CATEGORIES = 2,
COMBO_LIST = 3,
ENROLLED_COURSES = 5,
LIST_OF_COURSE = 6,
COURSE_SEARCH_BOX = 7,
}
/**
* Service that provides some features regarding site home.
*/
@Injectable({
providedIn: 'root',
})
export class CoreSiteHomeProvider {
/**
* Get the news forum for the Site Home.
*
* @param siteHomeId Site Home ID.
* @return Promise resolved with the forum if found, rejected otherwise.
*/
getNewsForum(): void {
// @todo params and logic.
}
/**
* Invalidate the WS call to get the news forum for the Site Home.
*
* @param siteHomeId Site Home ID.
* @return Promise resolved when invalidated.
*/
invalidateNewsForum(): void {
// @todo params and logic.
}
/**
* Returns whether or not the frontpage is available for the current site.
*
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with boolean: whether it's available.
*/
async isAvailable(siteId?: string): Promise<boolean> {
try {
const site = await CoreSites.instance.getSite(siteId);
// 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();
const preSets: CoreSiteWSPreSets = { emergencyCache: false };
try {
const sections: CoreCourseSection[] =
await CoreCourse.instance.getSections(siteHomeId, false, true, preSets, site.id);
if (!sections || !sections.length) {
throw Error('No sections found');
}
const hasContent = sections.some((section) => section.summary || (section.modules && section.modules.length));
if (hasContent) {
// There's a section with content.
return true;
}
} catch {
// Ignore errors.
}
const config = site.getStoredConfig();
if (config && config.frontpageloggedin) {
const items = await this.getFrontPageItems(config.frontpageloggedin);
// There are items to show.
return items.length > 0;
}
} catch {
// Ignore errors.
}
return false;
}
/**
* 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<boolean> {
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_CoreSiteHome');
}
/**
* Get the nams of the valid frontpage items.
*
* @param frontpageItemIds CSV string with indexes of site home components.
* @return Valid names for each item.
*/
async getFrontPageItems(frontpageItemIds?: string): Promise<string[]> {
if (!frontpageItemIds) {
return [];
}
const items = frontpageItemIds.split(',');
const filteredItems: string[] = [];
for (const item of items) {
let itemNumber = parseInt(item, 10);
let add = false;
switch (itemNumber) {
case FrontPageItemNames['NEWS_ITEMS']:
// @todo
add = true;
break;
case FrontPageItemNames['LIST_OF_CATEGORIES']:
case FrontPageItemNames['COMBO_LIST']:
case FrontPageItemNames['LIST_OF_COURSE']:
add = CoreCourses.instance.isGetCoursesByFieldAvailable();
if (add && itemNumber == FrontPageItemNames['COMBO_LIST']) {
itemNumber = FrontPageItemNames['LIST_OF_CATEGORIES'];
}
break;
case FrontPageItemNames['ENROLLED_COURSES']:
if (!CoreCourses.instance.isMyCoursesDisabledInSite()) {
const courses = await CoreCourses.instance.getUserCourses();
add = courses.length > 0;
}
break;
case FrontPageItemNames['COURSE_SEARCH_BOX']:
add = !CoreCourses.instance.isSearchCoursesDisabledInSite();
break;
default:
break;
}
// Do not add an item twice.
if (add && filteredItems.indexOf(FrontPageItemNames[itemNumber]) < 0) {
filteredItems.push(FrontPageItemNames[itemNumber]);
}
}
return filteredItems;
}
}
export class CoreSiteHome extends makeSingleton(CoreSiteHomeProvider) {}

View File

@ -0,0 +1,53 @@
// (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 { NgModule } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreSiteHomeIndexLinkHandler } from './services/handlers/index.link';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks.delegate';
import { CoreSiteHomeHomeHandler } from './services/handlers/sitehome.home';
import { CoreHomeDelegate } from '@features/mainmenu/services/home.delegate';
import { CoreHomeRoutingModule } from '@features/mainmenu/pages/home/home-routing.module';
const routes: Routes = [
{
path: 'sitehome',
loadChildren: () =>
import('@features/sitehome/pages/index/index.page.module').then(m => m.CoreSiteHomeIndexPageModule),
},
];
@NgModule({
imports: [CoreHomeRoutingModule.forChild(routes)],
exports: [CoreHomeRoutingModule],
providers: [
CoreSiteHomeIndexLinkHandler,
CoreSiteHomeHomeHandler,
],
})
export class CoreSiteHomeInitModule {
constructor(
contentLinksDelegate: CoreContentLinksDelegate,
homeDelegate: CoreHomeDelegate,
siteHomeIndexLinkHandler: CoreSiteHomeIndexLinkHandler,
siteHomeDashboardHandler: CoreSiteHomeHomeHandler,
) {
contentLinksDelegate.registerHandler(siteHomeIndexLinkHandler);
homeDelegate.registerHandler(siteHomeDashboardHandler);
}
}