import { CitronNavigator } from '@stack-spot/citron-navigator' import { MenuSectionContent as SerializableMenu, MenuItem as SerializableMenuItem } from '@stack-spot/portal-extension-commons' import { MenuItem, MenuSectionContent, Selector } from '@stack-spot/portal-layout' import { pull } from 'lodash' import { useEffect, useState } from 'react' type Listener = (value: MenuSectionContent | undefined) => void class ExtensionMenu { private current: MenuSectionContent | undefined private listeners: Listener[] = [] /** * Slug and url of the extension currently controlling the menu. */ controlledBy?: { slug: string, url: string } private static deserializeMenuItem(item: SerializableMenuItem): MenuItem { return { ...item, icon: item.icon ? : undefined, ...('children' in item ? { children: item.children.map(c => ExtensionMenu.deserializeMenuItem(c)) } : {}), } } private static deserializeMenu(menu: SerializableMenu): MenuSectionContent { return { ...menu, options: menu.options?.map(ExtensionMenu.deserializeMenuItem), pageSelector: menu.pageSelector ? { ...menu.pageSelector, options: menu.pageSelector.options.map(o => ({ ...o, icon: o.icon ? : undefined, })), } : undefined, } } private static fixRelativeUrls(menu: MenuSectionContent): MenuSectionContent { return { ...menu, goBack: menu?.goBack ? { ...menu.goBack, href: fixRelativeHref(menu.goBack.href) } : undefined, pageSelector: fixRelativePageSelector(menu?.pageSelector), options: fixRelativeMenuOptions(menu?.options), } } /** * Changes the current menu. * @param menu the new menu. */ set(menu: SerializableMenu | undefined): void { this.current = menu ? ExtensionMenu.fixRelativeUrls(ExtensionMenu.deserializeMenu(menu)) : undefined this.listeners.forEach(l => l(this.current)) } /** * Observes changes in the menu. * @param listener a function to call whenever the menu changes. * @returns a function that, when called, removes the listener. */ onChange(listener: Listener) { this.listeners.push(listener) return () => { pull(this.listeners, listener) } } get(): MenuSectionContent | undefined { return this.current } } export const extensionMenu = new ExtensionMenu() function fixRelativeHref(href: string | undefined) { const route = CitronNavigator.instance?.currentRoute if (!extensionMenu.controlledBy || !href || !href.startsWith('/') || !route) return href const relevantHref = href.replace(new RegExp(`^${new URL(extensionMenu.controlledBy.url).pathname}`), '') return `${route.$link({})}/${relevantHref}`.replace('/#/', '/').replace(/\/\//g, '/') } function fixRelativeMenuOptions(items?: MenuItem[]): MenuItem[] | undefined { return items?.map((item) => { const oldHref = 'href' in item ? item.href : undefined const href = fixRelativeHref(oldHref) const children = 'children' in item ? fixRelativeMenuOptions(item.children) : undefined return { ...item, href, ...(children ? { children } : {}) } }) } function fixRelativePageSelector(selector: Selector | undefined): Selector | undefined { return selector ? { ...selector, button: selector.button ? { ...selector.button, href: fixRelativeHref(selector.button.href), } : undefined, options: selector.options.map(o => ({ ...o, href: fixRelativeHref(o.href), })), } : undefined } export function useExtensionMenu(): MenuSectionContent | undefined { const [menu, setMenu] = useState(extensionMenu.get()) useEffect(() => extensionMenu.onChange(m => { if (m) setMenu(m) }), []) return menu }