MOBILE-2201 tag: Index page

main
Albert Gasset 2019-07-08 12:32:05 +02:00
parent 353c6823db
commit b2db3774e6
10 changed files with 538 additions and 3 deletions

View File

@ -1800,11 +1800,15 @@
"core.submit": "moodle",
"core.success": "moodle",
"core.tablet": "local_moodlemobileapp",
"core.tag.errorareanotsupported": "local_moodlemobileapp",
"core.tag.itemstaggedwith": "moodle",
"core.tag.tag": "moodle",
"core.tag.tagarea_course": "moodle",
"core.tag.tagarea_course_modules": "moodle",
"core.tag.tagarea_post": "moodle",
"core.tag.tagarea_user": "moodle",
"core.tag.tags": "moodle",
"core.tag.warningareasnotsupported": "local_moodlemobileapp",
"core.teachers": "moodle",
"core.thereisdatatosync": "local_moodlemobileapp",
"core.thisdirection": "langconfig",

View File

@ -1800,11 +1800,15 @@
"core.submit": "Submit",
"core.success": "Success",
"core.tablet": "Tablet",
"core.tag.errorareanotsupported": "This tag area is not supported by the app.",
"core.tag.itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"",
"core.tag.tag": "Tag",
"core.tag.tagarea_course": "Courses",
"core.tag.tagarea_course_modules": "Activities and resources",
"core.tag.tagarea_post": "Blog posts",
"core.tag.tagarea_user": "User interests",
"core.tag.tags": "Tags",
"core.tag.warningareasnotsupported": "Some of the tag areas are not displayed because they are not supported by the app.",
"core.teachers": "Teachers",
"core.thereisdatatosync": "There are offline {{$a}} to be synchronised.",
"core.thisdirection": "ltr",

View File

@ -1,7 +1,11 @@
{
"errorareanotsupported": "This tag area is not supported by the app.",
"itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"",
"tag": "Tag",
"tagarea_course": "Courses",
"tagarea_course_modules": "Activities and resources",
"tagarea_post": "Blog posts",
"tagarea_user": "User interests",
"tags": "Tags"
"tags": "Tags",
"warningareasnotsupported": "Some of the tag areas are not displayed because they are not supported by the app."
}

View File

@ -0,0 +1,16 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'core.tag.itemstaggedwith' | translate: { $a: {tagarea: areaNameKey | translate, tag: tagName} } }}</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>
<core-loading [hideUntil]="loaded">
<ng-container *ngIf="loaded">
<core-dynamic-component [component]="areaComponent" [data]="{items: items}"></core-dynamic-component>
</ng-container>
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError"></core-infinite-loading>
</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 { CoreTagIndexAreaPage } from './index-area';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
CoreTagIndexAreaPage
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(CoreTagIndexAreaPage),
TranslateModule.forChild()
],
})
export class CoreTagIndexAreaPageModule {}

View File

@ -0,0 +1,150 @@
// (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, Injector } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTagProvider } from '@core/tag/providers/tag';
import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
/**
* Page that displays the tag index area.
*/
@IonicPage({ segment: 'core-tag-index-area' })
@Component({
selector: 'page-core-tag-index-area',
templateUrl: 'index-area.html',
})
export class CoreTagIndexAreaPage {
tagId: number;
tagName: string;
collectionId: number;
areaId: number;
fromContextId: number;
contextId: number;
recursive: boolean;
areaNameKey: string;
loaded = false;
componentName: string;
itemType: string;
items = [];
nextPage = 0;
canLoadMore = false;
areaComponent: any;
loadMoreError = false;
constructor(navParams: NavParams, private injector: Injector, private translate: TranslateService,
private tagProvider: CoreTagProvider, private domUtils: CoreDomUtilsProvider,
private tagAreaDelegate: CoreTagAreaDelegate) {
this.tagId = navParams.get('tagId');
this.tagName = navParams.get('tagName');
this.collectionId = navParams.get('collectionId');
this.areaId = navParams.get('areaId');
this.fromContextId = navParams.get('fromContextId');
this.contextId = navParams.get('contextId');
this.recursive = navParams.get('recursive');
this.areaNameKey = navParams.get('areaNameKey');
// Pass the the following parameters to avoid fetching the first page.
this.componentName = navParams.get('componentName');
this.itemType = navParams.get('itemType');
this.items = navParams.get('items') || [];
this.nextPage = navParams.get('nextPage') || 0;
this.canLoadMore = !!navParams.get('canLoadMore');
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
let promise: Promise<any>;
if (!this.componentName || !this.itemType || !this.items.length || this.nextPage == 0) {
promise = this.fetchData(true);
} else {
promise = Promise.resolve();
}
promise.then(() => {
return this.tagAreaDelegate.getComponent(this.componentName, this.itemType, this.injector).then((component) => {
this.areaComponent = component;
});
}).finally(() => {
this.loaded = true;
});
}
/**
* Fetch next page of the tag index area.
*
* @param {boolean} [refresh=false] Whether to refresh the data or fetch a new page.
* @return {Promise<any>} Resolved when done.
*/
fetchData(refresh: boolean = false): Promise<any> {
this.loadMoreError = false;
const page = refresh ? 0 : this.nextPage;
return this.tagProvider.getTagIndexPerArea(this.tagId, this.tagName, this.collectionId, this.areaId, this.fromContextId,
this.contextId, this.recursive, page).then((areas) => {
const area = areas[0];
return this.tagAreaDelegate.parseContent(area.component, area.itemtype, area.content).then((items) => {
if (!items || !items.length) {
// Tag area not supported.
return Promise.reject(this.translate.instant('core.tag.errorareanotsupported'));
}
if (page == 0) {
this.items = items;
} else {
this.items.push(...items);
}
this.componentName = area.component;
this.itemType = area.itemtype;
this.areaNameKey = this.tagAreaDelegate.getDisplayNameKey(area.component, area.itemtype);
this.canLoadMore = !!area.nextpageurl;
this.nextPage = page + 1;
});
}).catch((error) => {
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
this.domUtils.showErrorModalDefault(error, 'Error loading tag index');
});
}
/**
* Load more items.
*
* @param {any} infiniteComplete Infinite scroll complete function.
* @return {Promise<any>} Resolved when done.
*/
loadMore(infiniteComplete: any): Promise<any> {
return this.fetchData().finally(() => {
infiniteComplete();
});
}
/**
* Refresh data.
*
* @param {any} refresher Refresher.
*/
refreshData(refresher: any): void {
this.tagProvider.invalidateTagIndexPerArea(this.tagId, this.tagName, this.collectionId, this.areaId, this.fromContextId,
this.contextId, this.recursive).finally(() => {
this.fetchData(true).finally(() => {
refresher.complete();
});
});
}
}

View File

@ -0,0 +1,24 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'core.tag.tag' | translate }}: {{ tagName }}</ion-title>
</ion-navbar>
</ion-header>
<core-split-view>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshData($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ion-list>
<ion-item text-wrap *ngIf="hasUnsupportedAreas" class="core-warning-item">
<ion-icon item-start name="warning" color="warning"></ion-icon>
{{ 'core.tag.warningareasnotsupported' | translate }}
</ion-item>
<a ion-item text-wrap *ngFor="let area of areas" [title]="area.nameKey | translate" (click)="openArea(area)" [class.core-split-item-selected]="area.id == selectedAreaId">
<h2>{{ area.nameKey | translate }}</h2>
<ion-badge item-end *ngIf="area.badge">{{ area.badge }}</ion-badge>
</a>
</ion-list>
</core-loading>
</ion-content>
</core-split-view>

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 { CoreTagIndexPage } from './index';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
CoreTagIndexPage
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(CoreTagIndexPage),
TranslateModule.forChild()
],
})
export class CoreTagIndexPageModule {}

View File

