MOBILE-3914 course: Add side blocks component and replace it

main
Pau Ferrer Ocaña 2021-11-12 16:32:54 +01:00
parent 80f1b96f61
commit 8cd89ead85
17 changed files with 328 additions and 313 deletions

View File

@ -1399,6 +1399,8 @@
"core.areyousure": "moodle", "core.areyousure": "moodle",
"core.back": "moodle", "core.back": "moodle",
"core.block.blocks": "moodle", "core.block.blocks": "moodle",
"core.block.noblocks": "error",
"core.block.opendrawerblocks": "moodle",
"core.browser": "local_moodlemobileapp", "core.browser": "local_moodlemobileapp",
"core.cancel": "moodle", "core.cancel": "moodle",
"core.cannotconnect": "local_moodlemobileapp", "core.cannotconnect": "local_moodlemobileapp",

View File

@ -72,7 +72,3 @@
height: auto; height: auto;
} }
} }
:host-context(core-block-course-blocks) .core-empty-box {
position: relative;
}

View File

@ -16,15 +16,17 @@ import { NgModule } from '@angular/core';
import { CoreBlockComponent } from './block/block'; import { CoreBlockComponent } from './block/block';
import { CoreBlockOnlyTitleComponent } from './only-title-block/only-title-block'; import { CoreBlockOnlyTitleComponent } from './only-title-block/only-title-block';
import { CoreBlockPreRenderedComponent } from './pre-rendered-block/pre-rendered-block'; import { CoreBlockPreRenderedComponent } from './pre-rendered-block/pre-rendered-block';
import { CoreBlockCourseBlocksComponent } from './course-blocks/course-blocks';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreBlockSideBlocksComponent } from './side-blocks/side-blocks';
import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-blocks-button';
@NgModule({ @NgModule({
declarations: [ declarations: [
CoreBlockComponent, CoreBlockComponent,
CoreBlockOnlyTitleComponent, CoreBlockOnlyTitleComponent,
CoreBlockPreRenderedComponent, CoreBlockPreRenderedComponent,
CoreBlockCourseBlocksComponent, CoreBlockSideBlocksComponent,
CoreBlockSideBlocksButtonComponent,
], ],
imports: [ imports: [
CoreSharedModule, CoreSharedModule,
@ -33,7 +35,8 @@ import { CoreSharedModule } from '@/core/shared.module';
CoreBlockComponent, CoreBlockComponent,
CoreBlockOnlyTitleComponent, CoreBlockOnlyTitleComponent,
CoreBlockPreRenderedComponent, CoreBlockPreRenderedComponent,
CoreBlockCourseBlocksComponent, CoreBlockSideBlocksComponent,
CoreBlockSideBlocksButtonComponent,
], ],
}) })
export class CoreBlockComponentsModule {} export class CoreBlockComponentsModule {}

View File

@ -1,15 +0,0 @@
<div class="core-course-blocks-content">
<ng-content></ng-content>
</div>
<div *ngIf="blocks && blocks.length > 0 && !hideBlocks" [class.core-hide-blocks]="hideBottomBlocks" class="core-course-blocks-side">
<core-loading [hideUntil]="dataLoaded" [fullscreen]="false">
<ion-list>
<!-- Course expand="block"s. -->
<ng-container *ngFor="let block of blocks">
<core-block *ngIf="block.visible" [block]="block" contextLevel="course" [instanceId]="courseId"
[extraData]="{'downloadEnabled': downloadEnabled}"></core-block>
</ng-container>
</ion-list>
</core-loading>
</div>

View File

@ -1,61 +0,0 @@
:host {
--side-blocks-box-shadow: var(--core-menu-box-shadow-start);
&.core-no-blocks .core-course-blocks-content {
height: auto;
}
&.core-has-blocks {
@media (min-width: 768px) {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
.core-course-blocks-content {
box-shadow: none !important;
flex-grow: 1;
max-width: 100%;
--ion-safe-area-right: 0px;
}
div.core-course-blocks-side {
max-width: var(--side-blocks-max-width);
min-width: var(--side-blocks-min-width);
box-shadow: var(--side-blocks-box-shadow);
z-index: 2;
}
.core-course-blocks-content,
div.core-course-blocks-side {
position: relative;
height: 100%;
.core-loading-center,
core-loading.core-loading-loaded {
position: initial;
}
}
}
@media (max-width: 767.98px) {
// Disable scroll on individual columns.
div.core-course-blocks-side {
height: auto;
&.core-hide-blocks {
display: none;
}
}
}
}
}
:host-context([dir="rtl"]).core-has-blocks {
@media (min-width: 768px) {
div.core-course-blocks-side {
box-shadow: var(--side-blocks-box-shadow);
}
}
}

View File

@ -0,0 +1,3 @@
<ion-button (click)="openBlocks()" [attr.aria-label]="'core.block.opendrawerblocks' | translate">
<ion-icon name="fas-cubes" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>

View File

@ -0,0 +1,29 @@
@import "~theme/globals";
:host {
@include position(50%, 0px, null, null);
position: fixed;
z-index: 10;
ion-button {
margin: 0;
--padding-start: 0.5em;
--padding-end: 0;
--border-radius: 2em 0 0 2em;
&::part(native) {
@include core-transition(padding, 200ms);
}
&:hover {
--padding-end: 1.2em;
--padding-start: 1em;
}
}
}
:host-context([dir=rtl]) {
ion-button {
--border-radius: 0 2em 2em 0;
}
}

View File

@ -0,0 +1,45 @@
// (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, Input } from '@angular/core';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreBlockSideBlocksComponent } from '../side-blocks/side-blocks';
/**
* Component that displays a button to open blocks.
*/
@Component({
selector: 'core-block-side-blocks-button',
templateUrl: 'side-blocks-button.html',
styleUrls: ['side-blocks-button.scss'],
})
export class CoreBlockSideBlocksButtonComponent {
@Input() courseId!: number;
@Input() downloadEnabled = false;
/**
* Open side blocks.
*/
openBlocks(): void {
CoreDomUtils.openSideModal({
component: CoreBlockSideBlocksComponent,
componentProps: {
courseId: this.courseId,
downloadEnabled: this.downloadEnabled,
},
});
}
}

View File

@ -0,0 +1,26 @@
<ion-header>
<ion-toolbar>
<h1>{{ 'core.block.blocks' | translate }}</h1>
<ion-buttons slot="end">
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="fas-times" slot="icon-only" aria-hidden=true></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ion-list *ngIf="blocks.length > 0">
<ng-container *ngFor="let block of blocks">
<core-block *ngIf="block.visible" [block]="block" contextLevel="course" [instanceId]="courseId"
[extraData]="{'downloadEnabled': downloadEnabled}"></core-block>
</ng-container>
</ion-list>
<core-empty-box *ngIf="blocks.length == 0" icon="fas-cubes" [message]="'core.block.noblocks' | translate">
</core-empty-box>
</core-loading>
</ion-content>

View File

