MOBILE-1501 participants: Allow searching participants
parent
0d945fa8ad
commit
4b3c18d03d
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue