resizeable tree (#1084)

This commit is contained in:
Athou
2023-06-16 21:24:34 +02:00
parent 6944d4dc0b
commit d1ddcb6ace
6 changed files with 63 additions and 20 deletions

View File

@@ -25,6 +25,7 @@
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"interweave": "^13.1.0", "interweave": "^13.1.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"re-resizable": "^6.9.9",
"react": "^18.2.0", "react": "^18.2.0",
"react-async-hook": "^4.0.0", "react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0", "react-contexify": "^6.0.0",
@@ -10035,6 +10036,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/re-resizable": {
"version": "6.9.9",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.9.tgz",
"integrity": "sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA==",
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react": { "node_modules/react": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",

View File

@@ -31,6 +31,7 @@
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"interweave": "^13.1.0", "interweave": "^13.1.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"re-resizable": "^6.9.9",
"react": "^18.2.0", "react": "^18.2.0",
"react-async-hook": "^4.0.0", "react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0", "react-contexify": "^6.0.0",

View File

@@ -66,6 +66,7 @@ function Providers(props: { children: React.ReactNode }) {
const ApiDocumentationPage = React.lazy(() => import("pages/app/ApiDocumentationPage")) const ApiDocumentationPage = React.lazy(() => import("pages/app/ApiDocumentationPage"))
function AppRoutes() { function AppRoutes() {
const sidebarWidth = useAppSelector(state => state.tree.sidebarWidth)
return ( return (
<Routes> <Routes>
<Route path="/" element={<Navigate to={`/app/category/${Constants.categories.all.id}`} replace />} /> <Route path="/" element={<Navigate to={`/app/category/${Constants.categories.all.id}`} replace />} />
@@ -74,7 +75,7 @@ function AppRoutes() {
<Route path="register" element={<RegistrationPage />} /> <Route path="register" element={<RegistrationPage />} />
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} /> <Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
<Route path="api" element={<ApiDocumentationPage />} /> <Route path="api" element={<ApiDocumentationPage />} />
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} />}> <Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} sidebarWidth={sidebarWidth} />}>
<Route path="category"> <Route path="category">
<Route path=":id" element={<FeedEntriesPage sourceType="category" />} /> <Route path=":id" element={<FeedEntriesPage sourceType="category" />} />
<Route path=":id/details" element={<CategoryDetailsPage />} /> <Route path=":id/details" element={<CategoryDetailsPage />} />
@@ -135,8 +136,11 @@ function FaviconHandler() {
const root = useAppSelector(state => state.tree.rootCategory) const root = useAppSelector(state => state.tree.rootCategory)
useEffect(() => { useEffect(() => {
const unreadCount = categoryUnreadCount(root) const unreadCount = categoryUnreadCount(root)
if (unreadCount === 0) Tinycon.reset() if (unreadCount === 0) {
else Tinycon.setBubble(unreadCount) Tinycon.reset()
} else {
Tinycon.setBubble(unreadCount)
}
}, [root]) }, [root])
return null return null

View File

@@ -88,7 +88,6 @@ export const Constants = {
layout: { layout: {
mobileBreakpoint: DEFAULT_THEME.breakpoints.md, mobileBreakpoint: DEFAULT_THEME.breakpoints.md,
headerHeight: 60, headerHeight: 60,
sidebarWidth: 350,
entryMaxWidth: 650, entryMaxWidth: 650,
isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight, isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight,
isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight, isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight,

View File

@@ -9,10 +9,12 @@ import { redirectTo } from "./redirect"
interface TreeState { interface TreeState {
rootCategory?: Category rootCategory?: Category
mobileMenuOpen: boolean mobileMenuOpen: boolean
sidebarWidth: number
} }
const initialState: TreeState = { const initialState: TreeState = {
mobileMenuOpen: false, mobileMenuOpen: false,
sidebarWidth: 350,
} }
export const reloadTree = createAsyncThunk("tree/reload", () => client.category.getRoot().then(r => r.data)) export const reloadTree = createAsyncThunk("tree/reload", () => client.category.getRoot().then(r => r.data))
@@ -27,6 +29,9 @@ export const treeSlice = createSlice({
setMobileMenuOpen: (state, action: PayloadAction<boolean>) => { setMobileMenuOpen: (state, action: PayloadAction<boolean>) => {
state.mobileMenuOpen = action.payload state.mobileMenuOpen = action.payload
}, },
setSidebarWidth: (state, action: PayloadAction<number>) => {
state.sidebarWidth = action.payload
},
}, },
extraReducers: builder => { extraReducers: builder => {
builder.addCase(reloadTree.fulfilled, (state, action) => { builder.addCase(reloadTree.fulfilled, (state, action) => {
@@ -54,5 +59,5 @@ export const treeSlice = createSlice({
}, },
}) })
export const { setMobileMenuOpen } = treeSlice.actions export const { setMobileMenuOpen, setSidebarWidth } = treeSlice.actions
export default treeSlice.reducer export default treeSlice.reducer

View File

@@ -16,7 +16,7 @@ import {
import { useViewportSize } from "@mantine/hooks" import { useViewportSize } from "@mantine/hooks"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { redirectToAdd, redirectToRootCategory } from "app/slices/redirect" import { redirectToAdd, redirectToRootCategory } from "app/slices/redirect"
import { reloadTree, setMobileMenuOpen } from "app/slices/tree" import { reloadTree, setMobileMenuOpen, setSidebarWidth } from "app/slices/tree"
import { reloadProfile, reloadSettings, reloadTags } from "app/slices/user" import { reloadProfile, reloadSettings, reloadTags } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
@@ -26,28 +26,34 @@ import { OnMobile } from "components/responsive/OnMobile"
import { useAppLoading } from "hooks/useAppLoading" import { useAppLoading } from "hooks/useAppLoading"
import { useWebSocket } from "hooks/useWebSocket" import { useWebSocket } from "hooks/useWebSocket"
import { LoadingPage } from "pages/LoadingPage" import { LoadingPage } from "pages/LoadingPage"
import { Resizable } from "re-resizable"
import { ReactNode, Suspense, useEffect } from "react" import { ReactNode, Suspense, useEffect } from "react"
import { TbPlus } from "react-icons/tb" import { TbPlus } from "react-icons/tb"
import { Outlet } from "react-router-dom" import { Outlet } from "react-router-dom"
interface LayoutProps { interface LayoutProps {
sidebar: ReactNode sidebar: ReactNode
sidebarWidth: number
header: ReactNode header: ReactNode
} }
const sidebarPadding = DEFAULT_THEME.spacing.xs const sidebarPadding = DEFAULT_THEME.spacing.xs
const sidebarRightBorderWidth = "1px" const sidebarRightBorderWidth = "1px"
const useStyles = createStyles(theme => ({ const useStyles = createStyles((theme, props: LayoutProps) => ({
sidebarContentResizeWrapper: {
padding: sidebarPadding,
minHeight: "100vh",
},
sidebarContent: { sidebarContent: {
maxWidth: `calc(${Constants.layout.sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`, maxWidth: `calc(${props.sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: { [theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
maxWidth: `calc(100vw - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`, maxWidth: `calc(100vw - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
}, },
}, },
mainContentWrapper: { mainContentWrapper: {
paddingTop: Constants.layout.headerHeight, paddingTop: Constants.layout.headerHeight,
paddingLeft: Constants.layout.sidebarWidth, paddingLeft: props.sidebarWidth,
paddingRight: 0, paddingRight: 0,
paddingBottom: 0, paddingBottom: 0,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: { [theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
@@ -55,7 +61,7 @@ const useStyles = createStyles(theme => ({
}, },
}, },
mainContent: { mainContent: {
maxWidth: `calc(100vw - ${Constants.layout.sidebarWidth}px)`, maxWidth: `calc(100vw - ${props.sidebarWidth}px)`,
padding: theme.spacing.md, padding: theme.spacing.md,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: { [theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
maxWidth: "100vw", maxWidth: "100vw",
@@ -76,8 +82,8 @@ function LogoAndTitle() {
) )
} }
export default function Layout({ sidebar, header }: LayoutProps) { export default function Layout(props: LayoutProps) {
const { classes } = useStyles() const { classes } = useStyles(props)
const theme = useMantineTheme() const theme = useMantineTheme()
const viewport = useViewportSize() const viewport = useViewportSize()
const { loading } = useAppLoading() const { loading } = useAppLoading()
@@ -85,6 +91,8 @@ export default function Layout({ sidebar, header }: LayoutProps) {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
useWebSocket() useWebSocket()
const handleResize = (element: HTMLElement) => dispatch(setSidebarWidth(element.offsetWidth))
useEffect(() => { useEffect(() => {
dispatch(reloadSettings()) dispatch(reloadSettings())
dispatch(reloadProfile()) dispatch(reloadProfile())
@@ -122,14 +130,30 @@ export default function Layout({ sidebar, header }: LayoutProps) {
navbar={ navbar={
<Navbar <Navbar
id="sidebar" id="sidebar"
p={sidebarPadding}
hiddenBreakpoint={Constants.layout.mobileBreakpoint} hiddenBreakpoint={Constants.layout.mobileBreakpoint}
hidden={!mobileMenuOpen} hidden={!mobileMenuOpen}
width={{ md: Constants.layout.sidebarWidth }} width={{ md: props.sidebarWidth }}
> >
<Navbar.Section grow component={ScrollArea} mx="-xs" px="xs"> <Resizable
<Box className={classes.sidebarContent}>{sidebar}</Box> enable={{
</Navbar.Section> top: false,
right: true,
bottom: false,
left: false,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}
onResize={(e, dir, el) => handleResize(el)}
minWidth={120}
>
<Box className={classes.sidebarContentResizeWrapper}>
<Navbar.Section grow component={ScrollArea} mx="-xs" px="xs">
<Box className={classes.sidebarContent}>{props.sidebar}</Box>
</Navbar.Section>
</Box>
</Resizable>
</Navbar> </Navbar>
} }
header={ header={
@@ -147,19 +171,19 @@ export default function Layout({ sidebar, header }: LayoutProps) {
{!mobileMenuOpen && ( {!mobileMenuOpen && (
<Group> <Group>
<Box mr="sm">{burger}</Box> <Box mr="sm">{burger}</Box>
<Box sx={{ flexGrow: 1 }}>{header}</Box> <Box sx={{ flexGrow: 1 }}>{props.header}</Box>
</Group> </Group>
)} )}
</OnMobile> </OnMobile>
<OnDesktop> <OnDesktop>
<Group> <Group>
<Group position="apart" sx={{ width: Constants.layout.sidebarWidth - 16 }}> <Group position="apart" sx={{ width: props.sidebarWidth - 16 }}>
<Box> <Box>
<LogoAndTitle /> <LogoAndTitle />
</Box> </Box>
<Box>{addButton}</Box> <Box>{addButton}</Box>
</Group> </Group>
<Box sx={{ flexGrow: 1 }}>{header}</Box> <Box sx={{ flexGrow: 1 }}>{props.header}</Box>
</Group> </Group>
</OnDesktop> </OnDesktop>
</Header> </Header>