mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 09:16:30 -05:00
enhancement(sidebar): move note info modal into sidebar
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
e3a9f70965
commit
b454e3be03
23 changed files with 258 additions and 327 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
|
||||
*/
|
||||
|
@ -11,32 +11,32 @@ describe('Test word count with', () => {
|
|||
|
||||
it('empty note', () => {
|
||||
cy.setCodemirrorContent('')
|
||||
cy.getByCypressId('sidebar-btn-document-info').click()
|
||||
cy.getByCypressId('document-info-modal').should('be.visible')
|
||||
cy.getByCypressId('sidebar-menu-info').click()
|
||||
cy.getByCypressId('document-info-word-count').should('be.visible')
|
||||
cy.getByCypressId('document-info-word-count').contains('0')
|
||||
})
|
||||
|
||||
it('simple words', () => {
|
||||
cy.setCodemirrorContent('five words should be enough')
|
||||
cy.getMarkdownBody().contains('five words should be enough')
|
||||
cy.getByCypressId('sidebar-btn-document-info').click()
|
||||
cy.getByCypressId('document-info-modal').should('be.visible')
|
||||
cy.getByCypressId('sidebar-menu-info').click()
|
||||
cy.getByCypressId('document-info-word-count').should('be.visible')
|
||||
cy.getByCypressId('document-info-word-count').contains('5')
|
||||
})
|
||||
|
||||
it('excluded codeblocks', () => {
|
||||
cy.setCodemirrorContent('```\nthis is should be ignored\n```\n\ntwo `words`')
|
||||
cy.getMarkdownBody().contains('two words')
|
||||
cy.getByCypressId('sidebar-btn-document-info').click()
|
||||
cy.getByCypressId('document-info-modal').should('be.visible')
|
||||
cy.getByCypressId('sidebar-menu-info').click()
|
||||
cy.getByCypressId('document-info-word-count').should('be.visible')
|
||||
cy.getByCypressId('document-info-word-count').contains('2')
|
||||
})
|
||||
|
||||
it('excluded images', () => {
|
||||
cy.setCodemirrorContent('![ignored alt text](https://dummyimage.com/48) not ignored text')
|
||||
cy.getMarkdownBody().contains('not ignored text')
|
||||
cy.getByCypressId('sidebar-btn-document-info').click()
|
||||
cy.getByCypressId('document-info-modal').should('be.visible')
|
||||
cy.getByCypressId('sidebar-menu-info').click()
|
||||
cy.getByCypressId('document-info-word-count').should('be.visible')
|
||||
cy.getByCypressId('document-info-word-count').contains('3')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -383,20 +383,20 @@
|
|||
"tagTime": "Time tag",
|
||||
"spoiler": "Spoiler"
|
||||
},
|
||||
"noteInfo": {
|
||||
"title": "Note info",
|
||||
"created": "Note created",
|
||||
"lastUpdated": "Last updated",
|
||||
"lastUpdatedBy": "Last updated by",
|
||||
"contributors": "Count of contributors",
|
||||
"wordCount": "Count of words"
|
||||
},
|
||||
"modal": {
|
||||
"snippetImport": {
|
||||
"title": "Import from Snippet",
|
||||
"selectProject": "Select From Available Projects",
|
||||
"selectSnippet": "Select From Available Snippets"
|
||||
},
|
||||
"documentInfo": {
|
||||
"title": "Document info",
|
||||
"created": "This note was created <0></0>",
|
||||
"edited": "<0></0> was the last editor <1></1>",
|
||||
"usersContributed": "<0></0> users contributed to this document",
|
||||
"revisions": "<0></0> revisions are saved",
|
||||
"words": "<0></0> words in document"
|
||||
},
|
||||
"gistImport": {
|
||||
"title": "Import from Gist",
|
||||
"insertGistUrl": "Paste your gist url here…"
|
||||
|
@ -645,7 +645,7 @@
|
|||
},
|
||||
"cheatsheet": {
|
||||
"button": "Open Cheatsheet",
|
||||
"modal":{
|
||||
"modal": {
|
||||
"popup": "Open in new tab",
|
||||
"title": "Cheatsheet",
|
||||
"headlines": {
|
||||
|
@ -778,11 +778,11 @@
|
|||
"example": "I :heart: :hedgehog:"
|
||||
},
|
||||
"csv": {
|
||||
"title" : "CSV",
|
||||
"table" : {
|
||||
"title": "CSV",
|
||||
"table": {
|
||||
"title": "Table",
|
||||
"description" : "You can render a CSV text as table by using a code block with `csv` as language. You must specify the delimiter.",
|
||||
"example" : "```csv delimiter=;\nUsername; Identifier;First name;Last name\n\"booker12; rbooker\";9012;Rachel;Booker\ngrey07;2070;Laura;Grey\njohnson81;4081;Craig;Johnson\njenkins46;9346;Mary;Jenkins\nsmith79;5079;Jamie;Smith\n```"
|
||||
"description": "You can render a CSV text as table by using a code block with `csv` as language. You must specify the delimiter.",
|
||||
"example": "```csv delimiter=;\nUsername; Identifier;First name;Last name\n\"booker12; rbooker\";9012;Rachel;Booker\ngrey07;2070;Laura;Grey\njohnson81;4081;Craig;Johnson\njenkins46;9346;Mary;Jenkins\nsmith79;5079;Jamie;Smith\n```"
|
||||
},
|
||||
"header": {
|
||||
"title": "Header",
|
||||
|
|
|
@ -17,7 +17,7 @@ export interface TimeFromNowProps {
|
|||
*/
|
||||
export const TimeFromNow: React.FC<TimeFromNowProps> = ({ time }) => {
|
||||
return (
|
||||
<time className={'mx-1'} title={time.toFormat('DDDD T')} dateTime={time.toString()}>
|
||||
<time title={time.toFormat('DDDD T')} dateTime={time.toString()}>
|
||||
{time.toRelative()}
|
||||
</time>
|
||||
)
|
|
@ -6,8 +6,8 @@
|
|||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||
import { InternalLink } from '../common/links/internal-link'
|
||||
import { ShowIf } from '../common/show-if/show-if'
|
||||
import { NoteInfoLineCreated } from '../editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-entry/note-info-modal/note-info-line-created'
|
||||
import { NoteInfoLineUpdated } from '../editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-entry/note-info-modal/note-info-line-updated'
|
||||
import { NoteInfoLineCreatedAt } from '../editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-created-at'
|
||||
import { NoteInfoLineUpdatedBy } from '../editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-updated-by'
|
||||
import styles from './document-infobar.module.scss'
|
||||
import React from 'react'
|
||||
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
||||
|
@ -26,8 +26,8 @@ export const DocumentInfobar: React.FC = () => {
|
|||
<div className={'col-md'}> </div>
|
||||
<div className={'d-flex flex-fill'}>
|
||||
<div className={'d-flex flex-column'}>
|
||||
<NoteInfoLineCreated />
|
||||
<NoteInfoLineUpdated />
|
||||
<NoteInfoLineCreatedAt />
|
||||
<NoteInfoLineUpdatedBy />
|
||||
<hr />
|
||||
</div>
|
||||
<span className={'ms-auto'}>
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
.entry {
|
||||
border: solid 1px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.title {
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75em;
|
||||
color: var(--bs-secondary);
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { UiIcon } from '../../../common/icons/ui-icon'
|
||||
import { ShowIf } from '../../../common/show-if/show-if'
|
||||
import styles from './sidebar-menu-info-entry.module.css'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import React from 'react'
|
||||
import type { Icon } from 'react-bootstrap-icons'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
export interface SidebarMenuInfoEntryProps {
|
||||
titleI18nKey: string
|
||||
icon?: Icon
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an info entry for a sidebar menu.
|
||||
*
|
||||
* @param children The content of the entry
|
||||
* @param titleI18nKey The i18n key for the title
|
||||
* @param icon An optional icon as prefix
|
||||
*/
|
||||
export const SidebarMenuInfoEntry: React.FC<PropsWithChildren<SidebarMenuInfoEntryProps>> = ({
|
||||
children,
|
||||
titleI18nKey,
|
||||
icon
|
||||
}) => {
|
||||
useTranslation()
|
||||
|
||||
return (
|
||||
<div className={`d-flex flex-row align-items-center p-1 ${styles['entry']}`}>
|
||||
<ShowIf condition={icon !== undefined}>
|
||||
<UiIcon icon={icon} className={'mx-2'} size={1.25} />
|
||||
</ShowIf>
|
||||
<div className={'d-flex flex-column px-1'}>
|
||||
<span className={styles['title']}>
|
||||
<Trans i18nKey={titleI18nKey} />
|
||||
</span>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -7,7 +7,7 @@ import { AliasesSidebarEntry } from './specific-sidebar-entries/aliases-sidebar-
|
|||
import { DeleteNoteSidebarEntry } from './specific-sidebar-entries/delete-note-sidebar-entry/delete-note-sidebar-entry'
|
||||
import { ExportMenuSidebarMenu } from './specific-sidebar-entries/export-menu-sidebar-menu'
|
||||
import { ImportMenuSidebarMenu } from './specific-sidebar-entries/import-menu-sidebar-menu'
|
||||
import { NoteInfoSidebarEntry } from './specific-sidebar-entries/note-info-sidebar-entry/note-info-sidebar-entry'
|
||||
import { NoteInfoSidebarMenu } from './specific-sidebar-entries/note-info-sidebar-menu/note-info-sidebar-menu'
|
||||
import { PermissionsSidebarEntry } from './specific-sidebar-entries/permissions-sidebar-entry/permissions-sidebar-entry'
|
||||
import { PinNoteSidebarEntry } from './specific-sidebar-entries/pin-note-sidebar-entry/pin-note-sidebar-entry'
|
||||
import { RevisionSidebarEntry } from './specific-sidebar-entries/revisions-sidebar-entry/revision-sidebar-entry'
|
||||
|
@ -47,7 +47,11 @@ export const Sidebar: React.FC = () => {
|
|||
selectedMenuId={selectedMenu}
|
||||
onClick={toggleValue}
|
||||
/>
|
||||
<NoteInfoSidebarEntry hide={selectionIsNotNone} />
|
||||
<NoteInfoSidebarMenu
|
||||
menuId={DocumentSidebarMenuSelection.NOTE_INFO}
|
||||
selectedMenuId={selectedMenu}
|
||||
onClick={toggleValue}
|
||||
/>
|
||||
<RevisionSidebarEntry hide={selectionIsNotNone} />
|
||||
<PermissionsSidebarEntry hide={selectionIsNotNone} />
|
||||
<AliasesSidebarEntry hide={selectionIsNotNone} />
|
||||
|
|
|
@ -1,31 +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 { NoteInfoLine } from './note-info-line'
|
||||
import type { NoteInfoTimeLineProps } from './note-info-time-line'
|
||||
import { UnitalicBoldTimeFromNow } from './utils/unitalic-bold-time-from-now'
|
||||
import { DateTime } from 'luxon'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Plus as IconPlus } from 'react-bootstrap-icons'
|
||||
import { Trans } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Renders an info line about the creation of the current note.
|
||||
*
|
||||
* @param size The size in which the line should be displayed.
|
||||
*/
|
||||
export const NoteInfoLineCreated: React.FC<NoteInfoTimeLineProps> = ({ size }) => {
|
||||
const noteCreateTime = useApplicationState((state) => state.noteDetails.createdAt)
|
||||
const noteCreateDateTime = useMemo(() => DateTime.fromSeconds(noteCreateTime), [noteCreateTime])
|
||||
|
||||
return (
|
||||
<NoteInfoLine icon={IconPlus} size={size}>
|
||||
<Trans i18nKey={'editor.modal.documentInfo.created'}>
|
||||
<UnitalicBoldTimeFromNow time={noteCreateDateTime} />
|
||||
</Trans>
|
||||
</NoteInfoLine>
|
||||
)
|
||||
}
|
|
@ -1,49 +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 { UserAvatarForUsername } from '../../../../../common/user-avatar/user-avatar-for-username'
|
||||
import { NoteInfoLine } from './note-info-line'
|
||||
import type { NoteInfoTimeLineProps } from './note-info-time-line'
|
||||
import { UnitalicBoldTimeFromNow } from './utils/unitalic-bold-time-from-now'
|
||||
import { UnitalicBoldTrans } from './utils/unitalic-bold-trans'
|
||||
import { DateTime } from 'luxon'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Renders an info line about the last update of the current note.
|
||||
*
|
||||
* @param size The size in which line and user avatar should be displayed.
|
||||
*/
|
||||
export const NoteInfoLineUpdated: React.FC<NoteInfoTimeLineProps> = ({ size }) => {
|
||||
useTranslation()
|
||||
const noteUpdateTime = useApplicationState((state) => state.noteDetails.updatedAt)
|
||||
const noteUpdateDateTime = useMemo(() => DateTime.fromSeconds(noteUpdateTime), [noteUpdateTime])
|
||||
const noteUpdateUser = useApplicationState((state) => state.noteDetails.updateUsername)
|
||||
|
||||
const userBlock = useMemo(() => {
|
||||
if (!noteUpdateUser) {
|
||||
return <UnitalicBoldTrans i18nKey={'common.guestUser'} />
|
||||
}
|
||||
return (
|
||||
<UserAvatarForUsername
|
||||
username={noteUpdateUser}
|
||||
additionalClasses={'font-style-normal bold font-weight-bold'}
|
||||
size={size ? 'lg' : undefined}
|
||||
/>
|
||||
)
|
||||
}, [noteUpdateUser, size])
|
||||
|
||||
return (
|
||||
<NoteInfoLine icon={IconPencil} size={size}>
|
||||
<Trans i18nKey={'editor.modal.documentInfo.edited'}>
|
||||
{userBlock}
|
||||
<UnitalicBoldTimeFromNow time={noteUpdateDateTime} />
|
||||
</Trans>
|
||||
</NoteInfoLine>
|
||||
)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import React from 'react'
|
||||
import type { Icon } from 'react-bootstrap-icons'
|
||||
|
||||
export interface NoteInfoLineProps {
|
||||
icon: Icon
|
||||
size?: 2 | 3 | 4 | 5 | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the base component for all note info lines.
|
||||
* It renders an icon and some children in italic.
|
||||
*
|
||||
* @param icon The icon be shown
|
||||
* @param size Which size the icon should be
|
||||
* @param children The children to render
|
||||
*/
|
||||
export const NoteInfoLine: React.FC<PropsWithChildren<NoteInfoLineProps>> = ({ icon, size, children }) => {
|
||||
return (
|
||||
<span className={'d-flex align-items-center'}>
|
||||
<UiIcon icon={icon} size={size} className={'mx-2'} />
|
||||
<i className={'d-flex align-items-center'}>{children}</i>
|
||||
</span>
|
||||
)
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { cypressId } from '../../../../../../utils/cypress-attribute'
|
||||
import type { ModalVisibilityProps } from '../../../../../common/modals/common-modal'
|
||||
import { CommonModal } from '../../../../../common/modals/common-modal'
|
||||
import { NoteInfoLineContributors } from './note-info-line-contributors'
|
||||
import { NoteInfoLineCreated } from './note-info-line-created'
|
||||
import { NoteInfoLineUpdated } from './note-info-line-updated'
|
||||
import { NoteInfoLineWordCount } from './note-info-line-word-count'
|
||||
import React from 'react'
|
||||
import { ListGroup, Modal } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Modal that shows informational data about the current note.
|
||||
*
|
||||
* @param show true when the modal should be visible, false otherwise.
|
||||
* @param onHide Callback that is fired when the modal is going to be closed.
|
||||
*/
|
||||
export const NoteInfoModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
|
||||
useTranslation()
|
||||
|
||||
return (
|
||||
<CommonModal
|
||||
show={show}
|
||||
onHide={onHide}
|
||||
showCloseButton={true}
|
||||
titleI18nKey={'editor.modal.documentInfo.title'}
|
||||
{...cypressId('document-info-modal')}>
|
||||
<Modal.Body>
|
||||
<ListGroup>
|
||||
<ListGroup.Item>
|
||||
<NoteInfoLineCreated size={2} />
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item>
|
||||
<NoteInfoLineUpdated size={2} />
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item>
|
||||
<NoteInfoLineContributors />
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item>
|
||||
<NoteInfoLineWordCount />
|
||||
</ListGroup.Item>
|
||||
</ListGroup>
|
||||
</Modal.Body>
|
||||
</CommonModal>
|
||||
)
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
export interface NoteInfoTimeLineProps {
|
||||
size?: 2 | 3 | 4 | 5
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { PropsWithDataCypressId } from '../../../../../../utils/cypress-attribute'
|
||||
import { cypressId } from '../../../../../../utils/cypress-attribute'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
export interface UnitalicBoldContentProps extends PropsWithDataCypressId {
|
||||
text?: string | number
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the children elements in a non-italic but bold style.
|
||||
*
|
||||
* @param text Optional text content that should be rendered.
|
||||
* @param children Children that may be rendered.
|
||||
* @param props Additional props for cypressId
|
||||
*/
|
||||
export const UnitalicBoldContent: React.FC<PropsWithChildren<UnitalicBoldContentProps>> = ({
|
||||
text,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<strong className={'font-style-normal me-1'} {...cypressId(props)}>
|
||||
{text}
|
||||
{children}
|
||||
</strong>
|
||||
)
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { TimeFromNowProps } from '../time-from-now'
|
||||
import { TimeFromNow } from '../time-from-now'
|
||||
import { UnitalicBoldContent } from '../unitalic-bold-content'
|
||||
import React from 'react'
|
||||
|
||||
export const UnitalicBoldTimeFromNow: React.FC<TimeFromNowProps> = ({ time }) => {
|
||||
return (
|
||||
<UnitalicBoldContent>
|
||||
<TimeFromNow time={time} />
|
||||
</UnitalicBoldContent>
|
||||
)
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { UnitalicBoldContent } from '../unitalic-bold-content'
|
||||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
|
||||
export interface UnitalicBoldTransProps {
|
||||
i18nKey?: string
|
||||
}
|
||||
|
||||
export const UnitalicBoldTrans: React.FC<UnitalicBoldTransProps> = ({ i18nKey }) => {
|
||||
return (
|
||||
<UnitalicBoldContent>
|
||||
<Trans i18nKey={i18nKey} />
|
||||
</UnitalicBoldContent>
|
||||
)
|
||||
}
|
|
@ -1,38 +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 { SidebarButton } from '../../sidebar-button/sidebar-button'
|
||||
import type { SpecificSidebarEntryProps } from '../../types'
|
||||
import { NoteInfoModal } from './note-info-modal/note-info-modal'
|
||||
import React, { Fragment } from 'react'
|
||||
import { GraphUp as IconGraphUp } from 'react-bootstrap-icons'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* 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={IconGraphUp}
|
||||
onClick={showModal}
|
||||
{...cypressId('sidebar-btn-document-info')}>
|
||||
<Trans i18nKey={'editor.modal.documentInfo.title'} />
|
||||
</SidebarButton>
|
||||
<NoteInfoModal show={modalVisibility} onHide={closeModal} />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -4,11 +4,9 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
||||
import { NoteInfoLine } from './note-info-line'
|
||||
import { UnitalicBoldContent } from './unitalic-bold-content'
|
||||
import { SidebarMenuInfoEntry } from '../../../sidebar-menu-info-entry/sidebar-menu-info-entry'
|
||||
import React from 'react'
|
||||
import { People as IconPeople } from 'react-bootstrap-icons'
|
||||
import { Trans } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Renders an info line about the number of contributors for the note.
|
||||
|
@ -17,10 +15,8 @@ export const NoteInfoLineContributors: React.FC = () => {
|
|||
const contributors = useApplicationState((state) => state.noteDetails.editedBy.length)
|
||||
|
||||
return (
|
||||
<NoteInfoLine icon={IconPeople} size={2}>
|
||||
<Trans i18nKey={'editor.modal.documentInfo.usersContributed'}>
|
||||
<UnitalicBoldContent text={contributors} />
|
||||
</Trans>
|
||||
</NoteInfoLine>
|
||||
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.contributors'} icon={IconPeople}>
|
||||
{contributors}
|
||||
</SidebarMenuInfoEntry>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { TimeFromNow } from '../../../../../common/time-from-now'
|
||||
import { SidebarMenuInfoEntry } from '../../../sidebar-menu-info-entry/sidebar-menu-info-entry'
|
||||
import { DateTime } from 'luxon'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Plus as IconPlus } from 'react-bootstrap-icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Renders an info line about the creation time of the current note.
|
||||
*/
|
||||
export const NoteInfoLineCreatedAt: React.FC = () => {
|
||||
useTranslation()
|
||||
const noteCreateTime = useApplicationState((state) => state.noteDetails.createdAt)
|
||||
const noteCreateDateTime = useMemo(() => DateTime.fromSeconds(noteCreateTime), [noteCreateTime])
|
||||
|
||||
return (
|
||||
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.created'} icon={IconPlus}>
|
||||
<TimeFromNow time={noteCreateDateTime} />
|
||||
</SidebarMenuInfoEntry>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { TimeFromNow } from '../../../../../common/time-from-now'
|
||||
import { SidebarMenuInfoEntry } from '../../../sidebar-menu-info-entry/sidebar-menu-info-entry'
|
||||
import { DateTime } from 'luxon'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Renders an info line about the time that the note content was last updated.
|
||||
*/
|
||||
export const NoteInfoLineUpdatedAt: React.FC = () => {
|
||||
useTranslation()
|
||||
const noteUpdateTime = useApplicationState((state) => state.noteDetails.updatedAt)
|
||||
const noteUpdateDateTime = useMemo(() => DateTime.fromSeconds(noteUpdateTime), [noteUpdateTime])
|
||||
|
||||
return (
|
||||
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.lastUpdated'} icon={IconPencil}>
|
||||
<TimeFromNow time={noteUpdateDateTime} />
|
||||
</SidebarMenuInfoEntry>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { UserAvatarForUsername } from '../../../../../common/user-avatar/user-avatar-for-username'
|
||||
import { SidebarMenuInfoEntry } from '../../../sidebar-menu-info-entry/sidebar-menu-info-entry'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Renders an info line about the user that last updated the note content.
|
||||
*/
|
||||
export const NoteInfoLineUpdatedBy: React.FC = () => {
|
||||
useTranslation()
|
||||
const noteUpdateUser = useApplicationState((state) => state.noteDetails.updateUsername)
|
||||
|
||||
const userBlock = useMemo(() => {
|
||||
if (!noteUpdateUser) {
|
||||
return <Trans i18nKey={'common.guestUser'} />
|
||||
}
|
||||
return (
|
||||
<UserAvatarForUsername username={noteUpdateUser} additionalClasses={'font-style-normal bold font-weight-bold'} />
|
||||
)
|
||||
}, [noteUpdateUser])
|
||||
|
||||
return (
|
||||
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.lastUpdatedBy'} icon={IconPencil}>
|
||||
{userBlock}
|
||||
</SidebarMenuInfoEntry>
|
||||
)
|
||||
}
|
|
@ -10,18 +10,20 @@ import { useEditorReceiveHandler } from '../../../../../render-page/window-post-
|
|||
import type { OnWordCountCalculatedMessage } from '../../../../../render-page/window-post-message-communicator/rendering-message'
|
||||
import { CommunicationMessageType } from '../../../../../render-page/window-post-message-communicator/rendering-message'
|
||||
import { useEditorToRendererCommunicator } from '../../../../render-context/editor-to-renderer-communicator-context-provider'
|
||||
import { NoteInfoLine } from './note-info-line'
|
||||
import { UnitalicBoldContent } from './unitalic-bold-content'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import { SidebarMenuInfoEntry } from '../../../sidebar-menu-info-entry/sidebar-menu-info-entry'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { AlignStart as IconAlignStart } from 'react-bootstrap-icons'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
interface NoteInfoLineWordCountProps {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new info line for the document information dialog that holds the
|
||||
* word count of the note, based on counting in the rendered output.
|
||||
*/
|
||||
export const NoteInfoLineWordCount: React.FC<PropsWithChildren<unknown>> = () => {
|
||||
export const NoteInfoLineWordCount: React.FC<NoteInfoLineWordCountProps> = ({ visible }) => {
|
||||
useTranslation()
|
||||
const editorToRendererCommunicator = useEditorToRendererCommunicator()
|
||||
const [wordCount, setWordCount] = useState<number | null>(null)
|
||||
|
@ -33,21 +35,19 @@ export const NoteInfoLineWordCount: React.FC<PropsWithChildren<unknown>> = () =>
|
|||
|
||||
const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady)
|
||||
useEffect(() => {
|
||||
if (rendererReady) {
|
||||
if (rendererReady && visible) {
|
||||
editorToRendererCommunicator.sendMessageToOtherSide({ type: CommunicationMessageType.GET_WORD_COUNT })
|
||||
}
|
||||
}, [editorToRendererCommunicator, rendererReady])
|
||||
}, [editorToRendererCommunicator, rendererReady, visible])
|
||||
|
||||
return (
|
||||
<NoteInfoLine icon={IconAlignStart} size={2}>
|
||||
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.wordCount'} icon={IconAlignStart}>
|
||||
<ShowIf condition={wordCount === null}>
|
||||
<Trans i18nKey={'common.loading'} />
|
||||
</ShowIf>
|
||||
<ShowIf condition={wordCount !== null}>
|
||||
<Trans i18nKey={'editor.modal.documentInfo.words'}>
|
||||
<UnitalicBoldContent text={wordCount ?? ''} {...cypressId('document-info-word-count')} />
|
||||
</Trans>
|
||||
<span {...cypressId('document-info-word-count')}>{wordCount}</span>
|
||||
</ShowIf>
|
||||
</NoteInfoLine>
|
||||
</SidebarMenuInfoEntry>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { cypressId } from '../../../../../utils/cypress-attribute'
|
||||
import { SidebarButton } from '../../sidebar-button/sidebar-button'
|
||||
import { SidebarMenu } from '../../sidebar-menu/sidebar-menu'
|
||||
import type { SpecificSidebarMenuProps } from '../../types'
|
||||
import { DocumentSidebarMenuSelection } from '../../types'
|
||||
import { NoteInfoLineContributors } from './note-info-line/note-info-line-contributors'
|
||||
import { NoteInfoLineCreatedAt } from './note-info-line/note-info-line-created-at'
|
||||
import { NoteInfoLineUpdatedAt } from './note-info-line/note-info-line-updated-at'
|
||||
import { NoteInfoLineUpdatedBy } from './note-info-line/note-info-line-updated-by'
|
||||
import { NoteInfoLineWordCount } from './note-info-line/note-info-line-word-count'
|
||||
import React, { Fragment, useCallback } from 'react'
|
||||
import { ArrowLeft as IconArrowLeft, GraphUp as IconGraphUp } from 'react-bootstrap-icons'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Renders the note info 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 NoteInfoSidebarMenu: 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('sidebar-menu-info')}
|
||||
hide={hide}
|
||||
icon={expand ? IconArrowLeft : IconGraphUp}
|
||||
className={className}
|
||||
onClick={onClickHandler}>
|
||||
<Trans i18nKey={'editor.noteInfo.title'} />
|
||||
</SidebarButton>
|
||||
<SidebarMenu expand={expand}>
|
||||
<NoteInfoLineCreatedAt />
|
||||
<NoteInfoLineUpdatedAt />
|
||||
<NoteInfoLineUpdatedBy />
|
||||
<NoteInfoLineContributors />
|
||||
<NoteInfoLineWordCount visible={expand} />
|
||||
</SidebarMenu>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
*/
|
||||
|
@ -29,6 +29,7 @@ export interface SidebarMenuProps {
|
|||
export enum DocumentSidebarMenuSelection {
|
||||
NONE,
|
||||
USERS_ONLINE,
|
||||
NOTE_INFO,
|
||||
IMPORT,
|
||||
EXPORT
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue