fix: Move content into to frontend directory

Doing this BEFORE the merge prevents a lot of merge conflicts.

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-11-11 11:16:18 +01:00
parent 4e18ce38f3
commit 762a0a850e
No known key found for this signature in database
GPG key ID: B97799103358209B
1051 changed files with 0 additions and 35 deletions

View file

@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { cypressId } from '../../../../utils/cypress-attribute'
import { Trans } from 'react-i18next'
import { DeletionModal } from '../../../common/modals/deletion-modal'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
import type { ModalVisibilityProps } from '../../../common/modals/common-modal'
export interface DeleteHistoryNoteModalProps {
modalTitleI18nKey?: string
modalQuestionI18nKey?: string
modalWarningI18nKey?: string
modalButtonI18nKey?: string
}
export interface DeleteNoteModalProps extends ModalVisibilityProps {
optionalNoteTitle?: string
onConfirm: () => void
}
/**
* A modal that asks the user if they really want to delete the current note.
*
* @param optionalNoteTitle optional note title
* @param show Defines if the modal should be shown
* @param onHide A callback that fires if the modal should be hidden without confirmation
* @param onConfirm A callback that fires if the user confirmed the request
* @param modalTitleI18nKey optional i18nKey for the title
* @param modalQuestionI18nKey optional i18nKey for the question
* @param modalWarningI18nKey optional i18nKey for the warning
* @param modalButtonI18nKey optional i18nKey for the button
*/
export const DeleteNoteModal: React.FC<DeleteNoteModalProps & DeleteHistoryNoteModalProps> = ({
optionalNoteTitle,
show,
onHide,
onConfirm,
modalTitleI18nKey,
modalQuestionI18nKey,
modalWarningI18nKey,
modalButtonI18nKey
}) => {
const noteTitle = useApplicationState((state) => state.noteDetails.title)
return (
<DeletionModal
{...cypressId('sidebar.deleteNote.modal')}
onConfirm={onConfirm}
deletionButtonI18nKey={modalButtonI18nKey ?? 'editor.modal.deleteNote.button'}
show={show}
onHide={onHide}
title={modalTitleI18nKey ?? 'editor.modal.deleteNote.title'}>
<h5>
<Trans i18nKey={modalQuestionI18nKey ?? 'editor.modal.deleteNote.question'} />
</h5>
<ul>
<li {...cypressId('sidebar.deleteNote.modal.noteTitle')}>{optionalNoteTitle ?? noteTitle}</li>
</ul>
<h6>
<Trans i18nKey={modalWarningI18nKey ?? 'editor.modal.deleteNote.warning'} />
</h6>
</DeletionModal>
)
}

View file

@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { PropsWithChildren } from 'react'
import React, { Fragment, useCallback } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
import { cypressId } from '../../../../utils/cypress-attribute'
import { deleteNote } from '../../../../api/notes'
import { DeleteNoteModal } from './delete-note-modal'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
import { useRouter } from 'next/router'
import { Logger } from '../../../../utils/logger'
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
const logger = new Logger('note-deletion')
/**
* Sidebar entry that can be used to delete the current note.
*
* @param hide {@link true} if the entry shouldn't be visible
* @param className Additional css class names for the sidebar entry
*/
export const DeleteNoteSidebarEntry: React.FC<PropsWithChildren<SpecificSidebarEntryProps>> = ({ hide, className }) => {
useTranslation()
const router = useRouter()
const noteId = useApplicationState((state) => state.noteDetails.id)
const [modalVisibility, showModal, closeModal] = useBooleanState()
const { showErrorNotification } = useUiNotifications()
const deleteNoteAndCloseDialog = useCallback(() => {
deleteNote(noteId)
.then(() => {
router.push('/history').catch((reason) => logger.error('Error while redirecting to /history', reason))
})
.catch(showErrorNotification('landing.history.error.deleteNote.text'))
.finally(closeModal)
}, [closeModal, noteId, router, showErrorNotification])
return (
<Fragment>
<SidebarButton
{...cypressId('sidebar.deleteNote.button')}
icon={'trash'}
className={className}
hide={hide}
onClick={showModal}>
<Trans i18nKey={'landing.history.menu.deleteNote'} />
</SidebarButton>
<DeleteNoteModal onHide={closeModal} onConfirm={deleteNoteAndCloseDialog} show={modalVisibility} />
</Fragment>
)
}

View file