@ -12,50 +12,38 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, ViewChildren, Input, OnInit, QueryList, ElementRef } from '@angular/core'; import { Component, ViewChildren, Input, OnInit, QueryList } from '@angular/core';
import { IonContent } from '@ionic/angular'; import { ModalController } from '@singletons';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourse, CoreCourseBlock } from '@features/course/services/course'; import { CoreCourse, CoreCourseBlock } from '@features/course/services/course';
import { CoreBlockHelper } from '../../services/block-helper'; import { CoreBlockHelper } from '../../services/block-helper';
import { CoreBlockComponent } from '../block/block'; import { CoreBlockComponent } from '../block/block';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { IonRefresher } from '@ionic/angular';
/** /**
* Component that displays the list of course blocks. * Component that displays the list of side blocks.
*/ */
@Component({ @Component({
selector: 'core-block-course-blocks', selector: 'core-block-side-blocks',
templateUrl: 'core-block-course-blocks.html', templateUrl: 'side-blocks.html',
styleUrls: ['course-blocks.scss'],
}) })
export class CoreBlockCourseBlocksComponent implements OnInit { export class CoreBlockSideBlocksComponent implements OnInit {
@Input() courseId!: number; @Input() courseId!: number;
@Input() hideBlocks = false;
@Input() hideBottomBlocks = false;
@Input() downloadEnabled = false; @Input() downloadEnabled = false;
@ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>; @ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>;
dataLoaded = false; loaded = false;
blocks: CoreCourseBlock[] = []; blocks: CoreCourseBlock[] = [];
protected element: HTMLElement;
constructor(
element: ElementRef,
protected content: IonContent,
) {
this.element = element.nativeElement;
}
/** /**
* Component being initialized. * @inheritdoc
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.element.classList.add('core-no-blocks');
this.loadContent().finally(() => { this.loadContent().finally(() => {
this.dataLoaded = true; this.loaded = true;
}); });
} }
@ -87,7 +75,6 @@ export class CoreBlockCourseBlocksComponent implements OnInit {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async loadContent(): Promise<void> { async loadContent(): Promise<void> {
try { try {
this.blocks = await CoreBlockHelper.getCourseBlocks(this.courseId); this.blocks = await CoreBlockHelper.getCourseBlocks(this.courseId);
} catch (error) { } catch (error) {
@ -95,29 +82,26 @@ export class CoreBlockCourseBlocksComponent implements OnInit {
this.blocks = []; this.blocks = [];
} }
const scrollElement = await this.content.getScrollElement();
if (!this.hideBlocks && this.blocks.length > 0) {
this.element.classList.add('core-has-blocks');
this.element.classList.remove('core-no-blocks');
scrollElement.classList.add('core-course-block-with-blocks');
} else {
this.element.classList.remove('core-has-blocks');
this.element.classList.add('core-no-blocks');
scrollElement.classList.remove('core-course-block-with-blocks');
}
} }
/** /**
* Refresh data. * Refresh the data.
* *
* @return Promise resolved when done. * @param refresher Refresher.
*/ */
async doRefresh(): Promise<void> { async doRefresh(refresher?: IonRefresher): Promise<void> {
await CoreUtils.ignoreErrors(this.invalidateBlocks()); await CoreUtils.ignoreErrors(this.invalidateBlocks());
await this.loadContent(); await this.loadContent().finally(() => {
refresher?.complete();
});
}
/**
* Close modal.
*/
closeModal(): void {
ModalController.dismiss();
} }
} }

View File

@ -1,3 +1,5 @@
{ {
"blocks": "Blocks" "blocks": "Blocks",
"noblocks": "No blocks found!",
"opendrawerblocks": "Open block drawer"
} }

View File

@ -54,6 +54,22 @@ export class CoreBlockHelperProvider {
return blocks; return blocks;
} }
/**
* Returns if the course has any block.
*
* @param courseId Course ID.
* @return Wether course has blocks.
*/
async hasCourseBlocks(courseId: number): Promise<boolean> {
try {
const blocks = await this.getCourseBlocks(courseId);
return blocks.length > 0;
} catch {
return false;
}
}
} }
export const CoreBlockHelper = makeSingleton(CoreBlockHelperProvider); export const CoreBlockHelper = makeSingleton(CoreBlockHelperProvider);

View File

