MOBILE-3675 course: Migrate participants tab
parent
2ef54f4cac
commit
6d5901ee28
|
@ -27,6 +27,7 @@ export abstract class CorePageItemsListManager<Item> {
|
|||
|
||||
protected itemsList: Item[] | null = null;
|
||||
protected itemsMap: Record<string, Item> | null = null;
|
||||
protected hasMoreItems = true;
|
||||
protected selectedItem: Item | null = null;
|
||||
protected pageComponent: unknown;
|
||||
protected splitView?: CoreSplitViewComponent;
|
||||
|
@ -44,6 +45,10 @@ export abstract class CorePageItemsListManager<Item> {
|
|||
return this.itemsMap !== null;
|
||||
}
|
||||
|
||||
get completed(): boolean {
|
||||
return !this.hasMoreItems;
|
||||
}
|
||||
|
||||
get empty(): boolean {
|
||||
return this.itemsList === null || this.itemsList.length === 0;
|
||||
}
|
||||
|
@ -133,8 +138,10 @@ export abstract class CorePageItemsListManager<Item> {
|
|||
* Set the list of 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.itemsMap = items.reduce((map, item) => {
|
||||
map[this.getItemPath(item)] = item;
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<ion-content>
|
||||
<core-split-view>
|
||||
<ion-refresher slot="fixed" [disabled]="!participants.loaded" (ionRefresh)="refreshParticipants($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="participants.loaded">
|
||||
<core-empty-box *ngIf="participants.empty" icon="person" [message]="'core.user.noparticipants' | 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>
|
||||
<h2>
|
||||
<core-format-text [text]="participant.fullname" [highlight]="searchQuery" [filter]="false"></core-format-text>
|
||||
</h2>
|
||||
</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,170 @@
|
|||
// (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, ViewChild } from '@angular/core';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
|
||||
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, CoreUserParticipant } 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 AfterViewInit, OnDestroy {
|
||||
|
||||
participants: CoreUserParticipantsManager;
|
||||
fetchMoreParticipantsFailed = false;
|
||||
|
||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||
|
||||
constructor(route: ActivatedRoute) {
|
||||
const courseId = parseInt(route.snapshot.queryParams.courseId);
|
||||
|
||||
this.participants = new CoreUserParticipantsManager(CoreUserParticipantsPage, courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngAfterViewInit(): Promise<void> {
|
||||
await this.fetchInitialParticipants();
|
||||
|
||||
this.participants.watchSplitViewOutlet(this.splitView);
|
||||
this.participants.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.participants.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[] = []): Promise<void> {
|
||||
const { participants, canLoadMore } = await CoreUser.instance.getParticipants(
|
||||
this.participants.courseId,
|
||||
loadedParticipants.length,
|
||||
);
|
||||
|
||||
this.participants.setItems(loadedParticipants.concat(participants), canLoadMore);
|
||||
this.fetchMoreParticipantsFailed = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to manage the list of participants.
|
||||
*/
|
||||
class CoreUserParticipantsManager extends CorePageItemsListManager<CoreUserParticipant> {
|
||||
|
||||
courseId: number;
|
||||
|
||||
constructor(pageComponent: unknown, courseId: number) {
|
||||
super(pageComponent);
|
||||
|
||||
this.courseId = courseId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async select(participant: CoreUserParticipant): 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): 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,50 @@
|
|||
// (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 { 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,
|
||||
],
|
||||
declarations: [
|
||||
CoreUserParticipantsPage,
|
||||
],
|
||||
})
|
||||
export class CoreUserCourseLazyModule {}
|
|
@ -27,6 +27,9 @@ import { CoreCronDelegate } from '@services/cron';
|
|||
import { CoreUserSyncCronHandler } from './services/handlers/sync-cron';
|
||||
import { CoreUserTagAreaHandler } from './services/handlers/tag-area';
|
||||
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 = [
|
||||
{
|
||||
|
@ -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({
|
||||
imports: [
|
||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||
CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }),
|
||||
CoreUserComponentsModule,
|
||||
],
|
||||
providers: [
|
||||
|
@ -58,6 +69,7 @@ const routes: Routes = [
|
|||
CoreContentLinksDelegate.instance.registerHandler(CoreUserProfileLinkHandler.instance);
|
||||
CoreCronDelegate.instance.register(CoreUserSyncCronHandler.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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until the next tick.
|
||||
*/
|
||||
nextTick(): Promise<void> {
|
||||
return this.wait(0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CoreUtils extends makeSingleton(CoreUtilsProvider) {}
|
||||
|
|
Loading…
Reference in New Issue