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
}