MOBILE-3675 course: Migrate participants search
parent
1786f908b9
commit
dda1ee13e9
|
@ -95,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.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
|
@ -31,7 +31,8 @@ export 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;
|
@Input() mode?: CoreSplitViewMode;
|
||||||
|
@ -56,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);
|
||||||
}),
|
}),
|
||||||
|
@ -83,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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +111,7 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CoreScreen.instance.isMobile) {
|
if (CoreScreen.instance.isMobile) {
|
||||||
return this.outlet.isActivated
|
return this.contentOutlet.isActivated
|
||||||
? CoreSplitViewMode.ContentOnly
|
? CoreSplitViewMode.ContentOnly
|
||||||
: CoreSplitViewMode.MenuOnly;
|
: CoreSplitViewMode.MenuOnly;
|
||||||
}
|
}
|
||||||
|
@ -124,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1,21 +1,50 @@
|
||||||
|
<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>
|
<ion-content>
|
||||||
<core-split-view>
|
<core-split-view>
|
||||||
<ion-refresher slot="fixed" [disabled]="!participants.loaded" (ionRefresh)="refreshParticipants($event.target)">
|
<ion-refresher slot="fixed" [disabled]="!participants.loaded || searchInProgress" (ionRefresh)="refreshParticipants($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-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-loading [hideUntil]="participants.loaded">
|
||||||
<core-empty-box *ngIf="participants.empty" icon="person" [message]="'core.user.noparticipants' | translate">
|
<core-empty-box *ngIf="participants.empty && !searchInProgress && !searchQuery" icon="person" [message]="'core.user.noparticipants' | translate">
|
||||||
</core-empty-box>
|
</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-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)">
|
<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 [user]="participant" [linkProfile]="false" [checkOnline]="true" slot="start">
|
||||||
</core-user-avatar>
|
</core-user-avatar>
|
||||||
|
|
||||||
<ion-label>
|
<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>
|
<h2>
|
||||||
<core-format-text [text]="participant.fullname" [highlight]="searchQuery" [filter]="false"></core-format-text>
|
<core-format-text [text]="participant.fullname" [highlight]="searchQuery" [filter]="false"></core-format-text>
|
||||||
</h2>
|
</h2>
|
||||||
|
</ng-container>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
<core-infinite-loading [enabled]="participants.loaded && !participants.completed" (action)="fetchMoreParticipants($event)" [error]="fetchMoreParticipantsFailed">
|
<core-infinite-loading [enabled]="participants.loaded && !participants.completed" (action)="fetchMoreParticipants($event)" [error]="fetchMoreParticipantsFailed">
|
||||||
|
|
|
@ -13,15 +13,16 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
||||||
import { CoreScreen } from '@services/screen';
|
import { CoreScreen } from '@services/screen';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreUser, CoreUserParticipant } from '@features/user/services/user';
|
import { CoreUser, CoreUserProvider, CoreUserParticipant, CoreUserData } from '@features/user/services/user';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,9 +32,13 @@ import { CoreUtils } from '@services/utils/utils';
|
||||||
selector: 'page-core-user-participants',
|
selector: 'page-core-user-participants',
|
||||||
templateUrl: 'participants.html',
|
templateUrl: 'participants.html',
|
||||||
})
|
})
|
||||||
export class CoreUserParticipantsPage implements AfterViewInit, OnDestroy {
|
export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
participants: CoreUserParticipantsManager;
|
participants: CoreUserParticipantsManager;
|
||||||
|
searchQuery: string | null = null;
|
||||||
|
searchInProgress = false;
|
||||||
|
searchEnabled = false;
|
||||||
|
showSearchBox = false;
|
||||||
fetchMoreParticipantsFailed = false;
|
fetchMoreParticipantsFailed = false;
|
||||||
|
|
||||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||||
|
@ -44,6 +49,13 @@ export class CoreUserParticipantsPage implements AfterViewInit, OnDestroy {
|
||||||
this.participants = new CoreUserParticipantsManager(CoreUserParticipantsPage, courseId);
|
this.participants = new CoreUserParticipantsManager(CoreUserParticipantsPage, courseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
this.searchEnabled = await CoreUser.instance.canSearchParticipantsInSite();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
@ -61,6 +73,53 @@ export class CoreUserParticipantsPage implements AfterViewInit, OnDestroy {
|
||||||
this.participants.destroy();
|
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.
|
* Refresh participants.
|
||||||
*
|
*
|
||||||
|
@ -108,13 +167,26 @@ export class CoreUserParticipantsPage implements AfterViewInit, OnDestroy {
|
||||||
*
|
*
|
||||||
* @param loadedParticipants Participants list to continue loading from.
|
* @param loadedParticipants Participants list to continue loading from.
|
||||||
*/
|
*/
|
||||||
private async fetchParticipants(loadedParticipants: CoreUserParticipant[] = []): Promise<void> {
|
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(
|
const { participants, canLoadMore } = await CoreUser.instance.getParticipants(
|
||||||
this.participants.courseId,
|
this.participants.courseId,
|
||||||
loadedParticipants.length,
|
loadedParticipants.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.participants.setItems(loadedParticipants.concat(participants), canLoadMore);
|
this.participants.setItems((loadedParticipants as CoreUserParticipant[]).concat(participants), canLoadMore);
|
||||||
|
}
|
||||||
|
|
||||||
this.fetchMoreParticipantsFailed = false;
|
this.fetchMoreParticipantsFailed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +195,7 @@ export class CoreUserParticipantsPage implements AfterViewInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Helper to manage the list of participants.
|
* Helper to manage the list of participants.
|
||||||
*/
|
*/
|
||||||
class CoreUserParticipantsManager extends CorePageItemsListManager<CoreUserParticipant> {
|
class CoreUserParticipantsManager extends CorePageItemsListManager<CoreUserParticipant | CoreUserData> {
|
||||||
|
|
||||||
courseId: number;
|
courseId: number;
|
||||||
|
|
||||||
|
@ -136,7 +208,7 @@ class CoreUserParticipantsManager extends CorePageItemsListManager<CoreUserParti
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async select(participant: CoreUserParticipant): Promise<void> {
|
async select(participant: CoreUserParticipant | CoreUserData): Promise<void> {
|
||||||
if (CoreScreen.instance.isMobile) {
|
if (CoreScreen.instance.isMobile) {
|
||||||
await CoreNavigator.instance.navigateToSitePath('/user/profile', { params: { userId: participant.id } });
|
await CoreNavigator.instance.navigateToSitePath('/user/profile', { params: { userId: participant.id } });
|
||||||
|
|
||||||
|
@ -149,7 +221,7 @@ class CoreUserParticipantsManager extends CorePageItemsListManager<CoreUserParti
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected getItemPath(participant: CoreUserParticipant): string {
|
protected getItemPath(participant: CoreUserParticipant | CoreUserData): string {
|
||||||
return participant.id.toString();
|
return participant.id.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { IonicModule } from '@ionic/angular';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { CoreSharedModule } from '@/core/shared.module';
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||||
|
|
||||||
import { CoreUserParticipantsPage } from './pages/participants/participants';
|
import { CoreUserParticipantsPage } from './pages/participants/participants';
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ const routes: Routes = [
|
||||||
IonicModule,
|
IonicModule,
|
||||||
TranslateModule.forChild(),
|
TranslateModule.forChild(),
|
||||||
CoreSharedModule,
|
CoreSharedModule,
|
||||||
|
CoreSearchComponentsModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CoreUserParticipantsPage,
|
CoreUserParticipantsPage,
|
||||||
|
|
Loading…
Reference in New Issue