commit
f9ea74067d
|
@ -1,5 +1,3 @@
|
||||||
<ion-app>
|
<ion-app>
|
||||||
<!-- @todo move /login/init UI here -->
|
|
||||||
|
|
||||||
<ion-router-outlet></ion-router-outlet>
|
<ion-router-outlet></ion-router-outlet>
|
||||||
</ion-app>
|
</ion-app>
|
||||||
|
|
|
@ -27,6 +27,7 @@ export abstract class CorePageItemsListManager<Item> {
|
||||||
|
|
||||||
protected itemsList: Item[] | null = null;
|
protected itemsList: Item[] | null = null;
|
||||||
protected itemsMap: Record<string, Item> | null = null;
|
protected itemsMap: Record<string, Item> | null = null;
|
||||||
|
protected hasMoreItems = true;
|
||||||
protected selectedItem: Item | null = null;
|
protected selectedItem: Item | null = null;
|
||||||
protected pageComponent: unknown;
|
protected pageComponent: unknown;
|
||||||
protected splitView?: CoreSplitViewComponent;
|
protected splitView?: CoreSplitViewComponent;
|
||||||
|
@ -44,6 +45,10 @@ export abstract class CorePageItemsListManager<Item> {
|
||||||
return this.itemsMap !== null;
|
return this.itemsMap !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get completed(): boolean {
|
||||||
|
return !this.hasMoreItems;
|
||||||
|
}
|
||||||
|
|
||||||
get empty(): boolean {
|
get empty(): boolean {
|
||||||
return this.itemsList === null || this.itemsList.length === 0;
|
return this.itemsList === null || this.itemsList.length === 0;
|
||||||
}
|
}
|
||||||
|
@ -90,6 +95,16 @@ export abstract class CorePageItemsListManager<Item> {
|
||||||
this.updateSelectedItem(splitView.outletRoute);
|
this.updateSelectedItem(splitView.outletRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset items data.
|
||||||
|
*/
|
||||||
|
resetItems(): void {
|
||||||
|
this.itemsList = null;
|
||||||
|
this.itemsMap = null;
|
||||||
|
this.hasMoreItems = true;
|
||||||
|
this.selectedItem = null;
|
||||||
|
}
|
||||||
|
|
||||||
// @todo Implement watchResize.
|
// @todo Implement watchResize.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -133,8 +148,10 @@ export abstract class CorePageItemsListManager<Item> {
|
||||||
* Set the list of items.
|
* Set the list of items.
|
||||||
*
|
*
|
||||||
* @param items Items.
|
* @param items Items.
|
||||||
|
* @param hasMoreItems Whether the list has more items that haven't been loaded.
|
||||||
*/
|
*/
|
||||||
setItems(items: Item[]): void {
|
setItems(items: Item[], hasMoreItems: boolean = false): void {
|
||||||
|
this.hasMoreItems = hasMoreItems;
|
||||||
this.itemsList = items.slice(0);
|
this.itemsList = items.slice(0);
|
||||||
this.itemsMap = items.reduce((map, item) => {
|
this.itemsMap = items.reduce((map, item) => {
|
||||||
map[this.getItemPath(item)] = item;
|
map[this.getItemPath(item)] = item;
|
||||||
|
|
|
@ -520,12 +520,12 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async selectByIndex(index: number, e?: Event): Promise<void> {
|
async selectByIndex(index: number, e?: Event): Promise<void> {
|
||||||
|
e?.preventDefault();
|
||||||
|
e?.stopPropagation();
|
||||||
|
|
||||||
if (index < 0 || index >= this.tabs.length) {
|
if (index < 0 || index >= this.tabs.length) {
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
// Invalid index do not change tab.
|
// Invalid index do not change tab.
|
||||||
e?.preventDefault();
|
|
||||||
e?.stopPropagation();
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -536,9 +536,6 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
|
||||||
const tabToSelect = this.tabs[index];
|
const tabToSelect = this.tabs[index];
|
||||||
if (!tabToSelect || !tabToSelect.enabled || tabToSelect.id == this.selected) {
|
if (!tabToSelect || !tabToSelect.enabled || tabToSelect.id == this.selected) {
|
||||||
// Already selected or not enabled.
|
// Already selected or not enabled.
|
||||||
e?.preventDefault();
|
|
||||||
e?.stopPropagation();
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,12 @@
|
||||||
// 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, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional, ViewChild, ElementRef } from '@angular/core';
|
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange, ViewChild, ElementRef } from '@angular/core';
|
||||||
import { IonContent, IonInfiniteScroll } from '@ionic/angular';
|
import { IonContent, IonInfiniteScroll } from '@ionic/angular';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
|
const THRESHOLD = .15; // % of the scroll element height that must be close to the edge to consider loading more items necessary.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to show a infinite loading trigger and spinner while more data is being loaded.
|
* Component to show a infinite loading trigger and spinner while more data is being loaded.
|
||||||
|
@ -41,12 +44,7 @@ export class CoreInfiniteLoadingComponent implements OnChanges {
|
||||||
|
|
||||||
loadingMore = false; // Hide button and avoid loading more.
|
loadingMore = false; // Hide button and avoid loading more.
|
||||||
|
|
||||||
protected threshold = parseFloat('15%') / 100;
|
constructor(protected element: ElementRef) {
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected element: ElementRef,
|
|
||||||
@Optional() protected content: IonContent,
|
|
||||||
) {
|
|
||||||
this.action = new EventEmitter();
|
this.action = new EventEmitter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,26 +68,31 @@ export class CoreInfiniteLoadingComponent implements OnChanges {
|
||||||
* like the Ionic component does.
|
* like the Ionic component does.
|
||||||
*/
|
*/
|
||||||
protected async checkScrollDistance(): Promise<void> {
|
protected async checkScrollDistance(): Promise<void> {
|
||||||
if (this.enabled) {
|
if (!this.enabled) {
|
||||||
const scrollElement = await this.content.getScrollElement();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const infiniteHeight = this.element.nativeElement.getBoundingClientRect().height;
|
// Wait until next tick to allow items to render and scroll content to grow.
|
||||||
|
await CoreUtils.instance.nextTick();
|
||||||
|
|
||||||
const scrollTop = scrollElement.scrollTop;
|
// Calculate distance from edge.
|
||||||
const height = scrollElement.offsetHeight;
|
const content = this.element.nativeElement.closest('ion-content') as IonContent;
|
||||||
const threshold = height * this.threshold;
|
const scrollElement = await content.getScrollElement();
|
||||||
|
|
||||||
const distanceFromInfinite = (this.position === 'bottom')
|
const infiniteHeight = this.element.nativeElement.getBoundingClientRect().height;
|
||||||
? scrollElement.scrollHeight - infiniteHeight - scrollTop - threshold - height
|
const scrollTop = scrollElement.scrollTop;
|
||||||
: scrollTop - infiniteHeight - threshold;
|
const height = scrollElement.offsetHeight;
|
||||||
|
const threshold = height * THRESHOLD;
|
||||||
|
const distanceFromInfinite = (this.position === 'bottom')
|
||||||
|
? scrollElement.scrollHeight - infiniteHeight - scrollTop - threshold - height
|
||||||
|
: scrollTop - infiniteHeight - threshold;
|
||||||
|
|
||||||
if (distanceFromInfinite < 0 && !this.loadingMore && this.enabled) {
|
// If it's close enough the edge, trigger the action to load more items.
|
||||||
this.loadMore();
|
if (distanceFromInfinite < 0 && !this.loadingMore && this.enabled) {
|
||||||
}
|
this.loadMore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load More items calling the action provided.
|
* Load More items calling the action provided.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -14,11 +14,11 @@
|
||||||
|
|
||||||
import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { IonRouterOutlet } from '@ionic/angular';
|
import { IonContent, IonRouterOutlet } from '@ionic/angular';
|
||||||
import { CoreScreen } from '@services/screen';
|
import { CoreScreen } from '@services/screen';
|
||||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||||
|
|
||||||
enum CoreSplitViewMode {
|
export enum CoreSplitViewMode {
|
||||||
MenuOnly = 'menu-only', // Hides content.
|
MenuOnly = 'menu-only', // Hides content.
|
||||||
ContentOnly = 'content-only', // Hides menu.
|
ContentOnly = 'content-only', // Hides menu.
|
||||||
MenuAndContent = 'menu-and-content', // Shows both menu and content.
|
MenuAndContent = 'menu-and-content', // Shows both menu and content.
|
||||||
|
@ -31,9 +31,11 @@ enum CoreSplitViewMode {
|
||||||
})
|
})
|
||||||
export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
@ViewChild(IonRouterOutlet) outlet!: IonRouterOutlet;
|
@ViewChild(IonContent) menuContent!: IonContent;
|
||||||
|
@ViewChild(IonRouterOutlet) contentOutlet!: IonRouterOutlet;
|
||||||
@HostBinding('class') classes = '';
|
@HostBinding('class') classes = '';
|
||||||
@Input() placeholderText = 'core.emptysplit';
|
@Input() placeholderText = 'core.emptysplit';
|
||||||
|
@Input() mode?: CoreSplitViewMode;
|
||||||
isNested = false;
|
isNested = false;
|
||||||
|
|
||||||
private outletRouteSubject: BehaviorSubject<ActivatedRouteSnapshot | null> = new BehaviorSubject(null);
|
private outletRouteSubject: BehaviorSubject<ActivatedRouteSnapshot | null> = new BehaviorSubject(null);
|
||||||
|
@ -55,11 +57,11 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.isNested = !!this.element.nativeElement.parentElement?.closest('core-split-view');
|
this.isNested = !!this.element.nativeElement.parentElement?.closest('core-split-view');
|
||||||
this.subscriptions = [
|
this.subscriptions = [
|
||||||
this.outlet.activateEvents.subscribe(() => {
|
this.contentOutlet.activateEvents.subscribe(() => {
|
||||||
this.updateClasses();
|
this.updateClasses();
|
||||||
this.outletRouteSubject.next(this.outlet.activatedRoute.snapshot);
|
this.outletRouteSubject.next(this.contentOutlet.activatedRoute.snapshot);
|
||||||
}),
|
}),
|
||||||
this.outlet.deactivateEvents.subscribe(() => {
|
this.contentOutlet.deactivateEvents.subscribe(() => {
|
||||||
this.updateClasses();
|
this.updateClasses();
|
||||||
this.outletRouteSubject.next(null);
|
this.outletRouteSubject.next(null);
|
||||||
}),
|
}),
|
||||||
|
@ -82,7 +84,7 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
||||||
private updateClasses(): void {
|
private updateClasses(): void {
|
||||||
const classes: string[] = [this.getCurrentMode()];
|
const classes: string[] = [this.getCurrentMode()];
|
||||||
|
|
||||||
if (this.outlet.isActivated) {
|
if (this.contentOutlet.isActivated) {
|
||||||
classes.push('outlet-activated');
|
classes.push('outlet-activated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,12 +102,16 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
||||||
* @return Split view mode.
|
* @return Split view mode.
|
||||||
*/
|
*/
|
||||||
private getCurrentMode(): CoreSplitViewMode {
|
private getCurrentMode(): CoreSplitViewMode {
|
||||||
|
if (this.mode) {
|
||||||
|
return this.mode;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isNested) {
|
if (this.isNested) {
|
||||||
return CoreSplitViewMode.MenuOnly;
|
return CoreSplitViewMode.MenuOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CoreScreen.instance.isMobile) {
|
if (CoreScreen.instance.isMobile) {
|
||||||
return this.outlet.isActivated
|
return this.contentOutlet.isActivated
|
||||||
? CoreSplitViewMode.ContentOnly
|
? CoreSplitViewMode.ContentOnly
|
||||||
: CoreSplitViewMode.MenuOnly;
|
: CoreSplitViewMode.MenuOnly;
|
||||||
}
|
}
|
||||||
|
@ -119,7 +125,7 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
||||||
* @return If split view is enabled.
|
* @return If split view is enabled.
|
||||||
*/
|
*/
|
||||||
isOn(): boolean {
|
isOn(): boolean {
|
||||||
return this.outlet.isActivated;
|
return this.contentOutlet.isActivated;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,8 +43,8 @@ import { CoreTabBase, CoreTabsBaseComponent } from '@classes/tabs';
|
||||||
*
|
*
|
||||||
* Tab contents will only be shown if that tab is selected.
|
* Tab contents will only be shown if that tab is selected.
|
||||||
*
|
*
|
||||||
* @todo: Test behaviour when tabs are added late.
|
|
||||||
* @todo: Test RTL and tab history.
|
* @todo: Test RTL and tab history.
|
||||||
|
* @todo: This should behave like the split-view in relation to routing (maybe we could reuse some code from CoreItemsListManager).
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'core-tabs-outlet',
|
selector: 'core-tabs-outlet',
|
||||||
|
|
|
@ -115,7 +115,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
// Load the course handlers.
|
// Load the course handlers.
|
||||||
const handlers = await CoreCourseOptionsDelegate.instance.getHandlersToDisplay(this.course!, false, false);
|
const handlers = await CoreCourseOptionsDelegate.instance.getHandlersToDisplay(this.course!, false, false);
|
||||||
|
|
||||||
this.tabs.concat(handlers.map(handler => handler.data));
|
this.tabs = [...this.tabs, ...handlers.map(handler => handler.data)];
|
||||||
|
|
||||||
let tabToLoad: number | undefined;
|
let tabToLoad: number | undefined;
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { CoreUserModule } from './user/user.module';
|
||||||
import { CorePushNotificationsModule } from './pushnotifications/pushnotifications.module';
|
import { CorePushNotificationsModule } from './pushnotifications/pushnotifications.module';
|
||||||
import { CoreXAPIModule } from './xapi/xapi.module';
|
import { CoreXAPIModule } from './xapi/xapi.module';
|
||||||
import { CoreViewerModule } from './viewer/viewer.module';
|
import { CoreViewerModule } from './viewer/viewer.module';
|
||||||
|
import { CoreSearchModule } from './search/search.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -44,6 +45,7 @@ import { CoreViewerModule } from './viewer/viewer.module';
|
||||||
CoreTagModule,
|
CoreTagModule,
|
||||||
CoreUserModule,
|
CoreUserModule,
|
||||||
CorePushNotificationsModule,
|
CorePushNotificationsModule,
|
||||||
|
CoreSearchModule,
|
||||||
CoreXAPIModule,
|
CoreXAPIModule,
|
||||||
CoreH5PModule,
|
CoreH5PModule,
|
||||||
CoreViewerModule,
|
CoreViewerModule,
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
// (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 { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { CoreGradesCoursePage } from './pages/course/course.page';
|
||||||
|
import { CoreGradesCoursePageModule } from './pages/course/course.module';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CoreGradesCoursePage,
|
||||||
|
data: {
|
||||||
|
useSplitView: false,
|
||||||
|
outsideGradesTab: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CoreGradesCoursePageModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreGradesCourseLazyModule {}
|
|
@ -18,13 +18,14 @@ import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { CoreSharedModule } from '@/core/shared.module';
|
import { conditionalRoutes } from '@/app/app-routing.module';
|
||||||
import { CoreScreen } from '@services/screen';
|
import { CoreScreen } from '@services/screen';
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
|
||||||
import { CoreGradesCoursePage } from './pages/course/course';
|
import { CoreGradesCoursePage } from './pages/course/course.page';
|
||||||
|
import { CoreGradesCoursePageModule } from './pages/course/course.module';
|
||||||
import { CoreGradesCoursesPage } from './pages/courses/courses';
|
import { CoreGradesCoursesPage } from './pages/courses/courses';
|
||||||
import { CoreGradesGradePage } from './pages/grade/grade';
|
import { CoreGradesGradePage } from './pages/grade/grade';
|
||||||
import { conditionalRoutes } from '@/app/app-routing.module';
|
|
||||||
|
|
||||||
const mobileRoutes: Routes = [
|
const mobileRoutes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -76,10 +77,10 @@ const routes: Routes = [
|
||||||
IonicModule,
|
IonicModule,
|
||||||
TranslateModule.forChild(),
|
TranslateModule.forChild(),
|
||||||
CoreSharedModule,
|
CoreSharedModule,
|
||||||
|
CoreGradesCoursePageModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CoreGradesCoursesPage,
|
CoreGradesCoursesPage,
|
||||||
CoreGradesCoursePage,
|
|
||||||
CoreGradesGradePage,
|
CoreGradesGradePage,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
|
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
|
||||||
|
import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module';
|
||||||
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
||||||
import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module';
|
import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module';
|
||||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||||
|
@ -33,10 +34,18 @@ const routes: Routes = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const courseIndexRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'grades',
|
||||||
|
loadChildren: () => import('@features/grades/grades-course-lazy.module').then(m => m.CoreGradesCourseLazyModule),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||||
CoreMainMenuRoutingModule.forChild({ children: routes }),
|
CoreMainMenuRoutingModule.forChild({ children: routes }),
|
||||||
|
CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<core-split-view>
|
<core-split-view [mode]="splitViewMode">
|
||||||
<ion-refresher slot="fixed" [disabled]="!grades.loaded" (ionRefresh)="refreshGrades($event.target)">
|
<ion-refresher slot="fixed" [disabled]="!grades.loaded" (ionRefresh)="refreshGrades($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>
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
// (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 { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
|
||||||
|
import { CoreGradesCoursePage } from './course.page';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreSharedModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
CoreGradesCoursePage,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreGradesCoursePageModule {}
|
|
@ -27,9 +27,10 @@ import {
|
||||||
} from '@features/grades/services/grades-helper';
|
} from '@features/grades/services/grades-helper';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent, CoreSplitViewMode } from '@components/split-view/split-view';
|
||||||
import { CoreObject } from '@singletons/object';
|
import { CoreObject } from '@singletons/object';
|
||||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays a course grades.
|
* Page that displays a course grades.
|
||||||
|
@ -42,14 +43,18 @@ import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
||||||
export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
|
export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
grades: CoreGradesCourseManager;
|
grades: CoreGradesCourseManager;
|
||||||
|
splitViewMode?: CoreSplitViewMode;
|
||||||
|
|
||||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||||
|
|
||||||
constructor(route: ActivatedRoute) {
|
constructor(route: ActivatedRoute) {
|
||||||
const courseId = parseInt(route.snapshot.params.courseId);
|
const courseId = parseInt(route.snapshot.params.courseId ?? route.snapshot.queryParams.courseId);
|
||||||
const userId = parseInt(route.snapshot.queryParams.userId ?? CoreSites.instance.getCurrentSiteUserId());
|
const userId = parseInt(route.snapshot.queryParams.userId ?? CoreSites.instance.getCurrentSiteUserId());
|
||||||
|
const useSplitView = route.snapshot.data.useSplitView ?? true;
|
||||||
|
const outsideGradesTab = route.snapshot.data.outsideGradesTab ?? false;
|
||||||
|
|
||||||
this.grades = new CoreGradesCourseManager(CoreGradesCoursePage, courseId, userId);
|
this.splitViewMode = useSplitView ? undefined : CoreSplitViewMode.MenuOnly;
|
||||||
|
this.grades = new CoreGradesCourseManager(CoreGradesCoursePage, courseId, userId, outsideGradesTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -118,11 +123,14 @@ class CoreGradesCourseManager extends CorePageItemsListManager<CoreGradesFormatt
|
||||||
columns?: CoreGradesFormattedTableColumn[];
|
columns?: CoreGradesFormattedTableColumn[];
|
||||||
rows?: CoreGradesFormattedTableRow[];
|
rows?: CoreGradesFormattedTableRow[];
|
||||||
|
|
||||||
constructor(pageComponent: unknown, courseId: number, userId: number) {
|
private outsideGradesTab: boolean;
|
||||||
|
|
||||||
|
constructor(pageComponent: unknown, courseId: number, userId: number, outsideGradesTab: boolean) {
|
||||||
super(pageComponent);
|
super(pageComponent);
|
||||||
|
|
||||||
this.courseId = courseId;
|
this.courseId = courseId;
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
|
this.outsideGradesTab = outsideGradesTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -137,6 +145,19 @@ class CoreGradesCourseManager extends CorePageItemsListManager<CoreGradesFormatt
|
||||||
this.setItems(table.rows.filter(this.isFilledRow));
|
this.setItems(table.rows.filter(this.isFilledRow));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async select(row: CoreGradesFormattedTableRowFilled): Promise<void> {
|
||||||
|
if (this.outsideGradesTab) {
|
||||||
|
await CoreNavigator.instance.navigateToSitePath(`/grades/${this.courseId}/${row.id}`);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.select(row);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
|
@ -83,19 +83,14 @@ export class CoreGradesCourseOptionHandlerService implements CoreCourseOptionsHa
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the data needed to render the handler.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @return Data or promise resolved with the data.
|
|
||||||
*/
|
*/
|
||||||
getDisplayData(): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData> {
|
getDisplayData(): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData> {
|
||||||
throw new Error('CoreGradesCourseOptionHandler.getDisplayData is not implemented');
|
return {
|
||||||
|
title: 'core.grades.grades',
|
||||||
// @todo
|
class: 'core-grades-course-handler',
|
||||||
// return {
|
page: 'grades',
|
||||||
// title: 'core.grades.grades',
|
};
|
||||||
// class: 'core-grades-course-handler',
|
|
||||||
// component: CoreGradesCourseComponent,
|
|
||||||
// };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
<core-navbar-buttons slot="end">
|
||||||
|
<ion-button [hidden]="!searchEnabled" (click)="toggleSearch()" [attr.aria-label]="'core.search' | translate">
|
||||||
|
<ion-icon name="fas-search" slot="icon-only"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</core-navbar-buttons>
|
||||||
|
|
||||||
|
<ion-content>
|
||||||
|
<core-split-view>
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!participants.loaded || searchInProgress" (ionRefresh)="refreshParticipants($event.target)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<core-search-box *ngIf="showSearchBox"
|
||||||
|
[disabled]="searchInProgress" [spellcheck]="false" [autoFocus]="true" [lengthCheck]="1"
|
||||||
|
autocorrect="off" searchArea="CoreUserParticipants"
|
||||||
|
(onSubmit)="search($event)" (onClear)="clearSearch()">
|
||||||
|
</core-search-box>
|
||||||
|
|
||||||
|
<core-loading [hideUntil]="participants.loaded">
|
||||||
|
<core-empty-box *ngIf="participants.empty && !searchInProgress && !searchQuery" icon="person" [message]="'core.user.noparticipants' | translate">
|
||||||
|
</core-empty-box>
|
||||||
|
|
||||||
|
<core-empty-box *ngIf="participants.empty && !searchInProgress && searchQuery" icon="search" [message]="'core.noresults' | translate">
|
||||||
|
</core-empty-box>
|
||||||
|
|
||||||
|
<ion-list *ngIf="!participants.empty">
|
||||||
|
<ion-item *ngFor="let participant of participants.items"
|
||||||
|
class="ion-text-wrap" [class.core-selected-item]="participants.isSelected(participant)" [title]="participant.fullname"
|
||||||
|
(click)="participants.select(participant)">
|
||||||
|
|
||||||
|
<core-user-avatar [user]="participant" [linkProfile]="false" [checkOnline]="true" slot="start">
|
||||||
|
</core-user-avatar>
|
||||||
|
|
||||||
|
<ion-label>
|
||||||
|
<ng-container *ngIf="!searchQuery">
|
||||||
|
<h2>{{ participant.fullname }}</h2>
|
||||||
|
<p *ngIf="participant.lastcourseaccess"><strong>{{ 'core.lastaccess' | translate }}: </strong>{{ participant.lastcourseaccess | coreTimeAgo }}</p>
|
||||||
|
<p *ngIf="participant.lastcourseaccess == null && participant.lastaccess"><strong>{{ 'core.lastaccess' | translate }}: </strong>{{ participant.lastaccess | coreTimeAgo }}</p>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="searchQuery">
|
||||||
|
<h2>
|
||||||
|
<core-format-text [text]="participant.fullname" [highlight]="searchQuery" [filter]="false"></core-format-text>
|
||||||
|
</h2>
|
||||||
|
</ng-container>
|
||||||
|
</ion-label>
|
||||||
|
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
<core-infinite-loading [enabled]="participants.loaded && !participants.completed" (action)="fetchMoreParticipants($event)" [error]="fetchMoreParticipantsFailed">
|
||||||
|
</core-infinite-loading>
|
||||||
|
</core-loading>
|
||||||
|
</core-split-view>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,242 @@
|
||||||
|
// (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 { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
|
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
||||||
|
import { CoreScreen } from '@services/screen';
|
||||||
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
|
import { CoreUser, CoreUserProvider, CoreUserParticipant, CoreUserData } from '@features/user/services/user';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the list of course participants.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-core-user-participants',
|
||||||
|
templateUrl: 'participants.html',
|
||||||
|
})
|
||||||
|
export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
|
participants: CoreUserParticipantsManager;
|
||||||
|
searchQuery: string | null = null;
|
||||||
|
searchInProgress = false;
|
||||||
|
searchEnabled = false;
|
||||||
|
showSearchBox = false;
|
||||||
|
fetchMoreParticipantsFailed = false;
|
||||||
|
|
||||||
|
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||||
|
|
||||||
|
constructor(route: ActivatedRoute) {
|
||||||
|
const courseId = parseInt(route.snapshot.queryParams.courseId);
|
||||||
|
|
||||||
|
this.participants = new CoreUserParticipantsManager(CoreUserParticipantsPage, courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
this.searchEnabled = await CoreUser.instance.canSearchParticipantsInSite();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngAfterViewInit(): Promise<void> {
|
||||||
|
await this.fetchInitialParticipants();
|
||||||
|
|
||||||
|
this.participants.watchSplitViewOutlet(this.splitView);
|
||||||
|
this.participants.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.participants.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show or hide search box.
|
||||||
|
*/
|
||||||
|
toggleSearch(): void {
|
||||||
|
this.showSearchBox = !this.showSearchBox;
|
||||||
|
|
||||||
|
if (this.showSearchBox) {
|
||||||
|
// Make search bar visible.
|
||||||
|
this.splitView.menuContent.scrollToTop();
|
||||||
|
} else {
|
||||||
|
this.clearSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear search.
|
||||||
|
*/
|
||||||
|
async clearSearch(): Promise<void> {
|
||||||
|
if (this.searchQuery === null) {
|
||||||
|
// Nothing to clear.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchQuery = null;
|
||||||
|
this.searchInProgress = false;
|
||||||
|
this.participants.resetItems();
|
||||||
|
|
||||||
|
await this.fetchInitialParticipants();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new search.
|
||||||
|
*
|
||||||
|
* @param query Text to search for.
|
||||||
|
*/
|
||||||
|
async search(query: string): Promise<void> {
|
||||||
|
CoreApp.instance.closeKeyboard();
|
||||||
|
|
||||||
|
this.searchInProgress = true;
|
||||||
|
this.searchQuery = query;
|
||||||
|
this.participants.resetItems();
|
||||||
|
|
||||||
|
await this.fetchInitialParticipants();
|
||||||
|
|
||||||
|
this.searchInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh participants.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
*/
|
||||||
|
async refreshParticipants(refresher: IonRefresher): Promise<void> {
|
||||||
|
await CoreUtils.instance.ignoreErrors(CoreUser.instance.invalidateParticipantsList(this.participants.courseId));
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.fetchParticipants());
|
||||||
|
|
||||||
|
refresher?.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a new batch of participants.
|
||||||
|
*
|
||||||
|
* @param complete Completion callback.
|
||||||
|
*/
|
||||||
|
async fetchMoreParticipants(complete: () => void): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.fetchParticipants(this.participants.items);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading more participants');
|
||||||
|
|
||||||
|
this.fetchMoreParticipantsFailed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain the initial batch of participants.
|
||||||
|
*/
|
||||||
|
private async fetchInitialParticipants(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.fetchParticipants();
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading participants');
|
||||||
|
|
||||||
|
this.participants.setItems([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the list of participants.
|
||||||
|
*
|
||||||
|
* @param loadedParticipants Participants list to continue loading from.
|
||||||
|
*/
|
||||||
|
private async fetchParticipants(loadedParticipants: CoreUserParticipant[] | CoreUserData[] = []): Promise<void> {
|
||||||
|
if (this.searchQuery) {
|
||||||
|
const { participants, canLoadMore } = await CoreUser.instance.searchParticipants(
|
||||||
|
this.participants.courseId,
|
||||||
|
this.searchQuery,
|
||||||
|
true,
|
||||||
|
Math.ceil(loadedParticipants.length / CoreUserProvider.PARTICIPANTS_LIST_LIMIT),
|
||||||
|
CoreUserProvider.PARTICIPANTS_LIST_LIMIT,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.participants.setItems((loadedParticipants as CoreUserData[]).concat(participants), canLoadMore);
|
||||||
|
} else {
|
||||||
|
const { participants, canLoadMore } = await CoreUser.instance.getParticipants(
|
||||||
|
this.participants.courseId,
|
||||||
|
loadedParticipants.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.participants.setItems((loadedParticipants as CoreUserParticipant[]).concat(participants), canLoadMore);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchMoreParticipantsFailed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage the list of participants.
|
||||||
|
*/
|
||||||
|
class CoreUserParticipantsManager extends CorePageItemsListManager<CoreUserParticipant | CoreUserData> {
|
||||||
|
|
||||||
|
courseId: number;
|
||||||
|
|
||||||
|
constructor(pageComponent: unknown, courseId: number) {
|
||||||
|
super(pageComponent);
|
||||||
|
|
||||||
|
this.courseId = courseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async select(participant: CoreUserParticipant | CoreUserData): Promise<void> {
|
||||||
|
if (CoreScreen.instance.isMobile) {
|
||||||
|
await CoreNavigator.instance.navigateToSitePath('/user/profile', { params: { userId: participant.id } });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.select(participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getItemPath(participant: CoreUserParticipant | CoreUserData): string {
|
||||||
|
return participant.id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null {
|
||||||
|
return route.params.userId ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async logActivity(): Promise<void> {
|
||||||
|
await CoreUser.instance.logParticipantsView(this.courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
// (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 { CoreCourseProvider } from '@features/course/services/course';
|
||||||
|
import {
|
||||||
|
CoreCourseAccess,
|
||||||
|
CoreCourseOptionsHandler,
|
||||||
|
CoreCourseOptionsHandlerData,
|
||||||
|
} from '@features/course/services/course-options-delegate';
|
||||||
|
import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses';
|
||||||
|
import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreUser } from '../user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Course nav handler.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreUserCourseOptionHandlerService implements CoreCourseOptionsHandler {
|
||||||
|
|
||||||
|
name = 'CoreUserParticipants';
|
||||||
|
priority = 600;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should invalidate the data to determine if the handler is enabled for a certain course.
|
||||||
|
*
|
||||||
|
* @param courseId The course ID.
|
||||||
|
* @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
invalidateEnabledForCourse(courseId: number, navOptions?: CoreCourseUserAdminOrNavOptionIndexed): Promise<void> {
|
||||||
|
if (navOptions && typeof navOptions.participants != 'undefined') {
|
||||||
|
// No need to invalidate anything.
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreUser.instance.invalidateParticipantsList(courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled for a certain course.
|
||||||
|
*
|
||||||
|
* @param courseId The course ID.
|
||||||
|
* @param accessData Access type and data. Default, guest, ...
|
||||||
|
* @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
|
||||||
|
* @return True or promise resolved with true if enabled.
|
||||||
|
*/
|
||||||
|
isEnabledForCourse(
|
||||||
|
courseId: number,
|
||||||
|
accessData: CoreCourseAccess,
|
||||||
|
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||||
|
): boolean | Promise<boolean> {
|
||||||
|
if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) {
|
||||||
|
return false; // Not enabled for guests.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navOptions && typeof navOptions.participants != 'undefined') {
|
||||||
|
return navOptions.participants;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreUser.instance.isPluginEnabledForCourse(courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getDisplayData(): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData> {
|
||||||
|
return {
|
||||||
|
title: 'core.user.participants',
|
||||||
|
class: 'core-user-participants-handler',
|
||||||
|
page: 'participants',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline.
|
||||||
|
*
|
||||||
|
* @param course The course.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async prefetch(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise<void> {
|
||||||
|
let offset = 0;
|
||||||
|
let canLoadMore = true;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const result = await CoreUser.instance.getParticipants(course.id, offset, undefined, undefined, true);
|
||||||
|
|
||||||
|
offset += result.participants.length;
|
||||||
|
canLoadMore = result.canLoadMore;
|
||||||
|
} while (canLoadMore);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreUserCourseOptionHandler extends makeSingleton(CoreUserCourseOptionHandlerService) {}
|
|
@ -0,0 +1,52 @@
|
||||||
|
// (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 { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||||
|
|
||||||
|
import { CoreUserParticipantsPage } from './pages/participants/participants';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CoreUserParticipantsPage,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':userId',
|
||||||
|
loadChildren: () => import('@features/user/pages/profile/profile.module').then(m => m.CoreUserProfilePageModule),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreSharedModule,
|
||||||
|
CoreSearchComponentsModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
CoreUserParticipantsPage,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreUserCourseLazyModule {}
|
|
@ -27,6 +27,9 @@ import { CoreCronDelegate } from '@services/cron';
|
||||||
import { CoreUserSyncCronHandler } from './services/handlers/sync-cron';
|
import { CoreUserSyncCronHandler } from './services/handlers/sync-cron';
|
||||||
import { CoreUserTagAreaHandler } from './services/handlers/tag-area';
|
import { CoreUserTagAreaHandler } from './services/handlers/tag-area';
|
||||||
import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate';
|
import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate';
|
||||||
|
import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module';
|
||||||
|
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
||||||
|
import { CoreUserCourseOptionHandler } from './services/handlers/course-option';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -35,9 +38,17 @@ const routes: Routes = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const courseIndexRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'participants',
|
||||||
|
loadChildren: () => import('@features/user/user-course-lazy.module').then(m => m.CoreUserCourseLazyModule),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||||
|
CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }),
|
||||||
CoreUserComponentsModule,
|
CoreUserComponentsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@ -58,6 +69,7 @@ const routes: Routes = [
|
||||||
CoreContentLinksDelegate.instance.registerHandler(CoreUserProfileLinkHandler.instance);
|
CoreContentLinksDelegate.instance.registerHandler(CoreUserProfileLinkHandler.instance);
|
||||||
CoreCronDelegate.instance.register(CoreUserSyncCronHandler.instance);
|
CoreCronDelegate.instance.register(CoreUserSyncCronHandler.instance);
|
||||||
CoreTagAreaDelegate.instance.registerHandler(CoreUserTagAreaHandler.instance);
|
CoreTagAreaDelegate.instance.registerHandler(CoreUserTagAreaHandler.instance);
|
||||||
|
CoreCourseOptionsDelegate.instance.registerHandler(CoreUserCourseOptionHandler.instance);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -1623,6 +1623,13 @@ export class CoreUtilsProvider {
|
||||||
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait until the next tick.
|
||||||
|
*/
|
||||||
|
nextTick(): Promise<void> {
|
||||||
|
return this.wait(0);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CoreUtils extends makeSingleton(CoreUtilsProvider) {}
|
export class CoreUtils extends makeSingleton(CoreUtilsProvider) {}
|
||||||
|
|
Loading…
Reference in New Issue