@ -0,0 +1,154 @@
// (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, ViewChild } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreTagProvider } from '@core/tag/providers/tag';
import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
/**
* Page that displays the tag index.
*/
@IonicPage({ segment: 'core-tag-index' })
@Component({
selector: 'page-core-tag-index',
templateUrl: 'index.html',
})
export class CoreTagIndexPage {
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
tagId: number;
tagName: string;
collectionId: number;
areaId: number;
fromContextId: number;
contextId: number;
recursive: boolean;
loaded = false;
areas: Array<{
id: number,
componentName: string,
itemType: string,
nameKey: string,
items: any[],
canLoadMore: boolean,
badge: string
}>;
selectedAreaId: number;
hasUnsupportedAreas = false;
constructor(navParams: NavParams, private tagProvider: CoreTagProvider, private domUtils: CoreDomUtilsProvider,
private tagAreaDelegate: CoreTagAreaDelegate) {
this.tagId = navParams.get('tagId') || 0;
this.tagName = navParams.get('tagName') || '';
this.collectionId = navParams.get('collectionId');
this.areaId = navParams.get('areaId') || 0;
this.fromContextId = navParams.get('fromContextId') || 0;
this.contextId = navParams.get('contextId') || 0;
this.recursive = navParams.get('recursive') || true;
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.fetchData().then(() => {
if (this.splitviewCtrl.isOn() && this.areas && this.areas.length > 0) {
const area = this.areas.find((area) => area.id == this.areaId);
this.openArea(area || this.areas[0]);
}
}).finally(() => {
this.loaded = true;
});
}
/**
* Fetch first page of tag index per area.
*
* @return {Promise<any>} Resolved when done.
*/
fetchData(): Promise<any> {
return this.tagProvider.getTagIndexPerArea(this.tagId, this.tagName, this.collectionId, this.areaId, this.fromContextId,
this.contextId, this.recursive, 0).then((areas) => {
this.areas = [];
this.hasUnsupportedAreas = false;
return Promise.all(areas.map((area) => {
return this.tagAreaDelegate.parseContent(area.component, area.itemtype, area.content).then((items) => {
if (!items || !items.length) {
// Tag area not supported, skip.
this.hasUnsupportedAreas = true;
return null;
}
return {
id: area.ta,
componentName: area.component,
itemType: area.itemtype,
nameKey: this.tagAreaDelegate.getDisplayNameKey(area.component, area.itemtype),
items,
canLoadMore: !!area.nextpageurl,
badge: items && items.length ? items.length + (area.nextpageurl ? '+' : '') : '',
};
});
})).then((areas) => {
this.areas = areas.filter((area) => area != null);
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error loading tag index');
});
}
/**
* Refresh data.
*
* @param {any} refresher Refresher.
*/
refreshData(refresher: any): void {
this.tagProvider.invalidateTagIndexPerArea(this.tagId, this.tagName, this.collectionId, this.areaId, this.fromContextId,
this.contextId, this.recursive).finally(() => {
this.fetchData().finally(() => {
refresher.complete();
});
});
}
/**
* Navigate to an index area.
*
* @param {any} area Area.
*/
openArea(area: any): void {
this.selectedAreaId = area.id;
const params = {
tagId: this.tagId,
tagName: this.tagName,
collectionId: this.collectionId,
areaId: area.id,
fromContextId: this.fromContextId,
contextId: this.contextId,
recursive: this.recursive,
areaNameKey: area.nameKey,
componentName: area.component,
itemType: area.itemType,
items: area.items.slice(),
canLoadMore: area.canLoadMore,
nextPage: 1
};
this.splitviewCtrl.push('CoreTagIndexAreaPage', params);
}
}

View File

@ -13,8 +13,27 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreSitesProvider } from '@providers/sites';
import { CoreSite } from '@classes/site';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
/**
* Structure of a tag index returned by WS.
*/
export interface CoreTagIndex {
tagid: number;
ta: number;
component: string;
itemtype: string;
nextpageurl: string;
prevpageurl: string;
exclusiveurl: string;
exclusivetext: string;
title: string;
content: string;
hascontent: number;
anchor: string;
}
/**
* Structure of a tag item returned by WS.
@ -38,7 +57,9 @@ export interface CoreTagItem {
@Injectable()
export class CoreTagProvider {
constructor(private sitesProvider: CoreSitesProvider) {}
protected ROOT_CACHE_KEY = 'CoreTag:';
constructor(private sitesProvider: CoreSitesProvider, private translate: TranslateService) {}
/**
* Check whether tags are available in a certain site.
@ -67,4 +88,96 @@ export class CoreTagProvider {
site.wsAvailable('core_tag_get_tag_collections') &&
!site.isFeatureDisabled('NoDelegate_CoreTag');
}
/**
* Fetch the tag index.
*
* @param {number} [id=0] Tag ID.
* @param {string} [name=''] Tag name.
* @param {number} [collectionId=0] Tag collection ID.
* @param {number} [areaId=0] Tag area ID.
* @param {number} [fromContextId=0] Context ID where the link was displayed.
* @param {number} [contextId=0] Context ID where to search for items.
* @param {boolean} [recursive=true] Search in the context and its children.
* @param {number} [page=0] Page number.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<CoreTagIndex[]>} Promise resolved with the tag index per area.
* @since 3.7
*/
getTagIndexPerArea(id: number, name: string = '', collectionId: number = 0, areaId: number = 0, fromContextId: number = 0,
contextId: number = 0, recursive: boolean = true, page: number = 0, siteId?: string): Promise<CoreTagIndex[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
tagindex: {
id: id,
tag: name,
tc: collectionId,
ta: areaId,
excl: true,
from: fromContextId,
ctx: contextId,
rec: recursive,
page: page
},
};
const preSets: CoreSiteWSPreSets = {
updateFrequency: CoreSite.FREQUENCY_OFTEN,
cacheKey: this.getTagIndexPerAreaKey(id, name, collectionId, areaId, fromContextId, contextId, recursive)
};
return site.read('core_tag_get_tagindex_per_area', params, preSets).catch((error) => {
// Workaround for WS not passing parameter to error string.
if (error && error.errorcode == 'notagsfound') {
error.message = this.translate.instant('core.tag.notagsfound', {$a: name || id || ''});
}
return Promise.reject(error);
}).then((response) => {
if (!response || !response.length) {
return Promise.reject(null);
}
return response;
});
});
}
/**
* Invalidate tag index.
*
* @param {number} [id=0] Tag ID.
* @param {string} [name=''] Tag name.
* @param {number} [collectionId=0] Tag collection ID.
* @param {number} [areaId=0] Tag area ID.
* @param {number} [fromContextId=0] Context ID where the link was displayed.
* @param {number} [contextId=0] Context ID where to search for items.
* @param {boolean} [recursive=true] Search in the context and its children.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateTagIndexPerArea(id: number, name: string = '', collectionId: number = 0, areaId: number = 0,
fromContextId: number = 0, contextId: number = 0, recursive: boolean = true, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const key = this.getTagIndexPerAreaKey(id, name, collectionId, areaId, fromContextId, contextId, recursive);
return site.invalidateWsCacheForKey(key);
});
}
/**
* Get cache key for tag index.
*
* @param {number} id Tag ID.
* @param {string} name Tag name.
* @param {number} collectionId Tag collection ID.
* @param {number} areaId Tag area ID.
* @param {number} fromContextId Context ID where the link was displayed.
* @param {number} contextId Context ID where to search for items.
* @param {boolean} [recursive=true] Search in the context and its children.
* @return {string} Cache key.
*/
protected getTagIndexPerAreaKey(id: number, name: string, collectionId: number, areaId: number, fromContextId: number,
contextId: number, recursive: boolean): string {
return this.ROOT_CACHE_KEY + 'index:' + id + ':' + name + ':' + collectionId + ':' + areaId + ':' + fromContextId + ':'
+ contextId + ':' + (recursive ? 1 : 0);
}
}