From 49baacf9044fbdde2d81e6166aafe0ca0896f5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 10 Nov 2020 16:43:36 +0100 Subject: [PATCH 1/5] MOBILE-3591 tags: Migrate tags structure --- src/core/features/features.module.ts | 2 + .../fileuploader/services/handlers/audio.ts | 2 +- .../components/search-box/search-box.scss | 4 + src/core/features/tag/lang/en.json | 17 + .../tag/pages/index-area/index-area.html | 22 + .../index-area/index-area.page.module.ts | 47 ++ .../tag/pages/index-area/index-area.page.ts | 175 +++++++ src/core/features/tag/pages/index/index.html | 31 ++ .../tag/pages/index/index.page.module.ts | 47 ++ .../features/tag/pages/index/index.page.ts | 186 ++++++++ .../features/tag/pages/search/search.html | 45 ++ .../tag/pages/search/search.page.module.ts | 50 ++ .../features/tag/pages/search/search.page.ts | 142 ++++++ .../features/tag/pages/search/search.scss | 95 ++++ .../tag/services/handlers/index.link.ts | 86 ++++ .../tag/services/handlers/search.link.ts | 68 +++ .../tag/services/handlers/tag.mainmenu.ts | 63 +++ .../tag/services/tag-area-delegate.ts | 101 ++++ src/core/features/tag/services/tag-helper.ts | 96 ++++ src/core/features/tag/services/tag.ts | 442 ++++++++++++++++++ src/core/features/tag/tag-lazy.module.ts | 42 ++ src/core/features/tag/tag.module.ts | 48 ++ 22 files changed, 1810 insertions(+), 1 deletion(-) create mode 100644 src/core/features/tag/lang/en.json create mode 100644 src/core/features/tag/pages/index-area/index-area.html create mode 100644 src/core/features/tag/pages/index-area/index-area.page.module.ts create mode 100644 src/core/features/tag/pages/index-area/index-area.page.ts create mode 100644 src/core/features/tag/pages/index/index.html create mode 100644 src/core/features/tag/pages/index/index.page.module.ts create mode 100644 src/core/features/tag/pages/index/index.page.ts create mode 100644 src/core/features/tag/pages/search/search.html create mode 100644 src/core/features/tag/pages/search/search.page.module.ts create mode 100644 src/core/features/tag/pages/search/search.page.ts create mode 100644 src/core/features/tag/pages/search/search.scss create mode 100644 src/core/features/tag/services/handlers/index.link.ts create mode 100644 src/core/features/tag/services/handlers/search.link.ts create mode 100644 src/core/features/tag/services/handlers/tag.mainmenu.ts create mode 100644 src/core/features/tag/services/tag-area-delegate.ts create mode 100644 src/core/features/tag/services/tag-helper.ts create mode 100644 src/core/features/tag/services/tag.ts create mode 100644 src/core/features/tag/tag-lazy.module.ts create mode 100644 src/core/features/tag/tag.module.ts diff --git a/src/core/features/features.module.ts b/src/core/features/features.module.ts index c56c3a632..aaf9d1cf5 100644 --- a/src/core/features/features.module.ts +++ b/src/core/features/features.module.ts @@ -22,6 +22,7 @@ import { CoreLoginModule } from './login/login.module'; import { CoreMainMenuModule } from './mainmenu/mainmenu.module'; import { CoreSettingsModule } from './settings/settings.module'; import { CoreSiteHomeModule } from './sitehome/sitehome.module'; +import { CoreTagModule } from './tag/tag.module'; @NgModule({ imports: [ @@ -33,6 +34,7 @@ import { CoreSiteHomeModule } from './sitehome/sitehome.module'; CoreMainMenuModule, CoreSettingsModule, CoreSiteHomeModule, + CoreTagModule, ], }) export class CoreFeaturesModule {} diff --git a/src/core/features/fileuploader/services/handlers/audio.ts b/src/core/features/fileuploader/services/handlers/audio.ts index a57cb3b8d..4d846e7f9 100644 --- a/src/core/features/fileuploader/services/handlers/audio.ts +++ b/src/core/features/fileuploader/services/handlers/audio.ts @@ -92,4 +92,4 @@ export class CoreFileUploaderAudioHandlerService implements CoreFileUploaderHand } -export class CoreFileUploaderAudioHandler extends makeSingleton(CoreFileUploaderAudioHandlerService) {} \ No newline at end of file +export class CoreFileUploaderAudioHandler extends makeSingleton(CoreFileUploaderAudioHandlerService) { } diff --git a/src/core/features/search/components/search-box/search-box.scss b/src/core/features/search/components/search-box/search-box.scss index 1884f0a13..1f5339d7e 100644 --- a/src/core/features/search/components/search-box/search-box.scss +++ b/src/core/features/search/components/search-box/search-box.scss @@ -24,4 +24,8 @@ cursor: pointer; } } + + ion-label { + margin: 0; + } } diff --git a/src/core/features/tag/lang/en.json b/src/core/features/tag/lang/en.json new file mode 100644 index 000000000..b94bad4fd --- /dev/null +++ b/src/core/features/tag/lang/en.json @@ -0,0 +1,17 @@ +{ + "defautltagcoll": "Default collection", + "errorareanotsupported": "This tag area is not supported by the app.", + "inalltagcoll": "Everywhere", + "itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"", + "notagsfound": "No tags matching \"{{$a}}\" found", + "noresultsfor": "No results for \"{{$a}}\"", + "searchtags": "Search tags", + "showingfirsttags": "Showing {{$a}} most popular tags", + "tag": "Tag", + "tagarea_course": "Courses", + "tagarea_course_modules": "Activities and resources", + "tagarea_post": "Blog posts", + "tagarea_user": "User interests", + "tags": "Tags", + "warningareasnotsupported": "Some of the tag areas are not displayed because they are not supported by the app." +} diff --git a/src/core/features/tag/pages/index-area/index-area.html b/src/core/features/tag/pages/index-area/index-area.html new file mode 100644 index 000000000..b08faa747 --- /dev/null +++ b/src/core/features/tag/pages/index-area/index-area.html @@ -0,0 +1,22 @@ + + + + + + + {{ 'core.tag.itemstaggedwith' | translate: { $a: {tagarea: areaNameKey | translate, tag: tagName} } }} + + + + + + + + + + + + + diff --git a/src/core/features/tag/pages/index-area/index-area.page.module.ts b/src/core/features/tag/pages/index-area/index-area.page.module.ts new file mode 100644 index 000000000..cbda0ac1b --- /dev/null +++ b/src/core/features/tag/pages/index-area/index-area.page.module.ts @@ -0,0 +1,47 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { CoreTagIndexAreaPage } from './index-area.page'; + +const routes: Routes = [ + { + path: '', + component: CoreTagIndexAreaPage, + }, +]; + +@NgModule({ + declarations: [ + CoreTagIndexAreaPage, + ], + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + exports: [RouterModule], +}) +export class CoreTagIndexAreaPageModule {} diff --git a/src/core/features/tag/pages/index-area/index-area.page.ts b/src/core/features/tag/pages/index-area/index-area.page.ts new file mode 100644 index 000000000..cdaead47a --- /dev/null +++ b/src/core/features/tag/pages/index-area/index-area.page.ts @@ -0,0 +1,175 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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, OnInit } from '@angular/core'; +import { IonInfiniteScroll, IonRefresher } from '@ionic/angular'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreTag } from '@features/tag/services/tag'; +import { CoreTagFeedElement } from '../../services/tag-helper'; +import { ActivatedRoute } from '@angular/router'; +import { CoreTagAreaDelegate } from '../../services/tag-area-delegate'; +import { Translate } from '@singletons'; + +/** + * Page that displays the tag index area. + * + * @todo testing. + */ +@Component({ + selector: 'page-core-tag-index-area', + templateUrl: 'index-area.html', +}) +export class CoreTagIndexAreaPage implements OnInit { + + tagId = 0; + tagName = ''; + collectionId = 0; + areaId = 0; + fromContextId = 0; + contextId = 0; + recursive = true; + + areaNameKey = ''; + loaded = false; + componentName?: string; + itemType?: string; + items: CoreTagFeedElement[] = []; + nextPage = 0; + canLoadMore = false; + areaComponent: any; // @todo + loadMoreError = false; + + constructor( + protected route: ActivatedRoute, + ) { } + + /** + * View loaded. + */ + async ngOnInit(): Promise { + + const navParams = this.route.snapshot.queryParamMap; + + this.tagId = navParams['tagId'] ? parseInt(navParams['tagId'], 10) : this.tagId; + this.tagName = navParams['tagName'] || this.tagName; + this.collectionId = navParams['collectionId'] ? parseInt(navParams['collectionId'], 10) : this.collectionId; + this.areaId = navParams['areaId'] ? parseInt(navParams['areaId']!, 10) : this.areaId; + this.fromContextId = parseInt(navParams['fromContextId'], 10) || this.fromContextId; + this.contextId = navParams['contextId'] ? parseInt(navParams['contextId'], 10) : this.contextId; + this.recursive = typeof navParams['recursive'] == 'undefined'? true : navParams['recursive']; + + this.areaNameKey = navParams['areaNameKey']; + // Pass the the following parameters to avoid fetching the first page. + this.componentName = navParams['componentName']; + this.itemType = navParams['itemType']; + this.items = []; // @todo navParams['items'] || []; + this.nextPage = navParams.has('nextPage') ? parseInt(navParams['nextPage']!, 10) : 0; + this.canLoadMore = !!navParams['canLoadMore']; + + try { + if (!this.componentName || !this.itemType || !this.items.length || this.nextPage == 0) { + await this.fetchData(true); + } + + this.areaComponent = await CoreTagAreaDelegate.instance.getComponent(this.componentName!, this.itemType!); + } finally { + this.loaded = true; + } + } + + /** + * Fetch next page of the tag index area. + * + * @param refresh Whether to refresh the data or fetch a new page. + * @return Resolved when done. + */ + async fetchData(refresh: boolean = false): Promise { + this.loadMoreError = false; + const page = refresh ? 0 : this.nextPage; + + try { + const areas = await CoreTag.instance.getTagIndexPerArea( + this.tagId, + this.tagName, + this.collectionId, + this.areaId, + this.fromContextId, + this.contextId, + this.recursive, + page, + ); + const area = areas[0]; + + const items = await CoreTagAreaDelegate.instance.parseContent(area.component, area.itemtype, area.content); + if (!items || !items.length) { + // Tag area not supported. + throw Translate.instance.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 = CoreTagAreaDelegate.instance.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. + CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading tag index'); + } + } + + /** + * Load more items. + * + * @param infiniteComplete Infinite scroll complete function. + * @return Resolved when done. + */ + async loadMore(infiniteComplete?: CustomEvent): Promise { + try { + await this.fetchData(); + } finally { + infiniteComplete?.detail.complete(); + } + } + + /** + * Refresh data. + * + * @param refresher Refresher. + */ + async refreshData(refresher?: CustomEvent): Promise { + try { + await CoreTag.instance.invalidateTagIndexPerArea( + this.tagId, + this.tagName, + this.collectionId, + this.areaId, + this.fromContextId, + this.contextId, + this.recursive, + ); + } finally { + try { + await this.fetchData(true); + } finally { + refresher?.detail.complete(); + } + } + } + +} diff --git a/src/core/features/tag/pages/index/index.html b/src/core/features/tag/pages/index/index.html new file mode 100644 index 000000000..1fbda1b2b --- /dev/null +++ b/src/core/features/tag/pages/index/index.html @@ -0,0 +1,31 @@ + + + + + + {{ 'core.tag.tag' | translate }}: {{ tagName }} + + + + + + + + + + + + {{ 'core.tag.warningareasnotsupported' | translate }} + + + +

{{ area!.nameKey | translate }}

+
+ {{ area!.badge }} +
+
+ +
+
diff --git a/src/core/features/tag/pages/index/index.page.module.ts b/src/core/features/tag/pages/index/index.page.module.ts new file mode 100644 index 000000000..ff96bc857 --- /dev/null +++ b/src/core/features/tag/pages/index/index.page.module.ts @@ -0,0 +1,47 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { CoreTagIndexPage } from './index.page'; + +const routes: Routes = [ + { + path: '', + component: CoreTagIndexPage, + }, +]; + +@NgModule({ + declarations: [ + CoreTagIndexPage, + ], + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + exports: [RouterModule], +}) +export class CoreTagIndexPageModule {} diff --git a/src/core/features/tag/pages/index/index.page.ts b/src/core/features/tag/pages/index/index.page.ts new file mode 100644 index 000000000..394d1c416 --- /dev/null +++ b/src/core/features/tag/pages/index/index.page.ts @@ -0,0 +1,186 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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, OnInit } from '@angular/core'; +import { IonRefresher } from '@ionic/angular'; +import { CoreDomUtils } from '@services/utils/dom'; +// import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreTag } from '@features/tag/services/tag'; +import { CoreTagAreaDelegate } from '@/core/features/tag/services/tag-area-delegate'; +import { ActivatedRoute, Router } from '@angular/router'; +import { CoreTagFeedElement } from '../../services/tag-helper'; + +/** + * Page that displays the tag index. + */ +@Component({ + selector: 'page-core-tag-index', + templateUrl: 'index.html', +}) +export class CoreTagIndexPage implements OnInit { + + // @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + + tagId = 0; + tagName = ''; + collectionId = 0; + areaId = 0; + fromContextId = 0; + contextId = 0; + recursive = true; + loaded = false; + selectedAreaId?: number; + hasUnsupportedAreas = false; + + areas: (CoreTagAreaDisplay | null)[] = []; + + constructor( + protected route: ActivatedRoute, + protected router: Router, + ) { } + + /** + * View loaded. + */ + async ngOnInit(): Promise { + const navParams = this.route.snapshot.queryParams; + + this.tagId = navParams['tagId'] ? parseInt(navParams['tagId'], 10) : this.tagId; + this.tagName = navParams['tagName'] || this.tagName; + this.collectionId = navParams['collectionId'] ? parseInt(navParams['collectionId'], 10) : this.collectionId; + this.areaId = navParams['areaId'] ? parseInt(navParams['areaId']!, 10) : this.areaId; + this.fromContextId = parseInt(navParams['fromContextId'], 10) || this.fromContextId; + this.contextId = navParams['contextId'] ? parseInt(navParams['contextId'], 10) : this.contextId; + this.recursive = typeof navParams['recursive'] == 'undefined'? true : navParams['recursive']; + + try { + await this.fetchData(); + /* 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 Resolved when done. + */ + async fetchData(): Promise { + try { + const areas = await CoreTag.instance.getTagIndexPerArea( + this.tagId, + this.tagName, + this.collectionId, + this.areaId, + this.fromContextId, + this.contextId, + this.recursive, + 0, + ); + + this.areas = []; + this.hasUnsupportedAreas = false; + + const areasDisplay: (CoreTagAreaDisplay | null)[] = await Promise.all(areas.map(async (area) => { + const items = await CoreTagAreaDelegate.instance.parseContent(area.component, area.itemtype, area.content); + + 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: CoreTagAreaDelegate.instance.getDisplayNameKey(area.component, area.itemtype), + items, + canLoadMore: !!area.nextpageurl, + badge: items && items.length ? items.length + (area.nextpageurl ? '+' : '') : '', + }; + })); + + this.areas = areasDisplay.filter((area) => area != null); + + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading tag index'); + } + } + + /** + * Refresh data. + * + * @param refresher Refresher. + */ + refreshData(refresher?: CustomEvent): void { + CoreTag.instance.invalidateTagIndexPerArea( + this.tagId, + this.tagName, + this.collectionId, + this.areaId, + this.fromContextId, + this.contextId, + this.recursive, + ).finally(() => { + this.fetchData().finally(() => { + refresher?.detail.complete(); + }); + }); + } + + /** + * Navigate to an index area. + * + * @param area Area. + */ + openArea(area: CoreTagAreaDisplay): 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.componentName, + itemType: area.itemType, + items: area.items.slice(), + canLoadMore: area.canLoadMore, + nextPage: 1, + }; + // this.splitviewCtrl.push('core-tag-index-area', params); + this.router.navigate(['core-tag-index-area'], { queryParams: params }); + + } + +} + +export type CoreTagAreaDisplay = { + id: number; + componentName: string; + itemType: string; + nameKey: string; + items: CoreTagFeedElement[]; + canLoadMore: boolean; + badge: string; +}; diff --git a/src/core/features/tag/pages/search/search.html b/src/core/features/tag/pages/search/search.html new file mode 100644 index 000000000..033ef2bef --- /dev/null +++ b/src/core/features/tag/pages/search/search.html @@ -0,0 +1,45 @@ + + + + + + {{ 'core.tag.searchtags' | translate }} + + + + + + + + + + + + + + {{ 'core.tag.inalltagcoll' | translate }} + + {{ collection.name }} + + + + + + + + +
+ + {{ tag.name }} + +
+

+ {{ 'core.tag.showingfirsttags' | translate: {$a: cloud!.tags.length} }} +

+
+
+
diff --git a/src/core/features/tag/pages/search/search.page.module.ts b/src/core/features/tag/pages/search/search.page.module.ts new file mode 100644 index 000000000..f370c592a --- /dev/null +++ b/src/core/features/tag/pages/search/search.page.module.ts @@ -0,0 +1,50 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreSearchComponentsModule } from '@features/search/components/components.module'; + +import { CoreTagSearchPage } from './search.page'; +import { FormsModule } from '@angular/forms'; + +const routes: Routes = [ + { + path: '', + component: CoreTagSearchPage, + }, +]; +@NgModule({ + declarations: [ + CoreTagSearchPage, + ], + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreSearchComponentsModule, + ], + exports: [RouterModule], +}) +export class CoreTagSearchPageModule {} diff --git a/src/core/features/tag/pages/search/search.page.ts b/src/core/features/tag/pages/search/search.page.ts new file mode 100644 index 000000000..ee1b80aac --- /dev/null +++ b/src/core/features/tag/pages/search/search.page.ts @@ -0,0 +1,142 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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, OnInit } from '@angular/core'; +import { IonRefresher, NavController } from '@ionic/angular'; +import { ActivatedRoute } from '@angular/router'; + +import { CoreApp } from '@services/app'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreTagCloud, CoreTagCollection, CoreTagCloudTag, CoreTag } from '@features/tag/services/tag'; +import { Translate } from '@singletons'; +import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; + +/** + * Page that displays most used tags and allows searching. + */ +@Component({ + selector: 'page-core-tag-search', + templateUrl: 'search.html', + styleUrls: ['search.scss'], +}) +export class CoreTagSearchPage implements OnInit { + + collectionId!: number; + query!: string; + collections: CoreTagCollection[] = []; + cloud?: CoreTagCloud; + loaded = false; + searching = false; + + constructor( + protected navCtrl: NavController, + protected route: ActivatedRoute, + ) { + + } + + /** + * View loaded. + */ + ngOnInit(): void { + // @todo: Check params work. + this.collectionId = this.route.snapshot.queryParamMap.has('collectionId') ? + parseInt(this.route.snapshot.queryParamMap.get('collectionId')!, 10) : 0; + this.query = this.route.snapshot.queryParamMap.get('query') || ''; + + this.fetchData().finally(() => { + this.loaded = true; + }); + } + + async fetchData(): Promise { + try { + await Promise.all([ + this.fetchCollections(), + this.fetchTags(), + ]); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading tags.'); + } + } + + /** + * Fetch tag collections. + * + * @return Resolved when done. + */ + async fetchCollections(): Promise { + const collections = await CoreTag.instance.getTagCollections(); + + collections.forEach((collection) => { + if (!collection.name && collection.isdefault) { + collection.name = Translate.instance.instant('core.tag.defautltagcoll'); + } + }); + + this.collections = collections; + } + + /** + * Fetch tags. + * + * @return Resolved when done. + */ + async fetchTags(): Promise { + this.cloud = await CoreTag.instance.getTagCloud(this.collectionId, undefined, undefined, this.query); + } + + /** + * Go to tag index page. + */ + openTag(tag: CoreTagCloudTag): void { + const url = CoreTextUtils.instance.decodeURI(tag.viewurl); + CoreContentLinksHelper.instance.handleLink(url); + } + + /** + * Refresh data. + * + * @param refresher Refresher event. + */ + refreshData(refresher?: CustomEvent): void { + CoreUtils.instance.allPromises([ + CoreTag.instance.invalidateTagCollections(), + CoreTag.instance.invalidateTagCloud(this.collectionId, undefined, undefined, this.query), + ]).finally(() => this.fetchData().finally(() => { + refresher?.detail.complete(); + })); + } + + /** + * Search tags. + * + * @param query Search query. + * @return Resolved when done. + */ + searchTags(query: string): Promise { + this.searching = true; + this.query = query; + CoreApp.instance.closeKeyboard(); + + return this.fetchTags().catch((error) => { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading tags.'); + }).finally(() => { + this.searching = false; + }); + } + +} diff --git a/src/core/features/tag/pages/search/search.scss b/src/core/features/tag/pages/search/search.scss new file mode 100644 index 000000000..76f52e023 --- /dev/null +++ b/src/core/features/tag/pages/search/search.scss @@ -0,0 +1,95 @@ +:host { + 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; + } + } +} diff --git a/src/core/features/tag/services/handlers/index.link.ts b/src/core/features/tag/services/handlers/index.link.ts new file mode 100644 index 000000000..01bb95568 --- /dev/null +++ b/src/core/features/tag/services/handlers/index.link.ts @@ -0,0 +1,86 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { Params } from '@angular/router'; +import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; +import { makeSingleton } from '@singletons'; +import { CoreTag } from '../tag'; + +/** + * Handler to treat links to tag index. + */ +@Injectable({ providedIn: 'root' }) +export class CoreTagIndexLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'CoreTagIndexLinkHandler'; + pattern = /\/tag\/index\.php/; + + /** + * Get the list of actions for a link (url). + * + * @param siteIds List of sites the URL belongs to. + * @param url The URL to treat. + * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param courseId Course ID related to the URL. Optional but recommended. + * @param data Extra data to handle the URL. + * @return List of (or promise resolved with list of) actions. + */ + getActions( + siteIds: string[], + url: string, + params: Params, + ): CoreContentLinksAction[] | Promise { + return [{ + action: (siteId): void => { + const pageParams = { + tagId: parseInt(params.id, 10) || 0, + tagName: params.tag || '', + collectionId: parseInt(params.tc, 10) || 0, + areaId: parseInt(params.ta, 10) || 0, + fromContextId: parseInt(params.from, 10) || 0, + contextId: parseInt(params.ctx, 10) || 0, + recursive: parseInt(params.rec, 10) || 1, + }; + + if (!pageParams.tagId && (!pageParams.tagName || !pageParams.collectionId)) { + CoreContentLinksHelper.instance.goInSite('/main/tag/search', {}, siteId); + } else if (pageParams.areaId) { + CoreContentLinksHelper.instance.goInSite('/main/tag/index-area', pageParams, siteId); + } else { + CoreContentLinksHelper.instance.goInSite('/main/tag/index', pageParams, siteId); + } + }, + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param siteId The site ID. + * @param url The URL to treat. + * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param courseId Course ID related to the URL. Optional but recommended. + * @return Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string): boolean | Promise { + return CoreTag.instance.areTagsAvailable(siteId); + } + +} + +export class CoreTagIndexLinkHandler extends makeSingleton(CoreTagIndexLinkHandlerService) {} diff --git a/src/core/features/tag/services/handlers/search.link.ts b/src/core/features/tag/services/handlers/search.link.ts new file mode 100644 index 000000000..610af58b8 --- /dev/null +++ b/src/core/features/tag/services/handlers/search.link.ts @@ -0,0 +1,68 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { Params } from '@angular/router'; +import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; +import { makeSingleton } from '@singletons'; +import { CoreTag } from '../tag'; + +/** + * Handler to treat links to tag search. + */ +@Injectable({ providedIn: 'root' }) +export class CoreTagSearchLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'CoreTagSearchLinkHandler'; + pattern = /\/tag\/search\.php/; + + /** + * Get the list of actions for a link (url). + * + * @param siteIds List of sites the URL belongs to. + * @param url The URL to treat. + * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param courseId Course ID related to the URL. Optional but recommended. + * @param data Extra data to handle the URL. + * @return List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] | Promise { + return [{ + action: (siteId): void => { + const pageParams = { + collectionId: parseInt(params.tc, 10) || 0, + query: params.query || '', + }; + + CoreContentLinksHelper.instance.goInSite('/main/tag/search', pageParams, siteId); + }, + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param siteId The site ID. + * @return Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string): boolean | Promise { + return CoreTag.instance.areTagsAvailable(siteId); + } + +} + +export class CoreTagSearchLinkHandler extends makeSingleton(CoreTagSearchLinkHandlerService) {} diff --git a/src/core/features/tag/services/handlers/tag.mainmenu.ts b/src/core/features/tag/services/handlers/tag.mainmenu.ts new file mode 100644 index 000000000..7c4e28631 --- /dev/null +++ b/src/core/features/tag/services/handlers/tag.mainmenu.ts @@ -0,0 +1,63 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreTag } from '../tag'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu/services/mainmenu-delegate'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to inject an option into main menu. + */ +@Injectable({ providedIn: 'root' }) +export class CoreTagMainMenuHandlerService implements CoreMainMenuHandler { + + static readonly PAGE_NAME = 'tag'; + + name = 'CoreTag'; + priority = 300; + + /** + * Check if the handler is enabled on a site level. + * + * @return Whether or not the handler is enabled on a site level. + */ + async isEnabled(): Promise { + const available = await CoreTag.instance.areTagsAvailable(); + if (!available) { + return false; + } + + // The only way to check whether tags are enabled on web is to perform a WS call. + return CoreUtils.instance.promiseWorks(CoreTag.instance.getTagCollections()); + } + + /** + * Returns the data needed to render the handler. + * + * @return Data needed to render the handler. + */ + getDisplayData(): CoreMainMenuHandlerData { + return { + icon: 'fas-tags', + title: 'core.tag.tags', + page: CoreTagMainMenuHandlerService.PAGE_NAME, + class: 'core-tag-search-handler', + }; + } + +} + +export class CoreTagMainMenuHandler extends makeSingleton(CoreTagMainMenuHandlerService) {} diff --git a/src/core/features/tag/services/tag-area-delegate.ts b/src/core/features/tag/services/tag-area-delegate.ts new file mode 100644 index 000000000..6b69ad6de --- /dev/null +++ b/src/core/features/tag/services/tag-area-delegate.ts @@ -0,0 +1,101 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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, Type } from '@angular/core'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { makeSingleton } from '@singletons'; +import { CoreTagFeedElement } from './tag-helper'; + +/** + * Interface that all tag area handlers must implement. + */ +export interface CoreTagAreaHandler extends CoreDelegateHandler { + /** + * Component and item type separated by a slash. E.g. 'core/course_modules'. + */ + type: string; + + /** + * Parses the rendered content of a tag index and returns the items. + * + * @param content Rendered content. + * @return Area items (or promise resolved with the items). + */ + parseContent(content: string): CoreTagFeedElement[] | Promise; + + /** + * Get the component to use to display items. + * + * @return The component (or promise resolved with component) to use, undefined if not found. + * @todo, check return types. + */ + getComponent(): Type | Promise>; +} + +/** + * Delegate to register tag area handlers. + */ +@Injectable({ + providedIn: 'root', +}) +export class CoreTagAreaDelegateService extends CoreDelegate { + + protected handlerNameProperty = 'type'; + + constructor() { + super('CoreTagAreaDelegate'); + } + + /** + * Returns the display name string for this area. + * + * @param component Component name. + * @param itemType Item type. + * @return String key. + */ + getDisplayNameKey(component: string, itemType: string): string { + return (component == 'core' ? 'core.tag' : 'addon.' + component) + '.tagarea_' + itemType; + } + + /** + * Parses the rendered content of a tag index and returns the items. + * + * @param component Component name. + * @param itemType Item type. + * @param content Rendered content. + * @return Promise resolved with the area items, or undefined if not found. + */ + async parseContent(component: string, itemType: string, content: string): Promise { + const type = component + '/' + itemType; + + return await this.executeFunctionOnEnabled(type, 'parseContent', [content]); + } + + /** + * Get the component to use to display an area item. + * + * @param component Component name. + * @param itemType Item type. + * @return The component (or promise resolved with component) to use, undefined if not found. + * @todo, check return types. + */ + async getComponent(component: string, itemType: string): Promise | undefined> { + const type = component + '/' + itemType; + + return await this.executeFunctionOnEnabled(type, 'getComponent'); + } + +} + +export class CoreTagAreaDelegate extends makeSingleton(CoreTagAreaDelegateService) {} diff --git a/src/core/features/tag/services/tag-helper.ts b/src/core/features/tag/services/tag-helper.ts new file mode 100644 index 000000000..b363cfad7 --- /dev/null +++ b/src/core/features/tag/services/tag-helper.ts @@ -0,0 +1,96 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { makeSingleton } from '@singletons'; +import { Injectable } from '@angular/core'; +import { CoreDomUtils } from '@services/utils/dom'; + +/** + * Service with helper functions for tags. + */ +@Injectable({ + providedIn: 'root', +}) +export class CoreTagHelperProvider { + + /** + * Parses the rendered content of the "core_tag/tagfeed" web template and returns the items. + * + * @param content Rendered content. + * @return Area items. + */ + parseFeedContent(content: string): CoreTagFeedElement[] { + const items: CoreTagFeedElement[] = []; + const element = CoreDomUtils.instance.convertToElement(content); + + Array.from(element.querySelectorAll('ul.tag_feed > li.media')).forEach((itemElement) => { + const item: CoreTagFeedElement = { details: [] }; + + Array.from(itemElement.querySelectorAll('div.media-body > div')).forEach((div: HTMLElement) => { + if (div.classList.contains('media-heading')) { + item.heading = div.innerText.trim(); + const link = div.querySelector('a'); + if (link) { + item.url = link.getAttribute('href'); + } + } else { + // Separate details by lines. + const lines = ['']; + Array.from(div.childNodes).forEach((childNode: Node) => { + if (childNode.nodeType == Node.TEXT_NODE) { + lines[lines.length - 1] += childNode.textContent; + } else if (childNode.nodeType == Node.ELEMENT_NODE) { + const childElement = childNode as HTMLElement; + if (childElement.tagName == 'BR') { + lines.push(''); + } else { + lines[lines.length - 1] += childElement.innerText; + } + } + }); + item.details.push(...lines.map((line) => line.trim()).filter((line) => line != '')); + } + }); + + const image = itemElement.querySelector('div.itemimage img'); + if (image) { + if (image.classList.contains('userpicture')) { + item.avatarUrl = image.getAttribute('src'); + } else { + item.iconUrl = image.getAttribute('src'); + } + } + + if (item.heading && item.url) { + items.push(item); + } + }); + + return items; + } + +} + +export class CoreTagHelper extends makeSingleton(CoreTagHelperProvider) {} + +/** + * Feed area element type. + */ +export type CoreTagFeedElement = { + details: string[]; + heading?: string; + iconUrl?: string | null; + avatarUrl?: string | null; + url?: string | null; +}; diff --git a/src/core/features/tag/services/tag.ts b/src/core/features/tag/services/tag.ts new file mode 100644 index 000000000..eaf86d774 --- /dev/null +++ b/src/core/features/tag/services/tag.ts @@ -0,0 +1,442 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreSites } from '@services/sites'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton, Translate } from '@singletons'; + +const ROOT_CACHE_KEY = 'CoreTag:'; + +/** + * Service to handle tags. + */ +@Injectable({ + providedIn: 'root', +}) +export class CoreTagProvider { + + static readonly SEARCH_LIMIT = 150; + + /** + * Check whether tags are available in a certain site. + * + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved with true if available, resolved with false otherwise. + * @since 3.7 + */ + async areTagsAvailable(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return this.areTagsAvailableInSite(site); + } + + /** + * Check whether tags are available in a certain site. + * + * @param site Site. If not defined, use current site. + * @return True if available. + */ + areTagsAvailableInSite(site?: CoreSite): boolean { + site = site || CoreSites.instance.getCurrentSite(); + + return !!site && site.wsAvailable('core_tag_get_tagindex_per_area') && + site.wsAvailable('core_tag_get_tag_cloud') && + site.wsAvailable('core_tag_get_tag_collections') && + !site.isFeatureDisabled('NoDelegate_CoreTag'); + } + + /** + * Fetch the tag cloud. + * + * @param collectionId Tag collection ID. + * @param isStandard Whether to return only standard tags. + * @param sort Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid). + * @param search Search string. + * @param fromContextId Context ID where this tag cloud is displayed. + * @param contextId Only retrieve tag instances in this context. + * @param recursive Retrieve tag instances in the context and its children. + * @param limit Maximum number of tags to retrieve. Defaults to SEARCH_LIMIT. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the tag cloud. + * @since 3.7 + */ + async 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 { + limit = limit || CoreTagProvider.SEARCH_LIMIT; + + const site = await CoreSites.instance.getSite(siteId); + const params: CoreTagGetTagCloudWSParams = { + tagcollid: collectionId, + isstandard: isStandard, + limit, + sort, + 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 siteId Site ID. If not defined, current site. + * @return Promise resolved with the tag collections. + * @since 3.7 + */ + async getTagCollections(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + const preSets: CoreSiteWSPreSets = { + updateFrequency: CoreSite.FREQUENCY_RARELY, + cacheKey: this.getTagCollectionsKey(), + }; + + const response: CoreTagCollections = await site.read('core_tag_get_tag_collections', null, preSets); + + if (!response || !response.collections) { + throw null; + } + + return response.collections; + } + + /** + * Fetch the tag index. + * + * @param id Tag ID. + * @param name Tag name. + * @param collectionId Tag collection ID. + * @param areaId Tag area ID. + * @param fromContextId Context ID where the link was displayed. + * @param contextId Context ID where to search for items. + * @param recursive Search in the context and its children. + * @param page Page number. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the tag index per area. + * @since 3.7 + */ + async 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 { + const site = await CoreSites.instance.getSite(siteId); + const params: CoreTagGetTagindexPerAreaWSParams = { + tagindex: { + id, + tag: name, + tc: collectionId, + ta: areaId, + excl: true, + from: fromContextId, + ctx: contextId, + rec: recursive, + page, + }, + }; + const preSets: CoreSiteWSPreSets = { + updateFrequency: CoreSite.FREQUENCY_OFTEN, + cacheKey: this.getTagIndexPerAreaKey(id, name, collectionId, areaId, fromContextId, contextId, recursive), + }; + + let response: CoreTagIndex[]; + try { + response = await 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 = Translate.instance.instant('core.tag.notagsfound', { $a: name || id || '' }); + } + + throw error; + } + + if (!response) { + throw null; + } + + return response; + } + + /** + * Invalidate tag cloud. + * + * @param collectionId Tag collection ID. + * @param isStandard Whether to return only standard tags. + * @param sort Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid). + * @param search Search string. + * @param fromContextId Context ID where this tag cloud is displayed. + * @param contextId Only retrieve tag instances in this context. + * @param recursive Retrieve tag instances in the context and its children. + * @return Promise resolved when the data is invalidated. + */ + async invalidateTagCloud( + collectionId: number = 0, + isStandard: boolean = false, + sort: string = 'name', + search: string = '', + fromContextId: number = 0, + contextId: number = 0, + recursive: boolean = true, + siteId?: string, + ): Promise { + const site = await CoreSites.instance.getSite(siteId); + const key = this.getTagCloudKey(collectionId, isStandard, sort, search, fromContextId, contextId, recursive); + + return site.invalidateWsCacheForKey(key); + } + + /** + * Invalidate tag collections. + * + * @return Promise resolved when the data is invalidated. + */ + async invalidateTagCollections(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + const key = this.getTagCollectionsKey(); + + return site.invalidateWsCacheForKey(key); + } + + /** + * Invalidate tag index. + * + * @param id Tag ID. + * @param name Tag name. + * @param collectionId Tag collection ID. + * @param areaId Tag area ID. + * @param fromContextId Context ID where the link was displayed. + * @param contextId Context ID where to search for items. + * @param recursive Search in the context and its children. + * @return Promise resolved when the data is invalidated. + */ + async invalidateTagIndexPerArea( + id: number, + name: string = '', + collectionId: number = 0, + areaId: number = 0, + fromContextId: number = 0, + contextId: number = 0, + recursive: boolean = true, + siteId?: string, + ): Promise { + const site = await CoreSites.instance.getSite(siteId); + const key = this.getTagIndexPerAreaKey(id, name, collectionId, areaId, fromContextId, contextId, recursive); + + return site.invalidateWsCacheForKey(key); + } + + /** + * Get cache key for tag cloud. + * + * @param collectionId Tag collection ID. + * @param isStandard Whether to return only standard tags. + * @param sort Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid). + * @param search Search string. + * @param fromContextId Context ID where this tag cloud is displayed. + * @param contextId Only retrieve tag instances in this context. + * @param recursive Retrieve tag instances in the context and it's children. + * @return Cache key. + */ + protected getTagCloudKey( + collectionId: number, + isStandard: boolean, + sort: string, + search: string, + fromContextId: number, + contextId: number, + recursive: boolean, + ): string { + return ROOT_CACHE_KEY + + 'cloud:' + + collectionId + ':' + + (isStandard ? 1 : 0) + ':' + + sort + ':' + search + ':' + + fromContextId + ':' + + contextId + ':' + + (recursive ? 1 : 0); + } + + /** + * Get cache key for tag collections. + * + * @return Cache key. + */ + protected getTagCollectionsKey(): string { + return ROOT_CACHE_KEY + 'collections'; + } + + /** + * Get cache key for tag index. + * + * @param id Tag ID. + * @param name Tag name. + * @param collectionId Tag collection ID. + * @param areaId Tag area ID. + * @param fromContextId Context ID where the link was displayed. + * @param contextId Context ID where to search for items. + * @param recursive Search in the context and its children. + * @return Cache key. + */ + protected getTagIndexPerAreaKey( + id: number, + name: string, + collectionId: number, + areaId: number, + fromContextId: number, + contextId: number, + recursive: boolean, + ): string { + return ROOT_CACHE_KEY + + 'index:' + id + ':' + + name + ':' + collectionId + ':' + + areaId + ':' + fromContextId + ':' + + contextId + ':' + + (recursive ? 1 : 0); + } + +} + +export class CoreTag extends makeSingleton(CoreTagProvider) {} + +/** + * Params of core_tag_get_tag_cloud WS. + */ +export type CoreTagGetTagCloudWSParams = { + tagcollid?: number; // Tag collection id. + isstandard?: boolean; // Whether to return only standard tags. + limit?: number; // Maximum number of tags to retrieve. + sort?: string; // Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid). + search?: string; // Search string. + fromctx?: number; // Context id where this tag cloud is displayed. + ctx?: number; // Only retrieve tag instances in this context. + rec?: boolean; // Retrieve tag instances in the $ctx context and it's children. +}; + +/** + * Structure of a tag cloud returned by WS. + */ +export type CoreTagCloud = { + tags: CoreTagCloudTag[]; + tagscount: number; + totalcount: number; +}; + +/** + * Structure of a tag cloud tag returned by WS. + */ +export type CoreTagCloudTag = { + name: string; + viewurl: string; + flag: boolean; + isstandard: boolean; + count: number; + size: number; +}; + +/** + * Structure of a tag collection returned by WS. + */ +export type CoreTagCollection = { + id: number; // Collection id. + name: string; // Collection name. + isdefault: boolean; // Whether is the default collection. + component: string; // Component the collection is related to. + sortorder: number; // Collection ordering in the list. + searchable: boolean; // Whether the tag collection is searchable. + customurl: string; // Custom URL for the tag page instead of /tag/index.php. +}; + +/** + * Structure of tag collections returned by WS. + */ +export type CoreTagCollections = { + collections: CoreTagCollection[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of core_tag_get_tagindex_per_area WS. + */ +export type CoreTagGetTagindexPerAreaWSParams = { + tagindex: { + id?: number; // Tag id. + tag?: string; // Tag name. + tc?: number; // Tag collection id. + ta?: number; // Tag area id. + excl?: boolean; // Exlusive mode for this tag area. + from?: number; // Context id where the link was displayed. + ctx?: number; // Context id where to search for items. + rec?: boolean; // Search in the context recursive. + page?: number; // Page number (0-based). + }; // Parameters. +}; + +/** + * Structure of a tag index returned by WS. + */ +export type 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. + */ +export type CoreTagItem = { + id: number; + name: string; + rawname: string; + isstandard: boolean; + tagcollid: number; + taginstanceid: number; + taginstancecontextid: number; + itemid: number; + ordering: number; + flag: number; +}; diff --git a/src/core/features/tag/tag-lazy.module.ts b/src/core/features/tag/tag-lazy.module.ts new file mode 100644 index 000000000..dca55c3f5 --- /dev/null +++ b/src/core/features/tag/tag-lazy.module.ts @@ -0,0 +1,42 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: 'index', + loadChildren: () => import('@features/tag/pages/index/index.page.module').then(m => m.CoreTagIndexPageModule), + }, + { + path: 'search', + loadChildren: () => import('@features/tag//pages/search/search.page.module').then(m => m.CoreTagSearchPageModule), + }, + { + path: 'index-area', + loadChildren: () => import('@features/tag/pages/index-area/index-area.page.module').then(m => m.CoreTagIndexAreaPageModule), + }, + { + path: '', + redirectTo: 'search', + pathMatch: 'full', + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class CoreTagLazyModule { } diff --git a/src/core/features/tag/tag.module.ts b/src/core/features/tag/tag.module.ts new file mode 100644 index 000000000..ec058f983 --- /dev/null +++ b/src/core/features/tag/tag.module.ts @@ -0,0 +1,48 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { APP_INITIALIZER, NgModule } from '@angular/core'; +import { Routes } from '@angular/router'; +import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate'; +import { CoreMainMenuRoutingModule } from '../mainmenu/mainmenu-routing.module'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreTagMainMenuHandler, CoreTagMainMenuHandlerService } from './services/handlers/tag.mainmenu'; +import { CoreTagIndexLinkHandler } from './services/handlers/index.link'; +import { CoreTagSearchLinkHandler } from './services/handlers/search.link'; + +const routes: Routes = [ + { + path: CoreTagMainMenuHandlerService.PAGE_NAME, + loadChildren: () => import('./tag-lazy.module').then(m => m.CoreTagLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuRoutingModule.forChild({ children: routes }), + ], + exports: [CoreMainMenuRoutingModule], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreMainMenuDelegate.instance.registerHandler(CoreTagMainMenuHandler.instance); + CoreContentLinksDelegate.instance.registerHandler(CoreTagIndexLinkHandler.instance); + CoreContentLinksDelegate.instance.registerHandler(CoreTagSearchLinkHandler.instance); + }, + }, + ], +}) +export class CoreTagModule {} From e4bb89d401f4d1145626e1d0d831879d0610810d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 4 Dec 2020 16:10:10 +0100 Subject: [PATCH 2/5] MOBILE-3591 courses: Fix courses navigation --- .../components/course-list-item/course-list-item.ts | 4 ++-- .../features/courses/pages/categories/categories.html | 2 +- src/core/features/courses/pages/dashboard/dashboard.ts | 2 +- src/core/features/sitehome/pages/index/index.html | 8 ++++---- src/core/features/sitehome/pages/index/index.ts | 2 +- src/theme/variables.scss | 3 +++ 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/core/features/courses/components/course-list-item/course-list-item.ts b/src/core/features/courses/components/course-list-item/course-list-item.ts index 6de42cd1f..5d6361075 100644 --- a/src/core/features/courses/components/course-list-item/course-list-item.ts +++ b/src/core/features/courses/components/course-list-item/course-list-item.ts @@ -98,10 +98,10 @@ export class CoreCoursesCourseListItemComponent implements OnInit { /* if (this.isEnrolled) { CoreCourseHelper.instance.openCourse(this.course); } else { - this.navCtrl.navigateForward('/courses/preview', { queryParams: { course: this.course } }); + this.navCtrl.navigateForward('/main/home/courses/preview', { queryParams: { course: this.course } }); } */ // @todo while opencourse function is not completed, open preview page. - this.navCtrl.navigateForward('/courses/preview', { queryParams: { course: this.course } }); + this.navCtrl.navigateForward('/main/home/courses/preview', { queryParams: { course: this.course } }); } } diff --git a/src/core/features/courses/pages/categories/categories.html b/src/core/features/courses/pages/categories/categories.html index 1ff31c8ab..b9b5aa421 100644 --- a/src/core/features/courses/pages/categories/categories.html +++ b/src/core/features/courses/pages/categories/categories.html @@ -39,7 +39,7 @@
- diff --git a/src/core/features/courses/pages/dashboard/dashboard.ts b/src/core/features/courses/pages/dashboard/dashboard.ts index 8e9d0ce84..997f5e5ac 100644 --- a/src/core/features/courses/pages/dashboard/dashboard.ts +++ b/src/core/features/courses/pages/dashboard/dashboard.ts @@ -83,7 +83,7 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { * Open page to manage courses storage. */ manageCoursesStorage(): void { - // @todo this.navCtrl.navigateForward(['/courses/storage']); + // @todo this.navCtrl.navigateForward(['/main/home/courses/storage']); } /** diff --git a/src/core/features/sitehome/pages/index/index.html b/src/core/features/sitehome/pages/index/index.html index ba85e91f8..e816db78e 100644 --- a/src/core/features/sitehome/pages/index/index.html +++ b/src/core/features/sitehome/pages/index/index.html @@ -60,7 +60,7 @@ - +

{{ 'core.courses.availablecourses' | translate}}

@@ -76,7 +76,7 @@
- +

{{ 'core.courses.categories' | translate}}

@@ -85,7 +85,7 @@
- +

{{ 'core.courses.mycourses' | translate}}

@@ -93,7 +93,7 @@
- +

{{ 'core.courses.searchcourses' | translate}}

diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index f9ce81694..bf62e8373 100644 --- a/src/core/features/sitehome/pages/index/index.ts +++ b/src/core/features/sitehome/pages/index/index.ts @@ -194,7 +194,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { * Open page to manage courses storage. */ manageCoursesStorage(): void { - // @todo this.navCtrl.navigateForward(['/courses/storage']); + // @todo this.navCtrl.navigateForward(['/main/home/courses/storage']); } /** diff --git a/src/theme/variables.scss b/src/theme/variables.scss index aeb85e89e..29eebdf2c 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -104,6 +104,9 @@ ion-toolbar { --color: var(--custom-toolbar-color, var(--ion-color-primary-contrast)); --background: var(--ion-statusbar-background); + ion-button { + --ion-toolbar-color: transparent; + } ion-spinner { --color: var(--custom-toolbar-color, var(--ion-color-primary-contrast)); From 9d87226ad85d62be414dcd117fde252aaa8ccfd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 7 Dec 2020 16:20:24 +0100 Subject: [PATCH 3/5] MOBILE-3591 course: Fix linting --- src/core/components/context-menu/context-menu.ts | 4 +++- .../infinite-loading/core-infinite-loading.html | 2 +- .../courses/pages/course-preview/course-preview.html | 10 +++++----- .../courses/pages/course-preview/course-preview.ts | 2 +- src/core/features/courses/pages/search/search.html | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/core/components/context-menu/context-menu.ts b/src/core/components/context-menu/context-menu.ts index ed46b46c2..bfcacec1f 100644 --- a/src/core/components/context-menu/context-menu.ts +++ b/src/core/components/context-menu/context-menu.ts @@ -37,12 +37,14 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { hideMenu = true; // It will be unhidden when items are added. expanded = false; + uniqueId: string; + protected items: CoreContextMenuItemComponent[] = []; protected itemsMovedToParent: CoreContextMenuItemComponent[] = []; protected itemsChangedStream: Subject; // Stream to update the hideMenu boolean when items change. protected instanceId: string; protected parentContextMenu?: CoreContextMenuComponent; - protected uniqueId: string; + constructor( protected popoverCtrl: PopoverController, diff --git a/src/core/components/infinite-loading/core-infinite-loading.html b/src/core/components/infinite-loading/core-infinite-loading.html index cb99cd201..94cd3663b 100644 --- a/src/core/components/infinite-loading/core-infinite-loading.html +++ b/src/core/components/infinite-loading/core-infinite-loading.html @@ -9,7 +9,7 @@ - + diff --git a/src/core/features/courses/pages/course-preview/course-preview.html b/src/core/features/courses/pages/course-preview/course-preview.html index 5d4e09c2b..444ad7027 100644 --- a/src/core/features/courses/pages/course-preview/course-preview.html +++ b/src/core/features/courses/pages/course-preview/course-preview.html @@ -16,7 +16,7 @@ -
+
@@ -43,7 +43,7 @@

{{ 'core.teachers' | translate }}

- @@ -55,7 +55,7 @@

{{contact.fullname}}

-
+
--> @@ -102,10 +102,10 @@ - - diff --git a/src/core/features/courses/pages/course-preview/course-preview.ts b/src/core/features/courses/pages/course-preview/course-preview.ts index f9e527be8..57da1fc46 100644 --- a/src/core/features/courses/pages/course-preview/course-preview.ts +++ b/src/core/features/courses/pages/course-preview/course-preview.ts @@ -64,6 +64,7 @@ export class CoreCoursesCoursePreviewPage implements OnInit, OnDestroy { downloadCourseEnabled: boolean; courseUrl = ''; courseImageUrl?: string; + isMobile: boolean; protected isGuestEnabled = false; protected guestInstanceId?: number; @@ -71,7 +72,6 @@ export class CoreCoursesCoursePreviewPage implements OnInit, OnDestroy { protected waitStart = 0; protected enrolUrl = ''; protected paypalReturnUrl = ''; - protected isMobile: boolean; protected pageDestroyed = false; protected courseStatusObserver?: CoreEventObserver; diff --git a/src/core/features/courses/pages/search/search.html b/src/core/features/courses/pages/search/search.html index a67c4bb35..9bb306c95 100644 --- a/src/core/features/courses/pages/search/search.html +++ b/src/core/features/courses/pages/search/search.html @@ -7,7 +7,7 @@ - From 0f76ce0eb78baf6600a81da10efa4161afe35f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 9 Dec 2020 14:38:53 +0100 Subject: [PATCH 4/5] MOBILE-3591 core: One line on one param Injectables --- src/addons/privatefiles/services/privatefiles-helper.ts | 4 +--- src/addons/privatefiles/services/privatefiles.ts | 4 +--- .../features/contentlinks/services/contentlinks-helper.ts | 4 +--- src/core/features/course/services/course-helper.ts | 4 +--- src/core/features/course/services/course-offline.ts | 4 +--- src/core/features/course/services/course.ts | 4 +--- src/core/features/courses/services/courses-helper.ts | 4 +--- src/core/features/courses/services/courses.ts | 4 +--- src/core/features/emulator/services/capture-helper.ts | 4 +--- .../features/fileuploader/services/fileuploader-delegate.ts | 4 +--- .../features/fileuploader/services/fileuploader-helper.ts | 4 +--- src/core/features/fileuploader/services/fileuploader.ts | 4 +--- src/core/features/login/services/login-helper.ts | 4 +--- src/core/features/mainmenu/services/home-delegate.ts | 4 +--- src/core/features/mainmenu/services/mainmenu-delegate.ts | 4 +--- src/core/features/mainmenu/services/mainmenu.ts | 4 +--- src/core/features/search/services/search-history.service.ts | 4 +--- src/core/features/settings/services/settings-delegate.ts | 4 +--- src/core/features/settings/services/settings-helper.ts | 4 +--- src/core/features/sitehome/services/sitehome.ts | 4 +--- src/core/features/tag/services/tag-area-delegate.ts | 4 +--- src/core/features/tag/services/tag-helper.ts | 4 +--- src/core/features/tag/services/tag.ts | 4 +--- 23 files changed, 23 insertions(+), 69 deletions(-) diff --git a/src/addons/privatefiles/services/privatefiles-helper.ts b/src/addons/privatefiles/services/privatefiles-helper.ts index e2e69fb86..096ae2e39 100644 --- a/src/addons/privatefiles/services/privatefiles-helper.ts +++ b/src/addons/privatefiles/services/privatefiles-helper.ts @@ -24,9 +24,7 @@ import { makeSingleton, Translate } from '@singletons'; /** * Service that provides some helper functions regarding private and site files. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class AddonPrivateFilesHelperProvider { /** diff --git a/src/addons/privatefiles/services/privatefiles.ts b/src/addons/privatefiles/services/privatefiles.ts index 997ec3426..04a0f27e9 100644 --- a/src/addons/privatefiles/services/privatefiles.ts +++ b/src/addons/privatefiles/services/privatefiles.ts @@ -25,9 +25,7 @@ const ROOT_CACHE_KEY = 'mmaFiles:'; /** * Service to handle my files and site files. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class AddonPrivateFilesProvider { // Keep old names for backwards compatibility. diff --git a/src/core/features/contentlinks/services/contentlinks-helper.ts b/src/core/features/contentlinks/services/contentlinks-helper.ts index 6ee01013a..1da43ad0a 100644 --- a/src/core/features/contentlinks/services/contentlinks-helper.ts +++ b/src/core/features/contentlinks/services/contentlinks-helper.ts @@ -27,9 +27,7 @@ import { Params } from '@angular/router'; /** * Service that provides some features regarding content links. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreContentLinksHelperProvider { constructor( diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 17663eb9e..b581aa167 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -107,9 +107,7 @@ export type CorePrefetchStatusInfo = { /** * Helper to gather some common course functions. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreCourseHelperProvider { protected courseDwnPromises: { [s: string]: { [id: number]: Promise } } = {}; diff --git a/src/core/features/course/services/course-offline.ts b/src/core/features/course/services/course-offline.ts index 3be751813..b33a12aa8 100644 --- a/src/core/features/course/services/course-offline.ts +++ b/src/core/features/course/services/course-offline.ts @@ -21,9 +21,7 @@ import { CoreStatusWithWarningsWSResponse } from '@services/ws'; /** * Service to handle offline data for courses. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreCourseOfflineProvider { /** diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 40fa21bce..02bdef195 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -41,9 +41,7 @@ const ROOT_CACHE_KEY = 'mmCourse:'; /** * Service that provides some features regarding a course. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreCourseProvider { static readonly ALL_SECTIONS_ID = -2; diff --git a/src/core/features/courses/services/courses-helper.ts b/src/core/features/courses/services/courses-helper.ts index bd91b85d4..b1e7ef96f 100644 --- a/src/core/features/courses/services/courses-helper.ts +++ b/src/core/features/courses/services/courses-helper.ts @@ -25,9 +25,7 @@ import { CoreWSExternalFile } from '@services/ws'; /** * Helper to gather some common courses functions. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreCoursesHelperProvider { /** diff --git a/src/core/features/courses/services/courses.ts b/src/core/features/courses/services/courses.ts index 3adb6fc92..dc1414c0d 100644 --- a/src/core/features/courses/services/courses.ts +++ b/src/core/features/courses/services/courses.ts @@ -26,9 +26,7 @@ const ROOT_CACHE_KEY = 'mmCourses:'; /** * Service that provides some features regarding lists of courses and categories. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreCoursesProvider { static readonly SEARCH_PER_PAGE = 20; diff --git a/src/core/features/emulator/services/capture-helper.ts b/src/core/features/emulator/services/capture-helper.ts index 5dd6a9659..d282ae4d6 100644 --- a/src/core/features/emulator/services/capture-helper.ts +++ b/src/core/features/emulator/services/capture-helper.ts @@ -23,9 +23,7 @@ import { CaptureMediaComponentInputs, CoreEmulatorCaptureMediaComponent } from ' /** * Helper service with some features to capture media (image, audio, video). */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreEmulatorCaptureHelperProvider { protected possibleAudioMimeTypes = { diff --git a/src/core/features/fileuploader/services/fileuploader-delegate.ts b/src/core/features/fileuploader/services/fileuploader-delegate.ts index ec00018ab..8826fece6 100644 --- a/src/core/features/fileuploader/services/fileuploader-delegate.ts +++ b/src/core/features/fileuploader/services/fileuploader-delegate.ts @@ -139,9 +139,7 @@ export interface CoreFileUploaderHandlerDataToReturn extends CoreFileUploaderHan /** * Delegate to register handlers to be shown in the file picker. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreFileUploaderDelegateService extends CoreDelegate { constructor() { diff --git a/src/core/features/fileuploader/services/fileuploader-helper.ts b/src/core/features/fileuploader/services/fileuploader-helper.ts index 4d903d7fe..36d2924ec 100644 --- a/src/core/features/fileuploader/services/fileuploader-helper.ts +++ b/src/core/features/fileuploader/services/fileuploader-helper.ts @@ -38,9 +38,7 @@ import { CoreWSUploadFileResult } from '@services/ws'; /** * Helper service to upload files. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreFileUploaderHelperProvider { protected logger: CoreLogger; diff --git a/src/core/features/fileuploader/services/fileuploader.ts b/src/core/features/fileuploader/services/fileuploader.ts index e893bed33..58dfa0e7e 100644 --- a/src/core/features/fileuploader/services/fileuploader.ts +++ b/src/core/features/fileuploader/services/fileuploader.ts @@ -45,9 +45,7 @@ export interface CoreFileUploaderOptions extends CoreWSFileUploadOptions { /** * Service to upload files. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreFileUploaderProvider { static readonly LIMITED_SIZE_WARNING = 1048576; // 1 MB. diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index a8d5a9b13..fbb3be5e1 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -40,9 +40,7 @@ import { CoreObject } from '@singletons/object'; /** * Helper provider that provides some common features regarding authentication. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreLoginHelperProvider { static readonly OPEN_COURSE = 'open_course'; diff --git a/src/core/features/mainmenu/services/home-delegate.ts b/src/core/features/mainmenu/services/home-delegate.ts index 2f553f9b1..0b407ade5 100644 --- a/src/core/features/mainmenu/services/home-delegate.ts +++ b/src/core/features/mainmenu/services/home-delegate.ts @@ -83,9 +83,7 @@ export interface CoreMainMenuHomeHandlerToDisplay extends CoreDelegateToDisplay, * Service to interact with plugins to be shown in the main menu. Provides functions to register a plugin * and notify an update in the data. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreMainMenuHomeDelegateService extends CoreSortedDelegate { protected featurePrefix = 'CoreMainMenuHomeDelegate_'; diff --git a/src/core/features/mainmenu/services/mainmenu-delegate.ts b/src/core/features/mainmenu/services/mainmenu-delegate.ts index 65290fdfe..37c15b3ee 100644 --- a/src/core/features/mainmenu/services/mainmenu-delegate.ts +++ b/src/core/features/mainmenu/services/mainmenu-delegate.ts @@ -95,9 +95,7 @@ export interface CoreMainMenuHandlerToDisplay extends CoreDelegateToDisplay, Cor * Service to interact with plugins to be shown in the main menu. Provides functions to register a plugin * and notify an update in the data. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreMainMenuDelegateService extends CoreSortedDelegate { protected featurePrefix = 'CoreMainMenuDelegate_'; diff --git a/src/core/features/mainmenu/services/mainmenu.ts b/src/core/features/mainmenu/services/mainmenu.ts index de6696757..42a9a51b0 100644 --- a/src/core/features/mainmenu/services/mainmenu.ts +++ b/src/core/features/mainmenu/services/mainmenu.ts @@ -25,9 +25,7 @@ import { makeSingleton } from '@singletons'; /** * Service that provides some features regarding Main Menu. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreMainMenuProvider { static readonly NUM_MAIN_HANDLERS = 4; diff --git a/src/core/features/search/services/search-history.service.ts b/src/core/features/search/services/search-history.service.ts index f660a5aca..32600c206 100644 --- a/src/core/features/search/services/search-history.service.ts +++ b/src/core/features/search/services/search-history.service.ts @@ -22,9 +22,7 @@ import { makeSingleton } from '@singletons'; /** * Service that enables adding a history to a search box. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreSearchHistoryProvider { protected static readonly HISTORY_LIMIT = 10; diff --git a/src/core/features/settings/services/settings-delegate.ts b/src/core/features/settings/services/settings-delegate.ts index e9872e02c..c708ad16e 100644 --- a/src/core/features/settings/services/settings-delegate.ts +++ b/src/core/features/settings/services/settings-delegate.ts @@ -63,9 +63,7 @@ export type CoreSettingsHandlerToDisplay = CoreDelegateToDisplay & CoreSettingsH * Service to interact with addons to be shown in app settings. Provides functions to register a plugin * and notify an update in the data. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreSettingsDelegateService extends CoreSortedDelegate { constructor() { diff --git a/src/core/features/settings/services/settings-helper.ts b/src/core/features/settings/services/settings-helper.ts index cc462ba2c..3b253a3d5 100644 --- a/src/core/features/settings/services/settings-helper.ts +++ b/src/core/features/settings/services/settings-helper.ts @@ -48,9 +48,7 @@ export const enum CoreColorScheme { /** * Settings helper service. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreSettingsHelperProvider { protected syncPromises: { [s: string]: Promise } = {}; diff --git a/src/core/features/sitehome/services/sitehome.ts b/src/core/features/sitehome/services/sitehome.ts index 614e3087b..98ddf2a8d 100644 --- a/src/core/features/sitehome/services/sitehome.ts +++ b/src/core/features/sitehome/services/sitehome.ts @@ -35,9 +35,7 @@ export enum FrontPageItemNames { /** * Service that provides some features regarding site home. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreSiteHomeProvider { /** diff --git a/src/core/features/tag/services/tag-area-delegate.ts b/src/core/features/tag/services/tag-area-delegate.ts index 6b69ad6de..8ef4cca71 100644 --- a/src/core/features/tag/services/tag-area-delegate.ts +++ b/src/core/features/tag/services/tag-area-delegate.ts @@ -46,9 +46,7 @@ export interface CoreTagAreaHandler extends CoreDelegateHandler { /** * Delegate to register tag area handlers. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreTagAreaDelegateService extends CoreDelegate { protected handlerNameProperty = 'type'; diff --git a/src/core/features/tag/services/tag-helper.ts b/src/core/features/tag/services/tag-helper.ts index b363cfad7..5219d08cf 100644 --- a/src/core/features/tag/services/tag-helper.ts +++ b/src/core/features/tag/services/tag-helper.ts @@ -19,9 +19,7 @@ import { CoreDomUtils } from '@services/utils/dom'; /** * Service with helper functions for tags. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreTagHelperProvider { /** diff --git a/src/core/features/tag/services/tag.ts b/src/core/features/tag/services/tag.ts index eaf86d774..9cdca6790 100644 --- a/src/core/features/tag/services/tag.ts +++ b/src/core/features/tag/services/tag.ts @@ -23,9 +23,7 @@ const ROOT_CACHE_KEY = 'CoreTag:'; /** * Service to handle tags. */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class CoreTagProvider { static readonly SEARCH_LIMIT = 150; From aab2aae0a41613bc96661cadb9663439315fbd94 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 10 Dec 2020 12:13:41 +0100 Subject: [PATCH 5/5] MOBILE-3591 tests: Fix format-text test --- src/core/directives/tests/format-text.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/directives/tests/format-text.test.ts b/src/core/directives/tests/format-text.test.ts index 79fcdb6e1..7d2e8d4fd 100644 --- a/src/core/directives/tests/format-text.test.ts +++ b/src/core/directives/tests/format-text.test.ts @@ -28,6 +28,7 @@ import { CoreUtils, CoreUtilsProvider } from '@services/utils/utils'; import { Platform } from '@singletons'; import { mock, mockSingleton, RenderConfig, renderWrapperComponent } from '@/testing/utils'; +import { CoreFilter } from '@features/filter/services/filter'; describe('CoreFormatTextDirective', () => { @@ -54,6 +55,7 @@ describe('CoreFormatTextDirective', () => { const sentence = Faker.lorem.sentence(); mockSingleton(CoreSites, { getSite: () => Promise.reject() }); + mockSingleton(CoreFilter, { formatText: (text) => Promise.resolve(text) }); // Act const fixture = await renderWrapperComponent( @@ -85,6 +87,7 @@ describe('CoreFormatTextDirective', () => { getSite: jest.fn(() => Promise.resolve(site)), getCurrentSite: () => Promise.resolve(site), }); + mockSingleton(CoreFilter, { formatText: (text) => Promise.resolve(text) }); // Act const fixture = await renderWrapperComponent(