MOBILE-2201 tag: Search page

main
Albert Gasset 2019-07-08 12:35:47 +02:00
parent b5b480e226
commit d00d401894
10 changed files with 544 additions and 1 deletions

View File

@ -1800,8 +1800,13 @@
"core.submit": "moodle", "core.submit": "moodle",
"core.success": "moodle", "core.success": "moodle",
"core.tablet": "local_moodlemobileapp", "core.tablet": "local_moodlemobileapp",
"core.tag.defautltagcoll": "moodle",
"core.tag.errorareanotsupported": "local_moodlemobileapp", "core.tag.errorareanotsupported": "local_moodlemobileapp",
"core.tag.inalltagcoll": "moodle",
"core.tag.itemstaggedwith": "moodle", "core.tag.itemstaggedwith": "moodle",
"core.tag.notagsfound": "moodle",
"core.tag.searchtags": "moodle",
"core.tag.showingfirsttags": "moodle",
"core.tag.tag": "moodle", "core.tag.tag": "moodle",
"core.tag.tagarea_course": "moodle", "core.tag.tagarea_course": "moodle",
"core.tag.tagarea_course_modules": "moodle", "core.tag.tagarea_course_modules": "moodle",

View File

@ -1800,8 +1800,13 @@
"core.submit": "Submit", "core.submit": "Submit",
"core.success": "Success", "core.success": "Success",
"core.tablet": "Tablet", "core.tablet": "Tablet",
"core.tag.defautltagcoll": "Default collection",
"core.tag.errorareanotsupported": "This tag area is not supported by the app.", "core.tag.errorareanotsupported": "This tag area is not supported by the app.",
"core.tag.inalltagcoll": "Everywhere",
"core.tag.itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"", "core.tag.itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"",
"core.tag.notagsfound": "No tags matching \"{{$a}}\" found",
"core.tag.searchtags": "Search tags",
"core.tag.showingfirsttags": "Showing {{$a}} most popular tags",
"core.tag.tag": "Tag", "core.tag.tag": "Tag",
"core.tag.tagarea_course": "Courses", "core.tag.tagarea_course": "Courses",
"core.tag.tagarea_course_modules": "Activities and resources", "core.tag.tagarea_course_modules": "Activities and resources",

View File

@ -1,6 +1,11 @@
{ {
"defautltagcoll": "Default collection",
"errorareanotsupported": "This tag area is not supported by the app.", "errorareanotsupported": "This tag area is not supported by the app.",
"inalltagcoll": "Everywhere",
"itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"", "itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"",
"notagsfound": "No tags matching \"{{$a}}\" found",
"searchtags": "Search tags",
"showingfirsttags": "Showing {{$a}} most popular tags",
"tag": "Tag", "tag": "Tag",
"tagarea_course": "Courses", "tagarea_course": "Courses",
"tagarea_course_modules": "Activities and resources", "tagarea_course_modules": "Activities and resources",

View File

@ -0,0 +1,37 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'core.tag.searchtags' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshData($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<ion-grid>
<ion-row>
<ion-col col-12 [attr.col-sm-6]="collections && collections.length > 1 ? '' : null">
<core-search-box (onSubmit)="searchTags($event)" (onClear)="searchTags('')" [initialSearch]="query" [disabled]="searching" autocorrect="off" [spellcheck]="false" [autoFocus]="true" [lengthCheck]="0"></core-search-box>
</ion-col>
<ion-col col-12 col-sm-6 *ngIf="collections && collections.length > 1">
<ion-select text-start [(ngModel)]="collectionId" (ngModelChange)="searchTags(query)" [disabled]="searching" interface="popover" class="core-button-select">
<ion-option [value]="0">{{ 'core.tag.inalltagcoll' | translate }}</ion-option>
<ion-option *ngFor="let collection of collections" [value]="collection.id">{{ collection.name }}</ion-option>
</ion-select>
</ion-col>
</ion-row>
</ion-grid>
<core-loading [hideUntil]="loaded && !searching">
<core-empty-box *ngIf="!cloud || !cloud.tags || !cloud.tags.length" icon="pricetags" [message]="'core.tag.notagsfound' | translate: {$a: query}"></core-empty-box>
<ng-container *ngIf="cloud && cloud.tags && cloud.tags.length > 0">
<div text-center class="core-tag-cloud">
<ion-badge *ngFor="let tag of cloud.tags" (click)="openTag(tag)" text-wrap>
<span [class]="'size' + tag.size" >{{ tag.name }}</span>
</ion-badge>
</div>
<p *ngIf="cloud.tags.length < cloud.totalcount" text-center>
{{ 'core.tag.showingfirsttags' | translate: {$a: cloud.tags.length} }}
</p>
</ng-container>
</core-loading>
</ion-content>

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreTagSearchPage } from './search';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
CoreTagSearchPage
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(CoreTagSearchPage),
TranslateModule.forChild()
],
})
export class CoreTagSerchPageModule {}

