From 56f8b9d7c5d287f518984586ec5f1dd77731317d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 21 Oct 2025 14:33:23 +0200 Subject: [PATCH 1/3] MOBILE-4910 mainmenu: Add open link method enum --- src/core/constants.ts | 7 +++++++ src/core/directives/external-content.ts | 4 ++-- src/core/directives/format-text.ts | 4 ++-- src/core/directives/link.ts | 6 +++--- src/core/features/mainmenu/services/mainmenu.ts | 14 ++++++++------ 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/core/constants.ts b/src/core/constants.ts index f30b24a3d49..32b5ac3dbfe 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -125,6 +125,13 @@ export const enum LMSBadgeStyle { INFO = 'info', } +export enum CoreLinkOpenMethod { + APP = 'app', + INAPPBROWSER = 'inappbrowser', + BROWSER = 'browser', + EMBEDDED = 'embedded', +} + /** * Static class to contain all the core constants. */ diff --git a/src/core/directives/external-content.ts b/src/core/directives/external-content.ts index 04d608f272e..34faf945424 100644 --- a/src/core/directives/external-content.ts +++ b/src/core/directives/external-content.ts @@ -32,7 +32,7 @@ import { CoreLogger } from '@singletons/logger'; import { CoreError } from '@classes/errors/error'; import { CoreSite } from '@classes/sites/site'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; -import { DownloadStatus } from '../constants'; +import { CoreLinkOpenMethod, DownloadStatus } from '../constants'; import { CoreNetwork } from '@services/network'; import { Translate } from '@singletons'; import { AsyncDirective } from '@classes/async-directive'; @@ -370,7 +370,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O const tagName = this.element.tagName; const openIn = tagName === 'A' && this.element.getAttribute('data-open-in'); - if (openIn === 'app' || openIn === 'browser') { + if (openIn === CoreLinkOpenMethod.APP || openIn === CoreLinkOpenMethod.BROWSER) { // The file is meant to be opened in browser or InAppBrowser, don't use the downloaded URL because it won't work. if (!site.isSitePluginFileUrl(url)) { return url; diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 38d184c710b..824172f65b6 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -53,7 +53,7 @@ import { MediaElementController } from '@classes/element-controllers/MediaElemen import { FrameElement, FrameElementController } from '@classes/element-controllers/FrameElementController'; import { CoreUrl } from '@singletons/url'; import { CoreIcons } from '@singletons/icons'; -import { ContextLevel } from '../constants'; +import { ContextLevel, CoreLinkOpenMethod } from '../constants'; import { CoreWait } from '@singletons/wait'; import { toBoolean } from '../transforms/boolean'; import { CoreViewer } from '@features/viewer/services/viewer'; @@ -839,7 +839,7 @@ export class CoreFormatTextDirective implements OnDestroy, AsyncDirective { // Try to convert the URL to absolute if needed. url = CoreUrl.toAbsoluteURL(site.getURL(), url); const confirmMessage = element.dataset.appUrlConfirm; - const openInApp = element.dataset.openIn === 'app'; + const openInApp = element.dataset.openIn === CoreLinkOpenMethod.APP; const refreshOnResume = element.dataset.appUrlResumeAction === 'refresh'; if (confirmMessage) { diff --git a/src/core/directives/link.ts b/src/core/directives/link.ts index 35127cf9bd9..2d33131c915 100644 --- a/src/core/directives/link.ts +++ b/src/core/directives/link.ts @@ -19,7 +19,7 @@ import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreUrl } from '@singletons/url'; import { CoreOpener } from '@singletons/opener'; -import { CoreConstants } from '@/core/constants'; +import { CoreConstants, CoreLinkOpenMethod } from '@/core/constants'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CoreCustomURLSchemes } from '@services/urlschemes'; import { DomSanitizer } from '@singletons'; @@ -187,8 +187,8 @@ export class CoreLinkDirective implements OnInit { protected async openExternalLink(href: string, openIn?: string | null): Promise { // Priority order is: core-link inApp attribute > forceOpenLinksIn setting > data-open-in HTML attribute. const openInApp = this.inApp ?? - (CoreConstants.CONFIG.forceOpenLinksIn !== 'browser' && - (CoreConstants.CONFIG.forceOpenLinksIn === 'app' || openIn === 'app')); + (CoreConstants.CONFIG.forceOpenLinksIn !== CoreLinkOpenMethod.BROWSER && + (CoreConstants.CONFIG.forceOpenLinksIn === CoreLinkOpenMethod.APP || openIn === CoreLinkOpenMethod.APP)); // Check if we need to auto-login. if (!CoreSites.isLoggedIn()) { diff --git a/src/core/features/mainmenu/services/mainmenu.ts b/src/core/features/mainmenu/services/mainmenu.ts index 94d0c8a7d06..f7e617c594e 100644 --- a/src/core/features/mainmenu/services/mainmenu.ts +++ b/src/core/features/mainmenu/services/mainmenu.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreLang, CoreLangFormat, CoreLangLanguage } from '@services/lang'; import { CoreSites } from '@services/sites'; -import { CoreConstants } from '@/core/constants'; +import { CoreConstants, CoreLinkOpenMethod } from '@/core/constants'; import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from './mainmenu-delegate'; import { Device, makeSingleton } from '@singletons'; import { CoreText } from '@singletons/text'; @@ -113,7 +113,7 @@ export class CoreMainMenuProvider { let position = 0; // Position of each item, to keep the same order as it's configured. - if (!itemsString || typeof itemsString != 'string') { + if (!itemsString || typeof itemsString !== 'string') { // Setting not valid. return result; } @@ -136,7 +136,9 @@ export class CoreMainMenuProvider { const id = `${url}#${type}`; if (!icon) { // Icon not defined, use default one. - icon = type == 'embedded' ? 'fas-expand' : 'fas-link'; // @todo Find a better icon for embedded. + icon = type == CoreLinkOpenMethod.EMBEDDED + ? 'fas-expand' // @todo Find a better icon for embedded. + : 'fas-link'; } if (!map[id]) { @@ -283,7 +285,7 @@ export class CoreMainMenuProvider { * @returns Promise resolved with boolean: whether it's the root of a main menu tab. */ async isMainMenuTab(pageName: string): Promise { - if (pageName == MAIN_MENU_MORE_PAGE_NAME) { + if (pageName === MAIN_MENU_MORE_PAGE_NAME) { return true; } @@ -330,7 +332,7 @@ export interface CoreMainMenuCustomItem { /** * Type of the item: app, inappbrowser, browser or embedded. */ - type: string; + type: CoreLinkOpenMethod; /** * Url of the item. @@ -360,7 +362,7 @@ export type CoreMainMenuLocalizedCustomItem = Omit Date: Tue, 21 Oct 2025 15:30:43 +0200 Subject: [PATCH 2/3] MOBILE-4910 mainmenu: Manage custom user menu items like main menu items --- src/core/features/mainmenu/pages/more/more.ts | 11 +- .../features/mainmenu/services/custommenu.ts | 257 ++++++++++++++++++ .../features/mainmenu/services/mainmenu.ts | 208 +------------- src/types/config.d.ts | 5 +- 4 files changed, 272 insertions(+), 209 deletions(-) create mode 100644 src/core/features/mainmenu/services/custommenu.ts diff --git a/src/core/features/mainmenu/pages/more/more.ts b/src/core/features/mainmenu/pages/more/more.ts index f50e42ab42b..1f4c3ff181e 100644 --- a/src/core/features/mainmenu/pages/more/more.ts +++ b/src/core/features/mainmenu/pages/more/more.ts @@ -18,7 +18,7 @@ import { Subscription } from 'rxjs'; import { CoreSites } from '@services/sites'; import { CoreQRScan } from '@services/qrscan'; import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../services/mainmenu-delegate'; -import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu'; +import { CoreMainMenu } from '../../services/mainmenu'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreNavigator } from '@services/navigator'; import { Translate } from '@singletons'; @@ -28,6 +28,7 @@ import { CoreSharedModule } from '@/core/shared.module'; import { CoreMainMenuUserButtonComponent } from '../../components/user-menu-button/user-menu-button'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CoreUrl } from '@singletons/url'; +import { CoreCustomMenu, CoreCustomMenuItem } from '@features/mainmenu/services/custommenu'; /** * Page that displays the more page of the app. @@ -46,7 +47,7 @@ export default class CoreMainMenuMorePage implements OnInit, OnDestroy { handlers?: CoreMainMenuHandlerData[]; handlersLoaded = false; showScanQR: boolean; - customItems?: CoreMainMenuCustomItem[]; + customItems?: CoreCustomMenuItem[]; protected allHandlers?: CoreMainMenuHandlerData[]; protected subscription!: Subscription; @@ -58,7 +59,7 @@ export default class CoreMainMenuMorePage implements OnInit, OnDestroy { this.langObserver = CoreEvents.on(CoreEvents.LANGUAGE_CHANGED, () => this.loadCustomMenuItems()); this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, async () => { - this.customItems = await CoreMainMenu.getCustomMenuItems(); + this.customItems = await CoreCustomMenu.getCustomMainMenuItems(); }, CoreSites.getCurrentSiteId()); this.loadCustomMenuItems(); @@ -118,7 +119,7 @@ export default class CoreMainMenuMorePage implements OnInit, OnDestroy { * Load custom menu items. */ protected async loadCustomMenuItems(): Promise { - this.customItems = await CoreMainMenu.getCustomMenuItems(); + this.customItems = await CoreCustomMenu.getCustomMainMenuItems(); } /** @@ -137,7 +138,7 @@ export default class CoreMainMenuMorePage implements OnInit, OnDestroy { * * @param item Item to open. */ - openItem(item: CoreMainMenuCustomItem): void { + openItem(item: CoreCustomMenuItem): void { CoreViewer.openIframeViewer(item.label, item.url); } diff --git a/src/core/features/mainmenu/services/custommenu.ts b/src/core/features/mainmenu/services/custommenu.ts new file mode 100644 index 00000000000..bea4ca905cc --- /dev/null +++ b/src/core/features/mainmenu/services/custommenu.ts @@ -0,0 +1,257 @@ +// (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 { CoreConstants, CoreLinkOpenMethod } from '@/core/constants'; +import { CoreLang, CoreLangFormat, CoreLangLanguage } from '@services/lang'; +import { Device, makeSingleton } from '@singletons'; +import { CorePlatform } from '@services/platform'; +import { CoreSites } from '@services/sites'; +import { CoreText } from '@singletons/text'; + +/** + * Service that provides some features regarding custom main and user menu. + */ +@Injectable({ providedIn: 'root' }) +export class CoreCustomMenuService { + + protected static readonly CUSTOM_MAIN_MENU_ITEMS_CONFIG = 'tool_mobile_custommenuitems'; + protected static readonly CUSTOM_USER_MENU_ITEMS_CONFIG = 'tool_mobile_customusermenuitems'; + + /** + * Get a list of custom main menu items. + * + * @param siteId Site to get custom items from. + * @returns List of custom menu items. + */ + async getCustomMainMenuItems(siteId?: string): Promise { + const customItems = await Promise.all([ + this.getCustomMenuItemsFromSite(CoreCustomMenuService.CUSTOM_MAIN_MENU_ITEMS_CONFIG, siteId), + this.getCustomItemsFromConfig(CoreConstants.CONFIG.customMainMenuItems), + ]); + + return customItems.flat(); + } + + /** + * Get a list of custom user menu items. + * + * @param siteId Site to get custom items from. + * @returns List of custom menu items. + */ + async getUserCustomMenuItems(siteId?: string): Promise { + const customItems = await Promise.all([ + this.getCustomMenuItemsFromSite(CoreCustomMenuService.CUSTOM_USER_MENU_ITEMS_CONFIG, siteId), + this.getCustomItemsFromConfig(CoreConstants.CONFIG.customUserMenuItems), + ]); + + return customItems.flat(); + } + + /** + * Get a list of custom menu items for a certain site. + * + * @param config Config key to get items from. + * @param siteId Site ID. If not defined, current site. + * @returns List of custom menu items. + */ + protected async getCustomMenuItemsFromSite(config: string, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const itemsString = site.getStoredConfig(config); + if (!itemsString || typeof itemsString !== 'string') { + // Setting not valid. + return []; + } + + const map: CustomMenuItemsMap = {}; + const result: CoreCustomMenuItem[] = []; + + let position = 0; // Position of each item, to keep the same order as it's configured. + + // Add items to the map. + const items = itemsString.split(/(?:\r\n|\r|\n)/); + items.forEach((item) => { + const values = item.split('|'); + const label = values[0] ? values[0].trim() : values[0]; + const url = values[1] ? values[1].trim() : values[1]; + const type = values[2] ? values[2].trim() : values[2]; + const lang = (values[3] ? values[3].trim() : values[3]) || 'none'; + let icon = values[4] ? values[4].trim() : values[4]; + + if (!label || !url || !type) { + // Invalid item, ignore it. + return; + } + + const id = `${url}#${type}`; + if (!icon) { + // Icon not defined, use default one. + icon = type === CoreLinkOpenMethod.EMBEDDED + ? 'fas-expand' // @todo Find a better icon for embedded. + : 'fas-link'; + } + + if (!map[id]) { + // New entry, add it to the map. + map[id] = { + url, + type: type as CoreLinkOpenMethod, + position, + labels: {}, + }; + position++; + } + + map[id].labels[lang.toLowerCase()] = { + label: label, + icon: icon, + }; + }); + + if (!position) { + // No valid items found, stop. + return result; + } + + const currentLangApp = await CoreLang.getCurrentLanguage(); + const currentLangLMS = CoreLang.formatLanguage(currentLangApp, CoreLangFormat.LMS); + const fallbackLang = CoreConstants.CONFIG.default_lang || 'en'; + + // Get the right label for each entry and add it to the result. + for (const id in map) { + const entry = map[id]; + let data = entry.labels[currentLangApp] + ?? entry.labels[currentLangLMS] + ?? entry.labels[`${currentLangApp}_only`] + ?? entry.labels[`${currentLangLMS}_only`] + ?? entry.labels.none + ?? entry.labels[fallbackLang]; + + if (!data) { + // No valid label found, get the first one that is not "_only". + for (const lang in entry.labels) { + if (!lang.includes('_only')) { + data = entry.labels[lang]; + break; + } + } + + if (!data) { + // No valid label, ignore this entry. + continue; + } + } + + result[entry.position] = { + url: entry.url, + type: entry.type, + label: data.label, + icon: data.icon, + }; + } + + // Remove undefined values. + return result.filter((entry) => entry !== undefined); + } + + /** + * Get a list of custom menu items from config. + * + * @param items Items from config. + * @returns List of custom menu items. + */ + protected async getCustomItemsFromConfig(items?: CoreCustomMenuLocalizedCustomItem[]): Promise { + if (!items) { + return []; + } + + const currentLang = await CoreLang.getCurrentLanguage(); + + const fallbackLang = CoreConstants.CONFIG.default_lang || 'en'; + const replacements = { + devicetype: '', + osversion: Device.version, + }; + + if (CorePlatform.isAndroid()) { + replacements.devicetype = 'Android'; + } else if (CorePlatform.isIOS()) { + replacements.devicetype = 'iPhone or iPad'; + } else { + replacements.devicetype = 'Other'; + } + + return items + .filter(item => typeof item.label === 'string' || currentLang in item.label || fallbackLang in item.label) + .map(item => ({ + ...item, + url: CoreText.replaceArguments(item.url, replacements, 'uri'), + label: typeof item.label === 'string' + ? item.label + : item.label[currentLang] ?? item.label[fallbackLang], + })); + } + +} + +export const CoreCustomMenu = makeSingleton(CoreCustomMenuService); + +/** + * Custom menu item. + */ +export interface CoreCustomMenuItem { + /** + * Type of the item: app, inappbrowser, browser or embedded. + */ + type: CoreLinkOpenMethod; + + /** + * Url of the item. + */ + url: string; + + /** + * Label to display for the item. + */ + label: string; + + /** + * Name of the icon to display for the item. + */ + icon: string; +} + +/** + * Custom custom menu item with localized text. + */ +export type CoreCustomMenuLocalizedCustomItem = Omit & { + label: string | Record; +}; + +/** + * Map of custom menu items. + */ +type CustomMenuItemsMap = Record; diff --git a/src/core/features/mainmenu/services/mainmenu.ts b/src/core/features/mainmenu/services/mainmenu.ts index f7e617c594e..6de0aafe929 100644 --- a/src/core/features/mainmenu/services/mainmenu.ts +++ b/src/core/features/mainmenu/services/mainmenu.ts @@ -14,14 +14,10 @@ import { Injectable } from '@angular/core'; -import { CoreLang, CoreLangFormat, CoreLangLanguage } from '@services/lang'; import { CoreSites } from '@services/sites'; -import { CoreConstants, CoreLinkOpenMethod } from '@/core/constants'; import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from './mainmenu-delegate'; -import { Device, makeSingleton } from '@singletons'; -import { CoreText } from '@singletons/text'; +import { makeSingleton } from '@singletons'; import { CoreScreen } from '@services/screen'; -import { CorePlatform } from '@services/platform'; import { CoreMainMenuPlacement, MAIN_MENU_HANDLER_BADGE_UPDATED_EVENT, @@ -30,6 +26,7 @@ import { MAIN_MENU_NUM_MAIN_HANDLERS, MAIN_MENU_VISIBILITY_UPDATED_EVENT, } from '../constants'; +import { CoreCustomMenuItem } from './custommenu'; declare module '@singletons/events' { @@ -88,159 +85,13 @@ export class CoreMainMenuProvider { * * @param siteId Site to get custom items from. * @returns List of custom menu items. - */ - async getCustomMenuItems(siteId?: string): Promise { - const customItems = await Promise.all([ - this.getCustomMenuItemsFromSite(siteId), - this.getCustomItemsFromConfig(), - ]); - - return customItems.flat(); - } - - /** - * Get a list of custom menu items for a certain site. * - * @param siteId Site ID. If not defined, current site. - * @returns List of custom menu items. + * @deprecated since 5.2. Use CoreCustomMenu.getCustomMenuItems() instead. */ - protected async getCustomMenuItemsFromSite(siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); - - const itemsString = site.getStoredConfig('tool_mobile_custommenuitems'); - const map: CustomMenuItemsMap = {}; - const result: CoreMainMenuCustomItem[] = []; - - let position = 0; // Position of each item, to keep the same order as it's configured. - - if (!itemsString || typeof itemsString !== 'string') { - // Setting not valid. - return result; - } - - // Add items to the map. - const items = itemsString.split(/(?:\r\n|\r|\n)/); - items.forEach((item) => { - const values = item.split('|'); - const label = values[0] ? values[0].trim() : values[0]; - const url = values[1] ? values[1].trim() : values[1]; - const type = values[2] ? values[2].trim() : values[2]; - const lang = (values[3] ? values[3].trim() : values[3]) || 'none'; - let icon = values[4] ? values[4].trim() : values[4]; - - if (!label || !url || !type) { - // Invalid item, ignore it. - return; - } - - const id = `${url}#${type}`; - if (!icon) { - // Icon not defined, use default one. - icon = type == CoreLinkOpenMethod.EMBEDDED - ? 'fas-expand' // @todo Find a better icon for embedded. - : 'fas-link'; - } - - if (!map[id]) { - // New entry, add it to the map. - map[id] = { - url: url, - type: type, - position: position, - labels: {}, - }; - position++; - } - - map[id].labels[lang.toLowerCase()] = { - label: label, - icon: icon, - }; - }); - - if (!position) { - // No valid items found, stop. - return result; - } - - const currentLangApp = await CoreLang.getCurrentLanguage(); - const currentLangLMS = CoreLang.formatLanguage(currentLangApp, CoreLangFormat.LMS); - const fallbackLang = CoreConstants.CONFIG.default_lang || 'en'; - - // Get the right label for each entry and add it to the result. - for (const id in map) { - const entry = map[id]; - let data = entry.labels[currentLangApp] - ?? entry.labels[currentLangLMS] - ?? entry.labels[`${currentLangApp}_only`] - ?? entry.labels[`${currentLangLMS}_only`] - ?? entry.labels.none - ?? entry.labels[fallbackLang]; - - if (!data) { - // No valid label found, get the first one that is not "_only". - for (const lang in entry.labels) { - if (lang.indexOf('_only') == -1) { - data = entry.labels[lang]; - break; - } - } + async getCustomMenuItems(siteId?: string): Promise { + const { CoreCustomMenu } = await import('./custommenu'); - if (!data) { - // No valid label, ignore this entry. - continue; - } - } - - result[entry.position] = { - url: entry.url, - type: entry.type, - label: data.label, - icon: data.icon, - }; - } - - // Remove undefined values. - return result.filter((entry) => entry !== undefined); - } - - /** - * Get a list of custom menu items from config. - * - * @returns List of custom menu items. - */ - protected async getCustomItemsFromConfig(): Promise { - const items = CoreConstants.CONFIG.customMainMenuItems; - - if (!items) { - return []; - } - - const currentLang = await CoreLang.getCurrentLanguage(); - - const fallbackLang = CoreConstants.CONFIG.default_lang || 'en'; - const replacements = { - devicetype: '', - osversion: Device.version, - }; - - if (CorePlatform.isAndroid()) { - replacements.devicetype = 'Android'; - } else if (CorePlatform.isIOS()) { - replacements.devicetype = 'iPhone or iPad'; - } else { - replacements.devicetype = 'Other'; - } - - return items - .filter(item => typeof item.label === 'string' || currentLang in item.label || fallbackLang in item.label) - .map(item => ({ - ...item, - url: CoreText.replaceArguments(item.url, replacements, 'uri'), - label: typeof item.label === 'string' - ? item.label - : item.label[currentLang] ?? item.label[fallbackLang], - })); + return CoreCustomMenu.getCustomMainMenuItems(siteId); } /** @@ -325,53 +176,6 @@ export class CoreMainMenuProvider { export const CoreMainMenu = makeSingleton(CoreMainMenuProvider); -/** - * Custom main menu item. - */ -export interface CoreMainMenuCustomItem { - /** - * Type of the item: app, inappbrowser, browser or embedded. - */ - type: CoreLinkOpenMethod; - - /** - * Url of the item. - */ - url: string; - - /** - * Label to display for the item. - */ - label: string; - - /** - * Name of the icon to display for the item. - */ - icon: string; -} - -/** - * Custom main menu item with localized text. - */ -export type CoreMainMenuLocalizedCustomItem = Omit & { - label: string | Record; -}; - -/** - * Map of custom menu items. - */ -type CustomMenuItemsMap = Record; - export type CoreMainMenuHandlerBadgeUpdatedEventData = { handler: string; // Handler name. value: number; // New counter value. diff --git a/src/types/config.d.ts b/src/types/config.d.ts index 2ea72b85b89..c75832c0376 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -13,7 +13,7 @@ // limitations under the License. import { CoreColorScheme, CoreZoomLevel } from '@features/settings/services/settings-helper'; -import { CoreMainMenuLocalizedCustomItem } from '@features/mainmenu/services/mainmenu'; +import { CoreCustomMenuLocalizedCustomItem } from '@features/mainmenu/services/custommenu'; import { CoreLoginSiteInfo, CoreSitesDemoSiteData } from '@services/sites'; import { OpenFileAction } from '@singletons/opener'; import { CoreLoginSiteFinderSettings, CoreLoginSiteSelectorListMethod } from '@features/login/services/login-helper'; @@ -60,7 +60,8 @@ export interface EnvironmentConfig { displayqronsitescreen?: boolean; forceOpenLinksIn?: 'app' | 'browser'; iOSDefaultOpenFileAction?: OpenFileAction; - customMainMenuItems?: CoreMainMenuLocalizedCustomItem[]; + customMainMenuItems?: CoreCustomMenuLocalizedCustomItem[]; + customUserMenuItems?: CoreCustomMenuLocalizedCustomItem[]; feedbackFormUrl?: string | false; a11yStatement?: string | false; legalDisclaimer?: string | false; From b6d705b63e45d471127a91189724277e8008dbc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 22 Oct 2025 11:53:27 +0200 Subject: [PATCH 3/3] MOBILE-4910 mainmenu: Show custom menu items on user menu --- .../custom-menu-item/custom-menu-item.html | 17 ++++++ .../custom-menu-item/custom-menu-item.ts | 59 +++++++++++++++++++ .../components/user-menu/user-menu.html | 4 ++ .../components/user-menu/user-menu.ts | 13 ++++ .../features/mainmenu/pages/more/more.html | 22 +------ src/core/features/mainmenu/pages/more/more.ts | 11 +--- 6 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 src/core/features/mainmenu/components/custom-menu-item/custom-menu-item.html create mode 100644 src/core/features/mainmenu/components/custom-menu-item/custom-menu-item.ts diff --git a/src/core/features/mainmenu/components/custom-menu-item/custom-menu-item.html b/src/core/features/mainmenu/components/custom-menu-item/custom-menu-item.html new file mode 100644 index 00000000000..2638afb4859 --- /dev/null +++ b/src/core/features/mainmenu/components/custom-menu-item/custom-menu-item.html @@ -0,0 +1,17 @@ +@if (type() !== 'embedded') { + + +} @else { + + +} diff --git a/src/core/features/mainmenu/components/custom-menu-item/custom-menu-item.ts b/src/core/features/mainmenu/components/custom-menu-item/custom-menu-item.ts new file mode 100644 index 00000000000..b06f5ef9128 --- /dev/null +++ b/src/core/features/mainmenu/components/custom-menu-item/custom-menu-item.ts @@ -0,0 +1,59 @@ +// (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 { CoreLinkOpenMethod } from '@/core/constants'; +import { CoreSharedModule } from '@/core/shared.module'; +import { Component, input } from '@angular/core'; +import { CoreViewer } from '@features/viewer/services/viewer'; + +/** + * Component to display a custom menu item. + */ +@Component({ + selector: 'core-custom-menu-item', + templateUrl: 'custom-menu-item.html', + imports: [ + CoreSharedModule, + ], +}) +export class CoreCustomMenuItemComponent { + + /** + * Type of the item: app, inappbrowser, browser or embedded. + */ + readonly type = input.required(); + + /** + * Url of the item. + */ + readonly url = input.required(); + + /** + * Label to display for the item. + */ + readonly label = input.required(); + + /** + * Name of the icon to display for the item. + */ + readonly icon = input.required(); + + /** + * Open an embedded custom item. + */ + openItem(): void { + CoreViewer.openIframeViewer(this.label(), this.url()); + } + +} diff --git a/src/core/features/mainmenu/components/user-menu/user-menu.html b/src/core/features/mainmenu/components/user-menu/user-menu.html index 4ed6a402233..50debd38e95 100644 --- a/src/core/features/mainmenu/components/user-menu/user-menu.html +++ b/src/core/features/mainmenu/components/user-menu/user-menu.html @@ -67,6 +67,10 @@

} + @for (item of customItems; track item) { + + } + { + this.customItems = await CoreCustomMenu.getUserCustomMenuItems(); + } + /** * Opens User profile page. * diff --git a/src/core/features/mainmenu/pages/more/more.html b/src/core/features/mainmenu/pages/more/more.html index 1bf31c88665..106ced8e88a 100644 --- a/src/core/features/mainmenu/pages/more/more.html +++ b/src/core/features/mainmenu/pages/more/more.html @@ -42,25 +42,9 @@

{{ 'core.more' | translate }}

} }
- - @if (item.type !== 'embedded') { - - - } @else { - - - } - + @for (item of customItems; track item) { + + } @if (showScanQR) {