diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e32318af1..0821fac7ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ __New features:__ * Added data export in CSV format for classes ([#1494](https://github.com/parse-community/parse-dashboard/pull/1494)), thanks to [Cory Imdieke](https://github.com/Vortec4800), [Manuel Trezza](https://github.com/mtrezza). +* Collapse sidebar ([#1760](https://github.com/parse-community/parse-dashboard/pull/1760)), thanks to [Douglas Muraoka](https://github.com/douglasmuraoka), [Manuel Trezza](https://github.com/mtrezza). ### 2.1.0 [Full Changelog](https://github.com/parse-community/parse-dashboard/compare/2.0.5...2.1.0) diff --git a/src/components/Sidebar/AppName.react.js b/src/components/Sidebar/AppName.react.js index 33f4658107..3856cedf15 100644 --- a/src/components/Sidebar/AppName.react.js +++ b/src/components/Sidebar/AppName.react.js @@ -1,8 +1,18 @@ import React from 'react'; +import Pin from 'components/Sidebar/Pin.react'; import styles from 'components/Sidebar/Sidebar.scss'; -export default ({ name, onClick }) => ( -
- {name} +const AppName = ({ name, onClick, onPinClick }) => ( +
+
+
+
+ {name} +
+
+ +
); + +export default AppName; \ No newline at end of file diff --git a/src/components/Sidebar/AppsMenu.react.js b/src/components/Sidebar/AppsMenu.react.js index 7bbbcdec39..2f185174f6 100644 --- a/src/components/Sidebar/AppsMenu.react.js +++ b/src/components/Sidebar/AppsMenu.react.js @@ -13,9 +13,9 @@ import React from 'react'; import styles from 'components/Sidebar/Sidebar.scss'; import { unselectable } from 'stylesheets/base.scss'; -let AppsMenu = ({ apps, current, height, onSelect }) => ( +const AppsMenu = ({ apps, current, height, onSelect, onPinClick }) => (
- +
All Apps
{apps.map((app) => { diff --git a/src/components/Sidebar/FooterMenu.react.js b/src/components/Sidebar/FooterMenu.react.js index 2852c9e44a..594bc0b8f5 100644 --- a/src/components/Sidebar/FooterMenu.react.js +++ b/src/components/Sidebar/FooterMenu.react.js @@ -33,6 +33,14 @@ export default class FooterMenu extends React.Component { } render() { + if (this.props.isCollapsed) { + return ( +
+ +
+ ); + } + let content = null; if (this.state.show) { content = ( diff --git a/src/components/Sidebar/Pin.react.js b/src/components/Sidebar/Pin.react.js new file mode 100644 index 0000000000..035610fd00 --- /dev/null +++ b/src/components/Sidebar/Pin.react.js @@ -0,0 +1,12 @@ +import React from "react"; + +import Icon from "components/Icon/Icon.react"; +import styles from "components/Sidebar/Sidebar.scss"; + +const Pin = ({ onClick }) => ( +
+ +
+); + +export default Pin; diff --git a/src/components/Sidebar/Sidebar.react.js b/src/components/Sidebar/Sidebar.react.js index 7959476140..c0dd363512 100644 --- a/src/components/Sidebar/Sidebar.react.js +++ b/src/components/Sidebar/Sidebar.react.js @@ -10,8 +10,10 @@ import AppsManager from 'lib/AppsManager'; import AppsMenu from 'components/Sidebar/AppsMenu.react'; import AppName from 'components/Sidebar/AppName.react'; import FooterMenu from 'components/Sidebar/FooterMenu.react'; -import React, { useState } from 'react'; +import isInsidePopover from 'lib/isInsidePopover'; import ParseApp from 'lib/ParseApp'; +import Pin from 'components/Sidebar/Pin.react'; +import React, { useEffect, useState } from 'react'; import SidebarHeader from 'components/Sidebar/SidebarHeader.react'; import SidebarSection from 'components/Sidebar/SidebarSection.react'; import SidebarSubItem from 'components/Sidebar/SidebarSubItem.react'; @@ -29,7 +31,46 @@ const Sidebar = ({ primaryBackgroundColor, secondaryBackgroundColor }, { currentApp }) => { + const collapseWidth = 980; const [ appsMenuOpen, setAppsMenuOpen ] = useState(false); + const [ collapsed, setCollapsed ] = useState(false); + const [ fixed, setFixed ] = useState(true); + let currentWidth = window.innerWidth; + + const windowResizeHandler = () => { + if (window.innerWidth <= collapseWidth && currentWidth > collapseWidth) { + if (document.body.className.indexOf(' expanded') === -1) { + document.body.className += ' expanded'; + } + setCollapsed(true); + setFixed(false); + } else if (window.innerWidth > collapseWidth && currentWidth <= collapseWidth) { + document.body.className = document.body.className.replace(' expanded', ''); + setCollapsed(false); + setFixed(true); + } + // Update window width + currentWidth = window.innerWidth; + } + + useEffect(() => { + window.addEventListener('resize', windowResizeHandler); + + return () => { + window.removeEventListener('resize', windowResizeHandler); + } + }); + + const sidebarClasses = [styles.sidebar]; + if (fixed) { + document.body.className = document.body.className.replace(' expanded', ''); + } else if (!fixed && collapsed) { + sidebarClasses.push(styles.collapsed); + if (document.body.className.indexOf(' expanded') === -1) { + document.body.className += ' expanded'; + } + } + const _subMenu = subsections => { if (!subsections) { return null; @@ -54,25 +95,40 @@ const Sidebar = ({ ); } - const apps = [].concat(AppsManager.apps()).sort((a, b) => (a.name < b.name ? -1 : (a.name > b.name ? 1 : 0))); + const onPinClick = () => { + if (fixed) { + setFixed(false); + setCollapsed(true); + setAppsMenuOpen(false); + } else { + setFixed(true); + setCollapsed(false); + } + }; let sidebarContent; if (appsMenuOpen) { + const apps = [].concat(AppsManager.apps()).sort((a, b) => (a.name < b.name ? -1 : (a.name > b.name ? 1 : 0))); sidebarContent = ( setAppsMenuOpen(false)} /> ); } else { + const topContent = collapsed + ? + : appSelector && ( +
+ setAppsMenuOpen(true)} onPinClick={onPinClick} /> +
+ ) || undefined; + sidebarContent = ( <> - {appSelector && ( -
- setAppsMenuOpen(true)} /> -
- )}
+ {topContent} {sections.map(({ name, icon, @@ -84,7 +140,7 @@ const Sidebar = ({ return ( - {active ? _subMenu(subsections) : null} + {!collapsed && active ? _subMenu(subsections) : null} ); })} @@ -101,16 +157,39 @@ const Sidebar = ({ ) } - return
- - {sidebarContent} -
- Open Source Hub - GitHub - Docs - + return ( +
setCollapsed(false) + : undefined + } + onMouseLeave={ + !collapsed && !fixed + ? (e => { + if (!isInsidePopover(e.relatedTarget)) { + setAppsMenuOpen(false); + setCollapsed(true); + } + }) + : undefined + } + > + + {sidebarContent} +
+ {!collapsed && ( + <> + Open Source Hub + GitHub + Docs + + )} + +
-
+ ); } Sidebar.contextTypes = { diff --git a/src/components/Sidebar/Sidebar.scss b/src/components/Sidebar/Sidebar.scss index 5ca30c99a0..65e9bfe414 100644 --- a/src/components/Sidebar/Sidebar.scss +++ b/src/components/Sidebar/Sidebar.scss @@ -20,38 +20,18 @@ $footerHeight: 36px; bottom: 0; background: #0c5582; color: #fff; - transition: left 0.5s ease-in; -} - -.toggle { - position: fixed; - border-radius: 5px; - width: 28px; - height: 28px; - top: 10px; - left: 310px; - opacity: 0; - transition: left 0.5s ease-in, opacity 0.5s 0.5s ease-in; -} + z-index: 100; -@media (max-width: 980px) { - .sidebar { + &.collapsed { left: 0; - } - - .toggle { - left: 310px; - top: 10px; - opacity: 1; - } + width: 54px; - body:global(.expanded) { - .sidebar { - left: -300px; + .section_header > svg { + margin: 0; } - .toggle { - left: 10px; + .pinContainer > svg { + fill: white; } } } @@ -128,10 +108,10 @@ $footerHeight: 36px; font-size: 18px; font-weight: 700; line-height: 30px; - cursor: pointer; } .menuRow { + cursor: pointer; border-bottom: 1px solid #0c5582; > *:first-child { @@ -149,31 +129,40 @@ $footerHeight: 36px; .currentApp { position: relative; - overflow: hidden; - text-overflow: ellipsis; - padding-right: 40px; + display: flex; + justify-content: space-between; + align-items: center; + + .appNameContainer { + display: flex; + align-items: center; + cursor: pointer; + + .currentAppName { + overflow: hidden; + text-overflow: ellipsis; + max-width: 215px; + } - &:hover{ + &:after { + @include arrow('down', 10px, 7px, #132B39); + content: ''; + margin-left: 10px; + } + + &:hover { &:after { border-top-color: white; } - } - - &:after { - @include arrow('down', 10px, 7px, #132B39); - position: absolute; - content: ''; - top: 20px; - right: 15px; + } } } -.appsMenu .currentApp:after { - @include arrow('up', 10px, 7px, #ffffff); - position: absolute; - content: ''; - top: 20px; - right: 15px; +.sidebarPin { + cursor: pointer; + height: 30px; + width: 30px; + padding: 6px; } .appsMenu { @@ -185,6 +174,20 @@ $footerHeight: 36px; background: #0c5582; } + .currentApp { + .currentAppName { + &:after { + @include arrow('up', 10px, 7px, #132B39); + } + + &:hover { + &:after { + border-bottom-color: white; + } + } + } + } + .appListContainer { overflow-y: auto; height: calc(100vh - #{$headerHeight} - #{$menuSectionHeight} - #{$sidebarMenuItemHeight} - #{$footerHeight}); @@ -392,3 +395,23 @@ a.subitem { } } } + +.pinContainer { + height: 48px; + display: flex; + justify-content: center; + align-items: center; + background-color: #094162; + + svg { + cursor: pointer; + height: 40px; + width: 40px; + padding: 10px 10px 10px 10px; + fill: #132B39; + + &:hover { + fill: white; + } + } +} \ No newline at end of file diff --git a/src/components/Sidebar/SidebarHeader.react.js b/src/components/Sidebar/SidebarHeader.react.js index 67de9c4cad..bab045fe4f 100644 --- a/src/components/Sidebar/SidebarHeader.react.js +++ b/src/components/Sidebar/SidebarHeader.react.js @@ -24,21 +24,33 @@ export default class SidebarHeader extends React.Component { }); } render() { - return ( -
- + const { isCollapsed = false } = this.props; + const headerContent = isCollapsed + ? ( +
- - -
-
- Parse Dashboard {version} +
+ ) + : ( + <> + + + + +
- {this.state.dashboardUser} + Parse Dashboard {version} +
+ {this.state.dashboardUser} +
-
- + + + ) + return ( +
+ {headerContent}
); } diff --git a/src/components/Sidebar/SidebarSection.react.js b/src/components/Sidebar/SidebarSection.react.js index a8ff39f833..fb86c00c60 100644 --- a/src/components/Sidebar/SidebarSection.react.js +++ b/src/components/Sidebar/SidebarSection.react.js @@ -10,7 +10,7 @@ import { Link } from 'react-router-dom'; import React from 'react'; import styles from 'components/Sidebar/Sidebar.scss'; -let SidebarSection = ({ active, children, name, link, icon, style, primaryBackgroundColor, secondaryBackgroundColor }) => { +let SidebarSection = ({ active, children, name, link, icon, style, primaryBackgroundColor, secondaryBackgroundColor, isCollapsed }) => { let classes = [styles.section]; if (active) { classes.push(styles.active); @@ -19,6 +19,16 @@ let SidebarSection = ({ active, children, name, link, icon, style, primaryBackgr if (icon) { iconContent = ; } + if (isCollapsed) { + classes.push(styles.collapsed); + return ( +
+
+ {iconContent} +
+
+ ); + } return (
{active ? diff --git a/src/components/Sidebar/SidebarToggle.react.js b/src/components/Sidebar/SidebarToggle.react.js deleted file mode 100644 index d1fb02660b..0000000000 --- a/src/components/Sidebar/SidebarToggle.react.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2016-present, Parse, LLC - * All rights reserved. - * - * This source code is licensed under the license found in the LICENSE file in - * the root directory of this source tree. - */ -import React from 'react'; -import styles from 'components/Sidebar/Sidebar.scss'; -import Icon from 'components/Icon/Icon.react'; - -function toggleSidebarExpansion() { - if (document.body.className.indexOf(' expanded') > -1) { - document.body.className = document.body.className.replace(' expanded', ''); - } else { - document.body.className += ' expanded'; - } -} - -let SidebarToggle = () => ; - -export default SidebarToggle; diff --git a/src/components/Toolbar/Toolbar.scss b/src/components/Toolbar/Toolbar.scss index 7deaf77159..847333f017 100644 --- a/src/components/Toolbar/Toolbar.scss +++ b/src/components/Toolbar/Toolbar.scss @@ -15,13 +15,12 @@ background: #353446; height: 96px; color: white; - transition: left 0.5s ease-in; z-index: 5; } -@media (max-width: 980px) { +body:global(.expanded) { .toolbar { - left: 0; + left: $sidebarCollapsedWidth; } } diff --git a/src/dashboard/AccountView.react.js b/src/dashboard/AccountView.react.js index 4c467c8e1a..1a44591f63 100644 --- a/src/dashboard/AccountView.react.js +++ b/src/dashboard/AccountView.react.js @@ -7,7 +7,6 @@ */ import React from 'react'; import { buildAccountSidebar } from './SidebarBuilder'; -import SidebarToggle from 'components/Sidebar/SidebarToggle.react'; import styles from 'dashboard/Dashboard.scss'; export default class AccountView extends React.Component { @@ -23,7 +22,6 @@ export default class AccountView extends React.Component { {this.props.children}
{sidebar} -
); } diff --git a/src/dashboard/Dashboard.scss b/src/dashboard/Dashboard.scss index b517ad6708..140c11b2bd 100644 --- a/src/dashboard/Dashboard.scss +++ b/src/dashboard/Dashboard.scss @@ -5,15 +5,16 @@ * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ +@import 'stylesheets/globals.scss'; + .content { margin-left: 300px; - transition: margin-left 0.5s ease-in; overflow: auto; max-height: 100vh; } -@media (max-width: 980px) { +body:global(.expanded) { .content { - margin-left: 0; + margin-left: $sidebarCollapsedWidth; } -} +} \ No newline at end of file diff --git a/src/dashboard/DashboardView.react.js b/src/dashboard/DashboardView.react.js index 849d823a1b..c1d53a6e0e 100644 --- a/src/dashboard/DashboardView.react.js +++ b/src/dashboard/DashboardView.react.js @@ -9,7 +9,6 @@ import PropTypes from 'lib/PropTypes'; import ParseApp from 'lib/ParseApp'; import React from 'react'; import Sidebar from 'components/Sidebar/Sidebar.react'; -import SidebarToggle from 'components/Sidebar/SidebarToggle.react'; import styles from 'dashboard/Dashboard.scss'; export default class DashboardView extends React.Component { @@ -255,7 +254,6 @@ export default class DashboardView extends React.Component { {this.renderContent()}
{sidebar} -
); } diff --git a/src/dashboard/Data/Browser/Browser.scss b/src/dashboard/Data/Browser/Browser.scss index b6acada937..970c84cf60 100644 --- a/src/dashboard/Data/Browser/Browser.scss +++ b/src/dashboard/Data/Browser/Browser.scss @@ -15,7 +15,6 @@ bottom: 0; overflow: auto; padding-top: 30px; - transition: left 0.5s ease-in; // fix for safari scrolling issue: // https://css-tricks.com/forums/topic/safari-for-ios-z-index-ordering-bug-while-scrolling-a-page-with-a-fixed-element/ // only applying to safari as a side effect of this is emptystate component centering is off @@ -24,9 +23,9 @@ } } -@media (max-width: 980px) { +body:global(.expanded) { .browser { - left: 0; + left: $sidebarCollapsedWidth; } } diff --git a/src/dashboard/TableView.scss b/src/dashboard/TableView.scss index fab0e39da2..5027e6a89f 100644 --- a/src/dashboard/TableView.scss +++ b/src/dashboard/TableView.scss @@ -14,13 +14,12 @@ right: 0; background: #66637A; height: 30px; - transition: left 0.5s ease-in; white-space: nowrap; } -@media (max-width: 980px) { +body:global(.expanded) { .headers { - left: 0; + left: $sidebarCollapsedWidth; } } diff --git a/src/icons/pin.svg b/src/icons/pin.svg new file mode 100644 index 0000000000..31a74cab87 --- /dev/null +++ b/src/icons/pin.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/lib/isInsidePopover.js b/src/lib/isInsidePopover.js new file mode 100644 index 0000000000..a5d7457980 --- /dev/null +++ b/src/lib/isInsidePopover.js @@ -0,0 +1,12 @@ +export default function isInsidePopover(node) { + let cur = node.parentNode; + while (cur && cur.nodeType === 1) { + // If id starts with "fixed_wrapper", we consider it as the + // root element of the Popover component + if (/^fixed_wrapper/g.test(cur.id)) { + return true; + } + cur = cur.parentNode; + } + return false; +} \ No newline at end of file diff --git a/src/stylesheets/globals.scss b/src/stylesheets/globals.scss index 98ae89247a..e70066e8b8 100644 --- a/src/stylesheets/globals.scss +++ b/src/stylesheets/globals.scss @@ -51,6 +51,8 @@ $pushDetailsContent: #66637A; $darkPurple: #8D11BA; $blueGreen: #11A4BA; +$sidebarCollapsedWidth: 54px; + @mixin NotoSansFont { font-family: 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; }