View File

@ -0,0 +1,95 @@
ion-app.app-root page-core-tag-search {
core-search-box ion-card {
width: 100% !important;
margin: 0 !important;
}
.core-tag-cloud ion-badge {
margin: 8px;
cursor: pointer;
.size20 {
font-size: 3.4rem;
}
.size19 {
font-size: 3.3rem;
}
.size18 {
font-size: 3.2rem;
}
.size17 {
font-size: 3.1rem;
}
.size16 {
font-size: 3rem;
}
.size15 {
font-size: 2.9rem;
}
.size14 {
font-size: 2.8rem;
}
.size13 {
font-size: 2.7rem;
}
.size12 {
font-size: 2.6rem;
}
.size11 {
font-size: 2.5rem;
}
.size10 {
font-size: 2.4rem;
}
.size9 {
font-size: 2.3rem;
}
.size8 {
font-size: 2.2rem;
}
.size7 {
font-size: 2.1rem;
}
.size6 {
font-size: 2rem;
}
.size5 {
font-size: 1.9rem;
}
.size4 {
font-size: 1.8rem;
}
.size3 {
font-size: 1.7rem;
}
.size2 {
font-size: 1.6rem;
}
.size1 {
font-size: 1.5rem;
}
.size0 {
font-size: 1.4rem;
}
}
}

View File

@ -0,0 +1,135 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { IonicPage, NavParams, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreTagProvider, CoreTagCloud, CoreTagCollection, CoreTagCloudTag } from '@core/tag/providers/tag';
/**
* Page that displays most used tags and allows searching.
*/
@IonicPage({ segment: 'core-tag-search' })
@Component({
selector: 'page-core-tag-search',
templateUrl: 'search.html',
})
export class CoreTagSearchPage {
collectionId: number;
query: string;
collections: CoreTagCollection[] = [];
cloud: CoreTagCloud;
loaded = false;
searching = false;
constructor(private navCtrl: NavController, navParams: NavParams, private appProvider: CoreAppProvider,
private translate: TranslateService, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider,
private textUtils: CoreTextUtilsProvider, private contentLinksHelper: CoreContentLinksHelperProvider,
private tagProvider: CoreTagProvider) {
this.collectionId = navParams.get('collectionId') || 0;
this.query = navParams.get('query') || '';
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.fetchData().finally(() => {
this.loaded = true;
});
}
fetchData(): Promise<any> {
return Promise.all([
this.fetchCollections(),
this.fetchTags()
]).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error loading tags.');
});
}
/**
* Fetch tag collections.
*
* @return {Promise<any>} Resolved when done.
*/
fetchCollections(): Promise<any> {
return this.tagProvider.getTagCollections().then((collections) => {
collections.forEach((collection) => {
if (!collection.name && collection.isdefault) {
collection.name = this.translate.instant('core.tag.defautltagcoll');
}
});
this.collections = collections;
});
}
/**
* Fetch tags.
*
* @return {Promise<any>} Resolved when done.
*/
fetchTags(): Promise<any> {
return this.tagProvider.getTagCloud(this.collectionId, undefined, undefined, this.query).then((cloud) => {
this.cloud = cloud;
});
}
/**
* Go to tag index page.
*/
openTag(tag: CoreTagCloudTag): void {
const url = this.textUtils.decodeURI(tag.viewurl);
this.contentLinksHelper.handleLink(url, undefined, this.navCtrl);
}
/**
* Refresh data.
*
* @param {any} refresher Refresher.
*/
refreshData(refresher: any): void {
this.utils.allPromises([
this.tagProvider.invalidateTagCollections(),
this.tagProvider.invalidateTagCloud(this.collectionId, undefined, undefined, this.query),
]).finally(() => {
return this.fetchData().finally(() => {
refresher.complete();
});
});
}
/**
* Search tags.
*
* @param {string} query Search query.
* @return {Promise<any>} Resolved when done.
*/
searchTags(query: string): Promise<any> {
this.searching = true;
this.query = query;
this.appProvider.closeKeyboard();
return this.fetchTags().catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error loading tags.');
}).finally(() => {
this.searching = false;
});
}
}

