fix: extract app bar into layout slot

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-07-02 18:54:09 +02:00
parent 18a1e79d9f
commit b3fb1bbf30
35 changed files with 258 additions and 207 deletions

View file

@ -3,7 +3,6 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { EditorAppBar } from '../layout/app-bar/editor-app-bar'
import { CommunicatorImageLightbox } from '../markdown-renderer/extensions/image/communicator-image-lightbox'
import { ExtensionEventEmitterProvider } from '../markdown-renderer/hooks/use-extension-event-emitter'
import { ChangeEditorContentContextProvider } from './change-content-context/codemirror-reference-context'
@ -69,16 +68,13 @@ export const EditorPageContent: React.FC = () => {
<ExtensionEventEmitterProvider>
{editorExtensionComponents}
<CommunicatorImageLightbox />
<div className={'d-flex flex-column vh-100'}>
<EditorAppBar />
<div className={'flex-fill d-flex h-100 w-100 overflow-hidden flex-row'}>
<Splitter
left={leftPane}
right={rightPane}
additionalContainerClassName={'overflow-hidden position-relative'}
/>
<Sidebar />
</div>
<div className={'flex-fill d-flex h-100 w-100 overflow-hidden flex-row'}>
<Splitter
left={leftPane}
right={rightPane}
additionalContainerClassName={'overflow-hidden position-relative'}
/>
<Sidebar />
</div>
</ExtensionEventEmitterProvider>
</ChangeEditorContentContextProvider>

View file

@ -20,6 +20,5 @@
& > div {
background: var(--bs-body-bg);
box-shadow: inset 0 7px 7px -6px #bbbbbb;
}
}

View file

@ -1,9 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.cover-button {
min-width: 200px;
}

View file

@ -1,23 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { SignInButton } from '../../landing-layout/navigation/sign-in-button'
import styles from './cover-buttons.module.scss'
import React from 'react'
export const CoverButtons: React.FC = () => {
const userExists = useApplicationState((state) => !!state.user)
if (userExists) {
return null
}
return (
<div className='mb-5'>
<SignInButton className={styles['cover-button']} variant='success' size='lg' />
</div>
)
}

View file

@ -3,8 +3,6 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { BaseAppBar } from '../layout/app-bar/base-app-bar'
import { HeaderBar } from './navigation/header-bar/header-bar'
import type { PropsWithChildren } from 'react'
import React from 'react'
import { Container } from 'react-bootstrap'
@ -16,14 +14,10 @@ import { Container } from 'react-bootstrap'
*/
export const LandingLayout: React.FC<PropsWithChildren> = ({ children }) => {
return (
<div>
<BaseAppBar />
<Container className='d-flex flex-column'>
<HeaderBar />
<div className={'d-flex flex-column justify-content-between flex-fill text-center'}>
<main>{children}</main>
</div>
</Container>
</div>
<Container>
<div className={'d-flex flex-column justify-content-between flex-fill text-center'}>
<main>{children}</main>
</div>
</Container>
)
}

View file

@ -1,32 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { cypressId } from '../../../../utils/cypress-attribute'
import { HeaderNavLink } from './header-nav-link'
import React from 'react'
import { Navbar } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
/**
* Renders a header bar for the intro and history page.
*/
const HeaderBar: React.FC = () => {
useTranslation()
return (
<Navbar className='justify-content-between'>
<div className='nav'>
<HeaderNavLink to='/intro' {...cypressId('navLinkIntro')}>
<Trans i18nKey='landing.navigation.intro' />
</HeaderNavLink>
<HeaderNavLink to='/history' {...cypressId('navLinkHistory')}>
<Trans i18nKey='landing.navigation.history' />
</HeaderNavLink>
</div>
</Navbar>
)
}
export { HeaderBar }

View file

@ -1,13 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.link {
border-bottom: 2px solid transparent
}
.active {
border-bottom-color: var(--bs-primary);
}

View file