@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.sidebar-button {
@import '../style/variables.scss';
height: $height;
flex: 0 0 $height;
width: 100%;
display: flex;
align-items: center;
border: solid 1px rgba(0, 0, 0, 0.15);
user-select: none;
cursor: pointer;
background: transparent;
padding: 0;
transition: height 0.2s, flex-basis 0.2s;
overflow: hidden;
&.hide {
flex-basis: 0;
height: 0px;
border-width: 0px;
.sidebar-icon {
opacity: 0;
}
.sidebar-text {
opacity: 0;
}
}
.sidebar-icon {
transition: opacity 0.2s;
opacity: 1;
height: $height;
width: $height;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
flex: 0 0 40px;
}
.sidebar-text {
height: 100%;
display: flex;
align-items: center;
opacity: 1;
transition: opacity 0.2s;
text-align: left;
flex: 1 1 0;
width: 0;
}
@mixin colors {
color: var(--bs-dark);
&.sidebar-button-primary {
color: var(--bs-light);
background: var(--bs-primary);
&:hover {
color: var(--bs-primary);
background: var(--bs-light);
}
}
&:hover {
color: var(--bs-light);
background: var(--bs-primary);
}
}
$entry-hover-bg: darken(black, 10%);
@include colors;
}

View file

@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { PropsWithChildren } from 'react'
import React from 'react'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import type { IconName } from '../../../common/fork-awesome/types'
import { ShowIf } from '../../../common/show-if/show-if'
import type { SidebarEntryProps } from '../types'
import styles from './sidebar-button.module.scss'
/**
* A button that should be rendered in the sidebar.
*
* @param children The react elements in the button
* @param icon The icon on the left side of the button
* @param className Additional css class names
* @param buttonRef A reference to the button
* @param hide Should be {@link true} if the button should be invisible
* @param variant An alternative theme for the button
* @param disabled If the button should be disabled
* @param props Other button props
*/
export const SidebarButton: React.FC<PropsWithChildren<SidebarEntryProps>> = ({
children,
icon,
className,
buttonRef,
hide,
disabled,
...props
}) => {
return (
<button
ref={buttonRef}
className={`${styles['sidebar-button']} ${hide ? styles['hide'] : ''} ${className ?? ''}`}
disabled={disabled}
{...props}>
<ShowIf condition={!!icon}>
<span className={`sidebar-button-icon ${styles['sidebar-icon']}`}>
<ForkAwesomeIcon icon={icon as IconName} />
</span>
</ShowIf>
<span className={styles['sidebar-text']}>{children}</span>
</button>
)
}

View file

@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.sidebar-menu {
transition: height 0.2s, flex-basis 0.2s;
display: flex;
flex-direction: column;
overflow: hidden;
flex: 0 1 0;
height: 0;
&.show {
height: 100%;
flex-basis: 100%;
overflow-y: auto;
}
& > div {
background: var(--bs-body-bg);
box-shadow: inset 0 7px 7px -6px #bbbbbb;
}
}

View file

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { PropsWithChildren } from 'react'
import React from 'react'
import type { SidebarMenuProps } from '../types'
import styles from './sidebar-menu.module.scss'
/**
* Renders a sidebar menu.
*
* @param children The children in the menu.
* @param expand If the menu is extended (and the children are shown) or not.
*/
export const SidebarMenu: React.FC<PropsWithChildren<SidebarMenuProps>> = ({ children, expand }) => {
return (
<div className={`${styles['sidebar-menu']} ${expand ? styles['show'] : ''}`}>
<div className={`d-flex flex-column`}>{children}</div>
</div>
)
}

View file

@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useRef, useState } from 'react'
import { useClickAway } from 'react-use'
import { DeleteNoteSidebarEntry } from './delete-note-sidebar-entry/delete-note-sidebar-entry'
import { NoteInfoSidebarEntry } from './specific-sidebar-entries/note-info-sidebar-entry'
import { ExportMenuSidebarMenu } from './specific-sidebar-entries/export-menu-sidebar-menu'
import { ImportMenuSidebarMenu } from './specific-sidebar-entries/import-menu-sidebar-menu'
import { PermissionsSidebarEntry } from './specific-sidebar-entries/permissions-sidebar-entry'
import { PinNoteSidebarEntry } from './specific-sidebar-entries/pin-note-sidebar-entry'
import { RevisionSidebarEntry } from './specific-sidebar-entries/revision-sidebar-entry'
import { ShareSidebarEntry } from './specific-sidebar-entries/share-sidebar-entry'
import styles from './style/sidebar.module.scss'
import { DocumentSidebarMenuSelection } from './types'
import { UsersOnlineSidebarMenu } from './users-online-sidebar-menu/users-online-sidebar-menu'
import { AliasesSidebarEntry } from './specific-sidebar-entries/aliases-sidebar-entry'
/**
* Renders the sidebar for the editor.
*/
export const Sidebar: React.FC = () => {
const sideBarRef = useRef<HTMLDivElement>(null)
const [selectedMenu, setSelectedMenu] = useState<DocumentSidebarMenuSelection>(DocumentSidebarMenuSelection.NONE)
useClickAway(sideBarRef, () => {
setSelectedMenu(DocumentSidebarMenuSelection.NONE)
})
const toggleValue = useCallback(
(toggleValue: DocumentSidebarMenuSelection): void => {
const newValue = selectedMenu === toggleValue ? DocumentSidebarMenuSelection.NONE : toggleValue
setSelectedMenu(newValue)
},
[selectedMenu]
)
const selectionIsNotNone = selectedMenu !== DocumentSidebarMenuSelection.NONE
return (
<div className={styles['slide-sidebar']}>
<div ref={sideBarRef} className={`${styles['sidebar-inner']} ${selectionIsNotNone ? styles['show'] : ''}`}>
<UsersOnlineSidebarMenu
menuId={DocumentSidebarMenuSelection.USERS_ONLINE}
selectedMenuId={selectedMenu}
onClick={toggleValue}
/>
<NoteInfoSidebarEntry hide={selectionIsNotNone} />
<RevisionSidebarEntry hide={selectionIsNotNone} />
<PermissionsSidebarEntry hide={selectionIsNotNone} />
<AliasesSidebarEntry hide={selectionIsNotNone} />
<ImportMenuSidebarMenu
menuId={DocumentSidebarMenuSelection.IMPORT}
selectedMenuId={selectedMenu}
onClick={toggleValue}
/>
<ExportMenuSidebarMenu
menuId={DocumentSidebarMenuSelection.EXPORT}
selectedMenuId={selectedMenu}
onClick={toggleValue}
/>
<ShareSidebarEntry hide={selectionIsNotNone} />
<DeleteNoteSidebarEntry hide={selectionIsNotNone} />
<PinNoteSidebarEntry hide={selectionIsNotNone} />
</div>
</div>
)
}

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import type { SpecificSidebarEntryProps } from '../types'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import { AliasesModal } from '../../document-bar/aliases/aliases-modal'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
/**
* Component that shows a button in the editor sidebar for opening the aliases modal.
*
* @param className Additional CSS classes that should be added to the sidebar button.
* @param hide True when the sidebar button should be hidden, False otherwise.
*/
export const AliasesSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
useTranslation()
const [showModal, setShowModal, setHideModal] = useBooleanState(false)
return (
<Fragment>
<SidebarButton hide={hide} className={className} icon={'tags'} onClick={setShowModal}>
<Trans i18nKey={'editor.modal.aliases.title'} />
</SidebarButton>
<AliasesModal show={showModal} onHide={setHideModal} />
</Fragment>
)
}

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback } from 'react'
import sanitize from 'sanitize-filename'
import { getGlobalState } from '../../../../redux'
import { Trans, useTranslation } from 'react-i18next'
import { download } from '../../../common/download/download'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import { useNoteMarkdownContent } from '../../../../hooks/common/use-note-markdown-content'
import { cypressId } from '../../../../utils/cypress-attribute'
/**
* Editor sidebar entry for exporting the markdown content into a local file.
*/
export const ExportMarkdownSidebarEntry: React.FC = () => {
const { t } = useTranslation()
const markdownContent = useNoteMarkdownContent()
const onClick = useCallback(() => {
const sanitized = sanitize(getGlobalState().noteDetails.title)
download(markdownContent, `${sanitized !== '' ? sanitized : t('editor.untitledNote')}.md`, 'text/markdown')
}, [markdownContent, t])
return (
<SidebarButton {...cypressId('menu-export-markdown')} onClick={onClick} icon={'file-text'}>
<Trans i18nKey={'editor.export.markdown-file'} />
</SidebarButton>
)
}

View file

@ -0,0 +1,61 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useCallback } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { ExportMarkdownSidebarEntry } from './export-markdown-sidebar-entry'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import { SidebarMenu } from '../sidebar-menu/sidebar-menu'
import type { SpecificSidebarMenuProps } from '../types'
import { DocumentSidebarMenuSelection } from '../types'
import { cypressId } from '../../../../utils/cypress-attribute'
/**
* Renders the export menu for the sidebar.
*
* @param className Additional class names given to the menu button
* @param menuId The id of the menu
* @param onClick The callback, that should be called when the menu button is pressed
* @param selectedMenuId The currently selected menu id
*/
export const ExportMenuSidebarMenu: React.FC<SpecificSidebarMenuProps> = ({
className,
menuId,
onClick,
selectedMenuId
}) => {
useTranslation()
const hide = selectedMenuId !== DocumentSidebarMenuSelection.NONE && selectedMenuId !== menuId
const expand = selectedMenuId === menuId
const onClickHandler = useCallback(() => {
onClick(menuId)
}, [menuId, onClick])
return (
<Fragment>
<SidebarButton
{...cypressId('menu-export')}
hide={hide}
icon={expand ? 'arrow-left' : 'cloud-download'}
className={className}
onClick={onClickHandler}>
<Trans i18nKey={'editor.documentBar.export'} />
</SidebarButton>
<SidebarMenu expand={expand}>
<SidebarButton icon={'github'}>Gist</SidebarButton>
<SidebarButton icon={'gitlab'}>Gitlab Snippet</SidebarButton>
<ExportMarkdownSidebarEntry />
<SidebarButton icon={'file-code-o'}>HTML</SidebarButton>
<SidebarButton icon={'file-code-o'}>
<Trans i18nKey='editor.export.rawHtml' />
</SidebarButton>
</SidebarMenu>
</Fragment>
)
}

View file

@ -0,0 +1,67 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useCallback, useRef } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import { UploadInput } from '../upload-input'
import { cypressId } from '../../../../utils/cypress-attribute'
import { useChangeEditorContentCallback } from '../../change-content-context/use-change-editor-content-callback'
import { ShowIf } from '../../../common/show-if/show-if'
import { FileContentFormat, readFile } from '../../../../utils/read-file'
/**
* Renders a sidebar entry that allows to import the content of markdown files into the currently opened note.
*/
export const ImportMarkdownSidebarEntry: React.FC = () => {
useTranslation()
const changeEditorContent = useChangeEditorContentCallback()
const onImportMarkdown = useCallback(
async (file: File): Promise<void> => {
const content = await readFile(file, FileContentFormat.TEXT)
changeEditorContent?.(({ markdownContent }) => {
const newContent = (markdownContent.length === 0 ? '' : '\n') + content
return [
[
{
from: markdownContent.length,
to: markdownContent.length,
insert: newContent
}
],
undefined
]
})
},
[changeEditorContent]
)
const clickRef = useRef<() => void>()
const buttonClick = useCallback(() => {
clickRef.current?.()
}, [])
return (
<Fragment>
<SidebarButton
{...cypressId('menu-import-markdown-button')}
icon={'file-text-o'}
onClick={buttonClick}
disabled={!changeEditorContent}>
<Trans i18nKey={'editor.import.file'} />
</SidebarButton>
<ShowIf condition={!!changeEditorContent}>
<UploadInput
onLoad={onImportMarkdown}
{...cypressId('menu-import-markdown-input')}
allowedFileTypes={'.md, text/markdown, text/plain'}
onClickRef={clickRef}
/>
</ShowIf>
</Fragment>
)
}

View file

@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useCallback } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { ImportMarkdownSidebarEntry } from './import-markdown-sidebar-entry'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import { SidebarMenu } from '../sidebar-menu/sidebar-menu'
import type { SpecificSidebarMenuProps } from '../types'
import { DocumentSidebarMenuSelection } from '../types'
import { cypressId } from '../../../../utils/cypress-attribute'
/**
* Renders the import menu for the sidebar.
*
* @param className Additional class names given to the menu button
* @param menuId The id of the menu
* @param onClick The callback, that should be called when the menu button is pressed
* @param selectedMenuId The currently selected menu id
*/
export const ImportMenuSidebarMenu: React.FC<SpecificSidebarMenuProps> = ({
className,
menuId,
onClick,
selectedMenuId
}) => {
useTranslation()
const hide = selectedMenuId !== DocumentSidebarMenuSelection.NONE && selectedMenuId !== menuId
const expand = selectedMenuId === menuId
const onClickHandler = useCallback(() => {
onClick(menuId)
}, [menuId, onClick])
return (
<Fragment>
<SidebarButton
{...cypressId('menu-import')}
hide={hide}
icon={expand ? 'arrow-left' : 'cloud-upload'}
className={className}
onClick={onClickHandler}>
<Trans i18nKey={'editor.documentBar.import'} />
</SidebarButton>
<SidebarMenu expand={expand}>
<SidebarButton icon={'github'}>Gist</SidebarButton>
<SidebarButton icon={'gitlab'}>Gitlab Snippet</SidebarButton>
<SidebarButton icon={'clipboard'}>
<Trans i18nKey={'editor.import.clipboard'} />
</SidebarButton>
<ImportMarkdownSidebarEntry />
</SidebarMenu>
</Fragment>
)
}

View file

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { NoteInfoModal } from '../../document-bar/note-info/note-info-modal'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types'
import { cypressId } from '../../../../utils/cypress-attribute'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
/**
* Sidebar entry that allows to open the {@link NoteInfoModal} containing information about the current note.
*
* @param className CSS classes to add to the sidebar button
* @param hide true when the sidebar button should be hidden, false otherwise
*/
export const NoteInfoSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const [modalVisibility, showModal, closeModal] = useBooleanState()
useTranslation()
return (
<Fragment>
<SidebarButton
hide={hide}
className={className}
icon={'line-chart'}
onClick={showModal}
{...cypressId('sidebar-btn-document-info')}>
<Trans i18nKey={'editor.modal.documentInfo.title'} />
</SidebarButton>
<NoteInfoModal show={modalVisibility} onHide={closeModal} />
</Fragment>
)
}

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { PermissionModal } from '../../document-bar/permissions/permission-modal'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
/**
* Renders a button to open the permission modal for the sidebar.
*
* @param className Additional classes directly given to the button
* @param hide If the button should be hidden
*/
export const PermissionsSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const [modalVisibility, showModal, closeModal] = useBooleanState()
useTranslation()
return (
<Fragment>
<SidebarButton hide={hide} className={className} icon={'lock'} onClick={showModal}>
<Trans i18nKey={'editor.modal.permissions.title'} />
</SidebarButton>
<PermissionModal show={modalVisibility} onHide={closeModal} />
</Fragment>
)
}

View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.highlighted {
color: #b51f08 !important;
}

View file

@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types'
import { toggleHistoryEntryPinning } from '../../../../redux/history/methods'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
import styles from './pin-note-sidebar-entry.module.css'
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
/**
* Sidebar entry button that toggles the pinned status of the current note in the history.
*
* @param className CSS classes to add to the sidebar button
* @param hide true when the sidebar button should be hidden, false otherwise
*/
export const PinNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
useTranslation()
const id = useApplicationState((state) => state.noteDetails.id)
const history = useApplicationState((state) => state.history)
const { showErrorNotification } = useUiNotifications()
const isPinned = useMemo(() => {
const entry = history.find((entry) => entry.identifier === id)
if (!entry) {
return false
}
return entry.pinStatus
}, [id, history])
const onPinClicked = useCallback(() => {
toggleHistoryEntryPinning(id).catch(showErrorNotification('landing.history.error.updateEntry.text'))
}, [id, showErrorNotification])
return (
<SidebarButton
icon={'thumb-tack'}
hide={hide}
onClick={onPinClicked}
className={`${className ?? ''} ${isPinned ? styles['highlighted'] : ''}`}>
<Trans i18nKey={isPinned ? 'editor.documentBar.pinnedToHistory' : 'editor.documentBar.pinNoteToHistory'} />
</SidebarButton>
)
}

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment } from 'react'
import { Trans } from 'react-i18next'
import { RevisionModal } from '../../document-bar/revisions/revision-modal'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
/**
* Renders a button to open the revision modal for the sidebar.
*
* @param className Additional classes directly given to the button
* @param hide If the button should be hidden
*/
export const RevisionSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const [modalVisibility, showModal, closeModal] = useBooleanState()
return (
<Fragment>
<SidebarButton hide={hide} className={className} icon={'history'} onClick={showModal}>
<Trans i18nKey={'editor.modal.revision.title'} />
</SidebarButton>
<RevisionModal show={modalVisibility} onHide={closeModal} />
</Fragment>
)
}

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { ShareModal } from '../../document-bar/share/share-modal'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
/**
* Renders a button to open the share modal for the sidebar.
*
* @param className Additional classes directly given to the button
* @param hide If the button should be hidden
*/
export const ShareSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const [modalVisibility, showModal, closeModal] = useBooleanState()
useTranslation()
return (
<Fragment>
<SidebarButton hide={hide} className={className} icon={'share'} onClick={showModal}>
<Trans i18nKey={'editor.modal.shareLink.title'} />
</SidebarButton>
<ShareModal show={modalVisibility} onHide={closeModal} />
</Fragment>
)
}

View file

@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.slide-sidebar {
@import "./variables.scss";
flex: 0 0 $height;
position: relative;
.sidebar-inner {
height: 100%;
display: flex;
overflow-y: auto;
flex-direction: column;
position: absolute;
z-index: 999;
width: $menu-width;
top: 0;
left: 0;
transition: left 0.3s;
box-shadow: 0 0 0 rgba(0, 0, 0, 0.15);
&:hover, &.show {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
left: (-$menu-width + $height);
}
background: var(--bs-light);
}
}

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
$height: 40px;
$menu-width: 175px;

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { RefObject } from 'react'
import type { IconName } from '../../common/fork-awesome/types'
import type { PropsWithDataCypressId } from '../../../utils/cypress-attribute'
export interface SpecificSidebarEntryProps {
className?: string
hide?: boolean
onClick?: () => void
}
export interface SidebarEntryProps extends PropsWithDataCypressId {
icon?: IconName
buttonRef?: RefObject<HTMLButtonElement>
hide?: boolean
className?: string
onClick?: () => void
disabled?: boolean
}
export interface SidebarMenuProps {
expand?: boolean
}
export enum DocumentSidebarMenuSelection {
NONE,
USERS_ONLINE,
IMPORT,
EXPORT
}
export interface SpecificSidebarMenuProps {
className?: string
onClick: (menuId: DocumentSidebarMenuSelection) => void
selectedMenuId: DocumentSidebarMenuSelection
menuId: DocumentSidebarMenuSelection
}

View file

@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { MutableRefObject } from 'react'
import React, { useCallback, useEffect, useRef } from 'react'
import { Logger } from '../../../utils/logger'
import type { PropsWithDataCypressId } from '../../../utils/cypress-attribute'
import { cypressId } from '../../../utils/cypress-attribute'
const log = new Logger('UploadInput')
export interface UploadInputProps extends PropsWithDataCypressId {
onLoad: (file: File) => Promise<void> | void
allowedFileTypes: string
onClickRef: MutableRefObject<(() => void) | undefined>
}
/**
* Renders an input field to upload something.
*
* @param onLoad The callback to load the file.
* @param allowedFileTypes A string indicating mime-types or file extensions.
* @param onClickRef A reference for the onClick handler of the input.
* @param props Additional props given to the input.
*/
export const UploadInput: React.FC<UploadInputProps> = ({ onLoad, allowedFileTypes, onClickRef, ...props }) => {
const fileInputReference = useRef<HTMLInputElement>(null)
const onClick = useCallback(() => {
fileInputReference.current?.click()
}, [])
const onChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
async (event) => {
const fileInput = event.currentTarget
if (!fileInput.files || fileInput.files.length < 1) {
return
}
const file = fileInput.files[0]
try {
const loadResult = onLoad(file)
if (loadResult instanceof Promise) {
await loadResult
}
} catch (error) {
log.error('Error while uploading file', error)
} finally {
fileInput.value = ''
}
},
[onLoad]
)
useEffect(() => {
onClickRef.current = onClick
})
return (
<input
{...cypressId(props)}
onChange={onChange}
type='file'
ref={fileInputReference}
className='d-none'
accept={allowedFileTypes}
/>
)
}

View file

@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.user-line-color-indicator {
border-left: 3px solid;
min-height: 30px;
height: 100%;
flex: 0 0 3px;
}
.user-avatar {
flex: 0 0 20px;
}
.user-line-name {
text-overflow: ellipsis;
flex: 1 1 0;
overflow: hidden;
}
.active-indicator-container {
height: 100%;
display: flex;
flex: 0 0 20px;
align-items: center;
justify-content: center;
}

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { ActiveIndicator } from '../users-online-sidebar-menu/active-indicator'
import styles from './user-line.module.scss'
import { UserAvatarForUsername } from '../../../common/user-avatar/user-avatar-for-username'
import type { ActiveIndicatorStatus } from '../../../../redux/realtime/types'
export interface UserLineProps {
username: string | null
color: string
status: ActiveIndicatorStatus
}
/**
* Represents a user in the realtime activity status.
*
* @param username The name of the user to show.
* @param color The color of the user's edits.
* @param status The user's current online status.
*/
export const UserLine: React.FC<UserLineProps> = ({ username, color, status }) => {
return (
<div className={'d-flex align-items-center h-100 w-100'}>
<div
className={`d-inline-flex align-items-bottom ${styles['user-line-color-indicator']}`}
style={{ borderLeftColor: color }}
/>
<UserAvatarForUsername
username={username}
additionalClasses={'flex-fill overflow-hidden px-2 text-nowrap w-100'}
/>
<div className={styles['active-indicator-container']}>
<ActiveIndicator status={status} />
</div>
</div>
)
}

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.activeIndicator {
$indicator-size: 12px;
border-radius: $indicator-size;
height: $indicator-size;
width: $indicator-size;
&.active {
background-color: #5cb85c;
}
&.inactive {
background-color: #d20000;
}
}

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import styles from './active-indicator.module.scss'
import type { ActiveIndicatorStatus } from '../../../../redux/realtime/types'
export interface ActiveIndicatorProps {
status: ActiveIndicatorStatus
}
/**
* Renders an indicator corresponding to the given status.
*
* @param status The state of the indicator to render
*/
export const ActiveIndicator: React.FC<ActiveIndicatorProps> = ({ status }) => {
return <span className={`${styles['activeIndicator']} ${status}`} />
}

View file

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.online-entry {
&:hover {
:global(.sidebar-button-icon):after {
color: inherit;
}
}
--users-online: '0';
:global(.sidebar-button-icon):after {
content: var(--users-online);
position: absolute;
right: 5px;
bottom: 3px;
font-size: 0.9rem;
color: var(--bg-primary);
line-height: 1;
}
}

View file

@ -0,0 +1,79 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useCallback, useEffect, useMemo, useRef } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import { SidebarMenu } from '../sidebar-menu/sidebar-menu'
import type { SpecificSidebarMenuProps } from '../types'
import { DocumentSidebarMenuSelection } from '../types'
import styles from './online-counter.module.scss'
import { UserLine } from '../user-line/user-line'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
/**
* Sidebar menu that contains the list of currently online users in the current note session.
* When the menu is collapsed, the amount of currently online users is shown, otherwise the full list.
*
* @param className CSS classes to add to the sidebar menu
* @param menuId The id of this sidebar menu
* @param onClick Callback that is fired when the menu is clicked
* @param selectedMenuId The currently opened sidebar menu
*/
export const UsersOnlineSidebarMenu: React.FC<SpecificSidebarMenuProps> = ({
className,
menuId,
onClick,
selectedMenuId
}) => {
const buttonRef = useRef<HTMLButtonElement>(null)
const onlineUsers = useApplicationState((state) => state.realtime.users)
useTranslation()
useEffect(() => {
const value = `${Object.keys(onlineUsers).length}`
buttonRef.current?.style.setProperty('--users-online', `"${value}"`)
}, [onlineUsers])
const hide = selectedMenuId !== DocumentSidebarMenuSelection.NONE && selectedMenuId !== menuId
const expand = selectedMenuId === menuId
const onClickHandler = useCallback(() => onClick(menuId), [menuId, onClick])
const onlineUserElements = useMemo(() => {
const entries = Object.entries(onlineUsers)
if (entries.length === 0) {
return (
<SidebarButton>
<span className={'ms-3'}>
<Trans i18nKey={'editor.onlineStatus.noUsers'}></Trans>
</span>
</SidebarButton>
)
} else {
return entries.map(([clientId, onlineUser]) => {
return (
<SidebarButton key={clientId}>
<UserLine username={onlineUser.username} color={onlineUser.color} status={onlineUser.active} />
</SidebarButton>
)
})
}
}, [onlineUsers])
return (
<Fragment>
<SidebarButton
hide={hide}
buttonRef={buttonRef}
onClick={onClickHandler}
icon={expand ? 'arrow-left' : 'users'}
className={`${styles['online-entry']} ${className ?? ''}`}>
<Trans i18nKey={'editor.onlineStatus.online'} />
</SidebarButton>
<SidebarMenu expand={expand}>{onlineUserElements}</SidebarMenu>
</Fragment>
)
}