mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-24 18:56:32 -05:00
refactor: move help entries into new global app bar
Co-authored-by: Tilman Vatteroth <git@tilmanvatteroth.de> Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de> Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
74b92f2bbb
commit
d10c6d3290
43 changed files with 749 additions and 397 deletions
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -36,14 +36,4 @@ describe('Intro page', () => {
|
||||||
cy.getByCypressId('sign-in-button').should('exist')
|
cy.getByCypressId('sign-in-button').should('exist')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('version dialog', () => {
|
|
||||||
it('can be opened and closed', () => {
|
|
||||||
cy.getByCypressId('version-modal').should('not.exist')
|
|
||||||
cy.getByCypressId('show-version-modal').click()
|
|
||||||
cy.getByCypressId('version-modal').should('be.visible')
|
|
||||||
cy.getByCypressId('version-modal').find('.modal-header .btn-close').click()
|
|
||||||
cy.getByCypressId('version-modal').should('not.exist')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -147,11 +147,7 @@
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"releases": "Releases",
|
"releases": "Releases",
|
||||||
"poweredBy": "Powered by <0></0>",
|
"poweredBy": "Powered by <0></0>"
|
||||||
"imprint": "Imprint",
|
|
||||||
"followUs": "Follow us on <0></0>, <1></1>, <2></2>, <3></3>, and <4></4>.",
|
|
||||||
"privacy": "Privacy",
|
|
||||||
"termsOfUse": "Terms of Use"
|
|
||||||
},
|
},
|
||||||
"versionInfo": {
|
"versionInfo": {
|
||||||
"issueTracker": "Found a bug? Fill an issue!",
|
"issueTracker": "Found a bug? Fill an issue!",
|
||||||
|
@ -485,11 +481,36 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appbar": {
|
"appbar": {
|
||||||
|
"editor": {
|
||||||
|
"readOnly": "read-only"
|
||||||
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"help": {
|
"help": {
|
||||||
"header": "Help",
|
"header": "Help",
|
||||||
"shortcuts": "Shortcuts",
|
"shortcuts": "Shortcuts",
|
||||||
"cheatsheet": "Cheatsheet"
|
"cheatsheet": "Cheatsheet"
|
||||||
|
},
|
||||||
|
"instance": {
|
||||||
|
"header": "About this instance",
|
||||||
|
"versionInfo": "Running version"
|
||||||
|
},
|
||||||
|
"legal": {
|
||||||
|
"header": "Legal",
|
||||||
|
"privacy": "Privacy Policy",
|
||||||
|
"termsOfUse": "Terms of use",
|
||||||
|
"imprint": "Imprint"
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"header": "Project",
|
||||||
|
"github": "Project on GitHub",
|
||||||
|
"reportIssue": "Report an issue",
|
||||||
|
"helpTranslating": "Help us translating"
|
||||||
|
},
|
||||||
|
"social": {
|
||||||
|
"header": "Social",
|
||||||
|
"discourse": "Join the community",
|
||||||
|
"matrix": "Chat with us on Matrix",
|
||||||
|
"mastodon": "Follow us on Mastodon"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -642,7 +663,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cheatsheet": {
|
"cheatsheet": {
|
||||||
"button": "Open Cheatsheet",
|
|
||||||
"search": "Search for Cheatsheets",
|
"search": "Search for Cheatsheets",
|
||||||
"modal": {
|
"modal": {
|
||||||
"popup": "Open in new tab",
|
"popup": "Open in new tab",
|
||||||
|
|
|
@ -35,7 +35,7 @@ exports[`custom branding with inline=false will prefer the logo over the text 1`
|
||||||
exports[`custom branding with inline=true shows an image if branding logo is defined 1`] = `
|
exports[`custom branding with inline=true shows an image if branding logo is defined 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
class="inline-size"
|
class="inline-logo"
|
||||||
src="mockBrandingUrl"
|
src="mockBrandingUrl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,7 +44,7 @@ exports[`custom branding with inline=true shows an image if branding logo is def
|
||||||
exports[`custom branding with inline=true shows an text if branding text is defined 1`] = `
|
exports[`custom branding with inline=true shows an text if branding text is defined 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
class="inline-size"
|
class="inline-text"
|
||||||
>
|
>
|
||||||
mockedBranding
|
mockedBranding
|
||||||
</span>
|
</span>
|
||||||
|
@ -55,7 +55,7 @@ exports[`custom branding with inline=true will prefer the logo over the text 1`]
|
||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
alt="mockedBranding"
|
alt="mockedBranding"
|
||||||
class="inline-size"
|
class="inline-logo"
|
||||||
src="mockBrandingUrl"
|
src="mockBrandingUrl"
|
||||||
title="mockedBranding"
|
title="mockedBranding"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useBrandingDetails } from '../../common/custom-branding/use-branding-details'
|
import { useBrandingDetails } from './use-branding-details'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,4 +1,4 @@
|
||||||
/*
|
/*!
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -8,6 +8,11 @@
|
||||||
height: 50px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-size {
|
.inline-logo {
|
||||||
height: 30px;
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-text {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,10 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { concatCssClasses } from '../../../utils/concat-css-classes'
|
||||||
import styles from './branding.module.scss'
|
import styles from './branding.module.scss'
|
||||||
import { useBrandingDetails } from './use-branding-details'
|
import { useBrandingDetails } from './use-branding-details'
|
||||||
import React from 'react'
|
import React, { useMemo } from 'react'
|
||||||
|
|
||||||
export interface BrandingProps {
|
export interface BrandingProps {
|
||||||
inline?: boolean
|
inline?: boolean
|
||||||
|
@ -16,12 +17,19 @@ export interface BrandingProps {
|
||||||
* This branding can either be a text, a logo or both (in that case the text is used as the title and alt of the image).
|
* This branding can either be a text, a logo or both (in that case the text is used as the title and alt of the image).
|
||||||
*
|
*
|
||||||
* @param inline If the logo should be using the inline-size or the regular-size css class.
|
* @param inline If the logo should be using the inline-size or the regular-size css class.
|
||||||
* @param delimiter If the delimiter between the HedgeDoc logo and the branding should be shown.
|
|
||||||
*/
|
*/
|
||||||
export const CustomBranding: React.FC<BrandingProps> = ({ inline = false }) => {
|
export const CustomBranding: React.FC<BrandingProps> = ({ inline = false }) => {
|
||||||
const branding = useBrandingDetails()
|
const branding = useBrandingDetails()
|
||||||
|
|
||||||
const className = inline ? styles['inline-size'] : styles['regular-size']
|
const className = useMemo(
|
||||||
|
() =>
|
||||||
|
concatCssClasses({
|
||||||
|
[styles['regular-size']]: !inline,
|
||||||
|
[styles['inline-logo']]: inline && branding && !!branding.logo,
|
||||||
|
[styles['inline-text']]: inline && branding && !branding.logo
|
||||||
|
}),
|
||||||
|
[inline, branding]
|
||||||
|
)
|
||||||
|
|
||||||
if (!branding) {
|
if (!branding) {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -1,68 +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 { useOutlineButtonVariant } from '../../../hooks/dark-mode/use-outline-button-variant'
|
|
||||||
import { CheatsheetButton } from '../../cheatsheet/cheatsheet-button'
|
|
||||||
import { NewNoteButton } from '../../common/new-note-button/new-note-button'
|
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
|
||||||
import { SettingsButton } from '../../global-dialogs/settings-dialog/settings-button'
|
|
||||||
import { SignInButton } from '../../landing-layout/navigation/sign-in-button'
|
|
||||||
import { UserDropdown } from '../../landing-layout/navigation/user-dropdown'
|
|
||||||
import { HelpButton } from './help-button/help-button'
|
|
||||||
import { NavbarBranding } from './navbar-branding'
|
|
||||||
import { ReadOnlyModeButton } from './read-only-mode-button'
|
|
||||||
import { SlideModeButton } from './slide-mode-button'
|
|
||||||
import { NoteType } from '@hedgedoc/commons'
|
|
||||||
import React from 'react'
|
|
||||||
import { Nav, Navbar } from 'react-bootstrap'
|
|
||||||
|
|
||||||
export enum AppBarMode {
|
|
||||||
BASIC,
|
|
||||||
EDITOR
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppBarProps {
|
|
||||||
mode: AppBarMode
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the app bar.
|
|
||||||
*
|
|
||||||
* @param mode Which mode the app bar should be rendered in. This mainly adds / removes buttons for the editor.
|
|
||||||
*/
|
|
||||||
export const AppBar: React.FC<AppBarProps> = ({ mode }) => {
|
|
||||||
const userExists = useApplicationState((state) => !!state.user)
|
|
||||||
const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter)
|
|
||||||
const buttonVariant = useOutlineButtonVariant()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Navbar expand={true} className={'px-3'}>
|
|
||||||
<Nav className='me-auto d-flex align-items-center'>
|
|
||||||
<NavbarBranding />
|
|
||||||
<ShowIf condition={mode === AppBarMode.EDITOR}>
|
|
||||||
<ShowIf condition={noteFrontmatter.type === NoteType.SLIDE}>
|
|
||||||
<SlideModeButton />
|
|
||||||
</ShowIf>
|
|
||||||
<ShowIf condition={noteFrontmatter.type !== NoteType.SLIDE}>
|
|
||||||
<ReadOnlyModeButton />
|
|
||||||
</ShowIf>
|
|
||||||
<HelpButton />
|
|
||||||
<CheatsheetButton />
|
|
||||||
</ShowIf>
|
|
||||||
</Nav>
|
|
||||||
<Nav className='d-flex gap-2 align-items-center text-secondary justify-content-end'>
|
|
||||||
<SettingsButton variant={buttonVariant} />
|
|
||||||
<NewNoteButton />
|
|
||||||
<ShowIf condition={!userExists}>
|
|
||||||
<SignInButton size={'sm'} />
|
|
||||||
</ShowIf>
|
|
||||||
<ShowIf condition={userExists}>
|
|
||||||
<UserDropdown />
|
|
||||||
</ShowIf>
|
|
||||||
</Nav>
|
|
||||||
</Navbar>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
|
||||||
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
|
|
||||||
import { useOutlineButtonVariant } from '../../../../hooks/dark-mode/use-outline-button-variant'
|
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
|
||||||
import { IconButton } from '../../../common/icon-button/icon-button'
|
|
||||||
import { ShortcutsModal } from '../../../global-dialogs/shortcuts-modal/shortcuts-modal'
|
|
||||||
import React, { Fragment } from 'react'
|
|
||||||
import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons'
|
|
||||||
import { Trans } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the button to open the shortcuts modal.
|
|
||||||
*/
|
|
||||||
export const HelpButton: React.FC = () => {
|
|
||||||
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
|
||||||
const buttonVariant = useOutlineButtonVariant()
|
|
||||||
const buttonTitle = useTranslatedText('editor.documentBar.help')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<IconButton
|
|
||||||
icon={IconQuestionCircle}
|
|
||||||
{...cypressId('editor-help-button')}
|
|
||||||
title={buttonTitle}
|
|
||||||
className='ms-2'
|
|
||||||
size='sm'
|
|
||||||
variant={buttonVariant}
|
|
||||||
onClick={showModal}>
|
|
||||||
<Trans i18nKey={'editor.documentBar.help'} />
|
|
||||||
</IconButton>
|
|
||||||
<ShortcutsModal show={modalVisibility} onHide={closeModal} />
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { useDarkModeState } from '../../../hooks/dark-mode/use-dark-mode-state'
|
|
||||||
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 branding for the {@link AppBar}
|
|
||||||
*/
|
|
||||||
export const NavbarBranding: React.FC = () => {
|
|
||||||
const darkModeActivated = useDarkModeState()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Navbar.Brand>
|
|
||||||
<Link href='/intro' className='text-secondary text-decoration-none d-flex align-items-center'>
|
|
||||||
<HedgeDocLogoHorizontalGrey size={LogoSize.SMALL} color={darkModeActivated ? 'dark' : 'light'} />
|
|
||||||
<BrandingSeparatorDash />
|
|
||||||
<CustomBranding inline={true} />
|
|
||||||
</Link>
|
|
||||||
</Navbar.Brand>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,30 +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 { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
|
||||||
import { useOutlineButtonVariant } from '../../../hooks/dark-mode/use-outline-button-variant'
|
|
||||||
import { UiIcon } from '../../common/icons/ui-icon'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import React from 'react'
|
|
||||||
import { Button } from 'react-bootstrap'
|
|
||||||
import { FileEarmarkTextFill as IconFileEarmarkTextFill } from 'react-bootstrap-icons'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Button that links to the read-only version of a note.
|
|
||||||
*/
|
|
||||||
export const ReadOnlyModeButton: React.FC = () => {
|
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
|
||||||
const buttonVariant = useOutlineButtonVariant()
|
|
||||||
const buttonTitle = useTranslatedText('editor.documentBar.readOnlyMode')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link href={`/s/${noteIdentifier}`} target='_blank'>
|
|
||||||
<Button title={buttonTitle} size='sm' variant={buttonVariant}>
|
|
||||||
<UiIcon icon={IconFileEarmarkTextFill} />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,30 +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 { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
|
||||||
import { useOutlineButtonVariant } from '../../../hooks/dark-mode/use-outline-button-variant'
|
|
||||||
import { UiIcon } from '../../common/icons/ui-icon'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import React from 'react'
|
|
||||||
import { Button } from 'react-bootstrap'
|
|
||||||
import { Tv as IconTv } from 'react-bootstrap-icons'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Button that links to the slide-show presentation of the current note.
|
|
||||||
*/
|
|
||||||
export const SlideModeButton: React.FC = () => {
|
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
|
||||||
const buttonVariant = useOutlineButtonVariant()
|
|
||||||
const buttonTitle = useTranslatedText('editor.documentBar.slideMode')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link href={`/p/${noteIdentifier}`} target='_blank'>
|
|
||||||
<Button title={buttonTitle} className='ms-2' size='sm' variant={buttonVariant}>
|
|
||||||
<UiIcon icon={IconTv} />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -6,9 +6,9 @@
|
||||||
import { useApplyDarkModeStyle } from '../../hooks/dark-mode/use-apply-dark-mode-style'
|
import { useApplyDarkModeStyle } from '../../hooks/dark-mode/use-apply-dark-mode-style'
|
||||||
import { useSaveDarkModePreferenceToLocalStorage } from '../../hooks/dark-mode/use-save-dark-mode-preference-to-local-storage'
|
import { useSaveDarkModePreferenceToLocalStorage } from '../../hooks/dark-mode/use-save-dark-mode-preference-to-local-storage'
|
||||||
import { MotdModal } from '../global-dialogs/motd-modal/motd-modal'
|
import { MotdModal } from '../global-dialogs/motd-modal/motd-modal'
|
||||||
|
import { EditorAppBar } from '../layout/app-bar/editor-app-bar'
|
||||||
import { CommunicatorImageLightbox } from '../markdown-renderer/extensions/image/communicator-image-lightbox'
|
import { CommunicatorImageLightbox } from '../markdown-renderer/extensions/image/communicator-image-lightbox'
|
||||||
import { ExtensionEventEmitterProvider } from '../markdown-renderer/hooks/use-extension-event-emitter'
|
import { ExtensionEventEmitterProvider } from '../markdown-renderer/hooks/use-extension-event-emitter'
|
||||||
import { AppBar, AppBarMode } from './app-bar/app-bar'
|
|
||||||
import { ChangeEditorContentContextProvider } from './change-content-context/codemirror-reference-context'
|
import { ChangeEditorContentContextProvider } from './change-content-context/codemirror-reference-context'
|
||||||
import { EditorPane } from './editor-pane/editor-pane'
|
import { EditorPane } from './editor-pane/editor-pane'
|
||||||
import { useComponentsFromAppExtensions } from './editor-pane/hooks/use-components-from-app-extensions'
|
import { useComponentsFromAppExtensions } from './editor-pane/hooks/use-components-from-app-extensions'
|
||||||
|
@ -16,7 +16,6 @@ import { HeadMetaProperties } from './head-meta-properties/head-meta-properties'
|
||||||
import { useScrollState } from './hooks/use-scroll-state'
|
import { useScrollState } from './hooks/use-scroll-state'
|
||||||
import { useSetScrollSource } from './hooks/use-set-scroll-source'
|
import { useSetScrollSource } from './hooks/use-set-scroll-source'
|
||||||
import { useUpdateLocalHistoryEntry } from './hooks/use-update-local-history-entry'
|
import { useUpdateLocalHistoryEntry } from './hooks/use-update-local-history-entry'
|
||||||
import { RealtimeConnectionAlert } from './realtime-connection-alert/realtime-connection-alert'
|
|
||||||
import { RendererPane } from './renderer-pane/renderer-pane'
|
import { RendererPane } from './renderer-pane/renderer-pane'
|
||||||
import { Sidebar } from './sidebar/sidebar'
|
import { Sidebar } from './sidebar/sidebar'
|
||||||
import { Splitter } from './splitter/splitter'
|
import { Splitter } from './splitter/splitter'
|
||||||
|
@ -77,8 +76,7 @@ export const EditorPageContent: React.FC = () => {
|
||||||
<HeadMetaProperties />
|
<HeadMetaProperties />
|
||||||
<MotdModal />
|
<MotdModal />
|
||||||
<div className={'d-flex flex-column vh-100'}>
|
<div className={'d-flex flex-column vh-100'}>
|
||||||
<AppBar mode={AppBarMode.EDITOR} />
|
<EditorAppBar />
|
||||||
<RealtimeConnectionAlert />
|
|
||||||
<div className={'flex-fill d-flex h-100 w-100 overflow-hidden flex-row'}>
|
<div className={'flex-fill d-flex h-100 w-100 overflow-hidden flex-row'}>
|
||||||
<Splitter
|
<Splitter
|
||||||
left={leftPane}
|
left={leftPane}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`realtime connection alert will show if not synced 1`] = `
|
exports[`realtime connection alert will render correctly 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="fade m-0 rounded-0 alert alert-warning show"
|
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"
|
role="alert"
|
||||||
>
|
>
|
||||||
realtime.connecting
|
realtime.connecting
|
||||||
|
@ -11,5 +11,3 @@ exports[`realtime connection alert will show if not synced 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`realtime connection alert won't show if synced 1`] = `<div />`;
|
|
||||||
|
|
|
@ -3,26 +3,14 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import * as UseApplicationStateModule from '../../../hooks/common/use-application-state'
|
|
||||||
import { mockI18n } from '../../../test-utils/mock-i18n'
|
import { mockI18n } from '../../../test-utils/mock-i18n'
|
||||||
import { RealtimeConnectionAlert } from './realtime-connection-alert'
|
import { RealtimeConnectionAlert } from './realtime-connection-alert'
|
||||||
import { render } from '@testing-library/react'
|
import { render } from '@testing-library/react'
|
||||||
|
|
||||||
jest.mock('../../../hooks/common/use-application-state')
|
|
||||||
|
|
||||||
describe('realtime connection alert', () => {
|
describe('realtime connection alert', () => {
|
||||||
beforeAll(mockI18n)
|
beforeAll(mockI18n)
|
||||||
|
|
||||||
it("won't show if synced", () => {
|
it('will render correctly', () => {
|
||||||
jest.spyOn(UseApplicationStateModule, 'useApplicationState').mockImplementation(() => true)
|
|
||||||
|
|
||||||
const view = render(<RealtimeConnectionAlert />)
|
|
||||||
expect(view.container).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('will show if not synced', () => {
|
|
||||||
jest.spyOn(UseApplicationStateModule, 'useApplicationState').mockImplementation(() => false)
|
|
||||||
|
|
||||||
const view = render(<RealtimeConnectionAlert />)
|
const view = render(<RealtimeConnectionAlert />)
|
||||||
expect(view.container).toMatchSnapshot()
|
expect(view.container).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
|
||||||
import { UiIcon } from '../../common/icons/ui-icon'
|
import { UiIcon } from '../../common/icons/ui-icon'
|
||||||
import React, { Fragment } from 'react'
|
import React from 'react'
|
||||||
import { Alert } from 'react-bootstrap'
|
import { Alert } from 'react-bootstrap'
|
||||||
import { ArrowRepeat as IconArrowRepeat } from 'react-bootstrap-icons'
|
import { ArrowRepeat as IconArrowRepeat } from 'react-bootstrap-icons'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
@ -14,15 +13,10 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
* Modal with a spinner that is only shown while reconnecting to the realtime backend
|
* Modal with a spinner that is only shown while reconnecting to the realtime backend
|
||||||
*/
|
*/
|
||||||
export const RealtimeConnectionAlert: React.FC = () => {
|
export const RealtimeConnectionAlert: React.FC = () => {
|
||||||
const isSynced = useApplicationState((state) => state.realtimeStatus.isSynced)
|
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
if (isSynced) {
|
|
||||||
return <Fragment />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert variant={'warning'} className={'m-0 rounded-0'}>
|
<Alert variant={'warning'} className={'w-100 m-0 px-2 py-1 border-top-0 border-bottom-0 d-flex align-items-center'}>
|
||||||
<Trans i18nKey={'realtime.connecting'}></Trans>
|
<Trans i18nKey={'realtime.connecting'}></Trans>
|
||||||
<UiIcon icon={IconArrowRepeat} spin={true} className={'ms-2 mb-1'} />
|
<UiIcon icon={IconArrowRepeat} spin={true} className={'ms-2 mb-1'} />
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { PoweredByLinks } from './powered-by-links'
|
|
||||||
import { SocialLink } from './social-links'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the footer.
|
|
||||||
*/
|
|
||||||
export const Footer: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<footer className='small'>
|
|
||||||
<PoweredByLinks />
|
|
||||||
<SocialLink />
|
|
||||||
</footer>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import links from '../../../links.json'
|
|
||||||
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
|
||||||
import { ExternalLink } from '../../common/links/external-link'
|
|
||||||
import { TranslatedExternalLink } from '../../common/links/translated-external-link'
|
|
||||||
import { TranslatedInternalLink } from '../../common/links/translated-internal-link'
|
|
||||||
import { VersionInfoLink } from './version-info/version-info-link'
|
|
||||||
import React, { Fragment, useMemo } from 'react'
|
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a powered-by link.
|
|
||||||
*/
|
|
||||||
export const PoweredByLinks: React.FC = () => {
|
|
||||||
useTranslation()
|
|
||||||
|
|
||||||
const rawSpecialUrls = useFrontendConfig().specialUrls
|
|
||||||
|
|
||||||
const specialUrls = useMemo(
|
|
||||||
() => Object.entries(rawSpecialUrls).map(([i18nkey, url]) => [i18nkey, String(url)]),
|
|
||||||
[rawSpecialUrls]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p>
|
|
||||||
<Trans i18nKey='landing.footer.poweredBy'>
|
|
||||||
<ExternalLink href={links.webpage} text='HedgeDoc' />
|
|
||||||
</Trans>
|
|
||||||
|
|
|
||||||
<TranslatedInternalLink href='/n/release-notes' i18nKey='landing.footer.releases' />
|
|
||||||
{specialUrls.map(([i18nKey, href]) => (
|
|
||||||
<Fragment key={i18nKey}>
|
|
||||||
|
|
|
||||||
<TranslatedExternalLink href={href} i18nKey={'landing.footer.' + i18nKey} />
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
|
|
|
||||||
<VersionInfoLink />
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import links from '../../../links.json'
|
|
||||||
import { IconDiscourse } from '../../common/icons/additional/icon-discourse'
|
|
||||||
import { IconMatrixOrg } from '../../common/icons/additional/icon-matrix-org'
|
|
||||||
import { ExternalLink } from '../../common/links/external-link'
|
|
||||||
import React from 'react'
|
|
||||||
import { Github as IconGithub, Globe as IconGlobe, Mastodon as IconMastodon } from 'react-bootstrap-icons'
|
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders links to the social networks.
|
|
||||||
*/
|
|
||||||
export const SocialLink: React.FC = () => {
|
|
||||||
useTranslation()
|
|
||||||
return (
|
|
||||||
<p>
|
|
||||||
<Trans
|
|
||||||
i18nKey='landing.footer.followUs'
|
|
||||||
components={[
|
|
||||||
<ExternalLink href={links.githubOrg} icon={IconGithub} key={'github'} text='GitHub' />,
|
|
||||||
<ExternalLink href={links.community} icon={IconDiscourse} key={'users'} text='Discourse' />,
|
|
||||||
<ExternalLink href={links.chat} icon={IconMatrixOrg} key={'comment'} text='Matrix' />,
|
|
||||||
<ExternalLink href={links.mastodon} icon={IconMastodon} key={'mastodon'} text='Mastodon' />,
|
|
||||||
<ExternalLink href={links.translate} icon={IconGlobe} key={'globe'} text='POEditor' />
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
|
||||||
import { VersionInfoModal } from '../../../global-dialogs/version-info-modal/version-info-modal'
|
|
||||||
import React, { Fragment } from 'react'
|
|
||||||
import { Button } from 'react-bootstrap'
|
|
||||||
import { Trans } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a link for the version info and the {@link VersionInfoModal}.
|
|
||||||
*/
|
|
||||||
export const VersionInfoLink: React.FC = () => {
|
|
||||||
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<Button size={'sm'} variant={'link'} {...cypressId('show-version-modal')} className={'p-0'} onClick={showModal}>
|
|
||||||
<Trans i18nKey={'landing.versionInfo.versionInfo'} />
|
|
||||||
</Button>
|
|
||||||
<VersionInfoModal onHide={closeModal} show={modalVisibility} />
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { useApplyDarkModeStyle } from '../../hooks/dark-mode/use-apply-dark-mode-style'
|
import { useApplyDarkModeStyle } from '../../hooks/dark-mode/use-apply-dark-mode-style'
|
||||||
import { useSaveDarkModePreferenceToLocalStorage } from '../../hooks/dark-mode/use-save-dark-mode-preference-to-local-storage'
|
import { useSaveDarkModePreferenceToLocalStorage } from '../../hooks/dark-mode/use-save-dark-mode-preference-to-local-storage'
|
||||||
import { MotdModal } from '../global-dialogs/motd-modal/motd-modal'
|
import { MotdModal } from '../global-dialogs/motd-modal/motd-modal'
|
||||||
import { Footer } from './footer/footer'
|
import { BaseAppBar } from '../layout/app-bar/base-app-bar'
|
||||||
import { HeaderBar } from './navigation/header-bar/header-bar'
|
import { HeaderBar } from './navigation/header-bar/header-bar'
|
||||||
import type { PropsWithChildren } from 'react'
|
import type { PropsWithChildren } from 'react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
@ -23,12 +23,12 @@ export const LandingLayout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<BaseAppBar />
|
||||||
<MotdModal />
|
<MotdModal />
|
||||||
<Container className='d-flex flex-column mvh-100'>
|
<Container className='d-flex flex-column'>
|
||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
<div className={'d-flex flex-column justify-content-between flex-fill text-center'}>
|
<div className={'d-flex flex-column justify-content-between flex-fill text-center'}>
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
<Footer />
|
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,12 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
import { NewNoteButton } from '../../../common/new-note-button/new-note-button'
|
|
||||||
import { SettingsButton } from '../../../global-dialogs/settings-dialog/settings-button'
|
|
||||||
import { SignInButton } from '../sign-in-button'
|
|
||||||
import { UserDropdown } from '../user-dropdown'
|
|
||||||
import { HeaderNavLink } from './header-nav-link'
|
import { HeaderNavLink } from './header-nav-link'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Navbar } from 'react-bootstrap'
|
import { Navbar } from 'react-bootstrap'
|
||||||
|
@ -19,7 +14,6 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
*/
|
*/
|
||||||
const HeaderBar: React.FC = () => {
|
const HeaderBar: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const userExists = useApplicationState((state) => !!state.user)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar className='justify-content-between'>
|
<Navbar className='justify-content-between'>
|
||||||
|
@ -31,11 +25,6 @@ const HeaderBar: React.FC = () => {
|
||||||
<Trans i18nKey='landing.navigation.history' />
|
<Trans i18nKey='landing.navigation.history' />
|
||||||
</HeaderNavLink>
|
</HeaderNavLink>
|
||||||
</div>
|
</div>
|
||||||
<div className='d-inline-flex gap-2'>
|
|
||||||
<SettingsButton variant={'outline-dark'} />
|
|
||||||
<NewNoteButton />
|
|
||||||
{!userExists ? <SignInButton size='sm' /> : <UserDropdown />}
|
|
||||||
</div>
|
|
||||||
</Navbar>
|
</Navbar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
// 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>
|
||||||
|
`;
|
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
.custom {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.0;
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* 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 Link from 'next/link'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the HedgeDoc branding and branding customizations for the app bar.
|
||||||
|
*/
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import React from 'react'
|
||||||
|
import { Dropdown } from 'react-bootstrap'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
interface DropdownHeaderProps {
|
||||||
|
i18nKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a dropdown header with a translation.
|
||||||
|
* @param i18nKey The i18n key for the header
|
||||||
|
*/
|
||||||
|
export const DropdownHeader: React.FC<DropdownHeaderProps> = ({ i18nKey }) => {
|
||||||
|
useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown.Header>
|
||||||
|
<Trans i18nKey={i18nKey} />
|
||||||
|
</Dropdown.Header>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { UiIcon } from '../../../../common/icons/ui-icon'
|
||||||
|
import { HelpSubmenu } from './submenues/help-submenu'
|
||||||
|
import { InstanceSubmenu } from './submenues/instance-submenu'
|
||||||
|
import { LegalSubmenu } from './submenues/legal-submenu'
|
||||||
|
import { ProjectLinksSubmenu } from './submenues/project-links-submenu'
|
||||||
|
import { SocialLinksSubmenu } from './submenues/social-links-submenu'
|
||||||
|
import React from 'react'
|
||||||
|
import { Dropdown } from 'react-bootstrap'
|
||||||
|
import { QuestionLg as IconQuestion } from 'react-bootstrap-icons'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the help dropdown in the app bar.
|
||||||
|
*/
|
||||||
|
export const HelpDropdown: React.FC = () => {
|
||||||
|
useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown>
|
||||||
|
<Dropdown.Toggle size={'sm'}>
|
||||||
|
<UiIcon icon={IconQuestion} />
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<HelpSubmenu />
|
||||||
|
<Dropdown.Divider />
|
||||||
|
<InstanceSubmenu />
|
||||||
|
<LegalSubmenu />
|
||||||
|
<Dropdown.Divider />
|
||||||
|
<ProjectLinksSubmenu />
|
||||||
|
<Dropdown.Divider />
|
||||||
|
<SocialLinksSubmenu />
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { DropdownHeader } from '../dropdown-header'
|
||||||
|
import { CheatsheetHelpMenuEntry } from './help/cheatsheet-help-menu-entry'
|
||||||
|
import { ShortcutsHelpMenuEntry } from './help/shortcuts-help-menu-entry'
|
||||||
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the help submenu for the help dropdown.
|
||||||
|
*/
|
||||||
|
export const HelpSubmenu: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<DropdownHeader i18nKey={'appbar.help.help.header'} />
|
||||||
|
<ShortcutsHelpMenuEntry />
|
||||||
|
<CheatsheetHelpMenuEntry />
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { useBooleanState } from '../../../../../../../hooks/common/use-boolean-state'
|
||||||
|
import { cypressId } from '../../../../../../../utils/cypress-attribute'
|
||||||
|
import { CheatsheetContent } from '../../../../../../cheatsheet/cheatsheet-content'
|
||||||
|
import { CheatsheetInNewTabButton } from '../../../../../../cheatsheet/cheatsheet-in-new-tab-button'
|
||||||
|
import { CommonModal } from '../../../../../../common/modals/common-modal'
|
||||||
|
import { TranslatedDropdownItem } from '../../translated-dropdown-item'
|
||||||
|
import React, { Fragment } from 'react'
|
||||||
|
import { Modal } from 'react-bootstrap'
|
||||||
|
import { Search as IconSearch } from 'react-bootstrap-icons'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the cheatsheet menu entry for the help dropdown.
|
||||||
|
*/
|
||||||
|
export const CheatsheetHelpMenuEntry: React.FC = () => {
|
||||||
|
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<TranslatedDropdownItem
|
||||||
|
i18nKey={'appbar.help.help.cheatsheet'}
|
||||||
|
icon={IconSearch}
|
||||||
|
onClick={showModal}
|
||||||
|
{...cypressId('open.cheatsheet-button')}
|
||||||
|
/>
|
||||||
|
<CommonModal
|
||||||
|
modalSize={'xl'}
|
||||||
|
show={modalVisibility}
|
||||||
|
onHide={closeModal}
|
||||||
|
showCloseButton={true}
|
||||||
|
titleI18nKey={'cheatsheet.modal.title'}
|
||||||
|
additionalTitleElement={
|
||||||
|
<div className={'d-flex flex-row-reverse w-100 mx-2'}>
|
||||||
|
<CheatsheetInNewTabButton onClick={closeModal} />
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<Modal.Body>
|
||||||
|
<CheatsheetContent />
|
||||||
|
</Modal.Body>
|
||||||
|
</CommonModal>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { useBooleanState } from '../../../../../../../hooks/common/use-boolean-state'
|
||||||
|
import { ShortcutsModal } from '../../../../../../global-dialogs/shortcuts-modal/shortcuts-modal'
|
||||||
|
import { TranslatedDropdownItem } from '../../translated-dropdown-item'
|
||||||
|
import React, { Fragment } from 'react'
|
||||||
|
import { KeyboardFill as IconKeyboardFill } from 'react-bootstrap-icons'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the shortcuts menu entry for the help dropdown.
|
||||||
|
*/
|
||||||
|
export const ShortcutsHelpMenuEntry: React.FC = () => {
|
||||||
|
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<TranslatedDropdownItem icon={IconKeyboardFill} i18nKey={'appbar.help.help.shortcuts'} onClick={showModal} />
|
||||||
|
<ShortcutsModal show={modalVisibility} onHide={closeModal} />
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { DropdownHeader } from '../dropdown-header'
|
||||||
|
import { VersionInfoHelpMenuEntry } from './instance/version-info-help-menu-entry'
|
||||||
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the instance submenu for the help dropdown.
|
||||||
|
*/
|
||||||
|
export const InstanceSubmenu: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<DropdownHeader i18nKey={'appbar.help.instance.header'} />
|
||||||
|
<VersionInfoHelpMenuEntry />
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { useBooleanState } from '../../../../../../../hooks/common/use-boolean-state'
|
||||||
|
import { VersionInfoModal } from '../../../../../../global-dialogs/version-info-modal/version-info-modal'
|
||||||
|
import { TranslatedDropdownItem } from '../../translated-dropdown-item'
|
||||||
|
import React, { Fragment } from 'react'
|
||||||
|
import { Server as IconServer } from 'react-bootstrap-icons'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the version info menu entry for the help dropdown.
|
||||||
|
*/
|
||||||
|
export const VersionInfoHelpMenuEntry: React.FC = () => {
|
||||||
|
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<TranslatedDropdownItem icon={IconServer} i18nKey={'appbar.help.instance.versionInfo'} onClick={showModal} />
|
||||||
|
<VersionInfoModal show={modalVisibility} onHide={closeModal} />
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { useFrontendConfig } from '../../../../../common/frontend-config-context/use-frontend-config'
|
||||||
|
import { ShowIf } from '../../../../../common/show-if/show-if'
|
||||||
|
import { DropdownHeader } from '../dropdown-header'
|
||||||
|
import { TranslatedDropdownItem } from '../translated-dropdown-item'
|
||||||
|
import React, { Fragment, useMemo } from 'react'
|
||||||
|
import { Dropdown } from 'react-bootstrap'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the legal submenu for the help dropdown.
|
||||||
|
*/
|
||||||
|
export const LegalSubmenu: React.FC = () => {
|
||||||
|
useTranslation()
|
||||||
|
const specialUrls = useFrontendConfig().specialUrls
|
||||||
|
const linksConfigured = useMemo(
|
||||||
|
() => specialUrls.privacy || specialUrls.termsOfUse || specialUrls.imprint,
|
||||||
|
[specialUrls]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!linksConfigured) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Dropdown.Divider />
|
||||||
|
<DropdownHeader i18nKey={'appbar.help.legal.header'} />
|
||||||
|
<ShowIf condition={!!specialUrls.privacy}>
|
||||||
|
<TranslatedDropdownItem href={specialUrls.privacy} i18nKey={'appbar.help.legal.privacy'} />
|
||||||
|
</ShowIf>
|
||||||
|
<ShowIf condition={!!specialUrls.termsOfUse}>
|
||||||
|
<TranslatedDropdownItem href={specialUrls.termsOfUse} i18nKey={'appbar.help.legal.termsOfUse'} />
|
||||||
|
</ShowIf>
|
||||||
|
<ShowIf condition={!!specialUrls.imprint}>
|
||||||
|
<TranslatedDropdownItem href={specialUrls.imprint} i18nKey={'appbar.help.legal.imprint'} />
|
||||||
|
</ShowIf>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import links from '../../../../../../links.json'
|
||||||
|
import { DropdownHeader } from '../dropdown-header'
|
||||||
|
import { TranslatedDropdownItem } from '../translated-dropdown-item'
|
||||||
|
import React, { Fragment } from 'react'
|
||||||
|
import { Flag as IconFlag, Tag as IconTag, Github as IconGithub } from 'react-bootstrap-icons'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the project links submenu for the help dropdown.
|
||||||
|
*/
|
||||||
|
export const ProjectLinksSubmenu: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<DropdownHeader i18nKey={'appbar.help.project.header'} />
|
||||||
|
<TranslatedDropdownItem
|
||||||
|
i18nKey={'appbar.help.project.github'}
|
||||||
|
icon={IconGithub}
|
||||||
|
href={links.githubOrg}
|
||||||
|
target={'_blank'}
|
||||||
|
/>
|
||||||
|
<TranslatedDropdownItem
|
||||||
|
i18nKey={'appbar.help.project.reportIssue'}
|
||||||
|
icon={IconTag}
|
||||||
|
href={links.issues}
|
||||||
|
target={'_blank'}
|
||||||
|
/>
|
||||||
|
<TranslatedDropdownItem
|
||||||
|
i18nKey={'appbar.help.project.helpTranslating'}
|
||||||
|
icon={IconFlag}
|
||||||
|
href={links.translate}
|
||||||
|
target={'_blank'}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import links from '../../../../../../links.json'
|
||||||
|
import { IconDiscourse } from '../../../../../common/icons/additional/icon-discourse'
|
||||||
|
import { IconMatrixOrg } from '../../../../../common/icons/additional/icon-matrix-org'
|
||||||
|
import { DropdownHeader } from '../dropdown-header'
|
||||||
|
import { TranslatedDropdownItem } from '../translated-dropdown-item'
|
||||||
|
import React, { Fragment } from 'react'
|
||||||
|
import { Mastodon as IconMastodon } from 'react-bootstrap-icons'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the social links submenu for the help dropdown.
|
||||||
|
*/
|
||||||
|
export const SocialLinksSubmenu: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<DropdownHeader i18nKey={'appbar.help.social.header'} />
|
||||||
|
<TranslatedDropdownItem
|
||||||
|
i18nKey={'appbar.help.social.discourse'}
|
||||||
|
icon={IconDiscourse}
|
||||||
|
href={links.community}
|
||||||
|
target={'_blank'}
|
||||||
|
/>
|
||||||
|
<TranslatedDropdownItem
|
||||||
|
i18nKey={'appbar.help.social.matrix'}
|
||||||
|
icon={IconMatrixOrg}
|
||||||
|
href={links.chat}
|
||||||
|
target={'_blank'}
|
||||||
|
/>
|
||||||
|
<TranslatedDropdownItem
|
||||||
|
i18nKey={'appbar.help.social.mastodon'}
|
||||||
|
icon={IconMastodon}
|
||||||
|
href={links.mastodon}
|
||||||
|
target={'_blank'}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { useTranslatedText } from '../../../../../hooks/common/use-translated-text'
|
||||||
|
import { UiIcon } from '../../../../common/icons/ui-icon'
|
||||||
|
import { ShowIf } from '../../../../common/show-if/show-if'
|
||||||
|
import type { TOptions } from 'i18next'
|
||||||
|
import React from 'react'
|
||||||
|
import { Dropdown } from 'react-bootstrap'
|
||||||
|
import type { Icon } from 'react-bootstrap-icons'
|
||||||
|
import type { DropdownItemProps } from 'react-bootstrap/DropdownItem'
|
||||||
|
|
||||||
|
interface TranslatedDropdownItemProps extends DropdownItemProps {
|
||||||
|
i18nKey: string
|
||||||
|
i18nKeyOptions?: TOptions
|
||||||
|
icon?: Icon
|
||||||
|
target?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a dropdown item with translated title.
|
||||||
|
* @param i18nKey The i18n key for the title
|
||||||
|
* @param i18nKeyOptions The options for the i18n key
|
||||||
|
* @param icon The icon that should be rendered before the title
|
||||||
|
* @param props Other props for the dropdown item
|
||||||
|
*/
|
||||||
|
export const TranslatedDropdownItem: React.FC<TranslatedDropdownItemProps> = ({
|
||||||
|
i18nKey,
|
||||||
|
i18nKeyOptions,
|
||||||
|
icon,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const title = useTranslatedText(i18nKey, i18nKeyOptions)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown.Item {...props} title={title} className={'d-flex align-items-center'}>
|
||||||
|
<ShowIf condition={!!icon}>
|
||||||
|
<UiIcon icon={icon} className={'me-2'} />
|
||||||
|
</ShowIf>
|
||||||
|
<span>{title}</span>
|
||||||
|
</Dropdown.Item>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
.read-only-marker {
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-top: -0.3em;
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { useMayEdit } from '../../../../../hooks/common/use-may-edit'
|
||||||
|
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 { Lock as IconLock } from 'react-bootstrap-icons'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the title of the current note and an optional read-only marker.
|
||||||
|
*/
|
||||||
|
export const NoteTitleElement: React.FC = () => {
|
||||||
|
const isWriteable = useMayEdit()
|
||||||
|
const noteTitle = useNoteTitle()
|
||||||
|
const readOnlyLabel = useTranslatedText('appbar.editor.readOnly')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<ShowIf condition={!isWriteable}>
|
||||||
|
<span className={'text-secondary me-2'}>
|
||||||
|
<UiIcon icon={IconLock} title={readOnlyLabel} />
|
||||||
|
</span>
|
||||||
|
</ShowIf>
|
||||||
|
<span className={'text-truncate mw-100'}>{noteTitle}</span>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* 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 { SignInButton } from '../../../landing-layout/navigation/sign-in-button'
|
||||||
|
import { UserDropdown } from '../../../landing-layout/navigation/user-dropdown'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders either the user dropdown or the sign-in button depending on the user state.
|
||||||
|
*/
|
||||||
|
export const UserElement: React.FC = () => {
|
||||||
|
const userExists = useApplicationState((state) => !!state.user)
|
||||||
|
return userExists ? <UserDropdown /> : <SignInButton size={'sm'} />
|
||||||
|
}
|
37
frontend/src/components/layout/app-bar/base-app-bar.tsx
Normal file
37
frontend/src/components/layout/app-bar/base-app-bar.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { NewNoteButton } from '../../common/new-note-button/new-note-button'
|
||||||
|
import { SettingsButton } from '../../global-dialogs/settings-dialog/settings-button'
|
||||||
|
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 type { PropsWithChildren } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { Col, Nav, Navbar } from 'react-bootstrap'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
<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'}>
|
||||||
|
<HelpDropdown />
|
||||||
|
<SettingsButton />
|
||||||
|
<NewNoteButton />
|
||||||
|
<UserElement />
|
||||||
|
</Nav>
|
||||||
|
</Col>
|
||||||
|
</Navbar>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
})
|
||||||
|
})
|
19
frontend/src/components/layout/app-bar/editor-app-bar.tsx
Normal file
19
frontend/src/components/layout/app-bar/editor-app-bar.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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>
|
||||||
|
}
|
|
@ -33,7 +33,6 @@ const IntroPage: NextPage = () => {
|
||||||
</div>
|
</div>
|
||||||
<CoverButtons />
|
<CoverButtons />
|
||||||
<IntroCustomContent />
|
<IntroCustomContent />
|
||||||
<hr className={'mb-5'} />
|
|
||||||
</div>
|
</div>
|
||||||
</EditorToRendererCommunicatorContextProvider>
|
</EditorToRendererCommunicatorContextProvider>
|
||||||
</LandingLayout>
|
</LandingLayout>
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { MotdModal } from '../../components/common/motd-modal/motd-modal'
|
|
||||||
import { NoteLoadingBoundary } from '../../components/common/note-loading-boundary/note-loading-boundary'
|
import { NoteLoadingBoundary } from '../../components/common/note-loading-boundary/note-loading-boundary'
|
||||||
import { DocumentReadOnlyPageContent } from '../../components/document-read-only-page/document-read-only-page-content'
|
import { DocumentReadOnlyPageContent } from '../../components/document-read-only-page/document-read-only-page-content'
|
||||||
import { AppBar, AppBarMode } from '../../components/editor-page/app-bar/app-bar'
|
|
||||||
import { HeadMetaProperties } from '../../components/editor-page/head-meta-properties/head-meta-properties'
|
import { HeadMetaProperties } from '../../components/editor-page/head-meta-properties/head-meta-properties'
|
||||||
import { EditorToRendererCommunicatorContextProvider } from '../../components/editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
import { EditorToRendererCommunicatorContextProvider } from '../../components/editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
||||||
|
import { MotdModal } from '../../components/global-dialogs/motd-modal/motd-modal'
|
||||||
|
import { BaseAppBar } from '../../components/layout/app-bar/base-app-bar'
|
||||||
import { useApplyDarkModeStyle } from '../../hooks/dark-mode/use-apply-dark-mode-style'
|
import { useApplyDarkModeStyle } from '../../hooks/dark-mode/use-apply-dark-mode-style'
|
||||||
import { useSaveDarkModePreferenceToLocalStorage } from '../../hooks/dark-mode/use-save-dark-mode-preference-to-local-storage'
|
import { useSaveDarkModePreferenceToLocalStorage } from '../../hooks/dark-mode/use-save-dark-mode-preference-to-local-storage'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
@ -26,7 +26,7 @@ export const DocumentReadOnlyPage: React.FC = () => {
|
||||||
<HeadMetaProperties />
|
<HeadMetaProperties />
|
||||||
<MotdModal />
|
<MotdModal />
|
||||||
<div className={'d-flex flex-column mvh-100'}>
|
<div className={'d-flex flex-column mvh-100'}>
|
||||||
<AppBar mode={AppBarMode.BASIC} />
|
<BaseAppBar />
|
||||||
<DocumentReadOnlyPageContent />
|
<DocumentReadOnlyPageContent />
|
||||||
</div>
|
</div>
|
||||||
</NoteLoadingBoundary>
|
</NoteLoadingBoundary>
|
||||||
|
|
Loading…
Reference in a new issue