mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 17:26:29 -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -11,32 +11,32 @@ describe('Test word count with', () => {
|
||||||
|
|
||||||
it('empty note', () => {
|
it('empty note', () => {
|
||||||
cy.setCodemirrorContent('')
|
cy.setCodemirrorContent('')
|
||||||
cy.getByCypressId('sidebar-btn-document-info').click()
|
cy.getByCypressId('sidebar-menu-info').click()
|
||||||
cy.getByCypressId('document-info-modal').should('be.visible')
|
cy.getByCypressId('document-info-word-count').should('be.visible')
|
||||||
cy.getByCypressId('document-info-word-count').contains('0')
|
cy.getByCypressId('document-info-word-count').contains('0')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('simple words', () => {
|
it('simple words', () => {
|
||||||
cy.setCodemirrorContent('five words should be enough')
|
cy.setCodemirrorContent('five words should be enough')
|
||||||
cy.getMarkdownBody().contains('five words should be enough')
|
cy.getMarkdownBody().contains('five words should be enough')
|
||||||
cy.getByCypressId('sidebar-btn-document-info').click()
|
cy.getByCypressId('sidebar-menu-info').click()
|
||||||
cy.getByCypressId('document-info-modal').should('be.visible')
|
cy.getByCypressId('document-info-word-count').should('be.visible')
|
||||||
cy.getByCypressId('document-info-word-count').contains('5')
|
cy.getByCypressId('document-info-word-count').contains('5')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('excluded codeblocks', () => {
|
it('excluded codeblocks', () => {
|
||||||
cy.setCodemirrorContent('```\nthis is should be ignored\n```\n\ntwo `words`')
|
cy.setCodemirrorContent('```\nthis is should be ignored\n```\n\ntwo `words`')
|
||||||
cy.getMarkdownBody().contains('two words')
|
cy.getMarkdownBody().contains('two words')
|
||||||
cy.getByCypressId('sidebar-btn-document-info').click()
|
cy.getByCypressId('sidebar-menu-info').click()
|
||||||
cy.getByCypressId('document-info-modal').should('be.visible')
|
cy.getByCypressId('document-info-word-count').should('be.visible')
|
||||||
cy.getByCypressId('document-info-word-count').contains('2')
|
cy.getByCypressId('document-info-word-count').contains('2')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('excluded images', () => {
|
it('excluded images', () => {
|
||||||
cy.setCodemirrorContent('![ignored alt text](https://dummyimage.com/48) not ignored text')
|
cy.setCodemirrorContent('![ignored alt text](https://dummyimage.com/48) not ignored text')
|
||||||
cy.getMarkdownBody().contains('not ignored text')
|
cy.getMarkdownBody().contains('not ignored text')
|
||||||
cy.getByCypressId('sidebar-btn-document-info').click()
|
cy.getByCypressId('sidebar-menu-info').click()
|
||||||
cy.getByCypressId('document-info-modal').should('be.visible')
|
cy.getByCypressId('document-info-word-count').should('be.visible')
|
||||||
cy.getByCypressId('document-info-word-count').contains('3')
|
cy.getByCypressId('document-info-word-count').contains('3')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -383,20 +383,20 @@
|
||||||
"tagTime": "Time tag",
|
"tagTime": "Time tag",
|
||||||
"spoiler": "Spoiler"
|
"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": {
|
"modal": {
|
||||||
"snippetImport": {
|
"snippetImport": {
|
||||||
"title": "Import from Snippet",
|
"title": "Import from Snippet",
|
||||||
"selectProject": "Select From Available Projects",
|
"selectProject": "Select From Available Projects",
|
||||||
"selectSnippet": "Select From Available Snippets"
|
"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": {
|
"gistImport": {
|
||||||
"title": "Import from Gist",
|
"title": "Import from Gist",
|
||||||
"insertGistUrl": "Paste your gist url here…"
|
"insertGistUrl": "Paste your gist url here…"
|
||||||
|
@ -645,7 +645,7 @@
|
||||||
},
|
},
|
||||||
"cheatsheet": {
|
"cheatsheet": {
|
||||||
"button": "Open Cheatsheet",
|
"button": "Open Cheatsheet",
|
||||||
"modal":{
|
"modal": {
|
||||||
"popup": "Open in new tab",
|
"popup": "Open in new tab",
|
||||||
"title": "Cheatsheet",
|
"title": "Cheatsheet",
|
||||||
"headlines": {
|
"headlines": {
|
||||||
|
@ -778,11 +778,11 @@
|
||||||
"example": "I :heart: :hedgehog:"
|
"example": "I :heart: :hedgehog:"
|
||||||
},
|
},
|
||||||
"csv": {
|
"csv": {
|
||||||
"title" : "CSV",
|
"title": "CSV",
|
||||||
"table" : {
|
"table": {
|
||||||
"title": "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.",
|
"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```"
|
"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": {
|
"header": {
|
||||||
"title": "Header",
|
"title": "Header",
|
||||||
|
|
|
@ -17,7 +17,7 @@ export interface TimeFromNowProps {
|
||||||
*/
|
*/
|
||||||
export const TimeFromNow: React.FC<TimeFromNowProps> = ({ time }) => {
|
export const TimeFromNow: React.FC<TimeFromNowProps> = ({ time }) => {
|
||||||
return (
|
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.toRelative()}
|
||||||
</time>
|
</time>
|
||||||
)
|
)
|
|
@ -6,8 +6,8 @@
|
||||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||||
import { InternalLink } from '../common/links/internal-link'
|
import { InternalLink } from '../common/links/internal-link'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
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 { NoteInfoLineCreatedAt } from '../editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-created-at'
|
||||||
import { NoteInfoLineUpdated } from '../editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-entry/note-info-modal/note-info-line-updated'
|
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 styles from './document-infobar.module.scss'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
||||||
|
@ -26,8 +26,8 @@ export const DocumentInfobar: React.FC = () => {
|
||||||
<div className={'col-md'}> </div>
|
<div className={'col-md'}> </div>
|
||||||
<div className={'d-flex flex-fill'}>
|
<div className={'d-flex flex-fill'}>
|
||||||
<div className={'d-flex flex-column'}>
|
<div className={'d-flex flex-column'}>
|
||||||
<NoteInfoLineCreated />
|
<NoteInfoLineCreatedAt />
|
||||||
<NoteInfoLineUpdated />
|
<NoteInfoLineUpdatedBy />
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
<span className={'ms-auto'}>
|
<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 { DeleteNoteSidebarEntry } from './specific-sidebar-entries/delete-note-sidebar-entry/delete-note-sidebar-entry'
|
||||||
import { ExportMenuSidebarMenu } from './specific-sidebar-entries/export-menu-sidebar-menu'
|
import { ExportMenuSidebarMenu } from './specific-sidebar-entries/export-menu-sidebar-menu'
|
||||||
import { ImportMenuSidebarMenu } from './specific-sidebar-entries/import-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 { 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 { 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'
|
import { RevisionSidebarEntry } from './specific-sidebar-entries/revisions-sidebar-entry/revision-sidebar-entry'
|
||||||
|
@ -47,7 +47,11 @@ export const Sidebar: React.FC = () => {
|
||||||
selectedMenuId={selectedMenu}
|
selectedMenuId={selectedMenu}
|
||||||
onClick={toggleValue}
|
onClick={toggleValue}
|
||||||
/>
|
/>
|
||||||
<NoteInfoSidebarEntry hide={selectionIsNotNone} />
|
<NoteInfoSidebarMenu
|
||||||
|
menuId={DocumentSidebarMenuSelection.NOTE_INFO}
|
||||||
|
selectedMenuId={selectedMenu}
|
||||||
|
onClick={toggleValue}
|
||||||
|
/>
|
||||||
<RevisionSidebarEntry hide={selectionIsNotNone} />
|
<RevisionSidebarEntry hide={selectionIsNotNone} />
|
||||||
<PermissionsSidebarEntry hide={selectionIsNotNone} />
|
<PermissionsSidebarEntry hide={selectionIsNotNone} />
|
||||||
<AliasesSidebarEntry 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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
||||||
import { NoteInfoLine } from './note-info-line'
|
import { SidebarMenuInfoEntry } from '../../../sidebar-menu-info-entry/sidebar-menu-info-entry'
|
||||||
import { UnitalicBoldContent } from './unitalic-bold-content'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { People as IconPeople } from 'react-bootstrap-icons'
|
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.
|
* 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)
|
const contributors = useApplicationState((state) => state.noteDetails.editedBy.length)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NoteInfoLine icon={IconPeople} size={2}>
|
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.contributors'} icon={IconPeople}>
|
||||||
<Trans i18nKey={'editor.modal.documentInfo.usersContributed'}>
|
{contributors}
|
||||||
<UnitalicBoldContent text={contributors} />
|
</SidebarMenuInfoEntry>
|
||||||
</Trans>
|
|
||||||
</NoteInfoLine>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -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 type { OnWordCountCalculatedMessage } from '../../../../../render-page/window-post-message-communicator/rendering-message'
|
||||||
import { CommunicationMessageType } 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 { useEditorToRendererCommunicator } from '../../../../render-context/editor-to-renderer-communicator-context-provider'
|
||||||
import { NoteInfoLine } from './note-info-line'
|
import { SidebarMenuInfoEntry } from '../../../sidebar-menu-info-entry/sidebar-menu-info-entry'
|
||||||
import { UnitalicBoldContent } from './unitalic-bold-content'
|
|
||||||
import type { PropsWithChildren } from 'react'
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { AlignStart as IconAlignStart } from 'react-bootstrap-icons'
|
import { AlignStart as IconAlignStart } from 'react-bootstrap-icons'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
interface NoteInfoLineWordCountProps {
|
||||||
|
visible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new info line for the document information dialog that holds the
|
* 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.
|
* 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()
|
useTranslation()
|
||||||
const editorToRendererCommunicator = useEditorToRendererCommunicator()
|
const editorToRendererCommunicator = useEditorToRendererCommunicator()
|
||||||
const [wordCount, setWordCount] = useState<number | null>(null)
|
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)
|
const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rendererReady) {
|
if (rendererReady && visible) {
|
||||||
editorToRendererCommunicator.sendMessageToOtherSide({ type: CommunicationMessageType.GET_WORD_COUNT })
|
editorToRendererCommunicator.sendMessageToOtherSide({ type: CommunicationMessageType.GET_WORD_COUNT })
|
||||||
}
|
}
|
||||||
}, [editorToRendererCommunicator, rendererReady])
|
}, [editorToRendererCommunicator, rendererReady, visible])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NoteInfoLine icon={IconAlignStart} size={2}>
|
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.wordCount'} icon={IconAlignStart}>
|
||||||
<ShowIf condition={wordCount === null}>
|
<ShowIf condition={wordCount === null}>
|
||||||
<Trans i18nKey={'common.loading'} />
|
<Trans i18nKey={'common.loading'} />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={wordCount !== null}>
|
<ShowIf condition={wordCount !== null}>
|
||||||
<Trans i18nKey={'editor.modal.documentInfo.words'}>
|
<span {...cypressId('document-info-word-count')}>{wordCount}</span>
|
||||||
<UnitalicBoldContent text={wordCount ?? ''} {...cypressId('document-info-word-count')} />
|
|
||||||
</Trans>
|
|
||||||
</ShowIf>
|
</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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -29,6 +29,7 @@ export interface SidebarMenuProps {
|
||||||
export enum DocumentSidebarMenuSelection {
|
export enum DocumentSidebarMenuSelection {
|
||||||
NONE,
|
NONE,
|
||||||
USERS_ONLINE,
|
USERS_ONLINE,
|
||||||
|
NOTE_INFO,
|
||||||
IMPORT,
|
IMPORT,
|
||||||
EXPORT
|
EXPORT
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue