mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #9536 from overleaf/ii-adjustable-project-dashboard-panel
[web] Adjustable project dashboard sidebar GitOrigin-RevId: 1007ecb896bbe215af28fa92d295201b2457aeef
This commit is contained in:
parent
b0bd070018
commit
4f35333a39
5 changed files with 281 additions and 13 deletions
|
@ -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>
|
||||
|
|
|
@ -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
|
113
services/web/frontend/js/shared/hooks/use-resize.ts
Normal file
113
services/web/frontend/js/shared/hooks/use-resize.ts
Normal 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 }
|
|
@ -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%;
|
||||
|
|
117
services/web/test/frontend/shared/hooks/use-resize.spec.tsx
Normal file
117
services/web/test/frontend/shared/hooks/use-resize.spec.tsx
Normal 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`)
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue