import React from "react" import { Menu, Avatar, Button, Dropdown } from "antd" import { Translation } from "react-i18next" import classnames from "classnames" import config from "config" import { Icons, createIconRender } from "components/Icons" import sidebarItems from "schemas/sidebar" import "./index.less" const onClickHandlers = { settings: () => { window.app.navigation.goToSettings() }, notifications: () => { window.app.controls.openNotifications() }, search: () => { window.app.controls.openSearcher() }, create: () => { window.app.controls.openCreator() }, account: () => { window.app.navigation.goToAccount() }, login: () => { window.app.navigation.goAuth() }, logout: () => { app.eventBus.emit("app.logout_request") } } const getSidebarComponents = () => { const items = {} sidebarItems.forEach((item, index) => { items[item.id] = { ...item, index, content: ( <> {createIconRender(item.icon)} {item.title} ), } }) return items } const generateItems = () => { const components = getSidebarComponents() const itemsMap = [] const pathResolvers = {} Object.keys(components).forEach((key, index) => { const component = components[key] if (typeof component.path !== "undefined") { pathResolvers[component.id] = component.path } itemsMap.push(component) }) return { itemsMap, pathResolvers, } } const CustomRender = (props) => { const handleClickOutside = (event) => { if (props.sidebarRef.current && !props.sidebarRef.current.contains(event.target)) { if (event.target.closest(".ant-select-item")) { return } props.closeRender() } } React.useEffect(() => { document.addEventListener("mousedown", handleClickOutside) return () => { document.removeEventListener("mousedown", handleClickOutside) } }, []) return
{ props.customRenderTitle ?? null }
{props.children}
} const handleRenderIcon = (icon) => { if (typeof icon === "undefined") { return null } return createIconRender(icon) } export default class Sidebar extends React.Component { constructor(props) { super(props) this.controller = window.app["SidebarController"] = { toggleVisibility: this.toggleVisibility, toggleElevation: this.toggleElevation, toggleCollapse: this.toggleExpanded, isVisible: () => this.state.visible, isExpanded: () => this.state.expanded, setCustomRender: this.setRender, closeCustomRender: this.closeRender, updateBottomItemProps: (id, newProps) => { let updatedValue = this.state.bottomItems updatedValue = updatedValue.map((item) => { if (item.id === id) { item.props = { ...item.props, ...newProps, } } }) this.setState({ bottomItems: updatedValue }) }, attachBottomItem: (id, children, options) => { if (!id) { throw new Error("ID is required") } if (!children) { throw new Error("Children is required") } if (this.state.bottomItems.find((item) => item.id === id)) { throw new Error("Item already exists") } let updatedValue = this.state.bottomItems updatedValue.push({ id, children, ...options }) this.setState({ bottomItems: updatedValue }) }, removeBottomItem: (id) => { let updatedValue = this.state.bottomItems updatedValue = updatedValue.filter((item) => item.id !== id) this.setState({ bottomItems: updatedValue }) } } this.state = { visible: false, elevated: false, expanded: false, dropdownOpen: false, pathResolvers: null, menus: null, customRenderTitle: null, customRender: null, bottomItems: [], } // handle sidedrawer open/close window.app.eventBus.on("sidedrawer.hasDrawers", () => { this.toggleElevation(true) }) window.app.eventBus.on("sidedrawer.noDrawers", () => { this.toggleElevation(false) }) } sidebarRef = React.createRef() collapseDebounce = null componentDidMount = async () => { await this.loadItems() setTimeout(() => { this.controller.toggleVisibility(true) }, 100) } setRender = async (render, options = {}) => { if (!typeof render === "function") { throw new Error("Render is required to be a function") } await this.setState({ customRenderTitle:
{ options.icon && createIconRender(options.icon) } { options.title &&

{t => t(options.title)}

}
, customRender: React.createElement(render, { ...options.props ?? {}, close: this.closeRender, }) }) } closeRender = () => { this.setState({ customRenderTitle: null, customRender: null, }) } loadItems = async () => { const generation = generateItems() // update states await this.setState({ menus: generation.itemsMap, pathResolvers: generation.pathResolvers, }) } renderMenuItems(items) { return items.map((item) => { if (Array.isArray(item.children)) { return {t => t(item.title)} } {...item.props} disabled={item.disabled ?? false} > {this.renderMenuItems(item.children)} } return {t => t(item.title ?? item.id)} }) } handleClick = (e) => { if (e.item.props.ignoreClick) { return } if (e.item.props.override_event) { return app.eventBus.emit(e.item.props.override_event, e.item.props.override_event_props) } if (typeof e.key === "undefined") { window.app.eventBus.emit("invalidSidebarKey", e) return false } if (typeof onClickHandlers[e.key] === "function") { return onClickHandlers[e.key](e) } window.app.cores.sound.useUIAudio("sidebar.switch_tab") if (typeof this.state.pathResolvers === "object") { if (typeof this.state.pathResolvers[e.key] !== "undefined") { return window.app.location.push(`/${this.state.pathResolvers[e.key]}`, 150) } } return window.app.location.push(`/${e.key}`, 150) } toggleExpanded = (to, force) => { to = to ?? !this.state.expanded if (this.collapseDebounce) { clearTimeout(this.collapseDebounce) this.collapseDebounce = null } if (!to & this.state.dropdownOpen && !force) { // FIXME: This is a walkaround for a bug in antd, causing when dropdown set to close, item click event is not fired // The desing defines when sidebar should be collapsed, dropdown should be closed, but in this case, gonna to keep it open untils dropdown is closed //this.setState({ dropdownOpen: false }) return false } if (!to) { this.collapseDebounce = setTimeout(() => { this.setState({ expanded: to }) }, window.app.cores.settings.get("autoCollapseDelay") ?? 500) } else { this.setState({ expanded: to }) } app.eventBus.emit("sidebar.expanded", to) } toggleVisibility = (to) => { this.setState({ visible: to ?? !this.state.visible }) } toggleElevation = (to) => { this.setState({ elevated: to ?? !this.state.elevated }) } onMouseEnter = () => { if (!this.state.visible) return if (window.app.cores.settings.is("collapseOnLooseFocus", false)) return this.toggleExpanded(true) } handleMouseLeave = () => { if (!this.state.visible) return if (window.app.cores.settings.is("collapseOnLooseFocus", false)) return this.toggleExpanded(false) } onDropdownOpenChange = (to) => { // this is another walkaround for a bug in antd, causing when dropdown set to close, item click event is not fired if (!to && this.state.expanded) { this.toggleExpanded(false, true) } this.setState({ dropdownOpen: to }) } generateDropdownItems = () => { return [ { key: "account", label: <> {t => t("Account")} , }, { type: "divider" }, { key: "switch_account", label: <> {t => t("Switch account")} , }, { key: "logout", label: <> {t => t("Logout")} , danger: true } ] } onClickDropdownItem = (item) => { const handler = onClickHandlers[item.key] if (typeof handler === "function") { handler() } } render() { if (!this.state.menus) return null const defaultSelectedKey = window.location.pathname.replace("/", "") return
{ this.state.customRender && {this.state.customRender} } { !this.state.customRender && <>
{this.renderMenuItems(this.state.menus)}
{ this.state.bottomItems.map((item) => { if (item.noContainer) { return React.createElement(item.children, item.childrenProps) } return { React.createElement(item.children, item.childrenProps) } }) } } > {(t) => t("Search")} }> {t => t("Notifications")} }> {t => t("Settings")} { app.userData && } { !app.userData && }> {t => t("Login")} }
}
} }