MOBILE-1501 participants: Allow searching participants

main
Dani Palou 2019-10-24 10:54:06 +02:00
parent 0d945fa8ad
commit 4b3c18d03d
4 changed files with 184 additions and 14 deletions

View File

@ -2,10 +2,10 @@
<form #f="ngForm" (ngSubmit)="submitForm($event)" role="search">
<ion-item>
<ion-input type="search" name="search" [(ngModel)]="searchText" [placeholder]="placeholder" [autocorrect]="autocorrect" [spellcheck]="spellcheck" [core-auto-focus]="autoFocus" [disabled]="disabled" role="searchbox"></ion-input>
<button item-end ion-button clear icon-only type="submit" class="button-small" [attr.aria-label]="searchLabel" [disabled]="!searchText || (searchText.length < lengthCheck)" [disabled]="disabled">
<button item-end ion-button clear icon-only type="submit" class="button-small" [attr.aria-label]="searchLabel" [disabled]="disabled || !searchText || (searchText.length < lengthCheck)">
<ion-icon name="search"></ion-icon>
</button>
<button *ngIf="showClear" item-end ion-button clear icon-only class="button-small" [attr.aria-label]="'core.clearsearch' | translate" [disabled]="!searched" (click)="clearForm()" [disabled]="disabled">
<button *ngIf="showClear" item-end ion-button clear icon-only class="button-small" [attr.aria-label]="'core.clearsearch' | translate" [disabled]="!searched || disabled" (click)="clearForm()">
<ion-icon name="close"></ion-icon>
</button>
</ion-item>

View File

@ -1,21 +1,37 @@
<core-navbar-buttons>
<button [hidden]="!canSearch" ion-button icon-only (click)="toggleSearch()" [attr.aria-label]="'core.search' | translate">
<ion-icon name="search"></ion-icon>
</button>
</core-navbar-buttons>
<core-split-view>
<ion-content>
<ion-refresher [enabled]="participantsLoaded" (ionRefresh)="refreshParticipants($event)">
<ion-refresher [enabled]="participantsLoaded && !displaySearchResult" (ionRefresh)="refreshParticipants($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-search-box *ngIf="showSearchBox" (onSubmit)="search($event)" (onClear)="clearSearch($event)" [disabled]="disableSearch" autocorrect="off" [spellcheck]="false" [autoFocus]="true" [lengthCheck]="1"></core-search-box>
<core-loading [hideUntil]="participantsLoaded">
<core-empty-box *ngIf="participants && participants.length == 0" icon="person" [message]="'core.user.noparticipants' | translate">
<core-empty-box *ngIf="!displaySearchResults && !participants.length" icon="person" [message]="'core.user.noparticipants' | translate">
</core-empty-box>
<core-empty-box *ngIf="displaySearchResults && !participants.length" icon="search" [message]="'core.noresults' | translate"></core-empty-box>
<ion-list *ngIf="participants && participants.length > 0" no-margin>
<a ion-item text-wrap *ngFor="let participant of participants" [title]="participant.fullname" (click)="gotoParticipant(participant.id)" [class.core-split-item-selected]="participant.id == participantId">
<ion-avatar core-user-avatar [user]="participant" item-start [userId]="participant.id" [checkOnline]="true"></ion-avatar>
<ng-container *ngIf="!displaySearchResults">
<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="displaySearchResults">
<h2><core-format-text [text]="participant.fullname" [highlight]="searchQuery" [filter]="false"></core-format-text></h2>
</ng-container>
</a>
</ion-list>
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMoreData($event)" [error]="loadMoreError"></core-infinite-loading>
<core-infinite-loading [enabled]="canLoadMore && participantsLoaded" (action)="loadMoreData($event)" [error]="loadMoreError"></core-infinite-loading>
</core-loading>
</ion-content>
</core-split-view>

View File

@ -17,6 +17,7 @@ import { Content } from 'ionic-angular';
import { CoreUserProvider } from '../../providers/user';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreAppProvider } from '@providers/app';
/**
* Component that displays the list of course participants.
@ -36,13 +37,24 @@ export class CoreUserParticipantsComponent implements OnInit {
canLoadMore = false;
loadMoreError = false;
participantsLoaded = false;
canSearch = false;
showSearchBox = false;
disableSearch = false;
displaySearchResults = false;
searchQuery = '';
constructor(private userProvider: CoreUserProvider, private domUtils: CoreDomUtilsProvider) { }
protected searchPage = 0;
constructor(private userProvider: CoreUserProvider,
private domUtils: CoreDomUtilsProvider,
private appProvider: CoreAppProvider) { }
/**
* View loaded.
*/
ngOnInit(): void {
this.canSearch = this.userProvider.canSearchParticipantsInSite();
// Get first participants.
this.fetchData(true).then(() => {
if (!this.participantId && this.splitviewCtrl.isOn() && this.participants.length > 0) {
@ -54,8 +66,6 @@ export class CoreUserParticipantsComponent implements OnInit {
// Ignore errors.
});
}).finally(() => {
this.participantsLoaded = true;
// Call resize to make infinite loading work, in some cases the content dimensions aren't read.
this.content && this.content.resize();
});
@ -81,6 +91,8 @@ export class CoreUserParticipantsComponent implements OnInit {
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error loading participants');
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
}).finally(() => {
this.participantsLoaded = true;
});
}
@ -91,10 +103,16 @@ export class CoreUserParticipantsComponent implements OnInit {
* @return Resolved when done.
*/
loadMoreData(infiniteComplete?: any): Promise<any> {
if (this.displaySearchResults) {
return this.search(this.searchQuery, true).finally(() => {
infiniteComplete && infiniteComplete();
});
} else {
return this.fetchData().finally(() => {
infiniteComplete && infiniteComplete();
});
}
}
/**
* Refresh data.
@ -111,10 +129,83 @@ export class CoreUserParticipantsComponent implements OnInit {
/**
* Navigate to a particular user profile.
*
* @param userId User Id where to navigate.
*/
gotoParticipant(userId: number): void {
this.participantId = userId;
this.splitviewCtrl.push('CoreUserProfilePage', {userId: userId, courseId: this.courseId});
}
/**
* Show or hide search box.
*/
toggleSearch(): void {
this.showSearchBox = !this.showSearchBox;
if (!this.showSearchBox && this.displaySearchResults) {
this.clearSearch();
}
}
/**
* Clear search.
*/
clearSearch(): void {
if (!this.displaySearchResults) {
// Nothing to clear.
return;
}
this.searchQuery = '';
this.displaySearchResults = false;
this.participants = [];
this.searchPage = 0;
this.splitviewCtrl.emptyDetails();
// Remove search results and display all participants.
this.participantsLoaded = false;
this.fetchData(true);
}
/**
* Start a new search or load more results.
*
* @param query Text to search for.
* @param loadMore Whether it's loading more or doing a new search.
* @return Resolved when done.
*/
search(query: string, loadMore?: boolean): Promise<any> {
this.appProvider.closeKeyboard();
this.disableSearch = true;
this.participantsLoaded = loadMore;
this.loadMoreError = false;
if (!loadMore) {
this.participantsLoaded = false;
this.searchQuery = query;
this.searchPage = 0;
this.participants = [];
}
return this.userProvider.searchParticipants(this.courseId, query, true, this.searchPage).then((result) => {
this.participants.push(...result.participants);
this.canLoadMore = result.canLoadMore;
this.searchPage++;
if (!loadMore && this.participants.length) {
this.gotoParticipant(this.participants[0].id);
}
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error searching users.');
this.loadMoreError = true;
}).finally(() => {
this.disableSearch = false;
this.participantsLoaded = true;
this.displaySearchResults = true;
});
}
}

View File

@ -69,6 +69,32 @@ export class CoreUserProvider {
this.sitesProvider.registerSiteSchema(this.siteSchema);
}
/**
* Check if WS to search participants is available in site.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: whether it's available.
* @since 3.8
*/
canSearchParticipants(siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
return this.canSearchParticipantsInSite(site);
});
}
/**
* Check if WS to search participants is available in site.
*
* @param site Site. If not defined, current site.
* @return Whether it's available.
* @since 3.8
*/
canSearchParticipantsInSite(site?: CoreSite): boolean {
site = site || this.sitesProvider.getCurrentSite();
return site.wsAvailable('core_enrol_search_users');
}
/**
* Change the given user profile picture.
*
@ -505,6 +531,43 @@ export class CoreUserProvider {
return Promise.all(promises);
}
/**
* Search participants in a certain course.
*
* @param courseId ID of the course.
* @param search The string to search.
* @param searchAnywhere Whether to find a match anywhere or only at the beginning.
* @param page Page to get.
* @param limitNumber Number of participants to get.
* @param siteId Site Id. If not defined, use current site.
* @return Promise resolved when the participants are retrieved.
* @since 3.8
*/
searchParticipants(courseId: number, search: string, searchAnywhere: boolean = true, page: number = 0,
perPage: number = CoreUserProvider.PARTICIPANTS_LIST_LIMIT, siteId?: string)
: Promise<{participants: any[], canLoadMore: boolean}> {
return this.sitesProvider.getSite(siteId).then((site) => {
const data = {
courseid: courseId,
search: search,
searchanywhere: searchAnywhere ? 1 : 0,
page: page,
perpage: perPage,
}, preSets: any = {
getFromCache: false // Always try to get updated data. If it fails, it will get it from cache.
};
return site.read('core_enrol_search_users', data, preSets).then((users) => {
const canLoadMore = users.length >= perPage;
this.storeUsers(users, siteId);
return { participants: users, canLoadMore: canLoadMore };
});
});
}
/**
* Store user basic information in local DB to be retrieved if the WS call fails.
*