337 lines
7.8 KiB
JavaScript
Executable File

import React from "react"
import { Menu, Avatar, Button } 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/routes.json"
import "./index.less"
const onClickHandlers = {
settings: (event) => {
window.app.openSettings()
},
}
const getSidebarComponents = () => {
const items = {}
sidebarItems.forEach((item, index) => {
if (!item.reachable) {
return
}
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,
}
}
export default class Sidebar extends React.Component {
constructor(props) {
super(props)
this.controller = window.app["SidebarController"] = {
toggleVisibility: this.toggleVisibility,
toggleElevation: this.toggleElevation,
toggleCollapse: this.toggleCollapse,
isVisible: () => this.state.visible,
isCollapsed: () => this.state.collapsed,
setCustomRender: this.setRender,
closeCustomRender: this.closeRender,
}
this.state = {
visible: false,
elevated: false,
collapsed: window.app.settings.get("collapseOnLooseFocus") ?? false,
pathResolvers: null,
menus: null,
customRenderTitle: null,
customRender: null,
}
// handle sidedrawer open/close
window.app.eventBus.on("sidedrawer.hasDrawers", () => {
this.toggleElevation(true)
})
window.app.eventBus.on("sidedrawer.noDrawers", () => {
this.toggleElevation(false)
})
}
collapseDebounce = null
componentDidMount = async () => {
await this.loadItems()
setTimeout(() => {
this.controller.toggleVisibility(true)
}, 100)
}
setRender = (render, options = {}) => {
if (!typeof render === "function") {
throw new Error("Render is required to be a function")
}
this.setState({
customRenderTitle: <div className="render_content_header_title">
{
options.icon && createIconRender(options.icon)
}
{
options.title && <h1>
<Translation>
{t => t(options.title)}
</Translation>
</h1>
}
</div>,
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) {
const handleRenderIcon = (icon) => {
if (typeof icon === "undefined") {
return null
}
return createIconRender(icon)
}
return items.map((item) => {
if (Array.isArray(item.children)) {
return <Menu.SubMenu
key={item.id}
icon={handleRenderIcon(item.icon)}
title={<span>
<Translation>
{t => t(item.title)}
</Translation>
</span>}
{...item.props}
>
{this.renderMenuItems(item.children)}
</Menu.SubMenu>
}
return <Menu.Item key={item.id} icon={handleRenderIcon(item.icon)} {...item.props}>
<Translation>
{t => t(item.title ?? item.id)}
</Translation>
</Menu.Item>
})
}
handleClick = (e) => {
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)
}
if (typeof this.state.pathResolvers === "object") {
if (typeof this.state.pathResolvers[e.key] !== "undefined") {
return window.app.setLocation(`/${this.state.pathResolvers[e.key]}`, 150)
}
}
return window.app.setLocation(`/${e.key}`, 150)
}
toggleCollapse = (to) => {
if (!this.state.editMode) {
this.setState({ collapsed: to ?? !this.state.collapsed })
}
}
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.settings.is("collapseOnLooseFocus", false)) return
clearTimeout(this.collapseDebounce)
this.collapseDebounce = null
if (this.state.collapsed) {
this.toggleCollapse(false)
}
}
handleMouseLeave = () => {
if (!this.state.visible) return
if (window.app.settings.is("collapseOnLooseFocus", false)) return
if (!this.state.collapsed) {
this.collapseDebounce = setTimeout(() => { this.toggleCollapse(true) }, window.app.settings.get("autoCollapseDelay") ?? 500)
}
}
render() {
if (!this.state.menus) return null
return (
<div
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.handleMouseLeave}
className={
classnames(
"app_sidebar",
{
["customRender"]: this.state.customRender,
["floating"]: window.app?.settings.get("sidebar.floating"),
["collapsed"]: !this.state.customRender && this.state.visible && this.state.collapsed,
["elevated"]: !this.state.visible && this.state.elevated,
["hidden"]: !this.state.visible,
}
)
}
>
{
this.state.customRender &&
<div className="render_content_wrapper">
<div className="render_content_header">
{
this.state.customRenderTitle ?? null
}
<Button
onClick={this.closeRender}
>
Close
</Button>
</div>
<div className="render_content">
{this.state.customRender}
</div>
</div>
}
{
!this.state.customRender && <>
<div className="app_sidebar_header">
<div className={classnames("app_sidebar_header_logo", { ["collapsed"]: this.state.collapsed })}>
<img src={this.state.collapsed ? config.logo?.alt : config.logo?.full} />
</div>
</div>
<div key="menu" className="app_sidebar_menu_wrapper">
<Menu selectable={true} mode="inline" onClick={this.handleClick}>
{this.renderMenuItems(this.state.menus)}
</Menu>
</div>
<div key="bottom" className={classnames("app_sidebar_menu_wrapper", "bottom")}>
<Menu selectable={false} mode="inline" onClick={this.handleClick}>
<Menu.Item key="search" icon={<Icons.Search />} override_event="app.openSearcher" >
<Translation>
{(t) => t("Search")}
</Translation>
</Menu.Item>
<Menu.Item key="create" icon={<Icons.PlusCircle />} override_event="app.openCreator" >
<Translation>
{(t) => t("Create")}
</Translation>
</Menu.Item>
<Menu.Item key="notifications" icon={<Icons.Bell />} override_event="app.openNotifications">
<Translation>
{t => t("Notifications")}
</Translation>
</Menu.Item>
<Menu.Item key="settings" icon={<Icons.Settings />}>
<Translation>
{t => t("Settings")}
</Translation>
</Menu.Item>
{
app.userData && <Menu.Item key="account" className="user_avatar">
<Avatar shape="square" src={app.userData?.avatar} />
</Menu.Item>
}
{
!app.userData && <Menu.Item key="login" icon={<Icons.LogIn />}>
<Translation>
{t => t("Login")}
</Translation>
</Menu.Item>
}
</Menu>
</div>
</>
}
</div>
)
}
}