View File

@ -0,0 +1,59 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreTagProvider } from './tag';
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate';
import { CoreUtilsProvider } from '@providers/utils/utils';
/**
* Handler to inject an option into main menu.
*/
@Injectable()
export class CoreTagMainMenuHandler implements CoreMainMenuHandler {
name = 'CoreTag';
priority = 300;
constructor(private tagProvider: CoreTagProvider, private utils: CoreUtilsProvider) { }
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean | Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return this.tagProvider.areTagsAvailable().then((available) => {
if (!available) {
return false;
}
// The only way to check whether tags are enabled on web is to perform a WS call.
return this.utils.promiseWorks(this.tagProvider.getTagCollections());
});
}
/**
* Returns the data needed to render the handler.
*
* @return {CoreMainMenuHandlerData} Data needed to render the handler.
*/
getDisplayData(): CoreMainMenuHandlerData {
return {
icon: 'pricetags',
title: 'core.tag.tags',
page: 'CoreTagSearchPage',
class: 'core-tag-search-handler'
};
}
}

View File

@ -17,6 +17,40 @@ import { TranslateService } from '@ngx-translate/core';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
/**
* Structure of a tag cloud returned by WS.
*/
export interface CoreTagCloud {
tags: CoreTagCloudTag[];
tagscount: number;
totalcount: number;
}
/**
* Structure of a tag cloud tag returned by WS.
*/
export interface CoreTagCloudTag {
name: string;
viewurl: string;
flag: boolean;
isstandard: boolean;
count: number;
size: number;
}
/**
* Structure of a tag collection returned by WS.
*/
export interface CoreTagCollection {
id: number;
name: string;
isdefault: boolean;
component: string;
sortoder: number;
searchable: boolean;
customurl: string;
}
/** /**
* Structure of a tag index returned by WS. * Structure of a tag index returned by WS.
*/ */
@ -57,6 +91,8 @@ export interface CoreTagItem {
@Injectable() @Injectable()
export class CoreTagProvider { export class CoreTagProvider {
static SEARCH_LIMIT = 150;
protected ROOT_CACHE_KEY = 'CoreTag:'; protected ROOT_CACHE_KEY = 'CoreTag:';
constructor(private sitesProvider: CoreSitesProvider, private translate: TranslateService) {} constructor(private sitesProvider: CoreSitesProvider, private translate: TranslateService) {}
@ -89,6 +125,71 @@ export class CoreTagProvider {
!site.isFeatureDisabled('NoDelegate_CoreTag'); !site.isFeatureDisabled('NoDelegate_CoreTag');
} }
/**
* Fetch the tag cloud.
*
* @param {number} [collectionId=0] Tag collection ID.
* @param {boolean} [isStandard=false] Whether to return only standard tags.
* @param {string} [sort='name'] Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid).
* @param {string} [search=''] Search string.
* @param {number} [fromContextId=0] Context ID where this tag cloud is displayed.
* @param {number} [contextId=0] Only retrieve tag instances in this context.
* @param {boolean} [recursive=true] Retrieve tag instances in the context and its children.
* @param {number} [limit] Maximum number of tags to retrieve. Defaults to SEARCH_LIMIT.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<CoreTagCloud>} Promise resolved with the tag cloud.
* @since 3.7
*/
getTagCloud(collectionId: number = 0, isStandard: boolean = false, sort: string = 'name', search: string = '',
fromContextId: number = 0, contextId: number = 0, recursive: boolean = true, limit?: number, siteId?: string):
Promise<CoreTagCloud> {
limit = limit || CoreTagProvider.SEARCH_LIMIT;
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
tagcollid: collectionId,
isstandard: isStandard,
limit: limit,
sort: sort,
search: search,
fromctx: fromContextId,
ctx: contextId,
rec: recursive
};
const preSets: CoreSiteWSPreSets = {
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
cacheKey: this.getTagCloudKey(collectionId, isStandard, sort, search, fromContextId, contextId, recursive),
getFromCache: search != '' // Try to get updated data when searching.
};
return site.read('core_tag_get_tag_cloud', params, preSets);
});
}
/**
* Fetch the tag collections.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<CoreTagCollection[]>} Promise resolved with the tag collections.
* @since 3.7
*/
getTagCollections(siteId?: string): Promise<CoreTagCollection[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const preSets: CoreSiteWSPreSets = {
updateFrequency: CoreSite.FREQUENCY_RARELY,
cacheKey: this.getTagCollectionsKey()
};
return site.read('core_tag_get_tag_collections', null, preSets).then((response) => {
if (!response || !response.collections) {
return Promise.reject(null);
}
return response.collections;
});
});
}
/** /**
* Fetch the tag index. * Fetch the tag index.
* *
@ -142,6 +243,40 @@ export class CoreTagProvider {
}); });
} }
/**
* Invalidate tag cloud.
*
* @param {number} [collectionId=0] Tag collection ID.
* @param {boolean} [isStandard=false] Whether to return only standard tags.
* @param {string} [sort='name'] Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid).
* @param {string} [search=''] Search string.
* @param {number} [fromContextId=0] Context ID where this tag cloud is displayed.
* @param {number} [contextId=0] Only retrieve tag instances in this context.
* @param {boolean} [recursive=true] Retrieve tag instances in the context and its children.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateTagCloud(collectionId: number = 0, isStandard: boolean = false, sort: string = 'name', search: string = '',
fromContextId: number = 0, contextId: number = 0, recursive: boolean = true, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const key = this.getTagCloudKey(collectionId, isStandard, sort, search, fromContextId, contextId, recursive);
return site.invalidateWsCacheForKey(key);
});
}
/**
* Invalidate tag collections.
*
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateTagCollections(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const key = this.getTagCollectionsKey();
return site.invalidateWsCacheForKey(key);
});
}
/** /**
* Invalidate tag index. * Invalidate tag index.
* *
@ -163,6 +298,33 @@ export class CoreTagProvider {
}); });
} }
/**
* Get cache key for tag cloud.
*
* @param {number} collectionId Tag collection ID.
* @param {boolean} isStandard Whether to return only standard tags.
* @param {string} sort Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid).
* @param {string} search Search string.
* @param {number} fromContextId Context ID where this tag cloud is displayed.
* @param {number} contextId Only retrieve tag instances in this context.
* @param {boolean} recursive Retrieve tag instances in the context and it's children.
* @return {string} Cache key.
*/
protected getTagCloudKey(collectionId: number, isStandard: boolean, sort: string, search: string, fromContextId: number,
contextId: number, recursive: boolean): string {
return this.ROOT_CACHE_KEY + 'cloud:' + collectionId + ':' + (isStandard ? 1 : 0) + ':' + sort + ':' + search + ':' +
fromContextId + ':' + contextId + ':' + (recursive ? 1 : 0);
}
/**
* Get cache key for tag collections.
*
* @return {string} Cache key.
*/
protected getTagCollectionsKey(): string {
return this.ROOT_CACHE_KEY + 'collections';
}
/** /**
* Get cache key for tag index. * Get cache key for tag index.
* *

View File

@ -13,9 +13,11 @@
// limitations under the License. // limitations under the License.
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate';
import { CoreTagProvider } from './providers/tag'; import { CoreTagProvider } from './providers/tag';
import { CoreTagHelperProvider } from './providers/helper'; import { CoreTagHelperProvider } from './providers/helper';
import { CoreTagAreaDelegate } from './providers/area-delegate'; import { CoreTagAreaDelegate } from './providers/area-delegate';
import { CoreTagMainMenuHandler } from './providers/mainmenu-handler';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -25,8 +27,13 @@ import { CoreTagAreaDelegate } from './providers/area-delegate';
providers: [ providers: [
CoreTagProvider, CoreTagProvider,
CoreTagHelperProvider, CoreTagHelperProvider,
CoreTagAreaDelegate CoreTagAreaDelegate,
CoreTagMainMenuHandler
] ]
}) })
export class CoreTagModule { export class CoreTagModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreTagMainMenuHandler) {
mainMenuDelegate.registerHandler(mainMenuHandler);
}
} }