diff --git a/src/addons/mod/forum/services/forum.ts b/src/addons/mod/forum/services/forum.ts index 66dbf417c..e860ef5de 100644 --- a/src/addons/mod/forum/services/forum.ts +++ b/src/addons/mod/forum/services/forum.ts @@ -18,6 +18,7 @@ import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreFileEntry } from '@features/fileuploader/services/fileuploader'; import { CoreRatingInfo } from '@features/rating/services/rating'; +import { CoreTagItem } from '@features/tag/services/tag'; import { CoreUser } from '@features/user/services/user'; import { CoreApp } from '@services/app'; import { CoreFilepool } from '@services/filepool'; @@ -1513,18 +1514,7 @@ export type AddonModForumLegacyPost = { userpictureurl?: string; // Post author picture. deleted: boolean; // This post has been removed. isprivatereply: boolean; // The post is a private reply. - tags?: { // Tags. - id: number; // Tag id. - name: string; // Tag name. - rawname: string; // The raw, unnormalised name for the tag as entered by users. - isstandard: boolean; // Whether this tag is standard. - tagcollid: number; // Tag collection id. - taginstanceid: number; // Tag instance id. - taginstancecontextid: number; // Context the tag instance belongs to. - itemid: number; // Id of the record tagged. - ordering: number; // Tag ordering. - flag: number; // Whether the tag is flagged as inappropriate. - }[]; + tags?: CoreTagItem[]; // Tags. }; /** diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index a3c6ba745..5aa0ad1bb 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -31,6 +31,7 @@ import { AddonModH5PActivityModule } from './h5pactivity/h5pactivity.module'; import { AddonModSurveyModule } from './survey/survey.module'; import { AddonModScormModule } from './scorm/scorm.module'; import { AddonModChoiceModule } from './choice/choice.module'; +import { AddonModWikiModule } from './wiki/wiki.module'; @NgModule({ imports: [ @@ -51,6 +52,7 @@ import { AddonModChoiceModule } from './choice/choice.module'; AddonModSurveyModule, AddonModScormModule, AddonModChoiceModule, + AddonModWikiModule, ], }) export class AddonModModule { } diff --git a/src/addons/mod/wiki/lang.json b/src/addons/mod/wiki/lang.json new file mode 100644 index 000000000..246965b9c --- /dev/null +++ b/src/addons/mod/wiki/lang.json @@ -0,0 +1,22 @@ +{ + "cannoteditpage": "You can not edit this page.", + "createpage": "Create page", + "editingpage": "Editing this page '{{$a}}'", + "errorloadingpage": "An error occurred while loading the page.", + "errornowikiavailable": "This wiki does not have any content yet.", + "gowikihome": "Go to the wiki first page", + "map": "Map", + "modulenameplural": "Wikis", + "newpagehdr": "New page", + "newpagetitle": "New page title", + "nocontent": "There is no content for this page", + "notingroup": "Not in group", + "pageexists": "This page already exists.", + "pagename": "Page name", + "subwiki": "Sub-wiki", + "tagarea_wiki_pages": "Wiki pages", + "titleshouldnotbeempty": "The title should not be empty", + "viewpage": "View page", + "wikipage": "Wiki page", + "wrongversionlock": "Another user has edited this page while you were editing and your content is obsolete." +} \ No newline at end of file diff --git a/src/addons/mod/wiki/services/database/wiki.ts b/src/addons/mod/wiki/services/database/wiki.ts new file mode 100644 index 000000000..551628a0e --- /dev/null +++ b/src/addons/mod/wiki/services/database/wiki.ts @@ -0,0 +1,93 @@ +// (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 { CoreSiteSchema } from '@services/sites'; + +/** + * Database variables for AddonModWikiOfflineProvider. + */ +export const NEW_PAGES_TABLE_NAME = 'addon_mod_wiki_new_pages_store'; +export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = { + name: 'AddonModWikiOfflineProvider', + version: 1, + tables: [ + { + name: NEW_PAGES_TABLE_NAME, + columns: [ + { + name: 'wikiid', + type: 'INTEGER', + }, + { + name: 'subwikiid', + type: 'INTEGER', + }, + { + name: 'userid', + type: 'INTEGER', + }, + { + name: 'groupid', + type: 'INTEGER', + }, + { + name: 'title', + type: 'TEXT', + }, + { + name: 'cachedcontent', + type: 'TEXT', + }, + { + name: 'contentformat', + type: 'TEXT', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'timecreated', + type: 'INTEGER', + }, + { + name: 'timemodified', + type: 'INTEGER', + }, + { + name: 'caneditpage', + type: 'INTEGER', + }, + ], + primaryKeys: ['wikiid', 'subwikiid', 'userid', 'groupid', 'title'], + }, + ], +}; + +/** + * Wiki new page data. + */ +export type AddonModWikiPageDBRecord = { + wikiid: number; + subwikiid: number; + userid: number; + groupid: number; + title: string; + cachedcontent: string; + contentformat: string; + courseid?: null; // Currently not used. + timecreated: number; + timemodified: number; + caneditpage: number; +}; diff --git a/src/addons/mod/wiki/services/handlers/create-link.ts b/src/addons/mod/wiki/services/handlers/create-link.ts new file mode 100644 index 000000000..b95d31817 --- /dev/null +++ b/src/addons/mod/wiki/services/handlers/create-link.ts @@ -0,0 +1,148 @@ +// (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 { ActivatedRoute } from '@angular/router'; +import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSitesReadingStrategy } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { makeSingleton, Translate } from '@singletons'; +import { AddonModWikiIndexPage } from '../../pages/index'; +import { AddonModWiki } from '../wiki'; +import { AddonModWikiModuleHandlerService } from './module'; + +/** + * Handler to treat links to create a wiki page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModWikiCreateLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonModWikiCreateLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModWiki'; + pattern = /\/mod\/wiki\/create\.php.*([&?]swid=\d+)/; + + /** + * Check if the current view is a wiki page of the same wiki. + * + * @param route Activated route if current route is wiki index page, null otherwise. + * @param subwikiId Subwiki ID to check. + * @param siteId Site ID. + * @return Promise resolved with boolean: whether current view belongs to the same wiki. + */ + protected async currentStateIsSameWiki(route: ActivatedRoute | null, subwikiId: number, siteId: string): Promise { + if (!route) { + // Current view isn't wiki index. + return false; + } + + const params = route.snapshot.params; + const queryParams = route.snapshot.queryParams; + + if (queryParams.subwikiId == subwikiId) { + // Same subwiki, so it's same wiki. + return true; + } + + const options = { + cmId: params.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }; + + if (queryParams.pageId) { + // Get the page contents to check the subwiki. + try { + const page = await AddonModWiki.getPageContents(queryParams.pageId, options); + + return page.subwikiid == subwikiId; + } catch { + // Not found, check next case. + } + } + + try { + // Get the wiki. + const wiki = await AddonModWiki.getWiki(params.courseId, params.cmId, options); + + // Check if the subwiki belongs to this wiki. + return await AddonModWiki.wikiHasSubwiki(wiki.id, subwikiId, options); + } catch { + // Not found, return false. + return false; + } + } + + /** + * @inheritdoc + */ + getActions( + siteIds: string[], + url: string, + params: Record, + courseId?: number, + ): CoreContentLinksAction[] | Promise { + courseId = Number(courseId || params.courseid || params.cid); + + return [{ + action: async (siteId: string) => { + const modal = await CoreDomUtils.showModalLoading(); + + try { + const route = CoreNavigator.getCurrentRoute({ pageComponent: AddonModWikiIndexPage }); + const subwikiId = parseInt(params.swid, 10); + const wikiId = parseInt(params.wid, 10); + let module: CoreCourseAnyModuleData; + + // Check if the link is inside the same wiki. + const isSameWiki = await this.currentStateIsSameWiki(route, subwikiId, siteId); + + if (isSameWiki) { + // User is seeing the wiki, we can get the module from the wiki params. + module = route!.snapshot.queryParams.module; + } else if (wikiId) { + // The URL specifies which wiki it belongs to. Get the module. + module = await CoreCourse.getModuleBasicInfoByInstance(wikiId, 'wiki', siteId); + } else { + // Not enough data. + CoreDomUtils.showErrorModal(Translate.instant('addon.mod_wiki.errorloadingpage')); + + return; + } + + // Open the page. + CoreNavigator.navigateToSitePath( + AddonModWikiModuleHandlerService.PAGE_NAME + `/${courseId}/${module.id}/edit`, + { + params: { + module: module, + courseId: courseId || module.course || route?.snapshot.params.courseId, + pageTitle: params.title, + subwikiId: subwikiId, + }, + siteId, + }, + ); + } finally { + modal.dismiss(); + } + }, + }]; + } + +} + +export const AddonModWikiCreateLinkHandler = makeSingleton(AddonModWikiCreateLinkHandlerService); diff --git a/src/addons/mod/wiki/services/handlers/edit-link.ts b/src/addons/mod/wiki/services/handlers/edit-link.ts new file mode 100644 index 000000000..f92a865ec --- /dev/null +++ b/src/addons/mod/wiki/services/handlers/edit-link.ts @@ -0,0 +1,62 @@ +// (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 { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to edit a wiki page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModWikiEditLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonModWikiEditLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModWiki'; + pattern = /\/mod\/wiki\/edit\.php.*([&?]pageid=\d+)/; + + /** + * @inheritdoc + */ + getActions( + siteIds: string[], + url: string, + params: Record, + courseId?: number, + ): CoreContentLinksAction[] | Promise { + courseId = Number(courseId || params.courseid || params.cid); + + return [{ + action: (siteId: string) => { + + let section = ''; + if (typeof params.section != 'undefined') { + section = params.section.replace(/\+/g, ' '); + } + + const pageParams = { + courseId: courseId, + section: section, + pageId: parseInt(params.pageid, 10), + }; + + // @todo this.linkHelper.goInSite(navCtrl, 'AddonModWikiEditPage', pageParams, siteId); + }, + }]; + } + +} + +export const AddonModWikiEditLinkHandler = makeSingleton(AddonModWikiEditLinkHandlerService); diff --git a/src/addons/mod/wiki/services/handlers/index-link.ts b/src/addons/mod/wiki/services/handlers/index-link.ts new file mode 100644 index 000000000..50c5c4531 --- /dev/null +++ b/src/addons/mod/wiki/services/handlers/index-link.ts @@ -0,0 +1,33 @@ +// (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 { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to wiki index. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModWikiIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { + + name = 'AddonModWikiIndexLinkHandler'; + + constructor() { + super('AddonModWiki', 'wiki', 'wid'); + } + +} + +export const AddonModWikiIndexLinkHandler = makeSingleton(AddonModWikiIndexLinkHandlerService); diff --git a/src/addons/mod/wiki/services/handlers/list-link.ts b/src/addons/mod/wiki/services/handlers/list-link.ts new file mode 100644 index 000000000..e2e999c29 --- /dev/null +++ b/src/addons/mod/wiki/services/handlers/list-link.ts @@ -0,0 +1,33 @@ +// (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 { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to wiki list page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModWikiListLinkHandlerService extends CoreContentLinksModuleListHandler { + + name = 'AddonModWikiListLinkHandler'; + + constructor() { + super('AddonModWiki', 'wiki'); + } + +} + +export const AddonModWikiListLinkHandler = makeSingleton(AddonModWikiListLinkHandlerService); diff --git a/src/addons/mod/wiki/services/handlers/module.ts b/src/addons/mod/wiki/services/handlers/module.ts new file mode 100644 index 000000000..793b2d479 --- /dev/null +++ b/src/addons/mod/wiki/services/handlers/module.ts @@ -0,0 +1,84 @@ +// (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 { CoreConstants } from '@/core/constants'; +import { Injectable, Type } from '@angular/core'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; +import { makeSingleton } from '@singletons'; +import { AddonModWikiIndexComponent } from '../../components/index'; + +/** + * Handler to support wiki modules. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModWikiModuleHandlerService implements CoreCourseModuleHandler { + + static readonly PAGE_NAME = 'mod_wiki'; + + name = 'AddonModWiki'; + modName = 'wiki'; + + supportedFeatures = { + [CoreConstants.FEATURE_GROUPS]: true, + [CoreConstants.FEATURE_GROUPINGS]: true, + [CoreConstants.FEATURE_MOD_INTRO]: true, + [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, + [CoreConstants.FEATURE_GRADE_HAS_GRADE]: false, + [CoreConstants.FEATURE_GRADE_OUTCOMES]: false, + [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, + [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + [CoreConstants.FEATURE_RATE]: false, + [CoreConstants.FEATURE_COMMENT]: true, + }; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData { + return { + icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), + title: module.name, + class: 'addon-mod_wiki-handler', + showDownloadButton: true, + action: (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => { + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + const routeParams = `/${courseId}/${module.id}/root`; + + CoreNavigator.navigateToSitePath(AddonModWikiModuleHandlerService.PAGE_NAME + routeParams, options); + }, + }; + } + + /** + * @inheritdoc + */ + async getMainComponent(): Promise> { + return AddonModWikiIndexComponent; + } + +} + +export const AddonModWikiModuleHandler = makeSingleton(AddonModWikiModuleHandlerService); diff --git a/src/addons/mod/wiki/services/handlers/page-or-map-link.ts b/src/addons/mod/wiki/services/handlers/page-or-map-link.ts new file mode 100644 index 000000000..28fecbfa2 --- /dev/null +++ b/src/addons/mod/wiki/services/handlers/page-or-map-link.ts @@ -0,0 +1,109 @@ +// (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 { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { makeSingleton } from '@singletons'; +import { Md5 } from 'ts-md5'; +import { AddonModWiki } from '../wiki'; +import { AddonModWikiModuleHandlerService } from './module'; + +/** + * Handler to treat links to a wiki page or the wiki map. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModWikiPageOrMapLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonModWikiPageOrMapLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModWiki'; + pattern = /\/mod\/wiki\/(view|map)\.php.*([&?]pageid=\d+)/; + + /** + * @inheritdoc + */ + getActions( + siteIds: string[], + url: string, + params: Record, + courseId?: number, + ): CoreContentLinksAction[] | Promise { + + courseId = Number(courseId || params.courseid || params.cid); + + return [{ + action: async (siteId: string) => { + const modal = await CoreDomUtils.showModalLoading(); + const pageId = parseInt(params.pageid, 10); + const action = url.indexOf('mod/wiki/map.php') != -1 ? 'map' : 'page'; + + try { + // Get the page data to obtain wikiId, subwikiId, etc. + const page = await AddonModWiki.getPageContents(pageId, { siteId }); + + const module = await CoreCourse.getModuleBasicInfoByInstance(page.wikiid, 'wiki', siteId); + + const hash = Md5.hashAsciiStr(JSON.stringify({ + pageId: page.id, + pageTitle: page.title, + subwikiId: page.subwikiid, + action: action, + timestamp: Date.now(), + })); + courseId = courseId || module.course; + + CoreNavigator.navigateToSitePath( + AddonModWikiModuleHandlerService.PAGE_NAME + `/${courseId}/${module.id}/${hash}`, + { + params: { + pageId: page.id, + pageTitle: page.title, + subwikiId: page.subwikiid, + action: action, + }, + siteId, + }, + ); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_wiki.errorloadingpage', true); + } finally { + modal.dismiss(); + } + }, + }]; + } + + /** + * @inheritdoc + */ + async isEnabled(siteId: string, url: string, params: Record): Promise { + const isMap = url.indexOf('mod/wiki/map.php') != -1; + + if (params.id && !isMap) { + // ID param is more prioritary than pageid in index page, it's a index URL. + return false; + } else if (isMap && typeof params.option != 'undefined' && params.option != '5') { + // Map link but the option isn't "Page list", not supported. + return false; + } + + return true; + } + +} + +export const AddonModWikiPageOrMapLinkHandler = makeSingleton(AddonModWikiPageOrMapLinkHandlerService); diff --git a/src/addons/mod/wiki/services/handlers/prefetch.ts b/src/addons/mod/wiki/services/handlers/prefetch.ts new file mode 100644 index 000000000..fee992462 --- /dev/null +++ b/src/addons/mod/wiki/services/handlers/prefetch.ts @@ -0,0 +1,206 @@ +// (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 { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; +import { CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { CoreFilepool } from '@services/filepool'; +import { CoreGroups } from '@services/groups'; +import { CoreFileSizeSum, CorePluginFileDelegate } from '@services/plugin-file-delegate'; +import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSExternalFile } from '@services/ws'; +import { makeSingleton } from '@singletons'; +import { AddonModWiki, AddonModWikiProvider, AddonModWikiSubwikiPage } from '../wiki'; +import { AddonModWikiSync, AddonModWikiSyncWikiResult } from '../wiki-sync'; + +/** + * Handler to prefetch wikis. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModWikiPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase { + + name = 'AddonModWiki'; + modName = 'wiki'; + component = AddonModWikiProvider.COMPONENT; + updatesNames = /^.*files$|^pages$/; + + /** + * Returns a list of pages that can be downloaded. + * + * @param module The module object returned by WS. + * @param courseId The course ID. + * @param options Other options. + * @return List of pages. + */ + protected async getAllPages( + module: CoreCourseAnyModuleData, + courseId: number, + options: CoreSitesCommonWSOptions = {}, + ): Promise { + options.siteId = options.siteId || CoreSites.getCurrentSiteId(); + + try { + const wiki = await AddonModWiki.getWiki(courseId, module.id, options); + + return await AddonModWiki.getWikiPageList(wiki, options); + } catch { + // Wiki not found, return empty list. + return []; + } + } + + /** + * @inheritdoc + */ + async getDownloadSize(module: CoreCourseAnyModuleData, courseId: number, single?: boolean): Promise { + const promises: Promise[] = []; + const siteId = CoreSites.getCurrentSiteId(); + + promises.push(this.getFiles(module, courseId, single, siteId).then((files) => + CorePluginFileDelegate.getFilesDownloadSize(files))); + + promises.push(this.getAllPages(module, courseId, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((pages) => { + let size = 0; + + pages.forEach((page) => { + if (page.contentsize) { + size = size + page.contentsize; + } + }); + + return { size: size, total: true }; + })); + + const sizes = await Promise.all(promises); + + return { + size: sizes[0].size + sizes[1].size, + total: sizes[0].total && sizes[1].total, + }; + } + + /** + * @inheritdoc + */ + async getFiles( + module: CoreCourseAnyModuleData, + courseId: number, + single?: boolean, + siteId?: string, + ): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + try { + const wiki = await AddonModWiki.getWiki(courseId, module.id, { siteId }); + + const introFiles = this.getIntroFilesFromInstance(module, wiki); + + const files = await AddonModWiki.getWikiFileList(wiki, { siteId }); + + return introFiles.concat(files); + } catch { + // Wiki not found, return empty list. + return []; + } + } + + /** + * @inheritdoc + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return AddonModWiki.invalidateContent(moduleId, courseId); + } + + /** + * @inheritdoc + */ + async prefetch(module: CoreCourseAnyModuleData, courseId?: number, single?: boolean): Promise { + // Get the download time of the package before starting the download (otherwise we'd always get current time). + const siteId = CoreSites.getCurrentSiteId(); + + const data = await CoreUtils.ignoreErrors(CoreFilepool.getPackageData(siteId, this.component, module.id)); + + const downloadTime = data?.downloadTime || 0; + + return this.prefetchPackage(module, courseId, this.prefetchWiki.bind(this, module, courseId, single, downloadTime, siteId)); + } + + /** + * Prefetch a wiki. + * + * @param module Module. + * @param courseId Course ID the module belongs to. + * @param single True if we're downloading a single module, false if we're downloading a whole section. + * @param downloadTime The previous download time, 0 if no previous download. + * @param siteId Site ID. + * @return Promise resolved when done. + */ + protected async prefetchWiki( + module: CoreCourseAnyModuleData, + courseId: number, + single: boolean, + downloadTime: number, + siteId: string, + ): Promise { + const userId = CoreSites.getCurrentSiteUserId(); + + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + + // Get the list of pages. + const pages = await this.getAllPages(module, courseId, commonOptions); + const promises: Promise[] = []; + + pages.forEach((page) => { + // Fetch page contents if it needs to be fetched. + if (page.timemodified > downloadTime) { + promises.push(AddonModWiki.getPageContents(page.id, modOptions)); + } + }); + + // Fetch group data. + promises.push(CoreGroups.getActivityGroupInfo(module.id, false, userId, siteId)); + + // Fetch info to provide wiki links. + promises.push(AddonModWiki.getWiki(courseId, module.id, { siteId }).then((wiki) => + CoreCourseHelper.getModuleCourseIdByInstance(wiki.id, 'wiki', siteId))); + + // Get related page files and fetch them. + promises.push(this.getFiles(module, courseId, single, siteId).then((files) => + CoreFilepool.addFilesToQueue(siteId, files, this.component, module.id))); + + await Promise.all(promises); + } + + /** + * @inheritdoc + */ + sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise { + return AddonModWikiSync.syncWiki(module.instance!, module.course, module.id, siteId); + } + +} + +export const AddonModWikiPrefetchHandler = makeSingleton(AddonModWikiPrefetchHandlerService); diff --git a/src/addons/mod/wiki/services/handlers/sync-cron.ts b/src/addons/mod/wiki/services/handlers/sync-cron.ts new file mode 100644 index 000000000..16848b150 --- /dev/null +++ b/src/addons/mod/wiki/services/handlers/sync-cron.ts @@ -0,0 +1,44 @@ +// (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 { CoreCronHandler } from '@services/cron'; +import { makeSingleton } from '@singletons'; +import { AddonModWikiSync } from '../wiki-sync'; + +/** + * Synchronization cron handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModWikiSyncCronHandlerService implements CoreCronHandler { + + name = 'AddonModWikiSyncCronHandler'; + + /** + * @inheritdoc + */ + execute(siteId?: string, force?: boolean): Promise { + return AddonModWikiSync.syncAllWikis(siteId, force); + } + + /** + * @inheritdoc + */ + getInterval(): number { + return AddonModWikiSync.syncInterval; + } + +} + +export const AddonModWikiSyncCronHandler = makeSingleton(AddonModWikiSyncCronHandlerService); diff --git a/src/addons/mod/wiki/services/handlers/tag-area.ts b/src/addons/mod/wiki/services/handlers/tag-area.ts new file mode 100644 index 000000000..878c3de19 --- /dev/null +++ b/src/addons/mod/wiki/services/handlers/tag-area.ts @@ -0,0 +1,53 @@ +// (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 { CoreTagFeedComponent } from '@features/tag/components/feed/feed'; +import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate'; +import { CoreTagFeedElement, CoreTagHelper } from '@features/tag/services/tag-helper'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to support tags. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModWikiTagAreaHandlerService implements CoreTagAreaHandler { + + name = 'AddonModWikiTagAreaHandler'; + type = 'mod_wiki/wiki_pages'; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + parseContent(content: string): CoreTagFeedElement[] { + return CoreTagHelper.parseFeedContent(content); + } + + /** + * @inheritdoc + */ + getComponent(): Type { + return CoreTagFeedComponent; + } + +} + +export const AddonModWikiTagAreaHandler = makeSingleton(AddonModWikiTagAreaHandlerService); diff --git a/src/addons/mod/wiki/services/wiki-offline.ts b/src/addons/mod/wiki/services/wiki-offline.ts new file mode 100644 index 000000000..69288f758 --- /dev/null +++ b/src/addons/mod/wiki/services/wiki-offline.ts @@ -0,0 +1,233 @@ +// (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 { makeSingleton } from '@singletons'; +import { AddonModWikiPageDBRecord, NEW_PAGES_TABLE_NAME } from './database/wiki'; +import { AddonModWikiSubwiki } from './wiki'; + +/** + * Service to handle offline wiki. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModWikiOfflineProvider { + + /** + * Convert a value to a positive number. If not a number or less than 0, 0 will be returned. + * + * @param value Value to convert. + * @return Converted value. + */ + convertToPositiveNumber(value: string | number | undefined): number { + value = Number(value); + + return value > 0 ? value : 0; + } + + /** + * Delete a new page. + * + * @param title Title of the page. + * @param subwikiId Subwiki ID. If not defined, wikiId, userId and groupId should be defined. + * @param wikiId Wiki ID. Optional, will be used create subwiki if not informed. + * @param userId User ID. Optional, will be used create subwiki if not informed. + * @param groupId Group ID. Optional, will be used create subwiki if not informed. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if deleted, rejected if failure. + */ + async deleteNewPage( + title: string, + subwikiId?: number, + wikiId?: number, + userId?: number, + groupId?: number, + siteId?: string, + ): Promise { + + const site = await CoreSites.getSite(siteId); + + subwikiId = this.convertToPositiveNumber(subwikiId); + wikiId = this.convertToPositiveNumber(wikiId); + userId = this.convertToPositiveNumber(userId); + groupId = this.convertToPositiveNumber(groupId); + + await site.getDb().deleteRecords(NEW_PAGES_TABLE_NAME, > { + subwikiid: subwikiId, + wikiid: wikiId, + userid: userId, + groupid: groupId, + title: title, + }); + } + + /** + * Get all the stored new pages from all the wikis. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with pages. + */ + async getAllNewPages(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.getDb().getAllRecords(NEW_PAGES_TABLE_NAME); + } + + /** + * Get a stored new page. + * + * @param title Title of the page. + * @param subwikiId Subwiki ID. If not defined, wikiId, userId and groupId should be defined. + * @param wikiId Wiki ID. Optional, will be used create subwiki if not informed. + * @param userId User ID. Optional, will be used create subwiki if not informed. + * @param groupId Group ID. Optional, will be used create subwiki if not informed. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with page. + */ + async getNewPage( + title: string, + subwikiId?: number, + wikiId?: number, + userId?: number, + groupId?: number, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + subwikiId = this.convertToPositiveNumber(subwikiId); + wikiId = this.convertToPositiveNumber(wikiId); + userId = this.convertToPositiveNumber(userId); + groupId = this.convertToPositiveNumber(groupId); + + return site.getDb().getRecord(NEW_PAGES_TABLE_NAME, > { + subwikiid: subwikiId, + wikiid: wikiId, + userid: userId, + groupid: groupId, + title: title, + }); + } + + /** + * Get all the stored new pages from a certain subwiki. + * + * @param subwikiId Subwiki ID. If not defined, wikiId, userId and groupId should be defined. + * @param wikiId Wiki ID. Optional, will be used create subwiki if not informed. + * @param userId User ID. Optional, will be used create subwiki if not informed. + * @param groupId Group ID. Optional, will be used create subwiki if not informed. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with pages. + */ + async getSubwikiNewPages( + subwikiId?: number, + wikiId?: number, + userId?: number, + groupId?: number, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + subwikiId = this.convertToPositiveNumber(subwikiId); + wikiId = this.convertToPositiveNumber(wikiId); + userId = this.convertToPositiveNumber(userId); + groupId = this.convertToPositiveNumber(groupId); + + return site.getDb().getRecords(NEW_PAGES_TABLE_NAME, > { + subwikiid: subwikiId, + wikiid: wikiId, + userid: userId, + groupid: groupId, + }); + } + + /** + * Get all the stored new pages from a list of subwikis. + * + * @param subwikis List of subwiki. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with pages. + */ + async getSubwikisNewPages(subwikis: AddonModWikiSubwiki[], siteId?: string): Promise { + let pages: AddonModWikiPageDBRecord[] = []; + + await Promise.all(subwikis.map(async (subwiki) => { + const subwikiPages = await this.getSubwikiNewPages(subwiki.id, subwiki.wikiid, subwiki.userid, subwiki.groupid, siteId); + + pages = pages.concat(subwikiPages); + })); + + return pages; + } + + /** + * Save a new page to be sent later. + * + * @param title Title of the page. + * @param content Content of the page. + * @param subwikiId Subwiki ID. If not defined, wikiId, userId and groupId should be defined. + * @param wikiId Wiki ID. Optional, will be used create subwiki if not informed. + * @param userId User ID. Optional, will be used create subwiki if not informed. + * @param groupId Group ID. Optional, will be used create subwiki if not informed. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if stored, rejected if failure. + */ + async saveNewPage( + title: string, + content: string, + subwikiId?: number, + wikiId?: number, + userId?: number, + groupId?: number, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const now = Date.now(); + const entry: AddonModWikiPageDBRecord = { + title: title, + cachedcontent: content, + subwikiid: this.convertToPositiveNumber(subwikiId), + wikiid: this.convertToPositiveNumber(wikiId), + userid: this.convertToPositiveNumber(userId), + groupid: this.convertToPositiveNumber(groupId), + contentformat: 'html', + timecreated: now, + timemodified: now, + caneditpage: 1, + }; + + await site.getDb().insertRecord(NEW_PAGES_TABLE_NAME, entry); + } + + /** + * Check if a list of subwikis have offline data stored. + * + * @param subwikis List of subwikis. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: whether it has offline data. + */ + async subwikisHaveOfflineData(subwikis: AddonModWikiSubwiki[], siteId?: string): Promise { + try { + const pages = await this.getSubwikisNewPages(subwikis, siteId); + + return !!pages.length; + } catch { + // Error, return false. + return false; + } + } + +} + +export const AddonModWikiOffline = makeSingleton(AddonModWikiOfflineProvider); diff --git a/src/addons/mod/wiki/services/wiki-sync.ts b/src/addons/mod/wiki/services/wiki-sync.ts new file mode 100644 index 000000000..f285bfe7c --- /dev/null +++ b/src/addons/mod/wiki/services/wiki-sync.ts @@ -0,0 +1,417 @@ +// (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 { CoreSyncBaseProvider, CoreSyncBlockedError } from '@classes/base-sync'; +import { CoreNetworkError } from '@classes/errors/network-error'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreApp } from '@services/app'; +import { CoreGroups } from '@services/groups'; +import { CoreSites } from '@services/sites'; +import { CoreSync } from '@services/sync'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton, Translate } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { AddonModWikiPageDBRecord } from './database/wiki'; +import { AddonModWiki, AddonModWikiProvider } from './wiki'; +import { AddonModWikiOffline } from './wiki-offline'; + +/** + * Service to sync wikis. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModWikiSyncProvider extends CoreSyncBaseProvider { + + static readonly AUTO_SYNCED = 'addon_mod_wiki_autom_synced'; + static readonly MANUAL_SYNCED = 'addon_mod_wiki_manual_synced'; + + protected componentTranslatableString = 'wiki'; + + constructor() { + super('AddonModWikiSyncProvider'); + } + + /** + * Get a string to identify a subwiki. If it doesn't have a subwiki ID it will be identified by wiki ID, user ID and group ID. + * + * @param subwikiId Subwiki ID. If not defined, wikiId, userId and groupId should be defined. + * @param wikiId Wiki ID. Optional, will be used to create the subwiki if subwiki ID not provided. + * @param userId User ID. Optional, will be used to create the subwiki if subwiki ID not provided. + * @param groupId Group ID. Optional, will be used to create the subwiki if subwiki ID not provided. + * @return Identifier. + */ + getSubwikiBlockId(subwikiId: number, wikiId?: number, userId?: number, groupId?: number): string { + subwikiId = AddonModWikiOffline.convertToPositiveNumber(subwikiId); + + if (subwikiId && subwikiId > 0) { + return String(subwikiId); + } + + wikiId = AddonModWikiOffline.convertToPositiveNumber(wikiId); + userId = AddonModWikiOffline.convertToPositiveNumber(userId); + groupId = AddonModWikiOffline.convertToPositiveNumber(groupId); + + return `${wikiId}:${userId}:${groupId}`; + } + + /** + * Try to synchronize all the wikis in a certain site or in all sites. + * + * @param siteId Site ID to sync. If not defined, sync all sites. + * @param force Wether to force sync not depending on last execution. + * @return Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllWikis(siteId?: string, force?: boolean): Promise { + return this.syncOnSites('all wikis', this.syncAllWikisFunc.bind(this, !!force), siteId); + } + + /** + * Sync all wikis on a site. + * + * @param force Wether to force sync not depending on last execution. + * @param siteId Site ID to sync. + * @param Promise resolved if sync is successful, rejected if sync fails. + */ + protected async syncAllWikisFunc(force: boolean, siteId: string): Promise { + // Get all the pages created in offline. + const pages = await AddonModWikiOffline.getAllNewPages(siteId); + + const subwikis: Record = {}; + + // Sync all subwikis. + await Promise.all(pages.map(async (page) => { + const index = this.getSubwikiBlockId(page.subwikiid, page.wikiid, page.userid, page.groupid); + + if (subwikis[index]) { + // Already synced. + return; + } + + subwikis[index] = true; + + const result = force ? + await this.syncSubwiki(page.subwikiid, page.wikiid, page.userid, page.groupid, siteId) : + await this.syncSubwikiIfNeeded(page.subwikiid, page.wikiid, page.userid, page.groupid, siteId); + + if (result?.updated) { + // Sync successful, send event. + CoreEvents.trigger(AddonModWikiSyncProvider.AUTO_SYNCED, { + siteId: siteId, + subwikiId: page.subwikiid, + wikiId: page.wikiid, + userId: page.userid, + groupId: page.groupid, + created: result.created, + discarded: result.discarded, + warnings: result.warnings, + }); + } + })); + } + + /** + * Sync a subwiki only if a certain time has passed since the last time. + * + * @param subwikiId Subwiki ID. If not defined, wikiId, userId and groupId should be defined. + * @param wikiId Wiki ID. Optional, will be used to create the subwiki if subwiki ID not provided. + * @param userId User ID. Optional, will be used to create the subwiki if subwiki ID not provided. + * @param groupId Group ID. Optional, will be used to create the subwiki if subwiki ID not provided. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when subwiki is synced or doesn't need to be synced. + */ + async syncSubwikiIfNeeded( + subwikiId: number, + wikiId?: number, + userId?: number, + groupId?: number, + siteId?: string, + ): Promise { + + const blockId = this.getSubwikiBlockId(subwikiId, wikiId, userId, groupId); + + const needed = await this.isSyncNeeded(blockId, siteId); + + if (needed) { + return this.syncSubwiki(subwikiId, wikiId, userId, groupId, siteId); + } + } + + /** + * Synchronize a subwiki. + * + * @param subwikiId Subwiki ID. If not defined, wikiId, userId and groupId should be defined. + * @param wikiId Wiki ID. Optional, will be used to create the subwiki if subwiki ID not provided. + * @param userId User ID. Optional, will be used to create the subwiki if subwiki ID not provided. + * @param groupId Group ID. Optional, will be used to create the subwiki if subwiki ID not provided. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + syncSubwiki( + subwikiId: number, + wikiId?: number, + userId?: number, + groupId?: number, + siteId?: string, + ): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const subwikiBlockId = this.getSubwikiBlockId(subwikiId, wikiId, userId, groupId); + + if (this.isSyncing(subwikiBlockId, siteId)) { + // There's already a sync ongoing for this subwiki, return the promise. + return this.getOngoingSync(subwikiBlockId, siteId)!; + } + + // Verify that subwiki isn't blocked. + if (CoreSync.isBlocked(AddonModWikiProvider.COMPONENT, subwikiBlockId, siteId)) { + this.logger.debug(`Cannot sync subwiki ${subwikiBlockId} because it is blocked.`); + + throw new CoreSyncBlockedError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate })); + } + + this.logger.debug(`Try to sync subwiki ${subwikiBlockId}`); + + return this.addOngoingSync(subwikiBlockId, this.performSyncSubwiki(subwikiId, wikiId, userId, groupId, siteId), siteId); + } + + /** + * Synchronize a subwiki. + * + * @param subwikiId Subwiki ID. If not defined, wikiId, userId and groupId should be defined. + * @param wikiId Wiki ID. Optional, will be used to create the subwiki if subwiki ID not provided. + * @param userId User ID. Optional, will be used to create the subwiki if subwiki ID not provided. + * @param groupId Group ID. Optional, will be used to create the subwiki if subwiki ID not provided. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + protected async performSyncSubwiki( + subwikiId: number, + wikiId?: number, + userId?: number, + groupId?: number, + siteId?: string, + ): Promise { + const result: AddonModWikiSyncSubwikiResult = { + warnings: [], + updated: false, + created: [], + discarded: [], + }; + const subwikiBlockId = this.getSubwikiBlockId(subwikiId, wikiId, userId, groupId); + + // Get offline pages to be sent. + const pages = await CoreUtils.ignoreErrors( + AddonModWikiOffline.getSubwikiNewPages(subwikiId, wikiId, userId, groupId, siteId), + [], + ); + + if (!pages || !pages.length) { + // Nothing to sync. + await CoreUtils.ignoreErrors(this.setSyncTime(subwikiBlockId, siteId)); + + return result; + } + + if (!CoreApp.isOnline()) { + // Cannot sync in offline. + throw new CoreNetworkError(); + } + + // Send the pages. + await Promise.all(pages.map(async (page) => { + try { + const pageId = await AddonModWiki.newPageOnline(page.title, page.cachedcontent, { + subwikiId, + wikiId, + userId, + groupId, + siteId, + }); + + result.updated = true; + result.created.push({ + pageId: pageId, + title: page.title, + }); + + // Delete the local page. + await AddonModWikiOffline.deleteNewPage(page.title, subwikiId, wikiId, userId, groupId, siteId); + } catch (error) { + if (!CoreUtils.isWebServiceError(error)) { + // Couldn't connect to server, reject. + throw error; + } + + // The WebService has thrown an error, this means that the page cannot be submitted. Delete it. + await AddonModWikiOffline.deleteNewPage(page.title, subwikiId, wikiId, userId, groupId, siteId); + + result.updated = true; + + // Page deleted, add the page to discarded pages and add a warning. + const warning = Translate.instant('core.warningofflinedatadeleted', { + component: Translate.instant('addon.mod_wiki.wikipage'), + name: page.title, + error: CoreTextUtils.getErrorMessageFromError(error), + }); + + result.discarded.push({ + title: page.title, + warning: warning, + }); + + result.warnings.push(warning); + } + })); + + // Sync finished, set sync time. + await CoreUtils.ignoreErrors(this.setSyncTime(subwikiBlockId, siteId)); + + return result; + } + + /** + * Tries to synchronize a wiki. + * + * @param wikiId Wiki ID. + * @param courseId Course ID. + * @param cmId Wiki course module ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + async syncWiki(wikiId: number, courseId?: number, cmId?: number, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + // Sync offline logs. + await CoreUtils.ignoreErrors(CoreCourseLogHelper.syncActivity(AddonModWikiProvider.COMPONENT, wikiId, siteId)); + + // Sync is done at subwiki level, get all the subwikis. + const subwikis = await AddonModWiki.getSubwikis(wikiId, { cmId, siteId }); + + const result: AddonModWikiSyncWikiResult = { + warnings: [], + updated: false, + subwikis: {}, + siteId: siteId, + }; + + await Promise.all(subwikis.map(async (subwiki) => { + const data = await this.syncSubwiki(subwiki.id, subwiki.wikiid, subwiki.userid, subwiki.groupid, siteId); + + if (data && data.updated) { + result.warnings = result.warnings.concat(data.warnings); + result.updated = true; + result.subwikis[subwiki.id] = { + created: data.created, + discarded: data.discarded, + }; + } + })); + + if (result.updated) { + const promises: Promise[] = []; + + // Something has changed, invalidate data. + if (wikiId) { + promises.push(AddonModWiki.invalidateSubwikis(wikiId)); + promises.push(AddonModWiki.invalidateSubwikiPages(wikiId)); + promises.push(AddonModWiki.invalidateSubwikiFiles(wikiId)); + } + if (courseId) { + promises.push(AddonModWiki.invalidateWikiData(courseId)); + } + if (cmId) { + promises.push(CoreGroups.invalidateActivityAllowedGroups(cmId)); + promises.push(CoreGroups.invalidateActivityGroupMode(cmId)); + } + + await CoreUtils.ignoreErrors(Promise.all(promises)); + } + + return result; + } + +} + +export const AddonModWikiSync = makeSingleton(AddonModWikiSyncProvider); + +/** + * Data returned by a subwiki sync. + */ +export type AddonModWikiSyncSubwikiResult = { + warnings: string[]; // List of warnings. + updated: boolean; // Whether data was updated in the site. + created: AddonModWikiCreatedPage[]; // List of created pages. + discarded: AddonModWikiDiscardedPage[]; // List of discarded pages. +}; + +/** + * Data returned by a wiki sync. + */ +export type AddonModWikiSyncWikiResult = { + warnings: string[]; // List of warnings. + updated: boolean; // Whether data was updated in the site. + subwikis: { + [subwikiId: number]: { // List of subwikis. + created: AddonModWikiCreatedPage[]; + discarded: AddonModWikiDiscardedPage[]; + }; + }; + siteId: string; // Site ID. +}; + +/** + * Data returned by a wiki sync for each subwiki synced. + */ +export type AddonModWikiSyncWikiSubwiki = { + created: AddonModWikiCreatedPage[]; + discarded: AddonModWikiDiscardedPage[]; +}; + +/** + * Data to identify a page created in sync. + */ +export type AddonModWikiCreatedPage = { + pageId: number; + title: string; +}; + +/** + * Data to identify a page discarded in sync. + */ +export type AddonModWikiDiscardedPage = { + title: string; + warning: string; +}; + +/** + * Data passed to AUTO_SYNCED event. + */ +export type AddonModWikiAutoSyncData = { + siteId: string; + subwikiId: number; + wikiId: number; + userId: number; + groupId: number; + created: AddonModWikiCreatedPage[]; + discarded: AddonModWikiDiscardedPage[]; + warnings: string[]; +}; + +/** + * Data passed to MANUAL_SYNCED event. + */ +export type AddonModWikiManualSyncData = AddonModWikiSyncWikiResult & { + wikiId: number; +}; diff --git a/src/addons/mod/wiki/services/wiki.ts b/src/addons/mod/wiki/services/wiki.ts new file mode 100644 index 000000000..a1f6b3e59 --- /dev/null +++ b/src/addons/mod/wiki/services/wiki.ts @@ -0,0 +1,1131 @@ +// (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 { CoreError } from '@classes/errors/error'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreTagItem } from '@features/tag/services/tag'; +import { CoreApp } from '@services/app'; +import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton, Translate } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { AddonModWikiPageDBRecord } from './database/wiki'; +import { AddonModWikiOffline } from './wiki-offline'; +import { AddonModWikiAutoSyncData, AddonModWikiManualSyncData, AddonModWikiSyncProvider } from './wiki-sync'; + +const ROOT_CACHE_KEY = 'mmaModWiki:'; + +/** + * Service that provides some features for wikis. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModWikiProvider { + + static readonly COMPONENT = 'mmaModWiki'; + static readonly PAGE_CREATED_EVENT = 'addon_mod_wiki_page_created'; + static readonly RENEW_LOCK_TIME = 30000; // Milliseconds. + + protected subwikiListsCache: {[wikiId: number]: AddonModWikiSubwikiListData} = {}; + + constructor() { + // Clear subwiki lists cache on logout. + CoreEvents.on(CoreEvents.LOGIN, () => { + this.clearSubwikiList(); + }); + } + + /** + * Clear subwiki list cache for a certain wiki or all of them. + * + * @param wikiId wiki Id, if not provided all will be cleared. + */ + clearSubwikiList(wikiId?: number): void { + if (typeof wikiId == 'undefined') { + this.subwikiListsCache = {}; + } else { + delete this.subwikiListsCache[wikiId]; + } + } + + /** + * Save wiki contents on a page or section. + * + * @param pageId Page ID. + * @param content content to be saved. + * @param section section to get. + * @return Promise resolved with the page ID. + */ + async editPage(pageId: number, content: string, section?: string, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModWikiEditPageWSParams = { + pageid: pageId, + content: content, + }; + + if (section) { + params.section = section; + } + + const response = await site.write('mod_wiki_edit_page', params); + + return response.pageid; + } + + /** + * Get a wiki page contents. + * + * @param pageId Page ID. + * @param options Other options. + * @return Promise resolved with the page data. + */ + async getPageContents(pageId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModWikiGetPageContentsWSParams = { + pageid: pageId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getPageContentsCacheKey(pageId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModWikiProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read('mod_wiki_get_page_contents', params, preSets); + + return response.page; + } + + /** + * Get cache key for wiki Pages Contents WS calls. + * + * @param pageId Wiki Page ID. + * @return Cache key. + */ + protected getPageContentsCacheKey(pageId: number): string { + return ROOT_CACHE_KEY + 'page:' + pageId; + } + + /** + * Get a wiki page contents for editing. It does not cache calls. + * + * @param pageId Page ID. + * @param section Section to get. + * @param lockOnly Just renew lock and not return content. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with page contents. + */ + async getPageForEditing( + pageId: number, + section?: string, + lockOnly?: boolean, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModWikiGetPageForEditingWSParams = { + pageid: pageId, + }; + if (section) { + params.section = section; + } + + // This parameter requires Moodle 3.2. It saves network usage. + if (lockOnly && site.isVersionGreaterEqualThan('3.2')) { + params.lockonly = true; + } + + const response = await site.write('mod_wiki_get_page_for_editing', params); + + return response.pagesection; + } + + /** + * Gets the list of files from a specific subwiki. + * + * @param wikiId Wiki ID. + * @param options Other options. + * @return Promise resolved with subwiki files. + */ + async getSubwikiFiles(wikiId: number, options: AddonModWikiGetSubwikiFilesOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const groupId = options.groupId || -1; + const userId = options.userId || 0; + + const params: AddonModWikiGetSubwikiFilesWSParams = { + wikiid: wikiId, + groupid: groupId, + userid: userId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getSubwikiFilesCacheKey(wikiId, groupId, userId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModWikiProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read('mod_wiki_get_subwiki_files', params, preSets); + + return response.files; + } + + /** + * Get cache key for wiki Subwiki Files WS calls. + * + * @param wikiId Wiki ID. + * @param groupId Group ID. + * @param userId User ID. + * @return Cache key. + */ + protected getSubwikiFilesCacheKey(wikiId: number, groupId: number, userId: number): string { + return this.getSubwikiFilesCacheKeyPrefix(wikiId) + ':' + groupId + ':' + userId; + } + + /** + * Get cache key for all wiki Subwiki Files WS calls. + * + * @param wikiId Wiki ID. + * @return Cache key. + */ + protected getSubwikiFilesCacheKeyPrefix(wikiId: number): string { + return ROOT_CACHE_KEY + 'subwikifiles:' + wikiId; + } + + /** + * Get a list of subwikis and related data for a certain wiki from the cache. + * + * @param wikiId wiki Id + * @return Subwiki list and related data. + */ + getSubwikiList(wikiId: number): AddonModWikiSubwikiListData { + return this.subwikiListsCache[wikiId]; + } + + /** + * Get the list of Pages of a SubWiki. + * + * @param wikiId Wiki ID. + * @param options Other options. + * @return Promise resolved with wiki subwiki pages. + */ + async getSubwikiPages(wikiId: number, options: AddonModWikiGetSubwikiPagesOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const groupId = options.groupId || -1; + const userId = options.userId || 0; + const sortBy = options.sortBy || 'title'; + const sortDirection = options.sortDirection || 'ASC'; + + const params: AddonModWikiGetSubwikiPagesWSParams = { + wikiid: wikiId, + groupid: groupId, + userid: userId, + options: { + sortby: sortBy, + sortdirection: sortDirection, + includecontent: options.includeContent ? 1 : 0, + }, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getSubwikiPagesCacheKey(wikiId, groupId, userId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModWikiProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read('mod_wiki_get_subwiki_pages', params, preSets); + + return response.pages; + } + + /** + * Get cache key for wiki Subwiki Pages WS calls. + * + * @param wikiId Wiki ID. + * @param groupId Group ID. + * @param userId User ID. + * @return Cache key. + */ + protected getSubwikiPagesCacheKey(wikiId: number, groupId: number, userId: number): string { + return this.getSubwikiPagesCacheKeyPrefix(wikiId) + ':' + groupId + ':' + userId; + } + + /** + * Get cache key for all wiki Subwiki Pages WS calls. + * + * @param wikiId Wiki ID. + * @return Cache key. + */ + protected getSubwikiPagesCacheKeyPrefix(wikiId: number): string { + return ROOT_CACHE_KEY + 'subwikipages:' + wikiId; + } + + /** + * Get all the subwikis of a wiki. + * + * @param wikiId Wiki ID. + * @param options Other options. + * @return Promise resolved with subwikis. + */ + async getSubwikis(wikiId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModWikiGetSubwikisWSParams = { + wikiid: wikiId, + }; + const preSets = { + cacheKey: this.getSubwikisCacheKey(wikiId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModWikiProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read('mod_wiki_get_subwikis', params, preSets); + + return response.subwikis; + } + + /** + * Get cache key for get wiki subWikis WS calls. + * + * @param wikiId Wiki ID. + * @return Cache key. + */ + protected getSubwikisCacheKey(wikiId: number): string { + return ROOT_CACHE_KEY + 'subwikis:' + wikiId; + } + + /** + * Get a wiki by module ID. + * + * @param courseId Course ID. + * @param cmId Course module ID. + * @param options Other options. + * @return Promise resolved when the wiki is retrieved. + */ + getWiki(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getWikiByField(courseId, 'coursemodule', cmId, options); + } + + /** + * Get a wiki with key=value. If more than one is found, only the first will be returned. + * + * @param courseId Course ID. + * @param key Name of the property to check. + * @param value Value to search. + * @param options Other options. + * @return Promise resolved when the wiki is retrieved. + */ + protected async getWikiByField( + courseId: number, + key: string, + value: unknown, + options: CoreSitesCommonWSOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModWikiGetWikisByCoursesWSParams = { + courseids: [courseId], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getWikiDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModWikiProvider.COMPONENT, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read('mod_wiki_get_wikis_by_courses', params, preSets); + + const currentWiki = response.wikis.find((wiki) => wiki[key] == value); + if (currentWiki) { + return currentWiki; + } + + throw new CoreError('Wiki not found.'); + } + + /** + * Get a wiki by wiki ID. + * + * @param courseId Course ID. + * @param id Wiki ID. + * @param options Other options. + * @return Promise resolved when the wiki is retrieved. + */ + getWikiById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getWikiByField(courseId, 'id', id, options); + } + + /** + * Get cache key for wiki data WS calls. + * + * @param courseId Course ID. + * @return Cache key. + */ + protected getWikiDataCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'wiki:' + courseId; + } + + /** + * Gets a list of files to download for a wiki, using a format similar to module.contents from get_course_contents. + * + * @param wiki Wiki. + * @param options Other options. + * @return Promise resolved with the list of files. + */ + async getWikiFileList(wiki: AddonModWikiWiki, options: CoreSitesCommonWSOptions = {}): Promise { + options.siteId = options.siteId || CoreSites.getCurrentSiteId(); + + let files: CoreWSExternalFile[] = []; + const modOptions = { + cmId: wiki.coursemodule, + ...options, // Include all options. + }; + + const subwikis = await this.getSubwikis(wiki.id, modOptions); + + await Promise.all(subwikis.map(async (subwiki) => { + const subwikiOptions = { + groupId: subwiki.groupid, + userId: subwiki.userid, + ...modOptions, // Include all options. + }; + + const subwikiFiles = await this.getSubwikiFiles(subwiki.wikiid, subwikiOptions); + + files = files.concat(subwikiFiles); + })); + + return files; + } + + /** + * Gets a list of all pages for a Wiki. + * + * @param wiki Wiki. + * @param options Other options. + * @return Page list. + */ + async getWikiPageList(wiki: AddonModWikiWiki, options: CoreSitesCommonWSOptions = {}): Promise { + options.siteId = options.siteId || CoreSites.getCurrentSiteId(); + + let pages: AddonModWikiSubwikiPage[] = []; + const modOptions = { + cmId: wiki.coursemodule, + ...options, // Include all options. + }; + + const subwikis = await this.getSubwikis(wiki.id, modOptions); + + await Promise.all(subwikis.map(async (subwiki) => { + const subwikiPages = await this.getSubwikiPages(subwiki.wikiid, { + groupId: subwiki.groupid, + userId: subwiki.userid, + ...modOptions, // Include all options. + }); + + pages = pages.concat(subwikiPages); + })); + + return pages; + } + + /** + * Invalidate the prefetched content except files. + * To invalidate files, use invalidateFiles. + * + * @param moduleId The module ID. + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const wiki = await this.getWiki(courseId, moduleId, { siteId }); + + await Promise.all([ + this.invalidateWikiData(courseId, siteId), + this.invalidateSubwikis(wiki.id, siteId), + this.invalidateSubwikiPages(wiki.id, siteId), + this.invalidateSubwikiFiles(wiki.id, siteId), + ]); + } + + /** + * Invalidates page content WS call for a certain page. + * + * @param pageId Wiki Page ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidatePage(pageId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getPageContentsCacheKey(pageId)); + } + + /** + * Invalidates all the subwiki files WS calls for a certain wiki. + * + * @param wikiId Wiki ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateSubwikiFiles(wikiId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getSubwikiFilesCacheKeyPrefix(wikiId)); + } + + /** + * Invalidates all the subwiki pages WS calls for a certain wiki. + * + * @param wikiId Wiki ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateSubwikiPages(wikiId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getSubwikiPagesCacheKeyPrefix(wikiId)); + } + + /** + * Invalidates all the get subwikis WS calls for a certain wiki. + * + * @param wikiId Wiki ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateSubwikis(wikiId: number, siteId?: string): Promise { + this.clearSubwikiList(wikiId); + + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getSubwikisCacheKey(wikiId)); + } + + /** + * Invalidates wiki data. + * + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateWikiData(courseId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getWikiDataCacheKey(courseId)); + } + + /** + * Check if a page title is already used. + * + * @param wikiId Wiki ID. + * @param subwikiId Subwiki ID. + * @param title Page title. + * @param options Other options. + * @return Promise resolved with true if used, resolved with false if not used or cannot determine. + */ + async isTitleUsed( + wikiId: number, + subwikiId: number, + title: string, + options: CoreCourseCommonModWSOptions = {}, + ): Promise { + try { + // First get the subwiki. + const subwikis = await this.getSubwikis(wikiId, options); + + // Search the subwiki. + const subwiki = subwikis.find((subwiki) => subwiki.id == subwikiId); + + if (!subwiki) { + return false; + } + + // Now get all the pages of the subwiki. + const pages = await this.getSubwikiPages(wikiId, { + groupId: subwiki.groupid, + userId: subwiki.userid, + ...options, // Include all options. + }); + + // Check if there's any page with the same title. + const page = pages.find((page) => page.title == title); + + return !!page; + } catch { + return false; + } + } + + /** + * Report a wiki page as being viewed. + * + * @param id Page ID. + * @param wikiId Wiki ID. + * @param name Name of the wiki. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + logPageView(id: number, wikiId: number, name?: string, siteId?: string): Promise { + const params: AddonModWikiViewPageWSParams = { + pageid: id, + }; + + return CoreCourseLogHelper.logSingle( + 'mod_wiki_view_page', + params, + AddonModWikiProvider.COMPONENT, + wikiId, + name, + 'wiki', + params, + siteId, + ); + } + + /** + * Report the wiki as being viewed. + * + * @param id Wiki ID. + * @param name Name of the wiki. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + logView(id: number, name?: string, siteId?: string): Promise { + const params: AddonModWikiViewWikiWSParams = { + wikiid: id, + }; + + return CoreCourseLogHelper.logSingle( + 'mod_wiki_view_wiki', + params, + AddonModWikiProvider.COMPONENT, + id, + name, + 'wiki', + {}, + siteId, + ); + } + + /** + * Create a new page on a subwiki. + * + * @param title Title to create the page. + * @param content Content to save on the page. + * @param options Other options. + * @return Promise resolved with page ID if page was created in server, -1 if stored in device. + */ + async newPage(title: string, content: string, options: AddonModWikiNewPageOptions = {}): Promise { + + options.siteId = options.siteId || CoreSites.getCurrentSiteId(); + + // Convenience function to store a new page to be synchronized later. + const storeOffline = async (): Promise => { + if (options.wikiId && options.subwikiId) { + // We have wiki ID, check if there's already an online page with this title and subwiki. + const used = await CoreUtils.ignoreErrors(this.isTitleUsed(options.wikiId, options.subwikiId, title, { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + })); + + if (used) { + throw new CoreError(Translate.instant('addon.mod_wiki.pageexists')); + } + } + + await AddonModWikiOffline.saveNewPage( + title, + content, + options.subwikiId, + options.wikiId, + options.userId, + options.groupId, + options.siteId, + ); + + return -1; + }; + + if (!CoreApp.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + // Discard stored content for this page. If it exists it means the user is editing it. + await AddonModWikiOffline.deleteNewPage( + title, + options.subwikiId, + options.wikiId, + options.userId, + options.groupId, + options.siteId, + ); + + try { + // Try to create it in online. + return this.newPageOnline(title, content, options); + } catch (error) { + if (CoreUtils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that the page cannot be added. + throw error; + } + + // Couldn't connect to server, store in offline. + return storeOffline(); + } + } + + /** + * Create a new page on a subwiki. It will fail if offline or cannot connect. + * + * @param title Title to create the page. + * @param content Content to save on the page. + * @param options Other options. + * @return Promise resolved with the page ID if created, rejected otherwise. + */ + async newPageOnline(title: string, content: string, options: AddonModWikiNewPageOnlineOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModWikiNewPageWSParams = { + title: title, + content: content, + contentformat: 'html', + }; + + const subwikiId = AddonModWikiOffline.convertToPositiveNumber(options.subwikiId); + const wikiId = AddonModWikiOffline.convertToPositiveNumber(options.wikiId); + + if (subwikiId && subwikiId > 0) { + params.subwikiid = subwikiId; + } else if (wikiId) { + params.wikiid = wikiId; + params.userid = AddonModWikiOffline.convertToPositiveNumber(options.userId); + params.groupid = AddonModWikiOffline.convertToPositiveNumber(options.groupId); + } + + const response = await site.write('mod_wiki_new_page', params); + + return response.pageid; + } + + /** + * Save subwiki list for a wiki to the cache. + * + * @param wikiId Wiki Id. + * @param subwikis List of subwikis. + * @param count Number of subwikis in the subwikis list. + * @param subwikiId Subwiki Id currently selected. + * @param userId User Id currently selected. + * @param groupId Group Id currently selected. + */ + setSubwikiList( + wikiId: number, + subwikis: AddonModWikiSubwikiListGrouping[], + count: number, + subwikiId: number, + userId: number, + groupId: number, + ): void { + this.subwikiListsCache[wikiId] = { + count: count, + subwikiSelected: subwikiId, + userSelected: userId, + groupSelected: groupId, + subwikis: subwikis, + }; + } + + /** + * Sort an array of wiki pages by title. + * + * @param pages Pages to sort. + * @param desc True to sort in descendent order, false to sort in ascendent order. Defaults to false. + * @return Sorted pages. + */ + sortPagesByTitle( + pages: T[], + desc?: boolean, + ): T[] { + return pages.sort((a, b) => { + let result = a.title >= b.title ? 1 : -1; + + if (desc) { + result = -result; + } + + return result; + }); + } + + /** + * Check if a wiki has a certain subwiki. + * + * @param wikiId Wiki ID. + * @param subwikiId Subwiki ID to search. + * @param options Other options. + * @return Promise resolved with true if it has subwiki, resolved with false otherwise. + */ + async wikiHasSubwiki(wikiId: number, subwikiId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + try { + // Get the subwikis to check if any of them matches the one passed as param. + const subwikis = await this.getSubwikis(wikiId, options); + + const subwiki = subwikis.find((subwiki) => subwiki.id == subwikiId); + + return !!subwiki; + } catch { + // Not found, return false. + return false; + } + } + +} + +export const AddonModWiki = makeSingleton(AddonModWikiProvider); + +declare module '@singletons/events' { + + /** + * Augment CoreEventsData interface with events specific to this service. + * + * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation + */ + export interface CoreEventsData { + [AddonModWikiProvider.PAGE_CREATED_EVENT]: AddonModWikiPageCreatedData; + [AddonModWikiSyncProvider.AUTO_SYNCED]: AddonModWikiAutoSyncData; + [AddonModWikiSyncProvider.MANUAL_SYNCED]: AddonModWikiManualSyncData; + } + +} + +/** + * Options to pass to getSubwikiFiles. + */ +export type AddonModWikiGetSubwikiFilesOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User to get files from. + groupId?: number; // Group to get files from. +}; + +/** + * Options to pass to getSubwikiPages. + */ +export type AddonModWikiGetSubwikiPagesOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User to get pages from. + groupId?: number; // Group to get pages from. + sortBy?: string; // The attribute to sort the returned list. Defaults to 'title'. + sortDirection?: string; // Direction to sort the returned list (ASC | DESC). Defaults to 'ASC'. + includeContent?: boolean; // Whether the pages have to include their content. +}; + +/** + * Options to pass to newPageOnline. + */ +export type AddonModWikiNewPageOnlineOptions = { + subwikiId?: number; // Subwiki ID. If not defined, wikiId, userId and groupId should be defined. + wikiId?: number; // Wiki ID. Optional, will be used to create a new subwiki if subwikiId not supplied. + userId?: number; // User ID. Optional, will be used to create a new subwiki if subwikiId not supplied. + groupId?: number; // Group ID. Optional, will be used to create a new subwiki if subwikiId not supplied. + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to newPage. + */ +export type AddonModWikiNewPageOptions = AddonModWikiNewPageOnlineOptions & { + cmId?: number; // Module ID. +}; + +export type AddonModWikiSubwikiListData = { + count: number; // Number of subwikis. + subwikiSelected: number; // Subwiki ID currently selected. + userSelected: number; // User of the subwiki currently selected. + groupSelected: number; // Group of the subwiki currently selected. + subwikis: AddonModWikiSubwikiListGrouping[]; // List of subwikis, grouped by a certain label. +}; + +export type AddonModWikiSubwikiListGrouping = { + label: string; + subwikis: AddonModWikiSubwikiListSubwiki[]; +}; + +export type AddonModWikiSubwikiListSubwiki = { + name: string; + id: number; + userid: number; + groupid: number; + groupLabel: string; + canedit: boolean; +}; + +/** + * Params of mod_wiki_edit_page WS. + */ +export type AddonModWikiEditPageWSParams = { + pageid: number; // Page ID. + content: string; // Page contents. + section?: string; // Section page title. +}; + +/** + * Data returned by mod_wiki_edit_page WS. + */ +export type AddonModWikiEditPageWSResponse = { + pageid: number; // Edited page id. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_wiki_get_page_contents WS. + */ +export type AddonModWikiGetPageContentsWSParams = { + pageid: number; // Page ID. +}; + +/** + * Data returned by mod_wiki_get_page_contents WS. + */ +export type AddonModWikiGetPageContentsWSResponse = { + page: AddonModWikiPageContents; // Page. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Page data returned by mod_wiki_get_page_contents WS. + */ +export type AddonModWikiPageContents = { + id: number; // Page ID. + wikiid: number; // Page's wiki ID. + subwikiid: number; // Page's subwiki ID. + groupid: number; // Page's group ID. + userid: number; // Page's user ID. + title: string; // Page title. + cachedcontent: string; // Page contents. + contentformat?: number; // Cachedcontent format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + caneditpage: boolean; // True if user can edit the page. + version?: number; // Latest version of the page. + tags?: CoreTagItem[]; // Tags. +}; + +/** + * Params of mod_wiki_get_page_for_editing WS. + */ +export type AddonModWikiGetPageForEditingWSParams = { + pageid: number; // Page ID to edit. + section?: string; // Section page title. + lockonly?: boolean; // Just renew lock and not return content. +}; + +/** + * Data returned by mod_wiki_get_page_for_editing WS. + */ +export type AddonModWikiGetPageForEditingWSResponse = { + pagesection: AddonModWikiWSEditPageSection; +}; + +/** + * Page section data returned by mod_wiki_get_page_for_editing WS. + */ +export type AddonModWikiWSEditPageSection = { + content?: string; // The contents of the page-section to be edited. + contentformat?: string; // Format of the original content of the page. + version: number; // Latest version of the page. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_wiki_get_subwiki_files WS. + */ +export type AddonModWikiGetSubwikiFilesWSParams = { + wikiid: number; // Wiki instance ID. + groupid?: number; // Subwiki's group ID, -1 means current group. It will be ignored if the wiki doesn't use groups. + userid?: number; // Subwiki's user ID, 0 means current user. It will be ignored in collaborative wikis. +}; + +/** + * Data returned by mod_wiki_get_subwiki_files WS. + */ +export type AddonModWikiGetSubwikiFilesWSResponse = { + files: CoreWSExternalFile[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_wiki_get_subwiki_pages WS. + */ +export type AddonModWikiGetSubwikiPagesWSParams = { + wikiid: number; // Wiki instance ID. + groupid?: number; // Subwiki's group ID, -1 means current group. It will be ignored if the wiki doesn't use groups. + userid?: number; // Subwiki's user ID, 0 means current user. It will be ignored in collaborative wikis. + options?: { + sortby?: string; // Field to sort by (id, title, ...). + sortdirection?: string; // Sort direction: ASC or DESC. + includecontent?: number; // Include each page contents or just the contents size. + }; // Options. +}; + +/** + * Data returned by mod_wiki_get_subwiki_pages WS. + */ +export type AddonModWikiGetSubwikiPagesWSResponse = { + pages: AddonModWikiSubwikiPage[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Page data returned by mod_wiki_get_subwiki_pages WS. + */ +export type AddonModWikiSubwikiPage = { + id: number; // Page ID. + subwikiid: number; // Page's subwiki ID. + title: string; // Page title. + timecreated: number; // Time of creation. + timemodified: number; // Time of last modification. + timerendered: number; // Time of last renderization. + userid: number; // ID of the user that last modified the page. + pageviews: number; // Number of times the page has been viewed. + readonly: number; // 1 if readonly, 0 otherwise. + caneditpage: boolean; // True if user can edit the page. + firstpage: boolean; // True if it's the first page. + cachedcontent?: string; // Page contents. + contentformat?: number; // Cachedcontent format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + contentsize?: number; // Size of page contents in bytes (doesn't include size of attached files). + tags?: CoreTagItem[]; // Tags. +}; + +/** + * Params of mod_wiki_get_subwikis WS. + */ +export type AddonModWikiGetSubwikisWSParams = { + wikiid: number; // Wiki instance ID. +}; + +/** + * Data returned by mod_wiki_get_subwikis WS. + */ +export type AddonModWikiGetSubwikisWSResponse = { + subwikis: AddonModWikiSubwiki[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Subwiki data returned by mod_wiki_get_subwikis WS. + */ +export type AddonModWikiSubwiki = { + id: number; // Subwiki ID. + wikiid: number; // Wiki ID. + groupid: number; // Group ID. + userid: number; // User ID. + canedit: boolean; // True if user can edit the subwiki. +}; + +/** + * Params of mod_wiki_get_wikis_by_courses WS. + */ +export type AddonModWikiGetWikisByCoursesWSParams = { + courseids?: number[]; // Array of course ids. +}; + +/** + * Data returned by mod_wiki_get_wikis_by_courses WS. + */ +export type AddonModWikiGetWikisByCoursesWSResponse = { + wikis: AddonModWikiWiki[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Wiki data returned by mod_wiki_get_wikis_by_courses WS. + */ +export type AddonModWikiWiki = { + id: number; // Wiki ID. + coursemodule: number; // Course module ID. + course: number; // Course ID. + name: string; // Wiki name. + intro?: string; // Wiki intro. + introformat?: number; // Wiki intro format. format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles?: CoreWSExternalFile[]; + timecreated?: number; // Time of creation. + timemodified?: number; // Time of last modification. + firstpagetitle?: string; // First page title. + wikimode?: string; // Wiki mode (individual, collaborative). + defaultformat?: string; // Wiki's default format (html, creole, nwiki). + forceformat?: number; // 1 if format is forced, 0 otherwise. + editbegin?: number; // Edit begin. + editend?: number; // Edit end. + section?: number; // Course section ID. + visible?: number; // 1 if visible, 0 otherwise. + groupmode?: number; // Group mode. + groupingid?: number; // Group ID. + cancreatepages: boolean; // True if user can create pages. +}; + +/** + * Params of mod_wiki_view_page WS. + */ +export type AddonModWikiViewPageWSParams = { + pageid: number; // Wiki page ID. +}; + +/** + * Params of mod_wiki_view_wiki WS. + */ +export type AddonModWikiViewWikiWSParams = { + wikiid: number; // Wiki instance ID. +}; + +/** + * Params of mod_wiki_new_page WS. + */ +export type AddonModWikiNewPageWSParams = { + title: string; // New page title. + content: string; // Page contents. + contentformat?: string; // Page contents format. If an invalid format is provided, default wiki format is used. + subwikiid?: number; // Page's subwiki ID. + wikiid?: number; // Page's wiki ID. Used if subwiki does not exists. + userid?: number; // Subwiki's user ID. Used if subwiki does not exists. + groupid?: number; // Subwiki's group ID. Used if subwiki does not exists. +}; + +/** + * Data returned by mod_wiki_new_page WS. + */ +export type AddonModWikiNewPageWSResponse = { + pageid: number; // New page id. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Data passed to PAGE_CREATED event. + */ +export type AddonModWikiPageCreatedData = { + pageId: number; + subwikiId: number; + pageTitle: string; +}; diff --git a/src/addons/mod/wiki/wiki-lazy.module.ts b/src/addons/mod/wiki/wiki-lazy.module.ts new file mode 100644 index 000000000..4049bbcea --- /dev/null +++ b/src/addons/mod/wiki/wiki-lazy.module.ts @@ -0,0 +1,38 @@ +// (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'; +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonModWikiComponentsModule } from './components/components.module'; +import { AddonModWikiIndexPage } from './pages/index/index'; + +const routes: Routes = [ + { + path: ':courseId/:cmId/:hash', + component: AddonModWikiIndexPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + AddonModWikiComponentsModule, + ], + declarations: [ + AddonModWikiIndexPage, + ], +}) +export class AddonModWikiLazyModule {} diff --git a/src/addons/mod/wiki/wiki.module.ts b/src/addons/mod/wiki/wiki.module.ts new file mode 100644 index 000000000..409c64783 --- /dev/null +++ b/src/addons/mod/wiki/wiki.module.ts @@ -0,0 +1,81 @@ +// (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, Type } from '@angular/core'; +import { Routes } from '@angular/router'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate'; +import { CoreCronDelegate } from '@services/cron'; +import { CORE_SITE_SCHEMAS } from '@services/sites'; +import { AddonModWikiComponentsModule } from './components/components.module'; +import { OFFLINE_SITE_SCHEMA } from './services/database/wiki'; +import { AddonModWikiCreateLinkHandler } from './services/handlers/create-link'; +import { AddonModWikiEditLinkHandler } from './services/handlers/edit-link'; +import { AddonModWikiIndexLinkHandler } from './services/handlers/index-link'; +import { AddonModWikiListLinkHandler } from './services/handlers/list-link'; +import { AddonModWikiModuleHandler, AddonModWikiModuleHandlerService } from './services/handlers/module'; +import { AddonModWikiPageOrMapLinkHandler } from './services/handlers/page-or-map-link'; +import { AddonModWikiPrefetchHandler } from './services/handlers/prefetch'; +import { AddonModWikiSyncCronHandler } from './services/handlers/sync-cron'; +import { AddonModWikiTagAreaHandler } from './services/handlers/tag-area'; +import { AddonModWikiProvider } from './services/wiki'; +import { AddonModWikiOfflineProvider } from './services/wiki-offline'; +import { AddonModWikiSyncProvider } from './services/wiki-sync'; + +export const ADDON_MOD_WIKI_SERVICES: Type[] = [ + AddonModWikiProvider, + AddonModWikiOfflineProvider, + AddonModWikiSyncProvider, +]; + +const routes: Routes = [ + { + path: AddonModWikiModuleHandlerService.PAGE_NAME, + loadChildren: () => import('./wiki-lazy.module').then(m => m.AddonModWikiLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + AddonModWikiComponentsModule, + ], + providers: [ + { + provide: CORE_SITE_SCHEMAS, + useValue: [OFFLINE_SITE_SCHEMA], + multi: true, + }, + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + CoreCourseModuleDelegate.registerHandler(AddonModWikiModuleHandler.instance); + CoreCourseModulePrefetchDelegate.registerHandler(AddonModWikiPrefetchHandler.instance); + CoreCronDelegate.register(AddonModWikiSyncCronHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModWikiIndexLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModWikiListLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModWikiCreateLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModWikiEditLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModWikiPageOrMapLinkHandler.instance); + CoreTagAreaDelegate.registerHandler(AddonModWikiTagAreaHandler.instance); + }, + }, + ], +}) +export class AddonModWikiModule {} diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 3ec49affb..cad7df44a 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -141,7 +141,7 @@ import { ADDON_MOD_RESOURCE_SERVICES } from '@addons/mod/resource/resource.modul import { ADDON_MOD_SCORM_SERVICES } from '@addons/mod/scorm/scorm.module'; import { ADDON_MOD_SURVEY_SERVICES } from '@addons/mod/survey/survey.module'; import { ADDON_MOD_URL_SERVICES } from '@addons/mod/url/url.module'; -// @todo import { ADDON_MOD_WIKI_SERVICES } from '@addons/mod/wiki/wiki.module'; +import { ADDON_MOD_WIKI_SERVICES } from '@addons/mod/wiki/wiki.module'; // @todo import { ADDON_MOD_WORKSHOP_SERVICES } from '@addons/mod/workshop/workshop.module'; import { ADDON_NOTES_SERVICES } from '@addons/notes/notes.module'; import { ADDON_NOTIFICATIONS_SERVICES } from '@addons/notifications/notifications.module'; @@ -307,7 +307,7 @@ export class CoreCompileProvider { ...ADDON_MOD_SCORM_SERVICES, ...ADDON_MOD_SURVEY_SERVICES, ...ADDON_MOD_URL_SERVICES, - // @todo ...ADDON_MOD_WIKI_SERVICES, + ...ADDON_MOD_WIKI_SERVICES, // @todo ...ADDON_MOD_WORKSHOP_SERVICES, ...ADDON_NOTES_SERVICES, ...ADDON_NOTIFICATIONS_SERVICES, diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index de3db9eef..36e5ef902 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -42,6 +42,7 @@ import { CoreCronDelegate } from '@services/cron'; import { CoreCourseLogCronHandler } from './handlers/log-cron'; import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync'; +import { CoreTagItem } from '@features/tag/services/tag'; const ROOT_CACHE_KEY = 'mmCourse:'; @@ -1473,18 +1474,7 @@ export type CoreCourseModuleContentFile = { userid: number; // User who added this content to moodle. author: string; // Content owner. license: string; // Content license. - tags?: { // Tags. - id: number; // Tag id. - name: string; // Tag name. - rawname: string; // The raw, unnormalised name for the tag as entered by users. - isstandard: boolean; // Whether this tag is standard. - tagcollid: number; // Tag collection id. - taginstanceid: number; // Tag instance id. - taginstancecontextid: number; // Context the tag instance belongs to. - itemid: number; // Id of the record tagged. - ordering: number; // Tag ordering. - flag: number; // Whether the tag is flagged as inappropriate. - }[]; + tags?: CoreTagItem[]; // Tags. }; /** diff --git a/src/core/features/tag/services/tag.ts b/src/core/features/tag/services/tag.ts index 13018d59e..4d1d6bd77 100644 --- a/src/core/features/tag/services/tag.ts +++ b/src/core/features/tag/services/tag.ts @@ -428,14 +428,14 @@ export type CoreTagIndex = { * 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; + id: number; // Tag id. + name: string; // Tag name. + rawname: string; // The raw, unnormalised name for the tag as entered by users. + isstandard: boolean; // Whether this tag is standard. + tagcollid: number; // Tag collection id. + taginstanceid: number; // Tag instance id. + taginstancecontextid: number; // Context the tag instance belongs to. + itemid: number; // Id of the record tagged. + ordering: number; // Tag ordering. + flag: number; // Whether the tag is flagged as inappropriate. };