@ -1,47 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { concatCssClasses } from '../../../../utils/concat-css-classes'
import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute'
import { cypressId } from '../../../../utils/cypress-attribute'
import styles from './header-nav-link.module.scss'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import type { PropsWithChildren } from 'react'
import React, { useMemo } from 'react'
import { Nav } from 'react-bootstrap'
export interface HeaderNavLinkProps extends PropsWithDataCypressId {
to: string
}
/**
* Renders a link for the navigation top bar.
*
* @param to The target url
* @param children The react elements inside of link for more description
* @param props Other navigation item props
*/
export const HeaderNavLink: React.FC<PropsWithChildren<HeaderNavLinkProps>> = ({ to, children, ...props }) => {
const pathname = usePathname()
const className = useMemo(() => {
return concatCssClasses(
{
[styles.active]: pathname === to
},
'nav-link',
styles.link
)
}, [pathname, to])
return (
<Nav.Item>
<Link href={to} passHref={true} className={className} {...cypressId(props)}>
{children}
</Link>
</Nav.Item>
)
}

View file

@ -1,68 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app bar contains alert when editor is not synced 1`] = `
<div>
<div>
<span>
first part
</span>
<div>
<div
class="fade w-100 m-0 px-2 py-1 border-top-0 border-bottom-0 d-flex align-items-center alert alert-warning show"
role="alert"
>
realtime.connecting
BootstrapIconMock_ArrowRepeat
</div>
</div>
<span>
last part
</span>
</div>
</div>
`;
exports[`app bar contains note title and read-only marker when having only read permissions 1`] = `
<div>
<div>
<span>
first part
</span>
<div>
<span
class="text-secondary me-2"
>
BootstrapIconMock_Lock
</span>
<span
class="text-truncate mw-100"
>
Note Title Test
</span>
</div>
<span>
last part
</span>
</div>
</div>
`;
exports[`app bar contains note title when editor is synced 1`] = `
<div>
<div>
<span>
first part
</span>
<div>
<span
class="text-truncate mw-100"
>
Note Title Test
</span>
</div>
<span>
last part
</span>
</div>
</div>
`;

View file

@ -4,12 +4,13 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useDarkModeState } from '../../../../hooks/dark-mode/use-dark-mode-state'
import { BrandingSeparatorDash } from '../../../common/custom-branding/branding-separator-dash'
import { CustomBranding } from '../../../common/custom-branding/custom-branding'
import { HedgeDocLogoHorizontalGrey } from '../../../common/hedge-doc-logo/hedge-doc-logo-horizontal-grey'
import { LogoSize } from '../../../common/hedge-doc-logo/logo-size'
import { BrandingSeparatorDash } from './branding-separator-dash'
import Link from 'next/link'
import React from 'react'
import { Navbar } from 'react-bootstrap'
/**
* Renders the HedgeDoc branding and branding customizations for the app bar.
@ -18,14 +19,16 @@ export const BrandingElement: React.FC = () => {
const darkModeActivated = useDarkModeState()
return (
<Link
href='/intro'
className={'text-secondary text-decoration-none d-flex align-items-center justify-content-start gap-1'}>
<div>
<HedgeDocLogoHorizontalGrey color={darkModeActivated ? 'dark' : 'light'} size={LogoSize.SMALL} />
</div>
<BrandingSeparatorDash />
<CustomBranding inline={true} />
</Link>
<Navbar.Brand>
<Link href='/' className='text-secondary text-decoration-none d-flex align-items-center'>
<HedgeDocLogoHorizontalGrey
size={LogoSize.SMALL}
className={'w-auto'}
color={darkModeActivated ? 'dark' : 'light'}
/>
<BrandingSeparatorDash />
<CustomBranding inline={true} />
</Link>
</Navbar.Brand>
)
}

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useBrandingDetails } from './use-branding-details'
import { useBrandingDetails } from '../../../common/custom-branding/use-branding-details'
import React from 'react'
/**

View file

@ -22,7 +22,7 @@ export const HelpDropdown: React.FC = () => {
return (
<Dropdown>
<Dropdown.Toggle size={'sm'}>
<Dropdown.Toggle size={'sm'} className={'h-100'}>
<UiIcon icon={IconQuestion} />
</Dropdown.Toggle>
<Dropdown.Menu>

View file

@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import Link from 'next/link'
/**
* A button that links to the history page.
*/
export const HistoryButton: React.FC = () => {
useTranslation()
return (
<Link href={'/history'}>
<Button variant={'secondary'} size={'sm'}>
<Trans i18nKey='landing.navigation.history' />
</Button>
</Link>
)
}

View file

@ -1,3 +1,5 @@
'use client'
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
@ -8,7 +10,7 @@ import { useNoteTitle } from '../../../../../hooks/common/use-note-title'
import { useTranslatedText } from '../../../../../hooks/common/use-translated-text'
import { UiIcon } from '../../../../common/icons/ui-icon'
import { ShowIf } from '../../../../common/show-if/show-if'
import React, { Fragment } from 'react'
import React from 'react'
import { Lock as IconLock } from 'react-bootstrap-icons'
/**
@ -20,13 +22,13 @@ export const NoteTitleElement: React.FC = () => {
const readOnlyLabel = useTranslatedText('appbar.editor.readOnly')
return (
<Fragment>
<span className={'m-0 text-truncate'}>
<ShowIf condition={!isWriteable}>
<span className={'text-secondary me-2'}>
<UiIcon icon={IconLock} title={readOnlyLabel} />
<UiIcon icon={IconLock} className={'me-2'} title={readOnlyLabel} />
</span>
</ShowIf>
<span className={'text-truncate mw-100'}>{noteTitle}</span>
</Fragment>
{noteTitle}
</span>
)
}

View file

@ -13,5 +13,5 @@ import React from 'react'
*/
export const UserElement: React.FC = () => {
const userExists = useApplicationState((state) => !!state.user)
return userExists ? <UserDropdown /> : <SignInButton size={'sm'} />
return userExists ? <UserDropdown /> : <SignInButton size={'sm'} className={'h-100'} />
}

View file

@ -1,3 +1,5 @@
'use client'
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
@ -8,30 +10,31 @@ import { SettingsButton } from '../../global-dialogs/settings-dialog/settings-bu
import { BrandingElement } from './app-bar-elements/branding-element'
import { HelpDropdown } from './app-bar-elements/help-dropdown/help-dropdown'
import { UserElement } from './app-bar-elements/user-element'
import styles from './navbar.module.scss'
import type { PropsWithChildren } from 'react'
import React from 'react'
import { Col, Nav, Navbar } from 'react-bootstrap'
import { Nav, Navbar } from 'react-bootstrap'
import { HistoryButton } from './app-bar-elements/help-dropdown/history-button'
/**
* Renders the base app bar with branding, help, settings user elements.
*/
export const BaseAppBar: React.FC<PropsWithChildren> = ({ children }) => {
return (
<Navbar expand={true} className={'px-2 py-2 shadow-sm'}>
<Col>
<Navbar expand={true} className={`px-2 py-1 align-items-center border-bottom ${styles.navbar}`}>
<Nav className={`align-items-center justify-content-start gap-2 flex-grow-1 ${styles.side}`}>
<BrandingElement />
</Col>
<Col md={6} className={'h-100'}>
<Nav className={'d-flex align-items-center justify-content-center h-100'}>{children}</Nav>
</Col>
<Col>
<Nav className={'d-flex align-items-center justify-content-end gap-2'}>
</Nav>
<Nav className={`align-items-center flex-fill overflow-hidden px-4 ${styles.center}`}>{children}</Nav>
<Nav className={`align-items-stretch justify-content-end flex-grow-1 ${styles.side} h-100 py-1`}>
<div className={'d-flex gap-2'}>
<HistoryButton />
<HelpDropdown />
<SettingsButton />
<NewNoteButton />
<UserElement />
</Nav>
</Col>
</div>
</Nav>
</Navbar>
)
}

View file

@ -1,89 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as UseApplicationStateModule from '../../../hooks/common/use-application-state'
import type { ApplicationState } from '../../../redux/application-state'
import { mockI18n } from '../../../test-utils/mock-i18n'
import { EditorAppBar } from './editor-app-bar'
import type { NoteGroupPermissionEntry, NoteUserPermissionEntry } from '@hedgedoc/commons'
import { render } from '@testing-library/react'
import type { PropsWithChildren } from 'react'
import React from 'react'
jest.mock('./base-app-bar', () => ({
__esModule: true,
BaseAppBar: ({ children }: PropsWithChildren) => (
<div>
<span>first part</span>
<div>{children}</div>
<span>last part</span>
</div>
)
}))
jest.mock('../../../hooks/common/use-application-state')
const mockedCommonAppState = {
noteDetails: {
title: 'Note Title Test',
permissions: {
owner: 'test',
sharedToGroups: [
{
groupName: '_EVERYONE',
canEdit: false
}
] as NoteGroupPermissionEntry[],
sharedToUsers: [] as NoteUserPermissionEntry[]
}
},
user: {
username: 'test'
}
}
describe('app bar', () => {
beforeAll(mockI18n)
afterAll(() => jest.restoreAllMocks())
it('contains note title when editor is synced', () => {
jest.spyOn(UseApplicationStateModule, 'useApplicationState').mockImplementation((fn) => {
return fn({
...mockedCommonAppState,
realtimeStatus: {
isSynced: true
}
} as ApplicationState)
})
const view = render(<EditorAppBar />)
expect(view.container).toMatchSnapshot()
})
it('contains alert when editor is not synced', () => {
jest.spyOn(UseApplicationStateModule, 'useApplicationState').mockImplementation((fn) => {
return fn({
...mockedCommonAppState,
realtimeStatus: {
isSynced: false
}
} as ApplicationState)
})
const view = render(<EditorAppBar />)
expect(view.container).toMatchSnapshot()
})
it('contains note title and read-only marker when having only read permissions', () => {
jest.spyOn(UseApplicationStateModule, 'useApplicationState').mockImplementation((fn) => {
return fn({
...mockedCommonAppState,
realtimeStatus: {
isSynced: true
},
user: null
} as ApplicationState)
})
const view = render(<EditorAppBar />)
expect(view.container).toMatchSnapshot()
})
})

View file

@ -1,19 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { RealtimeConnectionAlert } from '../../editor-page/realtime-connection-alert/realtime-connection-alert'
import { NoteTitleElement } from './app-bar-elements/note-title-element/note-title-element'
import { BaseAppBar } from './base-app-bar'
import React from 'react'
/**
* Renders the EditorAppBar that extends the {@link BaseAppBar} with the note title or realtime connection alert.
*/
export const EditorAppBar: React.FC = () => {
const isSynced = useApplicationState((state) => state.realtimeStatus.isSynced)
return <BaseAppBar>{isSynced ? <NoteTitleElement /> : <RealtimeConnectionAlert />}</BaseAppBar>
}

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.side {
flex: 1 1 0;
}
.center {
display: flex;
justify-content: center;
flex: 2 1 0;
}
.history {
flex: 2 2 0;
}
.navbar {
height: 48px;
}

View file

@ -5,7 +5,7 @@
*/
import useResizeObserver from '@react-hook/resize-observer'
import type { RefObject } from 'react'
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
/**
* Monitors the height of the referenced {@link HTMLElement} and executes the callback on change.
@ -18,6 +18,7 @@ export const useOnHeightChange = (
onHeightChange: undefined | ((value: number) => void)
): void => {
const [rendererSize, setRendererSize] = useState<number>(0)
const lastPostedSize = useRef<number>(0)
useResizeObserver(elementRef, (entry) => {
setRendererSize(entry.contentRect.height)
})
@ -29,6 +30,10 @@ export const useOnHeightChange = (
setRendererSize(value)
}, [elementRef])
useEffect(() => {
if (lastPostedSize.current === rendererSize) {
return
}
lastPostedSize.current = rendererSize
onHeightChange?.(rendererSize + 1)
}, [rendererSize, onHeightChange])
})
}