From a881722a50cb1fe4f8c650ebe4d139d950a635c3 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 6 Apr 2021 16:20:38 +0200 Subject: [PATCH] MOBILE-3656 wiki: Migrate index page --- .../mod/wiki/components/components.module.ts | 40 + .../index/addon-mod-wiki-index.html | 95 ++ .../mod/wiki/components/index/index.scss | 63 + src/addons/mod/wiki/components/index/index.ts | 1059 +++++++++++++++++ src/addons/mod/wiki/components/map/map.html | 39 + src/addons/mod/wiki/components/map/map.ts | 105 ++ .../addon-mod-wiki-subwiki-picker.html | 13 + .../subwiki-picker/subwiki-picker.ts | 61 + src/addons/mod/wiki/pages/index/index.html | 24 + src/addons/mod/wiki/pages/index/index.ts | 83 ++ 10 files changed, 1582 insertions(+) create mode 100644 src/addons/mod/wiki/components/components.module.ts create mode 100644 src/addons/mod/wiki/components/index/addon-mod-wiki-index.html create mode 100644 src/addons/mod/wiki/components/index/index.scss create mode 100644 src/addons/mod/wiki/components/index/index.ts create mode 100644 src/addons/mod/wiki/components/map/map.html create mode 100644 src/addons/mod/wiki/components/map/map.ts create mode 100644 src/addons/mod/wiki/components/subwiki-picker/addon-mod-wiki-subwiki-picker.html create mode 100644 src/addons/mod/wiki/components/subwiki-picker/subwiki-picker.ts create mode 100644 src/addons/mod/wiki/pages/index/index.html create mode 100644 src/addons/mod/wiki/pages/index/index.ts diff --git a/src/addons/mod/wiki/components/components.module.ts b/src/addons/mod/wiki/components/components.module.ts new file mode 100644 index 000000000..92b38259d --- /dev/null +++ b/src/addons/mod/wiki/components/components.module.ts @@ -0,0 +1,40 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { NgModule } from '@angular/core'; +import { CoreCourseComponentsModule } from '@features/course/components/components.module'; +import { CoreTagComponentsModule } from '@features/tag/components/components.module'; +import { AddonModWikiIndexComponent } from './index/index'; +import { AddonModWikiMapModalComponent } from './map/map'; +import { AddonModWikiSubwikiPickerComponent } from './subwiki-picker/subwiki-picker'; + +@NgModule({ + declarations: [ + AddonModWikiIndexComponent, + AddonModWikiSubwikiPickerComponent, + AddonModWikiMapModalComponent, + ], + imports: [ + CoreSharedModule, + CoreCourseComponentsModule, + CoreTagComponentsModule, + ], + exports: [ + AddonModWikiIndexComponent, + AddonModWikiSubwikiPickerComponent, + AddonModWikiMapModalComponent, + ], +}) +export class AddonModWikiComponentsModule {} diff --git a/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html b/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html new file mode 100644 index 000000000..a66d401c0 --- /dev/null +++ b/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + {{ 'core.hasdatatosync' | translate:{$a: pageStr} }} + {{ 'core.hasdatatosync' | translate:{$a: moduleName} }} + + + + + + + + + {{ pageWarning }} + + +
+
+
+ + + + +
+ +
+ {{ 'core.tag.tags' | translate }}: + +
+
+
+ + + + + + +
diff --git a/src/addons/mod/wiki/components/index/index.scss b/src/addons/mod/wiki/components/index/index.scss new file mode 100644 index 000000000..7b98cde4c --- /dev/null +++ b/src/addons/mod/wiki/components/index/index.scss @@ -0,0 +1,63 @@ +@import "~theme/globals"; + +$addon-mod-wiki-toc-level-padding: 12px !default; + +:host { + --addon-mod-wiki-newentry-link-color: var(--red); + --addon-mod-wiki-toc-border-color: var(--gray-dark); + --addon-mod-wiki-toc-background-color: var(--gray-light); + + background-color: var(--ion-item-background); + + .addon-mod_wiki-page-content { + background-color: var(--ion-item-background); + border-top: 1px solid var(--gray); + padding-bottom: 10px; + } + + .addon-mod_wiki-page-content core-format-text ::ng-deep { + .wiki-toc { + margin: 16px; + padding: 8px; + border: 1px solid var(--addon-mod-wiki-toc-border-color); + background: var(--addon-mod-wiki-toc-background-color); + p { + color: var(--ion-text-color); + } + } + + .wiki-toc-title { + font-size: 1.1em; + font-variant: small-caps; + text-align: center; + } + + .wiki-toc-section { + padding: 0; + margin: 2px 8px; + } + + .wiki-toc-section-2 { + @include padding-horizontal($addon-mod-wiki-toc-level-padding, null); + } + + .wiki-toc-section-3 { + @include padding-horizontal($addon-mod-wiki-toc-level-padding * 2, null); + } + + .wiki_newentry { + color: var(--addon-mod-wiki-newentry-link-color); + font-style: italic; + } + + /* Hide edit section links */ + .addon-mod_wiki-noedit a.wiki_edit_section { + display: none; + } + } +} + +:host-context(body.dark) { + --addon-mod-wiki-newentry-link-color: var(--red-light); + --addon-mod-wiki-toc-background-color: var(--gray-darker); +} diff --git a/src/addons/mod/wiki/components/index/index.ts b/src/addons/mod/wiki/components/index/index.ts new file mode 100644 index 000000000..c8c2a20d0 --- /dev/null +++ b/src/addons/mod/wiki/components/index/index.ts @@ -0,0 +1,1059 @@ +// (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, Optional, Input, OnInit, OnDestroy } from '@angular/core'; +import { CoreError } from '@classes/errors/error'; +import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component'; +import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreTag } from '@features/tag/services/tag'; +import { CoreUser } from '@features/user/services/user'; +import { IonContent } from '@ionic/angular'; +import { CoreGroup, CoreGroups } from '@services/groups'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { ModalController, PopoverController, Translate } from '@singletons'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { Md5 } from 'ts-md5'; +import { AddonModWikiPageDBRecord } from '../../services/database/wiki'; +import { + AddonModWiki, + AddonModWikiPageContents, + AddonModWikiProvider, + AddonModWikiSubwiki, + AddonModWikiSubwikiListData, + AddonModWikiSubwikiListGrouping, + AddonModWikiSubwikiListSubwiki, + AddonModWikiSubwikiPage, + AddonModWikiWiki, +} from '../../services/wiki'; +import { AddonModWikiOffline } from '../../services/wiki-offline'; +import { + AddonModWikiAutoSyncData, + AddonModWikiSync, + AddonModWikiSyncProvider, + AddonModWikiSyncWikiResult, + AddonModWikiSyncWikiSubwiki, +} from '../../services/wiki-sync'; +import { AddonModWikiMapModalComponent } from '../map/map'; +import { AddonModWikiSubwikiPickerComponent } from '../subwiki-picker/subwiki-picker'; + +/** + * Component that displays a wiki entry page. + */ +@Component({ + selector: 'addon-mod-wiki-index', + templateUrl: 'addon-mod-wiki-index.html', + styleUrls: ['index.scss'], +}) +export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy { + + @Input() action?: string; + @Input() pageId?: number; + @Input() pageTitle?: string; + @Input() subwikiId?: number; + @Input() userId?: number; + @Input() groupId?: number; + + component = AddonModWikiProvider.COMPONENT; + componentId?: number; + moduleName = 'wiki'; + groupWiki = false; + + wiki?: AddonModWikiWiki; // The wiki instance. + isMainPage = false; // Whether the user is viewing wiki's main page (just entered the wiki). + canEdit = false; // Whether user can edit the page. + pageStr = ''; + pageWarning?: string; // Message telling that the page was discarded. + loadedSubwikis: AddonModWikiSubwiki[] = []; // The loaded subwikis. + pageIsOffline = false; // Whether the loaded page is an offline page. + pageContent?: string; // Page content to display. + tagsEnabled = false; + currentPageObj?: AddonModWikiPageContents | AddonModWikiPageDBRecord; // Object of the current loaded page. + subwikiData: AddonModWikiSubwikiListData = { // Data for the subwiki selector. + subwikiSelected: 0, + userSelected: 0, + groupSelected: 0, + subwikis: [], + count: 0, + }; + + protected syncEventName = AddonModWikiSyncProvider.AUTO_SYNCED; + protected currentSubwiki?: AddonModWikiSubwiki; // Current selected subwiki. + protected currentPage?: number; // Current loaded page ID. + protected subwikiPages?: (AddonModWikiSubwikiPage | AddonModWikiPageDBRecord)[]; // List of subwiki pages. + protected newPageObserver?: CoreEventObserver; // Observer to check for new pages. + protected manualSyncObserver?: CoreEventObserver; // An observer to watch for manual sync events. + protected ignoreManualSyncEvent = false; // Whether manual sync event should be ignored. + protected currentUserId?: number; // Current user ID. + protected hasEdited = false; // Whether the user has opened the edit page. + + constructor( + protected content?: IonContent, + @Optional() courseContentsPage?: CoreCourseContentsPage, + ) { + super('AddonModLessonIndexComponent', content, courseContentsPage); + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + super.ngOnInit(); + + this.pageStr = Translate.instant('addon.mod_wiki.wikipage'); + this.tagsEnabled = CoreTag.areTagsAvailableInSite(); + this.currentUserId = CoreSites.getCurrentSiteUserId(); + this.isMainPage = !this.pageId && !this.pageTitle; + this.currentPage = this.pageId; + this.listenEvents(); + + try { + await this.loadContent(false, true); + } finally { + if (this.action == 'map') { + this.openMap(); + } + } + + if (!this.wiki) { + return; + } + + if (!this.pageId) { + try { + await AddonModWiki.logView(this.wiki.id, this.wiki.name); + + CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); + } catch (error) { + // Ignore errors. + } + } else { + CoreUtils.ignoreErrors(AddonModWiki.logPageView(this.pageId, this.wiki.id, this.wiki.name)); + } + } + + /** + * Listen to events. + */ + protected listenEvents(): void { + // Listen for manual sync events. + this.manualSyncObserver = CoreEvents.on(AddonModWikiSyncProvider.MANUAL_SYNCED, (data) => { + if (!data || !this.wiki || data.wikiId != this.wiki.id) { + return; + } + + if (this.ignoreManualSyncEvent) { + // Event needs to be ignored. + this.ignoreManualSyncEvent = false; + + return; + } + + if (this.currentSubwiki) { + this.checkPageCreatedOrDiscarded(data.subwikis[this.currentSubwiki.id]); + } + + if (!this.pageWarning) { + this.showLoadingAndFetch(false, false); + } + }, this.siteId); + } + + /** + * Check if the current page was created or discarded. + * + * @param data Data about created and deleted pages. + */ + protected checkPageCreatedOrDiscarded(data?: AddonModWikiSyncWikiSubwiki): void { + if (this.currentPage || !data) { + return; + } + + // This is an offline page. Check if the page was created. + const page = data.created.find((page) => page.title == this.pageTitle); + if (page) { + // Page was created, set the ID so it's retrieved from server. + this.currentPage = page.pageId; + this.pageIsOffline = false; + } else { + // Page not found in created list, check if it was discarded. + const page = data.discarded.find((page) => page.title == this.pageTitle); + if (page) { + // Page discarded, show warning. + this.pageWarning = page.warning; + this.pageContent = ''; + this.pageIsOffline = false; + this.hasOffline = false; + } + } + } + + /** + * @inheritdoc + */ + protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + try { + // Get the wiki instance. + this.wiki = await AddonModWiki.getWiki(this.courseId, this.module.id); + + this.dataRetrieved.emit(this.wiki); + + if (sync) { + // Try to synchronize the wiki. + await CoreUtils.ignoreErrors(this.syncActivity(showErrors)); + } + + if (this.pageWarning) { + // Page discarded, stop getting data. + return; + } + + // Get module instance if it's empty. + if (!this.module.id) { + this.module = await CoreCourse.getModule(this.wiki.coursemodule, this.wiki.course, undefined, true); + } + + this.description = this.wiki.intro || this.module.description; + this.externalUrl = this.module.url; + this.componentId = this.module.id; + + await this.fetchSubwikis(this.wiki.id); + + // Get the subwiki list data from the cache. + const subwikiList = AddonModWiki.getSubwikiList(this.wiki.id); + + if (!subwikiList) { + // Not found in cache, create a new one. + // Get real groupmode, in case it's forced by the course. + const groupInfo = await CoreGroups.getActivityGroupInfo(this.wiki.coursemodule); + + await this.createSubwikiList(groupInfo.groups); + } else { + this.subwikiData.count = subwikiList.count; + this.setSelectedWiki(this.subwikiId, this.userId, this.groupId); + + // If nothing was selected using nav params, use the selected from cache. + if (!this.isAnySubwikiSelected()) { + this.setSelectedWiki(subwikiList.subwikiSelected, subwikiList.userSelected, subwikiList.groupSelected); + } + + this.subwikiData.subwikis = subwikiList.subwikis; + } + + if (!this.isAnySubwikiSelected() || this.subwikiData.count <= 0) { + throw new CoreError(Translate.instant('addon.mod_wiki.errornowikiavailable')); + } + + await this.fetchWikiPage(); + } catch (error) { + if (this.pageWarning) { + // Warning is already shown in screen, no need to show a modal. + return; + } + + throw error; + } finally { + this.fillContextMenu(refresh); + } + } + + /** + * Get wiki page contents. + * + * @param pageId Page to get. + * @return Promise resolved with the page data. + */ + protected async fetchPageContents(pageId: number): Promise; + protected async fetchPageContents(): Promise; + protected async fetchPageContents(pageId?: number): Promise; + protected async fetchPageContents(pageId?: number): Promise { + if (pageId) { + // Online page. + this.pageIsOffline = false; + + return AddonModWiki.getPageContents(pageId, { cmId: this.module.id }); + } + + // No page ID but we received a title. This means we're trying to load an offline page. + if (!this.currentSubwiki) { + return; + } + + try { + const title = this.pageTitle || this.wiki!.firstpagetitle!; + + const offlinePage = await AddonModWikiOffline.getNewPage( + title, + this.currentSubwiki.id, + this.currentSubwiki.wikiid, + this.currentSubwiki.userid, + this.currentSubwiki.groupid, + ); + + this.pageIsOffline = true; + if (!this.newPageObserver) { + // It's an offline page, listen for new pages event to detect if the user goes to Edit and submits the page. + this.newPageObserver = CoreEvents.on(AddonModWikiProvider.PAGE_CREATED_EVENT, async (data) => { + if (data.subwikiId != this.currentSubwiki?.id || data.pageTitle != title) { + return; + } + + // The page has been submitted. Get the page from the server. + this.currentPage = data.pageId; + + // Stop listening for new page events. + this.newPageObserver!.off(); + this.newPageObserver = undefined; + + await this.showLoadingAndFetch(true, false); + + if (this.currentPage) { + CoreUtils.ignoreErrors(AddonModWiki.logPageView(this.currentPage, this.wiki!.id, this.wiki!.name)); + } + }, CoreSites.getCurrentSiteId()); + } + + return offlinePage; + } catch { + // Page not found, ignore. + } + } + + /** + * Fetch the list of pages of a subwiki. + * + * @param subwiki Subwiki. + */ + protected async fetchSubwikiPages(subwiki: AddonModWikiSubwiki): Promise { + const subwikiPages = await AddonModWiki.getSubwikiPages(subwiki.wikiid, { + groupId: subwiki.groupid, + userId: subwiki.userid, + cmId: this.module.id, + }); + + // If no page specified, search first page. + if (!this.currentPage && !this.pageTitle) { + const firstPage = subwikiPages.find((page) => page.firstpage ); + if (firstPage) { + this.currentPage = firstPage.id; + } + } + + // Now get the offline pages. + const dbPages = await AddonModWikiOffline.getSubwikiNewPages(subwiki.id, subwiki.wikiid, subwiki.userid, subwiki.groupid); + + // If no page specified, search page title in the offline pages. + if (!this.currentPage) { + const searchTitle = this.pageTitle ? this.pageTitle : this.wiki!.firstpagetitle; + const pageExists = dbPages.some((page) => page.title == searchTitle); + + if (pageExists) { + this.pageTitle = searchTitle; + } + } + + this.subwikiPages = AddonModWiki.sortPagesByTitle( + (<(AddonModWikiSubwikiPage | AddonModWikiPageDBRecord)[]> subwikiPages).concat(dbPages), + ); + + // Reject if no currentPage selected from the subwikis given (if no subwikis available, do not reject). + if (!this.currentPage && !this.pageTitle && this.subwikiPages.length > 0) { + throw new CoreError(); + } + } + + /** + * Get the subwikis. + * + * @param wikiId Wiki ID. + */ + protected async fetchSubwikis(wikiId: number): Promise { + this.loadedSubwikis = await AddonModWiki.getSubwikis(wikiId, { cmId: this.module.id }); + + this.hasOffline = await AddonModWikiOffline.subwikisHaveOfflineData(this.loadedSubwikis); + } + + /** + * Fetch the page to be shown. + * + * @return Promise resolved when done. + */ + protected async fetchWikiPage(): Promise { + // Search the current Subwiki. + this.currentSubwiki = this.loadedSubwikis.find((subwiki) => this.isSubwikiSelected(subwiki)); + + if (!this.currentSubwiki) { + throw new CoreError(); + } + + this.setSelectedWiki(this.currentSubwiki.id, this.currentSubwiki.userid, this.currentSubwiki.groupid); + + await this.fetchSubwikiPages(this.currentSubwiki); + + // Check can edit before to have the value if there's no valid page. + this.canEdit = this.currentSubwiki.canedit; + + const pageContents = await this.fetchPageContents(this.currentPage); + + if (pageContents) { + this.dataRetrieved.emit(pageContents.title); + this.setSelectedWiki(pageContents.subwikiid, pageContents.userid, pageContents.groupid); + + this.pageContent = this.replaceEditLinks(pageContents.cachedcontent); + this.canEdit = !!pageContents.caneditpage; + this.currentPageObj = pageContents; + } + } + + /** + * Get the wiki home view. If cannot determine or it's current view, return undefined. + * + * @return The view controller of the home view + */ + protected getWikiHomeView(): unknown { + if (!this.wiki?.id) { + return; + } + + // @todo + // const views = this.navCtrl.getViews(); + + // // Go back in history until we find a page that doesn't belong to current wiki. + // for (let i = views.length - 2; i >= 0; i--) { + // const view = views[i]; + + // if (view.component.name != 'AddonModWikiIndexPage') { + // if (i == views.length - 2) { + // // Next view is current view, return undefined. + // return; + // } + + // // This view is no longer from wiki, return the next view. + // return views[i + 1]; + // } + + // // Check that the view belongs to the same wiki as current view. + // const wikiId = view.data.wikiId ? view.data.wikiId : view.data.module.instance; + + // if (!wikiId || wikiId != this.wiki.id) { + // // Wiki has changed, return the next view. + // return views[i + 1]; + // } + // } + } + + /** + * Open the view to create the first page of the wiki. + */ + protected goToCreateFirstPage(): void { + // @todo + // CoreNavigator.push('AddonModWikiEditPage', { + // module: this.module, + // courseId: this.courseId, + // pageTitle: this.wiki.firstpagetitle, + // wikiId: this.currentSubwiki.wikiid, + // userId: this.currentSubwiki.userid, + // groupId: this.currentSubwiki.groupid + // }); + } + + /** + * Open the view to edit the current page. + */ + goToEditPage(): void { + if (!this.canEdit) { + return; + } + + // @todo + // if (this.currentPageObj) { + // // Current page exists, go to edit it. + // const pageParams: any = { + // module: this.module, + // courseId: this.courseId, + // pageId: this.currentPageObj.id, + // pageTitle: this.currentPageObj.title, + // subwikiId: this.currentPageObj.subwikiid + // }; + + // if (this.currentSubwiki) { + // pageParams.wikiId = this.currentSubwiki.wikiid; + // pageParams.userId = this.currentSubwiki.userid; + // pageParams.groupId = this.currentSubwiki.groupid; + // } + + // this.navCtrl.push('AddonModWikiEditPage', pageParams); + // } else if (this.currentSubwiki) { + // // No page loaded, the wiki doesn't have first page. + // this.goToCreateFirstPage(); + // } + } + + /** + * Go to the view to create a new page. + */ + goToNewPage(): void { + if (!this.canEdit) { + return; + } + + // @todo + // if (this.currentPageObj) { + // // Current page exists, go to edit it. + // const pageParams: any = { + // module: this.module, + // courseId: this.courseId, + // subwikiId: this.currentPageObj.subwikiid + // }; + + // if (this.currentSubwiki) { + // pageParams.wikiId = this.currentSubwiki.wikiid; + // pageParams.userId = this.currentSubwiki.userid; + // pageParams.groupId = this.currentSubwiki.groupid; + // } + + // this.navCtrl.push('AddonModWikiEditPage', pageParams); + // } else if (this.currentSubwiki) { + // // No page loaded, the wiki doesn't have first page. + // this.goToCreateFirstPage(); + // } + } + + /** + * Go to view a certain page. + * + * @param page Page to view. + */ + protected async goToPage(page: AddonModWikiSubwikiPage | AddonModWikiPageDBRecord): Promise { + if (!('id' in page)) { + // It's an offline page. Check if we are already in the same offline page. + if (this.currentPage || !this.pageTitle || page.title != this.pageTitle) { + const hash = Md5.hashAsciiStr(JSON.stringify({ + pageTitle: page.title, + subwikiId: page.subwikiid, + timestamp: Date.now(), + })); + + CoreNavigator.navigate(`../${hash}`, { + params: { + module: this.module, + pageTitle: page.title, + wikiId: this.wiki!.id, + subwikiId: page.subwikiid, + }, + }); + } + } else if (this.currentPage != page.id) { + // Add a new State. + const pageContents = await this.fetchPageContents(page.id); + + const hash = Md5.hashAsciiStr(JSON.stringify({ + pageTitle: pageContents.title, + pageId: pageContents.id, + subwikiId: page.subwikiid, + timestamp: Date.now(), + })); + + CoreNavigator.navigate(`../${hash}`, { + params: { + module: this.module, + pageTitle: pageContents.title, + pageId: pageContents.id, + wikiId: pageContents.wikiid, + subwikiId: pageContents.subwikiid, + }, + }); + } + } + + /** + * Show the map. + */ + async openMap(): Promise { + // Create the toc modal. + const modal = await ModalController.create({ + component: AddonModWikiMapModalComponent, + componentProps: { + pages: this.subwikiPages, + selected: this.currentPageObj && 'id' in this.currentPageObj && this.currentPageObj.id, + // homeView: @todo this.getWikiHomeView(), + moduleId: this.module.id, + courseId: this.courseId, + }, + cssClass: 'core-modal-lateral', + showBackdrop: true, + backdropDismiss: true, + // @todo enterAnimation: 'core-modal-lateral-transition', + // @todo leaveAnimation: 'core-modal-lateral-transition', + }); + + await modal.present(); + + const result = await modal.onDidDismiss(); + + if (result.data) { + if (result.data.type == 'home') { + // Go back to the initial page of the wiki. + // @todo this.navCtrl.popTo(page.goto); + } else { + this.goToPage(result.data.goto); + } + } + + } + + /** + * Go to the page to view a certain subwiki. + * + * @param subwikiId Subwiki ID. + * @param userId User ID of the subwiki. + * @param groupId Group ID of the subwiki. + * @param canEdit Whether the subwiki can be edited. + */ + goToSubwiki(subwikiId: number, userId: number, groupId: number, canEdit: boolean): void { + // Check if the subwiki is disabled. + if (subwikiId <= 0 && !canEdit) { + return; + } + + if (subwikiId != this.currentSubwiki!.id || userId != this.currentSubwiki!.userid || + groupId != this.currentSubwiki!.groupid) { + + const hash = Md5.hashAsciiStr(JSON.stringify({ + subwikiId: subwikiId, + userId: userId, + groupId: groupId, + timestamp: Date.now(), + })); + + CoreNavigator.navigate(`../${hash}`, { + params: { + module: this.module, + wikiId: this.wiki!.id, + subwikiId: subwikiId, + userId: userId, + groupId: groupId, + }, + }); + } + } + + /** + * Checks if there is any subwiki selected. + * + * @return Whether there is any subwiki selected. + */ + protected isAnySubwikiSelected(): boolean { + return this.subwikiData.subwikiSelected > 0 || this.subwikiData.userSelected > 0 || this.subwikiData.groupSelected > 0; + } + + /** + * Checks if the given subwiki is the one picked on the subwiki picker. + * + * @param subwiki Subwiki to check. + * @return Whether it's the selected subwiki. + */ + protected isSubwikiSelected(subwiki: AddonModWikiSubwiki): boolean { + if (subwiki.id > 0 && this.subwikiData.subwikiSelected > 0) { + return subwiki.id == this.subwikiData.subwikiSelected; + } + + return subwiki.userid == this.subwikiData.userSelected && subwiki.groupid == this.subwikiData.groupSelected; + } + + /** + * Replace edit links to have full url. + * + * @param content Content to treat. + * @return Treated content. + */ + protected replaceEditLinks(content: string): string { + content = content.trim(); + + if (content.length > 0) { + const editUrl = CoreTextUtils.concatenatePaths(CoreSites.getCurrentSite()!.getURL(), '/mod/wiki/edit.php'); + content = content.replace(/href="edit\.php/g, 'href="' + editUrl); + } + + return content; + } + + /** + * Sets the selected subwiki for the subwiki picker. + * + * @param subwikiId Subwiki ID to select. + * @param userId User ID of the subwiki to select. + * @param groupId Group ID of the subwiki to select. + */ + protected setSelectedWiki(subwikiId: number | undefined, userId: number | undefined, groupId: number | undefined): void { + this.subwikiData.subwikiSelected = AddonModWikiOffline.convertToPositiveNumber(subwikiId); + this.subwikiData.userSelected = AddonModWikiOffline.convertToPositiveNumber(userId); + this.subwikiData.groupSelected = AddonModWikiOffline.convertToPositiveNumber(groupId); + } + + /** + * Checks if sync has succeed from result sync data. + * + * @param result Data returned on the sync function. + * @return If suceed or not. + */ + protected hasSyncSucceed(result: AddonModWikiSyncWikiResult): boolean { + if (result.updated) { + // Trigger event. + this.ignoreManualSyncEvent = true; + CoreEvents.trigger(AddonModWikiSyncProvider.MANUAL_SYNCED, { + ...result, + wikiId: this.wiki!.id, + }); + } + + if (this.currentSubwiki) { + this.checkPageCreatedOrDiscarded(result.subwikis[this.currentSubwiki.id]); + } + + return result.updated; + } + + /** + * User entered the page that contains the component. + */ + ionViewDidEnter(): void { + super.ionViewDidEnter(); + + if (this.hasEdited) { + this.hasEdited = false; + this.showLoadingAndRefresh(true, false); + } + } + + /** + * User left the page that contains the component. + */ + ionViewDidLeave(): void { + super.ionViewDidLeave(); + + // @todo + // if (this.navCtrl.getActive().component.name == 'AddonModWikiEditPage') { + // this.hasEdited = true; + // } + } + + /** + * @inheritdoc + */ + protected async invalidateContent(): Promise { + const promises: Promise[] = []; + + promises.push(AddonModWiki.invalidateWikiData(this.courseId)); + + if (this.wiki) { + promises.push(AddonModWiki.invalidateSubwikis(this.wiki.id)); + promises.push(CoreGroups.invalidateActivityAllowedGroups(this.wiki.coursemodule)); + promises.push(CoreGroups.invalidateActivityGroupMode(this.wiki.coursemodule)); + } + + if (this.currentSubwiki) { + promises.push(AddonModWiki.invalidateSubwikiPages(this.currentSubwiki.wikiid)); + promises.push(AddonModWiki.invalidateSubwikiFiles(this.currentSubwiki.wikiid)); + } + + if (this.currentPage) { + promises.push(AddonModWiki.invalidatePage(this.currentPage)); + } + + await Promise.all(promises); + } + + /** + * @inheritdoc + */ + protected isRefreshSyncNeeded(syncEventData: AddonModWikiAutoSyncData): boolean { + if (this.currentSubwiki && syncEventData.subwikiId == this.currentSubwiki.id && + syncEventData.wikiId == this.currentSubwiki.wikiid && syncEventData.userId == this.currentSubwiki.userid && + syncEventData.groupId == this.currentSubwiki.groupid) { + + if (this.isCurrentView && syncEventData.warnings && syncEventData.warnings.length) { + // Show warnings. + CoreDomUtils.showErrorModal(syncEventData.warnings[0]); + } + + // Check if current page was created or discarded. + this.checkPageCreatedOrDiscarded(syncEventData); + } + + return !this.pageWarning; + } + + /** + * Show the TOC. + * + * @param event Event. + */ + async showSubwikiPicker(event: MouseEvent): Promise { + const popover = await PopoverController.create({ + component: AddonModWikiSubwikiPickerComponent, + componentProps: { + subwikis: this.subwikiData.subwikis, + currentSubwiki: this.currentSubwiki, + }, + event, + }); + + await popover.present(); + + const result = await popover.onDidDismiss(); + + if (result.data) { + this.goToSubwiki(result.data.id, result.data.userid, result.data.groupid, result.data.canedit); + } + } + + /** + * Performs the sync of the activity. + * + * @return Promise resolved when done. + */ + protected sync(): Promise { + return AddonModWikiSync.syncWiki(this.wiki!.id, this.courseId, this.wiki!.coursemodule); + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + + this.manualSyncObserver?.off(); + this.newPageObserver?.off(); + } + + /** + * Create the subwiki list for the selector and store it in the cache. + * + * @param userGroups Groups. + * @return Promise resolved when done. + */ + protected async createSubwikiList(userGroups?: CoreGroup[]): Promise { + const subwikiList: AddonModWikiSubwikiListSubwiki[] = []; + let allParticipants = false; + let showMyGroupsLabel = false; + let multiLevelList = false; + + this.subwikiData.subwikis = []; + this.setSelectedWiki(this.subwikiId, this.userId, this.groupId); + this.subwikiData.count = 0; + + // Add the subwikis to the subwikiList. + await Promise.all(this.loadedSubwikis.map(async (subwiki) => { + let groupLabel = ''; + + if (subwiki.groupid == 0 && subwiki.userid == 0) { + // Add 'All participants' subwiki if needed at the start. + if (!allParticipants) { + subwikiList.unshift({ + name: Translate.instant('core.allparticipants'), + id: subwiki.id, + userid: subwiki.userid, + groupid: subwiki.groupid, + groupLabel: '', + canedit: subwiki.canedit, + }); + allParticipants = true; + } + } else { + if (subwiki.groupid != 0 && userGroups && userGroups.length > 0) { + // Get groupLabel if it has groupId. + const group = userGroups.find(group => group.id == subwiki.groupid); + groupLabel = group?.name || ''; + } else { + groupLabel = Translate.instant('addon.mod_wiki.notingroup'); + } + + if (subwiki.userid != 0) { + if (!multiLevelList && subwiki.groupid != 0) { + multiLevelList = true; + } + + // Get user if it has userId. + const user = await CoreUser.getProfile(subwiki.userid, this.courseId, true); + + subwikiList.push({ + name: user.fullname, + id: subwiki.id, + userid: subwiki.userid, + groupid: subwiki.groupid, + groupLabel: groupLabel, + canedit: subwiki.canedit, + }); + + } else { + subwikiList.push({ + name: groupLabel, + id: subwiki.id, + userid: subwiki.userid, + groupid: subwiki.groupid, + groupLabel: groupLabel, + canedit: subwiki.canedit, + }); + showMyGroupsLabel = true; + } + } + })); + + this.fillSubwikiData(subwikiList, showMyGroupsLabel, multiLevelList); + } + + /** + * Fill the subwiki data. + * + * @param subwikiList List of subwikis. + * @param showMyGroupsLabel Whether subwikis should be grouped in "My groups" and "Other groups". + * @param multiLevelList Whether it's a multi level list. + */ + protected fillSubwikiData( + subwikiList: AddonModWikiSubwikiListSubwiki[], + showMyGroupsLabel: boolean, + multiLevelList: boolean, + ): void { + subwikiList.sort((a, b) => a.groupid - b.groupid); + + this.groupWiki = showMyGroupsLabel; + this.subwikiData.count = subwikiList.length; + + // If no subwiki is received as view param, select always the most appropiate. + if ((!this.subwikiId || (!this.userId && !this.groupId)) && !this.isAnySubwikiSelected() && subwikiList.length > 0) { + let firstCanEdit: number | undefined; + let candidateNoFirstPage: number | undefined; + let candidateFirstPage: number | undefined; + + for (const i in subwikiList) { + const subwiki = subwikiList[i]; + + if (subwiki.canedit) { + let candidateSubwikiId: number | undefined; + if (subwiki.userid > 0) { + // Check if it's the current user. + if (this.currentUserId == subwiki.userid) { + candidateSubwikiId = subwiki.id; + } + } else if (subwiki.groupid > 0) { + // Check if it's a current user' group. + if (showMyGroupsLabel) { + candidateSubwikiId = subwiki.id; + } + } else if (subwiki.id > 0) { + candidateSubwikiId = subwiki.id; + } + + if (typeof candidateSubwikiId != 'undefined') { + if (candidateSubwikiId > 0) { + // Subwiki found and created, no need to keep looking. + candidateFirstPage = Number(i); + break; + } else if (typeof candidateNoFirstPage == 'undefined') { + candidateNoFirstPage = Number(i); + } + } else if (typeof firstCanEdit == 'undefined') { + firstCanEdit = Number(i); + } + } + } + + let subWikiToTake: number; + if (typeof candidateFirstPage != 'undefined') { + // Take the candidate that already has the first page created. + subWikiToTake = candidateFirstPage; + } else if (typeof candidateNoFirstPage != 'undefined') { + // No first page created, take the first candidate. + subWikiToTake = candidateNoFirstPage; + } else if (typeof firstCanEdit != 'undefined') { + // None selected, take the first the user can edit. + subWikiToTake = firstCanEdit; + } else { + // Otherwise take the very first. + subWikiToTake = 0; + } + + const subwiki = subwikiList[subWikiToTake]; + if (typeof subwiki != 'undefined') { + this.setSelectedWiki(subwiki.id, subwiki.userid, subwiki.groupid); + } + } + + if (multiLevelList) { + // As we loop over each subwiki, add it to the current group + let groupValue = -1; + let grouping: AddonModWikiSubwikiListGrouping; + + subwikiList.forEach((subwiki) => { + // Should we create a new grouping? + if (subwiki.groupid !== groupValue) { + grouping = { label: subwiki.groupLabel, subwikis: [] }; + groupValue = subwiki.groupid; + + this.subwikiData.subwikis.push(grouping); + } + + // Add the subwiki to the currently active grouping. + grouping.subwikis.push(subwiki); + }); + } else if (showMyGroupsLabel) { + const noGrouping: AddonModWikiSubwikiListGrouping = { label: '', subwikis: [] }; + const myGroupsGrouping: AddonModWikiSubwikiListGrouping = { label: Translate.instant('core.mygroups'), subwikis: [] }; + const otherGroupsGrouping: AddonModWikiSubwikiListGrouping = { + label: Translate.instant('core.othergroups'), + subwikis: [], + }; + + // As we loop over each subwiki, add it to the current group + subwikiList.forEach((subwiki) => { + // Add the subwiki to the currently active grouping. + if (typeof subwiki.canedit == 'undefined') { + noGrouping.subwikis.push(subwiki); + } else if (subwiki.canedit) { + myGroupsGrouping.subwikis.push(subwiki); + } else { + otherGroupsGrouping.subwikis.push(subwiki); + } + }); + + // Add each grouping to the subwikis + if (noGrouping.subwikis.length > 0) { + this.subwikiData.subwikis.push(noGrouping); + } + if (myGroupsGrouping.subwikis.length > 0) { + this.subwikiData.subwikis.push(myGroupsGrouping); + } + if (otherGroupsGrouping.subwikis.length > 0) { + this.subwikiData.subwikis.push(otherGroupsGrouping); + } + } else { + this.subwikiData.subwikis.push({ label: '', subwikis: subwikiList }); + } + + AddonModWiki.setSubwikiList( + this.wiki!.id, + this.subwikiData.subwikis, + this.subwikiData.count, + this.subwikiData.subwikiSelected, + this.subwikiData.userSelected, + this.subwikiData.groupSelected, + ); + } + +} diff --git a/src/addons/mod/wiki/components/map/map.html b/src/addons/mod/wiki/components/map/map.html new file mode 100644 index 000000000..d22cfa78f --- /dev/null +++ b/src/addons/mod/wiki/components/map/map.html @@ -0,0 +1,39 @@ + + + {{ 'addon.mod_wiki.map' | translate }} + + + + + + + + + + diff --git a/src/addons/mod/wiki/components/map/map.ts b/src/addons/mod/wiki/components/map/map.ts new file mode 100644 index 000000000..9af327cf7 --- /dev/null +++ b/src/addons/mod/wiki/components/map/map.ts @@ -0,0 +1,105 @@ +// (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, Input, OnInit } from '@angular/core'; +import { ModalController } from '@singletons'; +import { AddonModWikiPageDBRecord } from '../../services/database/wiki'; +import { AddonModWikiSubwikiPage } from '../../services/wiki'; + +/** + * Modal to display the map of a Wiki. + */ +@Component({ + selector: 'page-addon-mod-wiki-map', + templateUrl: 'map.html', +}) +export class AddonModWikiMapModalComponent implements OnInit { + + @Input() pages: (AddonModWikiSubwikiPage | AddonModWikiPageDBRecord)[] = []; + @Input() selected?: number; + @Input() moduleId?: number; + @Input() courseId?: number; + @Input() homeView: unknown; // @todo + + map: AddonModWikiPagesMapLetter[] = []; // Map of pages, categorized by letter. + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.constructMap(); + } + + /** + * Function called when a page is clicked. + * + * @param page Clicked page. + */ + goToPage(page: AddonModWikiSubwikiPage | AddonModWikiPageDBRecord): void { + ModalController.dismiss({ type: 'page', goto: page }); + } + + /** + * Go back to the initial page of the wiki. + */ + goToWikiHome(): void { + // @todo ModalController.dismiss({ type: 'home', goto: this.homeView }); + } + + /** + * Construct the map of pages. + * + * @param pages List of pages. + */ + protected constructMap(): void { + let letter: AddonModWikiPagesMapLetter; + let initialLetter: string; + + this.map = []; + this.pages.sort((a, b) => { + const compareA = a.title.toLowerCase().trim(); + const compareB = b.title.toLowerCase().trim(); + + return compareA.localeCompare(compareB); + }); + + this.pages.forEach((page) => { + const letterCandidate = page.title.charAt(0).toLocaleUpperCase(); + + // Should we create a new grouping? + if (letterCandidate !== initialLetter) { + initialLetter = letterCandidate; + letter = { label: letterCandidate, pages: [] }; + + this.map.push(letter); + } + + // Add the subwiki to the currently active grouping. + letter.pages.push(page); + }); + } + + /** + * Close modal. + */ + closeModal(): void { + ModalController.dismiss(); + } + +} + +type AddonModWikiPagesMapLetter = { + label: string; + pages: (AddonModWikiSubwikiPage | AddonModWikiPageDBRecord)[]; +}; diff --git a/src/addons/mod/wiki/components/subwiki-picker/addon-mod-wiki-subwiki-picker.html b/src/addons/mod/wiki/components/subwiki-picker/addon-mod-wiki-subwiki-picker.html new file mode 100644 index 000000000..0fbf77270 --- /dev/null +++ b/src/addons/mod/wiki/components/subwiki-picker/addon-mod-wiki-subwiki-picker.html @@ -0,0 +1,13 @@ + + + + {{ group.label }} + + + {{ subwiki.name }} + + + + diff --git a/src/addons/mod/wiki/components/subwiki-picker/subwiki-picker.ts b/src/addons/mod/wiki/components/subwiki-picker/subwiki-picker.ts new file mode 100644 index 000000000..2e00dd4e3 --- /dev/null +++ b/src/addons/mod/wiki/components/subwiki-picker/subwiki-picker.ts @@ -0,0 +1,61 @@ +// (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, Input } from '@angular/core'; +import { PopoverController } from '@singletons'; +import { AddonModWikiSubwiki, AddonModWikiSubwikiListGrouping } from '../../services/wiki'; + +/** + * Component to display the a list of subwikis in a wiki. + */ +@Component({ + selector: 'addon-mod-wiki-subwiki-picker', + templateUrl: 'addon-mod-wiki-subwiki-picker.html', +}) +export class AddonModWikiSubwikiPickerComponent { + + @Input() subwikis: AddonModWikiSubwikiListGrouping[] = []; + @Input() currentSubwiki!: AddonModWikiSubwiki; + + /** + * Checks if the given subwiki is the one currently selected. + * + * @param subwiki Subwiki to check. + * @return Whether it's the selected subwiki. + */ + protected isSubwikiSelected(subwiki: AddonModWikiSubwiki): boolean { + + if (subwiki.id > 0 && this.currentSubwiki.id > 0) { + return subwiki.id == this.currentSubwiki.id; + } + + return subwiki.userid == this.currentSubwiki.userid && subwiki.groupid == this.currentSubwiki.groupid; + } + + /** + * Function called when a subwiki is clicked. + * + * @param subwiki The subwiki to open. + */ + openSubwiki(subwiki: AddonModWikiSubwiki): void { + // Check if the subwiki is disabled. + if (subwiki.id > 0 || subwiki.canedit) { + // Check if it isn't current subwiki. + if (subwiki != this.currentSubwiki) { + PopoverController.dismiss(subwiki); + } + } + } + +} diff --git a/src/addons/mod/wiki/pages/index/index.html b/src/addons/mod/wiki/pages/index/index.html new file mode 100644 index 000000000..a5e7f3c41 --- /dev/null +++ b/src/addons/mod/wiki/pages/index/index.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/addons/mod/wiki/pages/index/index.ts b/src/addons/mod/wiki/pages/index/index.ts new file mode 100644 index 000000000..3b6058055 --- /dev/null +++ b/src/addons/mod/wiki/pages/index/index.ts @@ -0,0 +1,83 @@ +// (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, ViewChild } from '@angular/core'; +import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page'; +import { CoreNavigator } from '@services/navigator'; +import { AddonModWikiIndexComponent } from '../../components/index/index'; + +/** + * Page that displays a wiki page. + */ +@Component({ + selector: 'page-addon-mod-wiki-index', + templateUrl: 'index.html', +}) +export class AddonModWikiIndexPage extends CoreCourseModuleMainActivityPage implements OnInit { + + @ViewChild(AddonModWikiIndexComponent) activityComponent?: AddonModWikiIndexComponent; + + action?: string; + pageId?: number; + pageTitle?: string; + subwikiId?: number; + userId?: number; + groupId?: number; + + /** + * @inheritdoc + */ + ngOnInit(): void { + super.ngOnInit(); + + this.action = CoreNavigator.getRouteParam('action') || 'page'; + this.pageId = CoreNavigator.getRouteNumberParam('pageId'); + this.pageTitle = CoreNavigator.getRouteParam('pageTitle'); + this.subwikiId = CoreNavigator.getRouteNumberParam('subwikiId'); + this.userId = CoreNavigator.getRouteNumberParam('userId'); + this.groupId = CoreNavigator.getRouteNumberParam('groupId'); + + this.title = this.pageTitle || this.module.name; + } + + /** + * Update some data based on the data received. + * + * @param data The data received. + */ + updateData(data: { name: string } | string): void { + if (typeof data == 'string') { + // We received the title to display. + this.title = data; + } else { + // We received a wiki instance. + this.title = this.pageTitle || data.name || this.title; + } + } + + /** + * User entered the page. + */ + ionViewDidEnter(): void { + this.activityComponent?.ionViewDidEnter(); + } + + /** + * User left the page. + */ + ionViewDidLeave(): void { + this.activityComponent?.ionViewDidLeave(); + } + +}