@ -6,11 +6,7 @@
</core-context-menu-item> </core-context-menu-item>
</core-context-menu> </core-context-menu>
</core-navbar-buttons> </core-navbar-buttons>
<core-dynamic-component [component]="courseFormatComponent" [data]="data">
<core-block-course-blocks *ngIf="loaded" [courseId]="course!.id" [hideBlocks]="!displayBlocks" [downloadEnabled]="downloadEnabled"
[hideBottomBlocks]="selectedSection && selectedSection.id == allSectionsId && canLoadMore">
<core-dynamic-component [component]="courseFormatComponent" [data]="data">
<!-- Default course format. --> <!-- Default course format. -->
<core-loading [hideUntil]="loaded"> <core-loading [hideUntil]="loaded">
<!-- Section selector. --> <!-- Section selector. -->
@ -19,15 +15,10 @@
<div *ngIf="displaySectionSelector && sections && hasSeveralSections" <div *ngIf="displaySectionSelector && sections && hasSeveralSections"
class="ion-text-wrap ion-justify-content-between ion-align-items-center core-button-selector-row" class="ion-text-wrap ion-justify-content-between ion-align-items-center core-button-selector-row"
[class.core-section-download]="downloadEnabled"> [class.core-section-download]="downloadEnabled">
<core-combobox <core-combobox [modalOptions]="sectionSelectorModalOptions" interface="modal" listboxId="core-course-section-button"
[modalOptions]="sectionSelectorModalOptions" icon="fas-folder" [label]="'core.course.section' | translate"
interface="modal"
listboxId="core-course-section-button"
icon="fas-folder"
[label]="'core.course.section' | translate"
[selection]="selectedSection ? selectedSection.name : 'core.course.sections' | translate" [selection]="selectedSection ? selectedSection.name : 'core.course.sections' | translate"
(onChange)="sectionChanged($event)" (onChange)="sectionChanged($event)">
>
<span slot="text"> <span slot="text">
<core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course" <core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course"
[contextInstanceId]="course?.id" [clean]="true" [singleLine]="true"> [contextInstanceId]="course?.id" [clean]="true" [singleLine]="true">
@ -42,16 +33,14 @@
<!-- Course summary. By default we only display the course progress. --> <!-- Course summary. By default we only display the course progress. -->
<core-dynamic-component [component]="courseSummaryComponent" [data]="data"> <core-dynamic-component [component]="courseSummaryComponent" [data]="data">
<ion-list lines="none" class="core-format-progress-list" <ion-list lines="none" class="core-format-progress-list" *ngIf="imageThumb || (selectedSection?.id == allSectionsId && progress !== undefined) ||
*ngIf="imageThumb || (selectedSection?.id == allSectionsId && progress !== undefined) ||
(selectedSection && selectedSection.id != allSectionsId && (selectedSection && selectedSection.id != allSectionsId &&
(selectedSection.availabilityinfo || selectedSection.visible === 0))"> (selectedSection.availabilityinfo || selectedSection.visible === 0))">
<div *ngIf="imageThumb" class="core-course-thumb"> <div *ngIf="imageThumb" class="core-course-thumb">
<img [src]="imageThumb" core-external-content alt=""/> <img [src]="imageThumb" core-external-content alt="" />
</div> </div>
<ng-container *ngIf="selectedSection"> <ng-container *ngIf="selectedSection">
<ion-item class="core-course-progress" <ion-item class="core-course-progress" *ngIf="selectedSection?.id == allSectionsId && progress !== undefined">
*ngIf="selectedSection?.id == allSectionsId && progress !== undefined">
<core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress"> <core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">
</core-progress-bar> </core-progress-bar>
</ion-item> </ion-item>
@ -103,29 +92,29 @@
<ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" fill="outline" color="primary" <ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" fill="outline" color="primary"
[attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name"> [attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name">
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
<core-format-text class="sr-only" [text]="previousSection.name" contextLevel="course" <core-format-text class="sr-only" [text]="previousSection.name" contextLevel="course" [contextInstanceId]="course?.id">
[contextInstanceId]="course?.id">
</core-format-text> </core-format-text>
</ion-button> </ion-button>
<ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" color="primary" <ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" color="primary"
[attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name"> [attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name">
<core-format-text class="sr-only" [text]="nextSection.name" contextLevel="course" <core-format-text class="sr-only" [text]="nextSection.name" contextLevel="course" [contextInstanceId]="course?.id">
[contextInstanceId]="course?.id">
</core-format-text> </core-format-text>
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
<core-block-side-blocks-button *ngIf="course && displayBlocks && hasBlocks" [courseId]="course.id"
[downloadEnabled]="downloadEnabled">
</core-block-side-blocks-button>
</core-loading> </core-loading>
</core-dynamic-component> </core-dynamic-component>
</core-block-course-blocks>
<!-- Template to render a section. --> <!-- Template to render a section. -->
<ng-template #sectionTemplate let-section="section"> <ng-template #sectionTemplate let-section="section">
<section *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId"> <section *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId">
<!-- Title is only displayed when viewing all sections. --> <!-- Title is only displayed when viewing all sections. -->
<ion-item-divider *ngIf="selectedSection?.id == allSectionsId && section.name" class="ion-text-wrap" color="light" <ion-item-divider *ngIf="selectedSection?.id == allSectionsId && section.name" class="ion-text-wrap" color="light"
[class.core-section-download]="downloadEnabled" [class.core-section-download]="downloadEnabled" [class.item-dimmed]="section.visible === 0 || section.uservisible === false">
[class.item-dimmed]="section.visible === 0 || section.uservisible === false">
<ion-label> <ion-label>
<h2> <h2>
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"> <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id">

View File

@ -24,7 +24,6 @@ import {
ViewChildren, ViewChildren,
QueryList, QueryList,
Type, Type,
ViewChild,
ElementRef, ElementRef,
} from '@angular/core'; } from '@angular/core';
import { ModalOptions } from '@ionic/core'; import { ModalOptions } from '@ionic/core';
@ -48,8 +47,8 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { IonContent, IonRefresher } from '@ionic/angular'; import { IonContent, IonRefresher } from '@ionic/angular';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks';
import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector'; import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector';
import { CoreBlockHelper } from '@features/block/services/block-helper';
/** /**
* Component to display course contents using a certain format. If the format isn't found, use default one. * Component to display course contents using a certain format. If the format isn't found, use default one.
@ -79,7 +78,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when any module completion changes. @Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when any module completion changes.
@ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>; @ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>;
@ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent?: CoreBlockCourseBlocksComponent;
// All the possible component classes. // All the possible component classes.
courseFormatComponent?: Type<unknown>; courseFormatComponent?: Type<unknown>;
@ -92,8 +90,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
showSectionId = 0; showSectionId = 0;
data: Record<string, unknown> = {}; // Data to pass to the components. data: Record<string, unknown> = {}; // Data to pass to the components.
displaySectionSelector?: boolean; displaySectionSelector = false;
displayBlocks?: boolean; displayBlocks = false;
hasBlocks = false;
selectedSection?: CoreCourseSection; selectedSection?: CoreCourseSection;
previousSection?: CoreCourseSection; previousSection?: CoreCourseSection;
nextSection?: CoreCourseSection; nextSection?: CoreCourseSection;
@ -180,7 +179,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
/** /**
* Detect changes on input properties. * Detect changes on input properties.
*/ */
ngOnChanges(changes: { [name: string]: SimpleChange }): void { async ngOnChanges(changes: { [name: string]: SimpleChange }): Promise<void> {
this.setInputData(); this.setInputData();
this.sectionSelectorModalOptions.componentProps!.course = this.course; this.sectionSelectorModalOptions.componentProps!.course = this.course;
this.sectionSelectorModalOptions.componentProps!.sections = this.sections; this.sectionSelectorModalOptions.componentProps!.sections = this.sections;
@ -191,6 +190,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.displaySectionSelector = CoreCourseFormatDelegate.displaySectionSelector(this.course); this.displaySectionSelector = CoreCourseFormatDelegate.displaySectionSelector(this.course);
this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course); this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course);
this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id);
this.updateProgress(); this.updateProgress();
if ('overviewfiles' in this.course) { if ('overviewfiles' in this.course) {
@ -498,8 +500,13 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
await component.callComponentFunction('doRefresh', [refresher, done, afterCompletionChange]); await component.callComponentFunction('doRefresh', [refresher, done, afterCompletionChange]);
}) || []; }) || [];
if (this.courseBlocksComponent) { if (this.course) {
promises.push(this.courseBlocksComponent.doRefresh()); const courseId = this.course.id;
promises.push(CoreCourse.invalidateCourseBlocks(courseId).then(async () => {
this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(courseId);
return;
}));
} }
await Promise.all(promises); await Promise.all(promises);

