Merge pull request #9536 from overleaf/ii-adjustable-project-dashboard-panel

[web] Adjustable project dashboard sidebar

GitOrigin-RevId: 1007ecb896bbe215af28fa92d295201b2457aeef
This commit is contained in:
ilkin-overleaf 2023-01-09 16:33:14 +02:00 committed by Copybot
parent b0bd070018
commit 4f35333a39
5 changed files with 281 additions and 13 deletions

View file

@ -9,8 +9,6 @@ import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
import CurrentPlanWidget from './current-plan-widget/current-plan-widget'
import NewProjectButton from './new-project-button'
import ProjectListTable from './table/project-list-table'
import SidebarFilters from './sidebar/sidebar-filters'
import AddAffiliation, { useAddAffiliation } from './sidebar/add-affiliation'
import SurveyWidget from './survey-widget'
import WelcomeMessage from './welcome-message'
import LoadingBranded from '../../../shared/components/loading-branded'
@ -20,6 +18,7 @@ import SearchForm from './search-form'
import ProjectsDropdown from './dropdown/projects-dropdown'
import SortByDropdown from './dropdown/sort-by-dropdown'
import ProjectTools from './table/project-tools/project-tools'
import Sidebar from './sidebar/sidebar'
import LoadMore from './load-more'
import { useEffect } from 'react'
@ -43,7 +42,6 @@ function ProjectListPageContent() {
setSearchText,
selectedProjects,
} = useProjectListContext()
const { show: showAddAffiliationWidget } = useAddAffiliation()
useEffect(() => {
eventTracking.sendMB('loads_v2_dash', {})
@ -59,16 +57,7 @@ function ProjectListPageContent() {
<div className="project-list-wrapper clearfix">
{totalProjectsCount > 0 ? (
<>
<div className="project-list-sidebar-wrapper-react hidden-xs">
<div className="project-list-sidebar-subwrapper">
<aside className="project-list-sidebar-react">
<NewProjectButton id="new-project-button-sidebar" />
<SidebarFilters />
{showAddAffiliationWidget && <hr />}
<AddAffiliation />
</aside>
</div>
</div>
<Sidebar />
<div className="project-list-main-react">
{error ? <DashApiError /> : ''}
<Row>

View file

@ -0,0 +1,45 @@
import NewProjectButton from '../new-project-button'
import SidebarFilters from './sidebar-filters'
import AddAffiliation, { useAddAffiliation } from './add-affiliation'
import { usePersistedResize } from '../../../../shared/hooks/use-resize'
function Sidebar() {
const { show: showAddAffiliationWidget } = useAddAffiliation()
const { mousePos, getHandleProps, getTargetProps } = usePersistedResize({
name: 'project-sidebar',
})
return (
<div
className="project-list-sidebar-wrapper-react hidden-xs"
{...getTargetProps({
style: {
...(mousePos?.x && { flexBasis: `${mousePos.x}px` }),
},
})}
>
<div className="project-list-sidebar-subwrapper">
<aside className="project-list-sidebar-react">
<NewProjectButton id="new-project-button-sidebar" />
<SidebarFilters />
{showAddAffiliationWidget && <hr />}
<AddAffiliation />
</aside>
</div>
<div
{...getHandleProps({
style: {
position: 'absolute',
zIndex: 1,
top: 0,
right: '-2px',
height: '100%',
width: '4px',
},
})}
/>
</div>
)
}
export default Sidebar

View file

@ -0,0 +1,113 @@
import { useState, useEffect, useRef } from 'react'
import usePersistedState from './use-persisted-state'
import { Nullable } from '../../../../types/utils'
type Pos = Nullable<{
x: number
}>
function useResizeBase(
state: [Pos, React.Dispatch<React.SetStateAction<Pos>>]
) {
const [mousePos, setMousePos] = state
const isResizingRef = useRef(false)
const handleRef = useRef<HTMLElement | null>(null)
const defaultHandleStyles = useRef<React.CSSProperties>({
cursor: 'col-resize',
userSelect: 'none',
})
useEffect(() => {
const handleMouseDown = function (e: MouseEvent) {
if (e.button !== 0) {
return
}
if (defaultHandleStyles.current.cursor) {
document.body.style.cursor = defaultHandleStyles.current.cursor
}
isResizingRef.current = true
}
const handle = handleRef.current
handle?.addEventListener('mousedown', handleMouseDown)
return () => {
handle?.removeEventListener('mousedown', handleMouseDown)
}
}, [])
useEffect(() => {
const handleMouseUp = function () {
document.body.style.cursor = 'default'
isResizingRef.current = false
}
document.addEventListener('mouseup', handleMouseUp)
return () => {
document.removeEventListener('mouseup', handleMouseUp)
}
}, [])
useEffect(() => {
const handleMouseMove = function (e: MouseEvent) {
if (isResizingRef.current) {
setMousePos({ x: e.clientX })
}
}
document.addEventListener('mousemove', handleMouseMove)
return () => {
document.removeEventListener('mousemove', handleMouseMove)
}
}, [setMousePos])
const getTargetProps = ({ style }: { style?: React.CSSProperties } = {}) => {
return {
style: {
...style,
},
}
}
const setHandleRef = (node: HTMLElement | null) => {
handleRef.current = node
}
const getHandleProps = ({ style }: { style?: React.CSSProperties } = {}) => {
if (style?.cursor) {
defaultHandleStyles.current.cursor = style.cursor
}
return {
style: {
...defaultHandleStyles.current,
...style,
},
ref: setHandleRef,
}
}
return <const>{
mousePos,
getHandleProps,
getTargetProps,
}
}
function useResize() {
const state = useState<Pos>(null)
return useResizeBase(state)
}
function usePersistedResize({ name }: { name: string }) {
const state = usePersistedState<Pos>(`resizeable-${name}`, null)
return useResizeBase(state)
}
export { useResize, usePersistedResize }

View file

@ -33,9 +33,12 @@
}
.project-list-sidebar-wrapper-react {
position: relative;
background-color: @sidebar-bg;
flex: @project-list-sidebar-wrapper-flex;
min-height: calc(~'100vh -' @header-height);
max-width: 320px;
min-width: 200px;
.project-list-sidebar-subwrapper {
display: flex;
@ -722,6 +725,7 @@
.project-list-sidebar-survey-wrapper {
position: fixed;
z-index: 1;
bottom: 0;
left: 0;
width: 15%;

View file

@ -0,0 +1,117 @@
import {
usePersistedResize,
useResize,
} from '../../../../frontend/js/shared/hooks/use-resize'
function Template({
mousePos,
getTargetProps,
getHandleProps,
}: ReturnType<typeof useResize>) {
return (
<div
style={{
height: '100vh',
display: 'flex',
alignItems: 'center',
}}
>
<div style={{ position: 'relative' }}>
<div
id="target"
{...getTargetProps({
style: {
width: '200px',
height: '200px',
border: '2px solid black',
...(mousePos?.x && { width: `${mousePos.x}px` }),
},
})}
>
Demo content demo content demo content demo content demo content demo
content
</div>
<div
id="handle"
{...getHandleProps({
style: {
position: 'absolute',
top: 0,
right: 0,
height: '100%',
width: '4px',
backgroundColor: 'red',
},
})}
/>
</div>
</div>
)
}
function PersistedResizeTest() {
const props = usePersistedResize({ name: 'test' })
return <Template {...props} />
}
function ResizeTest() {
const props = useResize()
return <Template {...props} />
}
describe('useResize', function () {
it('should apply provided styles to the target', function () {
cy.mount(<ResizeTest />)
// test a css prop being applied
cy.get('#target').should('have.css', 'width', '200px')
})
it('should apply provided styles to the handle', function () {
cy.mount(<ResizeTest />)
// test a css prop being applied
cy.get('#handle').should('have.css', 'width', '4px')
})
it('should apply default styles to the handle', function () {
cy.mount(<ResizeTest />)
cy.get('#handle')
.should('have.css', 'cursor', 'col-resize')
.and('have.css', 'user-select', 'none')
})
it('should resize the target horizontally on mousedown and mousemove', function () {
const xPos = 400
cy.mount(<ResizeTest />)
cy.get('#handle')
.trigger('mousedown', { button: 0 })
.trigger('mousemove', { clientX: xPos })
.trigger('mouseup')
cy.get('#target').should('have.css', 'width', `${xPos}px`)
})
it('should persist the resize data', function () {
const xPos = 400
cy.mount(<PersistedResizeTest />)
cy.get('#handle')
.trigger('mousedown', { button: 0 })
.trigger('mousemove', { clientX: xPos })
.trigger('mouseup')
cy.window()
.its('localStorage.resizeable-test')
.should('eq', `{"x":${xPos}}`)
// render the component again
cy.mount(<PersistedResizeTest />)
cy.get('#target').should('have.css', 'width', `${xPos}px`)
})
})