mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-10 02:54:15 +00:00
545 lines
12 KiB
JavaScript
Executable File
545 lines
12 KiB
JavaScript
Executable File
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 <div className="render_content_wrapper">
|
|
<div className="render_content_header">
|
|
{
|
|
props.customRenderTitle ?? null
|
|
}
|
|
<Button
|
|
onClick={props.closeRender}
|
|
>
|
|
Close
|
|
</Button>
|
|
</div>
|
|
<div className="render_content">
|
|
{props.children}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
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: <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) {
|
|
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}
|
|
disabled={item.disabled ?? false}
|
|
>
|
|
{this.renderMenuItems(item.children)}
|
|
</Menu.SubMenu>
|
|
}
|
|
|
|
return <Menu.Item
|
|
key={item.id}
|
|
icon={handleRenderIcon(item.icon)}
|
|
disabled={item.disabled ?? false}
|
|
{...item.props}
|
|
>
|
|
<Translation>
|
|
{t => t(item.title ?? item.id)}
|
|
</Translation>
|
|
</Menu.Item>
|
|
})
|
|
}
|
|
|
|
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: <>
|
|
<Icons.User />
|
|
<Translation>
|
|
{t => t("Account")}
|
|
</Translation>
|
|
</>,
|
|
},
|
|
{
|
|
type: "divider"
|
|
},
|
|
{
|
|
key: "switch_account",
|
|
label: <>
|
|
<Icons.MdSwitchAccount />
|
|
<Translation>
|
|
{t => t("Switch account")}
|
|
</Translation>
|
|
</>,
|
|
},
|
|
{
|
|
key: "logout",
|
|
label: <>
|
|
<Icons.LogOut />
|
|
<Translation>
|
|
{t => t("Logout")}
|
|
</Translation>
|
|
</>,
|
|
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 <div
|
|
onMouseEnter={this.onMouseEnter}
|
|
onMouseLeave={this.handleMouseLeave}
|
|
className={
|
|
classnames(
|
|
"app_sidebar",
|
|
{
|
|
["customRender"]: this.state.customRender,
|
|
["floating"]: window.app?.cores.settings.get("sidebar.floating"),
|
|
["elevated"]: this.state.visible && this.state.elevated,
|
|
["expanded"]: this.state.visible && this.state.expanded,
|
|
["hidden"]: !this.state.visible,
|
|
}
|
|
)
|
|
}
|
|
ref={this.sidebarRef}
|
|
>
|
|
{
|
|
this.state.customRender && <CustomRender
|
|
customRenderTitle={this.state.customRenderTitle}
|
|
closeRender={this.closeRender}
|
|
sidebarRef={this.sidebarRef}
|
|
>
|
|
{this.state.customRender}
|
|
</CustomRender>
|
|
}
|
|
|
|
{
|
|
!this.state.customRender && <>
|
|
<div className="app_sidebar_header">
|
|
<div className="app_sidebar_header_logo">
|
|
<img src={config.logo?.alt} />
|
|
</div>
|
|
</div>
|
|
|
|
<div key="menu" className="app_sidebar_menu_wrapper">
|
|
<Menu
|
|
selectable={true}
|
|
mode="inline"
|
|
onClick={this.handleClick}
|
|
defaultSelectedKeys={[defaultSelectedKey]}
|
|
>
|
|
{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}>
|
|
{
|
|
this.state.bottomItems.map((item) => {
|
|
if (item.noContainer) {
|
|
return React.createElement(item.children, item.childrenProps)
|
|
}
|
|
|
|
return <Menu.Item
|
|
key={item.id}
|
|
className="extra_bottom_item"
|
|
icon={handleRenderIcon(item.icon)}
|
|
disabled={item.disabled ?? false}
|
|
{...item.containerProps}
|
|
>
|
|
{
|
|
React.createElement(item.children, item.childrenProps)
|
|
}
|
|
</Menu.Item>
|
|
})
|
|
}
|
|
<Menu.Item key="search" icon={<Icons.Search />} >
|
|
<Translation>
|
|
{(t) => t("Search")}
|
|
</Translation>
|
|
</Menu.Item>
|
|
<Menu.Item key="notifications" icon={<Icons.Bell />}>
|
|
<Translation>
|
|
{t => t("Notifications")}
|
|
</Translation>
|
|
</Menu.Item>
|
|
<Menu.Item key="settings" icon={<Icons.Settings />}>
|
|
<Translation>
|
|
{t => t("Settings")}
|
|
</Translation>
|
|
</Menu.Item>
|
|
{
|
|
app.userData && <Dropdown
|
|
menu={{
|
|
items: this.generateDropdownItems(),
|
|
onClick: this.onClickDropdownItem
|
|
}}
|
|
autoFocus
|
|
placement="top"
|
|
trigger={["click"]}
|
|
onOpenChange={this.onDropdownOpenChange}
|
|
>
|
|
<Menu.Item
|
|
key="account"
|
|
className="user_avatar"
|
|
ignoreClick
|
|
>
|
|
<Avatar shape="square" src={app.userData?.avatar} />
|
|
</Menu.Item>
|
|
</Dropdown>
|
|
}
|
|
{
|
|
!app.userData && <Menu.Item key="login" icon={<Icons.LogIn />}>
|
|
<Translation>
|
|
{t => t("Login")}
|
|
</Translation>
|
|
</Menu.Item>
|
|
}
|
|
</Menu>
|
|
</div>
|
|
</>
|
|
}
|
|
</div>
|
|
}
|
|
} |