View File

@ -3,11 +3,9 @@
<ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
<core-context-menu> <core-context-menu>
<core-context-menu-item [priority]="1000" *ngIf="displayEnableDownload" <core-context-menu-item [priority]="1000" *ngIf="displayEnableDownload" [content]="'core.settings.showdownloadoptions' | translate"
[content]="'core.settings.showdownloadoptions' | translate" (action)="switchDownload()" (action)="switchDownload()" iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item> <core-context-menu-item [priority]="500" [content]="'addon.storagemanager.managestorage' | translate"
<core-context-menu-item [priority]="500"
[content]="'addon.storagemanager.managestorage' | translate"
(action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item> (action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item>
</core-context-menu> </core-context-menu>
</core-navbar-buttons> </core-navbar-buttons>
@ -15,14 +13,15 @@
<ion-refresher slot="fixed" [disabled]="!dataLoaded" (ionRefresh)="doRefresh($event.target)"> <ion-refresher slot="fixed" [disabled]="!dataLoaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-block-course-blocks [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled">
<core-loading [hideUntil]="dataLoaded"> <core-loading [hideUntil]="dataLoaded">
<ion-list> <ion-list>
<!-- Site home main contents. --> <!-- Site home main contents. -->
<ng-container *ngIf="section && section.hasContent"> <ng-container *ngIf="section && section.hasContent">
<ion-item class="ion-text-wrap" *ngIf="section.summary"> <ion-item class="ion-text-wrap" *ngIf="section.summary">
<ion-label><core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="siteHomeId"> <ion-label>
</core-format-text></ion-label> <core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="siteHomeId">
</core-format-text>
</ion-label>
</ion-item> </ion-item>
<core-course-module *ngFor="let module of section.modules" [module]="module" [courseId]="siteHomeId" <core-course-module *ngFor="let module of section.modules" [module]="module" [courseId]="siteHomeId"
@ -53,11 +52,13 @@
</ng-container> </ng-container>
</ng-container> </ng-container>
</ion-list> </ion-list>
<core-block-side-blocks-button *ngIf="hasBlocks" [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled">
</core-block-side-blocks-button>
<core-empty-box *ngIf="!hasContent" icon="fas-box-open" [message]="'core.course.nocontentavailable' | translate"> <core-empty-box *ngIf="!hasContent" icon="fas-box-open" [message]="'core.course.nocontentavailable' | translate">
</core-empty-box> </core-empty-box>
</core-loading> </core-loading>
</core-block-course-blocks>
</ion-content> </ion-content>
<ng-template #allCourseList> <ng-template #allCourseList>
@ -88,13 +89,17 @@
<ion-item button class="ion-text-wrap" (click)="openMyCourses()" detail="true"> <ion-item button class="ion-text-wrap" (click)="openMyCourses()" detail="true">
<ion-icon name="fas-graduation-cap" fixed-width slot="start" aria-hidden="true"> <ion-icon name="fas-graduation-cap" fixed-width slot="start" aria-hidden="true">
</ion-icon> </ion-icon>
<ion-label><h2>{{ 'core.courses.mycourses' | translate}}</h2></ion-label> <ion-label>
<h2>{{ 'core.courses.mycourses' | translate}}</h2>
</ion-label>
</ion-item> </ion-item>
</ng-template> </ng-template>
<ng-template #courseSearch> <ng-template #courseSearch>
<ion-item button class="ion-text-wrap" (click)="openSearch()" detail="true"> <ion-item button class="ion-text-wrap" (click)="openSearch()" detail="true">
<ion-icon name="fas-search" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="fas-search" slot="start" aria-hidden="true"></ion-icon>
<ion-label><h2>{{ 'core.courses.searchcourses' | translate}}</h2></ion-label> <ion-label>
<h2>{{ 'core.courses.searchcourses' | translate}}</h2>
</ion-label>
</ion-item> </ion-item>
</ng-template> </ng-template>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { IonRefresher } from '@ionic/angular'; import { IonRefresher } from '@ionic/angular';
import { Params } from '@angular/router'; import { Params } from '@angular/router';
@ -24,10 +24,11 @@ import { CoreSiteHome } from '@features/sitehome/services/sitehome';
import { CoreCourses, CoreCoursesProvider } from '@features//courses/services/courses'; import { CoreCourses, CoreCoursesProvider } from '@features//courses/services/courses';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreCourseHelper, CoreCourseModule } from '@features/course/services/course-helper'; import { CoreCourseHelper, CoreCourseModule } from '@features/course/services/course-helper';
import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks';
import { CoreCourseModuleDelegate, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; import { CoreCourseModuleDelegate, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreBlockHelper } from '@features/block/services/block-helper';
import { CoreUtils } from '@services/utils/utils';
/** /**
* Page that displays site home index. * Page that displays site home index.
@ -38,14 +39,13 @@ import { CoreNavigator } from '@services/navigator';
}) })
export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
@ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent?: CoreBlockCourseBlocksComponent;
dataLoaded = false; dataLoaded = false;
section?: CoreCourseWSSection & { section?: CoreCourseWSSection & {
hasContent?: boolean; hasContent?: boolean;
}; };
hasContent = false; hasContent = false;
hasBlocks = false;
items: string[] = []; items: string[] = [];
siteHomeId = 1; siteHomeId = 1;
currentSite!: CoreSite; currentSite!: CoreSite;
@ -106,8 +106,8 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
this.items = await CoreSiteHome.getFrontPageItems(config.frontpageloggedin); this.items = await CoreSiteHome.getFrontPageItems(config.frontpageloggedin);
this.hasContent = this.items.length > 0; this.hasContent = this.items.length > 0;
if (this.items.some((item) => item == 'NEWS_ITEMS')) {
// Get the news forum. // Get the news forum.
if (this.items.includes('NEWS_ITEMS')) {
try { try {
const forum = await CoreSiteHome.getNewsForum(this.siteHomeId); const forum = await CoreSiteHome.getNewsForum(this.siteHomeId);
this.newsForumModule = await CoreCourse.getModule(forum.cmid, forum.course); this.newsForumModule = await CoreCourse.getModule(forum.cmid, forum.course);
@ -140,17 +140,17 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
} }
// Add log in Moodle. // Add log in Moodle.
CoreCourse.logView( CoreUtils.ignoreErrors(CoreCourse.logView(
this.siteHomeId, this.siteHomeId,
undefined, undefined,
undefined, undefined,
this.currentSite.getInfo()?.sitename, this.currentSite.getInfo()?.sitename,
).catch(() => { ));
// Ignore errors.
});
} catch (error) { } catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true); CoreDomUtils.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true);
} }
this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.siteHomeId);
} }
/** /**
@ -170,24 +170,15 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
return; return;
})); }));
promises.push(CoreCourse.invalidateCourseBlocks(this.siteHomeId));
if (this.section && this.section.modules) { if (this.section && this.section.modules) {
// Invalidate modules prefetch data. // Invalidate modules prefetch data.
promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId)); promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId));
} }
if (this.courseBlocksComponent) {
promises.push(this.courseBlocksComponent.invalidateBlocks());
}
Promise.all(promises).finally(async () => { Promise.all(promises).finally(async () => {
const p2: Promise<unknown>[] = []; await this.loadContent().finally(() => {
p2.push(this.loadContent());
if (this.courseBlocksComponent) {
p2.push(this.courseBlocksComponent.loadContent());
}
await Promise.all(p2).finally(() => {
refresher?.complete(); refresher?.complete();
}); });
}); });

View File

@ -197,13 +197,6 @@
--background: var(--core-progressbar-background); --background: var(--core-progressbar-background);
} }
--core-side-blocks-max-width: 30%;
--core-side-blocks-min-width: 280px;
core-block-course-blocks {
--side-blocks-max-width: var(--core-side-blocks-max-width);
--side-blocks-min-width: var(--core-side-blocks-min-width);
}
--ion-item-background: #{$ion-item-background}; --ion-item-background: #{$ion-item-background};
--ion-item-detail-icon-color: var(--gray-darker); --ion-item-detail-icon-color: var(--gray-darker);
--ion-item-detail-icon-font-size: 20px; --ion-item-detail-icon-font-size: 20px;