diff --git a/scripts/langindex.json b/scripts/langindex.json
index 369488b84..679e4d559 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -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",
diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json
index 6fc2be13d..69730eab9 100644
--- a/src/assets/lang/en.json
+++ b/src/assets/lang/en.json
@@ -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",
diff --git a/src/core/tag/lang/en.json b/src/core/tag/lang/en.json
index 02e288849..b7f8b8794 100644
--- a/src/core/tag/lang/en.json
+++ b/src/core/tag/lang/en.json
@@ -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."
}
diff --git a/src/core/tag/pages/index-area/index-area.html b/src/core/tag/pages/index-area/index-area.html
new file mode 100644
index 000000000..8a43d2d51
--- /dev/null
+++ b/src/core/tag/pages/index-area/index-area.html
@@ -0,0 +1,16 @@
+
+
+ {{ 'core.tag.itemstaggedwith' | translate: { $a: {tagarea: areaNameKey | translate, tag: tagName} } }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/core/tag/pages/index-area/index-area.module.ts b/src/core/tag/pages/index-area/index-area.module.ts
new file mode 100644
index 000000000..87a49cd7f
--- /dev/null
+++ b/src/core/tag/pages/index-area/index-area.module.ts
@@ -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 {}
diff --git a/src/core/tag/pages/index-area/index-area.ts b/src/core/tag/pages/index-area/index-area.ts
new file mode 100644
index 000000000..9f71f0532
--- /dev/null
+++ b/src/core/tag/pages/index-area/index-area.ts
@@ -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;
+ 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} Resolved when done.
+ */
+ fetchData(refresh: boolean = false): Promise {
+ 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} Resolved when done.
+ */
+ loadMore(infiniteComplete: any): Promise {
+ 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();
+ });
+ });
+ }
+}
diff --git a/src/core/tag/pages/index/index.html b/src/core/tag/pages/index/index.html
new file mode 100644
index 000000000..5174fcd7c
--- /dev/null
+++ b/src/core/tag/pages/index/index.html
@@ -0,0 +1,24 @@
+
+
+ {{ 'core.tag.tag' | translate }}: {{ tagName }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'core.tag.warningareasnotsupported' | translate }}
+
+
+ {{ area.nameKey | translate }}
+ {{ area.badge }}
+
+
+
+
+
diff --git a/src/core/tag/pages/index/index.module.ts b/src/core/tag/pages/index/index.module.ts
new file mode 100644
index 000000000..bb3cd138d
--- /dev/null
+++ b/src/core/tag/pages/index/index.module.ts
@@ -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 {}
diff --git a/src/core/tag/pages/index/index.ts b/src/core/tag/pages/index/index.ts
new file mode 100644
index 000000000..9185bae0f
--- /dev/null
+++ b/src/core/tag/pages/index/index.ts
@@ -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} Resolved when done.
+ */
+ fetchData(): Promise {
+ 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);
+ }
+}
diff --git a/src/core/tag/providers/tag.ts b/src/core/tag/providers/tag.ts
index fdcdaf189..88b739f04 100644
--- a/src/core/tag/providers/tag.ts
+++ b/src/core/tag/providers/tag.ts
@@ -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} 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 {
+ 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} 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 {
+ 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);
+ }
}