Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/search-box/core-search-box.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
28 changes: 22 additions & 6 deletions src/core/user/components/participants/core-user-participants.html
Original file line number Diff line number Diff line change
@@ -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>
<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 *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>
103 changes: 97 additions & 6 deletions src/core/user/components/participants/participants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand All @@ -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();
});
Expand All @@ -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;
});
}

Expand All @@ -91,9 +103,15 @@ export class CoreUserParticipantsComponent implements OnInit {
* @return Resolved when done.
*/
loadMoreData(infiniteComplete?: any): Promise<any> {
return this.fetchData().finally(() => {
infiniteComplete && infiniteComplete();
});
if (this.displaySearchResults) {
return this.search(this.searchQuery, true).finally(() => {
infiniteComplete && infiniteComplete();
});
} else {
return this.fetchData().finally(() => {
infiniteComplete && infiniteComplete();
});
}
}

/**
Expand All @@ -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;
});
}
}
63 changes: 63 additions & 0 deletions src/core/user/providers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down