MOBILE-1501 participants: Allow searching participants
parent
0d945fa8ad
commit
4b3c18d03d
|
@ -2,10 +2,10 @@
|
||||||
<form #f="ngForm" (ngSubmit)="submitForm($event)" role="search">
|
<form #f="ngForm" (ngSubmit)="submitForm($event)" role="search">
|
||||||
<ion-item>
|
<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>
|
<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>
|
<ion-icon name="search"></ion-icon>
|
||||||
</button>
|
</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>
|
<ion-icon name="close"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
</ion-item>
|
</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>
|
<core-split-view>
|
||||||
<ion-content>
|
<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-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
</ion-refresher>
|
</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-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>
|
||||||
|
|
||||||
|
<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>
|
<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">
|
<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>
|
<ion-avatar core-user-avatar [user]="participant" item-start [userId]="participant.id" [checkOnline]="true"></ion-avatar>
|
||||||
|
<ng-container *ngIf="!displaySearchResults">
|
||||||
<h2>{{ participant.fullname }}</h2>
|
<h2>{{ participant.fullname }}</h2>
|
||||||
<p *ngIf="participant.lastcourseaccess"><strong>{{ 'core.lastaccess' | translate }}: </strong>{{ participant.lastcourseaccess | coreTimeAgo }}</p>
|
<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>
|
<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>
|
</a>
|
||||||
</ion-list>
|
</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>
|
</core-loading>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
</core-split-view>
|
</core-split-view>
|
|
@ -17,6 +17,7 @@ import { Content } from 'ionic-angular';
|
||||||
import { CoreUserProvider } from '../../providers/user';
|
import { CoreUserProvider } from '../../providers/user';
|
||||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
|
import { CoreAppProvider } from '@providers/app';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays the list of course participants.
|
* Component that displays the list of course participants.
|
||||||
|
@ -36,13 +37,24 @@ export class CoreUserParticipantsComponent implements OnInit {
|
||||||
canLoadMore = false;
|
canLoadMore = false;
|
||||||
loadMoreError = false;
|
loadMoreError = false;
|
||||||
participantsLoaded = 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.
|
* View loaded.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.canSearch = this.userProvider.canSearchParticipantsInSite();
|
||||||
|
|
||||||
// Get first participants.
|
// Get first participants.
|
||||||
this.fetchData(true).then(() => {
|
this.fetchData(true).then(() => {
|
||||||
if (!this.participantId && this.splitviewCtrl.isOn() && this.participants.length > 0) {
|
if (!this.participantId && this.splitviewCtrl.isOn() && this.participants.length > 0) {
|
||||||
|
@ -54,8 +66,6 @@ export class CoreUserParticipantsComponent implements OnInit {
|
||||||
// Ignore errors.
|
// Ignore errors.
|
||||||
});
|
});
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.participantsLoaded = true;
|
|
||||||
|
|
||||||
// Call resize to make infinite loading work, in some cases the content dimensions aren't read.
|
// Call resize to make infinite loading work, in some cases the content dimensions aren't read.
|
||||||
this.content && this.content.resize();
|
this.content && this.content.resize();
|
||||||
});
|
});
|
||||||
|
@ -81,6 +91,8 @@ export class CoreUserParticipantsComponent implements OnInit {
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
this.domUtils.showErrorModalDefault(error, 'Error loading participants');
|
this.domUtils.showErrorModalDefault(error, 'Error loading participants');
|
||||||
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
|
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.
|
* @return Resolved when done.
|
||||||
*/
|
*/
|
||||||
loadMoreData(infiniteComplete?: any): Promise<any> {
|
loadMoreData(infiniteComplete?: any): Promise<any> {
|
||||||
|
if (this.displaySearchResults) {
|
||||||
|
return this.search(this.searchQuery, true).finally(() => {
|
||||||
|
infiniteComplete && infiniteComplete();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
return this.fetchData().finally(() => {
|
return this.fetchData().finally(() => {
|
||||||
infiniteComplete && infiniteComplete();
|
infiniteComplete && infiniteComplete();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh data.
|
* Refresh data.
|
||||||
|
@ -111,10 +129,83 @@ export class CoreUserParticipantsComponent implements OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to a particular user profile.
|
* Navigate to a particular user profile.
|
||||||
|
*
|
||||||
* @param userId User Id where to navigate.
|
* @param userId User Id where to navigate.
|
||||||
*/
|
*/
|
||||||
gotoParticipant(userId: number): void {
|
gotoParticipant(userId: number): void {
|
||||||
this.participantId = userId;
|
this.participantId = userId;
|
||||||
this.splitviewCtrl.push('CoreUserProfilePage', {userId: userId, courseId: this.courseId});
|
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);
|
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.
|
* Change the given user profile picture.
|
||||||
*
|
*
|
||||||
|
@ -505,6 +531,43 @@ export class CoreUserProvider {
|
||||||
return Promise.all(promises);
|
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.
|
* Store user basic information in local DB to be retrieved if the WS call fails.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue