mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-06-08 10:22:47 -04:00
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:
parent
4e18ce38f3
commit
762a0a850e
1051 changed files with 0 additions and 35 deletions
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
71
frontend/src/components/editor-page/sidebar/sidebar.tsx
Normal file
71
frontend/src/components/editor-page/sidebar/sidebar.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.highlighted {
|
||||
color: #b51f08 !important;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
42
frontend/src/components/editor-page/sidebar/types.ts
Normal file
42
frontend/src/components/editor-page/sidebar/types.ts
Normal 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
|
||||
}
|
70
frontend/src/components/editor-page/sidebar/upload-input.tsx
Normal file
70
frontend/src/components/editor-page/sidebar/upload-input.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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}`} />
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue