mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-22 09:46:30 -05:00
Add revisions dialog (#485)
* Add mock files Note that revisions-list needs to be called revisions in the reality to be confirm with the API spec, but our mocking solution doesn't allow that... * Add revisions API calls * Fix line endings in mock files * Extend CommonModal to accept size and additionalClasses * Clarify variable name in API request * Add react-diff-viewer as dependency * Add revision chooser modal * Fix type of route params * Added and updated mock files * Added user-icon list per revision * Added translation to alt text of avatars * Updated mock file to remove inconsistencies * Add caching for revisions * Sort mock file revisions-list descending by timestamp * Pre-select first/newest revision on first modal open * Regenerated yarn.lock file from scratch * Applied requested changes in variable names and line lengths * User UserAvatar component instead of manually set image * Move revision-modal-list-entry to own component * Removed unnecessary return statements
This commit is contained in:
parent
0fecda027c
commit
d597438c42
23 changed files with 455 additions and 29 deletions
|
@ -75,6 +75,7 @@
|
|||
"react-bootstrap": "1.3.0",
|
||||
"react-bootstrap-typeahead": "5.1.1",
|
||||
"react-codemirror2": "7.2.1",
|
||||
"react-diff-viewer": "^3.1.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-html-parser": "2.0.2",
|
||||
"react-i18next": "11.7.2",
|
||||
|
|
12
public/api/v2/notes/features/revisions-list
Normal file
12
public/api/v2/notes/features/revisions-list
Normal file
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
{
|
||||
"timestamp": 1598390307,
|
||||
"length": 2788,
|
||||
"authors": ["dermolly", "mrdrogdrog"]
|
||||
},
|
||||
{
|
||||
"timestamp": 1598389571,
|
||||
"length": 2782,
|
||||
"authors": ["dermolly", "mrdrogdrog", "emcrx"]
|
||||
}
|
||||
]
|
5
public/api/v2/notes/features/revisions/1598389571
Normal file
5
public/api/v2/notes/features/revisions/1598389571
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"content": "---\ntitle: Features\ndescription: Many features, such wow!\nrobots: noindex\ntags: codimd, demo, react\nopengraph:\n title: Features\n---\n# Embedding demo\n[TOC]\n\n## some plain text\n\nLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.\n\n## MathJax\nYou can render *LaTeX* mathematical expressions using **MathJax**, as on [math.stackexchange.com](https:\/\/math.stackexchange.com\/):\n\nThe *Gamma function* satisfying $\\Gamma(n) = (n-1)!\\quad\\forall n\\in\\mathbb N$ is via the Euler integral\n\n$$\nx = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}.\n$$\n\n$$\n\\Gamma(z) = \\int_0^\\infty t^{z-1}e^{-t}dt\\,.\n$$\n\n> More information about **LaTeX** mathematical expressions [here](https:\/\/meta.math.stackexchange.com\/questions\/5020\/mathjax-basic-tutorial-and-quick-reference).\n\n## Blockquote\n> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n> [color=red] [name=John Doe] [time=2020-06-21 22:50]\n\n## Slideshare\n{%slideshare mazlan1\/internet-of-things-the-tip-of-an-iceberg %}\n\n## Gist\nhttps:\/\/gist.github.com\/schacon\/1\n\n## YouTube\nhttps:\/\/www.youtube.com\/watch?v=KgMpKsp23yY\n\n## Vimeo\nhttps:\/\/vimeo.com\/23237102\n\n## Asciinema\nhttps:\/\/asciinema.org\/a\/117928\n\n## PDF\n{%pdf https:\/\/www.w3.org\/WAI\/ER\/tests\/xhtml\/testfiles\/resources\/pdf\/dummy.pdf %}\n\n## Code highlighting\n```javascript=\n\nlet a = 1\n```\n\n## PlantUML\n```plantuml\n@startuml\nparticipant Alice\nparticipant \"The **Famous** Bob\" as Bob\n\nAlice -> Bob : hello --there--\n... Some ~~long delay~~ ...\nBob -> Alice : ok\nnote left\n This is **bold**\n This is \/\/italics\/\/\n This is \"\"monospaced\"\"\n This is --stroked--\n This is __underlined__\n This is ~~waved~~\nend note\n\nAlice -> Bob : A \/\/well formatted\/\/ message\nnote right of Alice\n This is <back:cadetblue><size:18>displayed<\/size><\/back>\n __left of__ Alice.\nend note\nnote left of Bob\n <u:red>This<\/u> is <color #118888>displayed<\/color>\n **<color purple>left of<\/color> <s:red>Alice<\/strike> Bob**.\nend note\nnote over Alice, Bob\n <w:#FF33FF>This is hosted<\/w> by <img sourceforge.jpg>\nend note\n@enduml\n```\n\n",
|
||||
"timestamp": 1598389571,
|
||||
"authors": ["mrdrogdrog", "dermolly", "emcrx"]
|
||||
}
|
5
public/api/v2/notes/features/revisions/1598390307
Normal file
5
public/api/v2/notes/features/revisions/1598390307
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"content": "---\ntitle: Features\ndescription: Many more features, such wow!\nrobots: noindex\ntags: codimd, demo, react\nopengraph:\n title: Features\n---\n# Embedding demo\n[TOC]\n\n## some plain text\n\nLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magnus aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetezur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam _et_ justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.\n\n## MathJax\nYou can render *LaTeX* mathematical expressions using **MathJax**, as on [math.stackexchange.com](https:\/\/math.stackexchange.com\/):\n\nThe *Gamma function* satisfying $\\Gamma(n) = (n-1)!\\quad\\forall n\\in\\mathbb N$ is via the Euler integral\n\n$$\nx = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}.\n$$\n\n$$\n\\Gamma(z) = \\int_0^\\infty t^{z-1}e^{-t}dt\\,.\n$$\n\n> More information about **LaTeX** mathematical expressions [here](https:\/\/meta.math.stackexchange.com\/questions\/5020\/mathjax-basic-tutorial-and-quick-reference).\n\n## Blockquote\n> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n> [color=red] [name=John Doe] [time=2020-06-21 22:50]\n\n## Slideshare\n{%slideshare mazlan1\/internet-of-things-the-tip-of-an-iceberg %}\n\n## Gist\nhttps:\/\/gist.github.com\/schacon\/1\n\n## YouTube\nhttps:\/\/www.youtube.com\/watch?v=zHAIuE5BQWk\n\n## Vimeo\nhttps:\/\/vimeo.com\/23237102\n\n## Asciinema\nhttps:\/\/asciinema.org\/a\/117928\n\n## PDF\n{%pdf https:\/\/www.w3.org\/WAI\/ER\/tests\/xhtml\/testfiles\/resources\/pdf\/dummy.pdf %}\n\n## Code highlighting\n```javascript=\n\nlet a = 1\n```\n\n## PlantUML\n```plantuml\n@startuml\nparticipant Alice\nparticipant \"The **Famous** Bob\" as Bob\n\nAlice -> Bob : bye --there--\n... Some ~~long delay~~ ...\nBob -> Alice : ok\nnote left\n This is **bold**\n This is \/\/italics\/\/\n This is \"\"monospaced\"\"\n This is --stroked--\n This is __underlined__\n This is ~~waved~~\nend note\n\nAlice -> Bob : A \/\/well formatted\/\/ message\nnote right of Alice\n This is <back:cadetblue><size:18>displayed<\/size><\/back>\n __left of__ Alice.\nend note\nnote left of Bob\n <u:red>This<\/u> is <color #118888>displayed<\/color>\n **<color purple>left of<\/color> <s:red>Alice<\/strike> Bob**.\nend note\nnote over Alice, Bob\n <w:#FF33FF>This is hosted<\/w> by <img sourceforge.jpg>\nend note\n@enduml\n```\n\n",
|
||||
"timestamp": 1598390307,
|
||||
"authors": ["mrdrogdrog", "dermolly"]
|
||||
}
|
7
public/api/v2/users/dermolly
Normal file
7
public/api/v2/users/dermolly
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"id": "dermolly",
|
||||
"photo": "/avatar.png",
|
||||
"name": "Philip",
|
||||
"status": "ok",
|
||||
"provider": "internal"
|
||||
}
|
7
public/api/v2/users/emcrx
Normal file
7
public/api/v2/users/emcrx
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"id": "emcrx",
|
||||
"photo": "/avatar.png",
|
||||
"name": "Erik",
|
||||
"status": "ok",
|
||||
"provider": "internal"
|
||||
}
|
7
public/api/v2/users/mrdrogdrog
Normal file
7
public/api/v2/users/mrdrogdrog
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"id": "mrdrogdrog",
|
||||
"photo": "/avatar.png",
|
||||
"name": "Tilman",
|
||||
"status": "ok",
|
||||
"provider": "internal"
|
||||
}
|
|
@ -303,8 +303,11 @@
|
|||
"visibilityLevel": "Select Visibility Level"
|
||||
},
|
||||
"revision": {
|
||||
"title": "Revision",
|
||||
"revertButton": "Revert"
|
||||
"title": "Revisions",
|
||||
"revertButton": "Revert",
|
||||
"error": "An error occurred while fetching the revisions of this note.",
|
||||
"length": "Length",
|
||||
"download": "Download selected revision"
|
||||
},
|
||||
"clipboardImport": {
|
||||
"title": "Import from clipboard",
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
import { LoginProvider } from '../../redux/user/types'
|
||||
import { UserResponse } from '../users/types'
|
||||
import { expectResponseCode, getApiUrl, defaultFetchConfig } from '../utils'
|
||||
|
||||
export const getMe = async (): Promise<meResponse> => {
|
||||
export const getMe = async (): Promise<UserResponse> => {
|
||||
const response = await fetch(getApiUrl() + '/me', {
|
||||
...defaultFetchConfig
|
||||
})
|
||||
expectResponseCode(response)
|
||||
return (await response.json()) as meResponse
|
||||
}
|
||||
|
||||
export interface meResponse {
|
||||
id: string
|
||||
name: string
|
||||
photo: string
|
||||
provider: LoginProvider
|
||||
return (await response.json()) as UserResponse
|
||||
}
|
||||
|
||||
export const updateDisplayName = async (displayName: string): Promise<void> => {
|
||||
|
|
30
src/api/revisions/index.ts
Normal file
30
src/api/revisions/index.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
|
||||
|
||||
export interface Revision {
|
||||
content: string
|
||||
timestamp: number
|
||||
authors: string[]
|
||||
}
|
||||
|
||||
export interface RevisionListEntry {
|
||||
timestamp: number
|
||||
length: number
|
||||
authors: string[]
|
||||
}
|
||||
|
||||
export const getRevision = async (noteId: string, timestamp: number): Promise<Revision> => {
|
||||
const response = await fetch(getApiUrl() + `/notes/${noteId}/revisions/${timestamp}`, {
|
||||
...defaultFetchConfig
|
||||
})
|
||||
expectResponseCode(response)
|
||||
return await response.json() as Promise<Revision>
|
||||
}
|
||||
|
||||
export const getAllRevisions = async (noteId: string): Promise<RevisionListEntry[]> => {
|
||||
// TODO Change 'revisions-list' to 'revisions' as soon as the backend is ready to serve some data!
|
||||
const response = await fetch(getApiUrl() + `/notes/${noteId}/revisions-list`, {
|
||||
...defaultFetchConfig
|
||||
})
|
||||
expectResponseCode(response)
|
||||
return await response.json() as Promise<RevisionListEntry[]>
|
||||
}
|
10
src/api/users/index.ts
Normal file
10
src/api/users/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
|
||||
import { UserResponse } from './types'
|
||||
|
||||
export const getUserById = async (userid: string): Promise<UserResponse> => {
|
||||
const response = await fetch(`${getApiUrl()}/users/${userid}`, {
|
||||
...defaultFetchConfig
|
||||
})
|
||||
expectResponseCode(response)
|
||||
return (await response.json()) as UserResponse
|
||||
}
|
8
src/api/users/types.d.ts
vendored
Normal file
8
src/api/users/types.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { LoginProvider } from '../../redux/user/types'
|
||||
|
||||
export interface UserResponse {
|
||||
id: string
|
||||
name: string
|
||||
photo: string
|
||||
provider: LoginProvider
|
||||
}
|
|
@ -11,13 +11,15 @@ export interface CommonModalProps {
|
|||
titleI18nKey: string
|
||||
closeButton?: boolean
|
||||
icon?: IconName
|
||||
size?: 'lg' | 'sm' | 'xl'
|
||||
additionalClasses?: string
|
||||
}
|
||||
|
||||
export const CommonModal: React.FC<CommonModalProps> = ({ show, onHide, titleI18nKey, closeButton, icon, children }) => {
|
||||
export const CommonModal: React.FC<CommonModalProps> = ({ show, onHide, titleI18nKey, closeButton, icon, additionalClasses, size, children }) => {
|
||||
useTranslation()
|
||||
|
||||
return (
|
||||
<Modal show={show} onHide={onHide} animation={true} className="text-dark">
|
||||
<Modal show={show} onHide={onHide} animation={true} dialogClassName={`text-dark ${additionalClasses ?? ''}`} size={size}>
|
||||
<Modal.Header closeButton={!!closeButton}>
|
||||
<Modal.Title>
|
||||
<ShowIf condition={!!icon}>
|
||||
|
|
|
@ -19,6 +19,7 @@ const UserAvatar: React.FC<UserAvatarProps> = ({ name, photo, additionalClasses
|
|||
src={photo}
|
||||
className="user-avatar rounded"
|
||||
alt={t('common.avatarOf', { name })}
|
||||
title={name}
|
||||
/>
|
||||
<ShowIf condition={showName}>
|
||||
<span className="mx-1 user-name">{name}</span>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import React from 'react'
|
||||
import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button'
|
||||
|
||||
export const RevisionButton: React.FC = () => {
|
||||
return <TranslatedIconButton size={'sm'} className={'mx-1'} icon={'history'} variant={'light'} i18nKey={'editor.documentBar.revision'}/>
|
||||
}
|
|
@ -8,13 +8,14 @@ import { ImportMenu } from './menus/import-menu'
|
|||
import { PermissionButton } from './buttons/permission-button'
|
||||
import { PinToHistoryButton } from './buttons/pin-to-history-button'
|
||||
import { ShareLinkButton } from './buttons/share-link-button'
|
||||
import { RevisionButton } from './buttons/revision-button'
|
||||
import { RevisionButton } from './revisions/revision-button'
|
||||
|
||||
export interface DocumentBarProps {
|
||||
title: string
|
||||
noteContent: string
|
||||
}
|
||||
|
||||
export const DocumentBar: React.FC<DocumentBarProps> = ({ title }) => {
|
||||
export const DocumentBar: React.FC<DocumentBarProps> = ({ title, noteContent }) => {
|
||||
useTranslation()
|
||||
|
||||
return (
|
||||
|
@ -22,7 +23,7 @@ export const DocumentBar: React.FC<DocumentBarProps> = ({ title }) => {
|
|||
<div className="navbar-nav">
|
||||
<ShareLinkButton/>
|
||||
<DocumentInfoButton/>
|
||||
<RevisionButton/>
|
||||
<RevisionButton noteContent={noteContent}/>
|
||||
<PinToHistoryButton/>
|
||||
<PermissionButton/>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import React, { Fragment, useState } from 'react'
|
||||
import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button'
|
||||
import { RevisionModal } from './revision-modal'
|
||||
|
||||
export interface RevisionButtonProps {
|
||||
noteContent: string
|
||||
}
|
||||
|
||||
export const RevisionButton: React.FC<RevisionButtonProps> = ({ noteContent }) => {
|
||||
const [show, setShow] = useState(false)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<TranslatedIconButton size={'sm'} className={'mx-1'} icon={'history'} variant={'light'} i18nKey={'editor.documentBar.revision'} onClick={() => setShow(true)}/>
|
||||
<RevisionModal show={show} onHide={() => setShow(false)} titleI18nKey={'editor.modal.revision.title'} icon={'history'} noteContent={noteContent}/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import moment from 'moment'
|
||||
import React from 'react'
|
||||
import { ListGroup } from 'react-bootstrap'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { RevisionListEntry } from '../../../../api/revisions'
|
||||
import { UserResponse } from '../../../../api/users/types'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { UserAvatar } from '../../../common/user-avatar/user-avatar'
|
||||
|
||||
export interface RevisionModalListEntryProps {
|
||||
active: boolean
|
||||
onClick: () => void
|
||||
revision: RevisionListEntry
|
||||
revisionAuthorListMap: Map<number, UserResponse[]>
|
||||
}
|
||||
|
||||
export const RevisionModalListEntry: React.FC<RevisionModalListEntryProps> = ({ active, onClick, revision, revisionAuthorListMap }) => (
|
||||
<ListGroup.Item
|
||||
as='li'
|
||||
active={active}
|
||||
onClick={onClick}
|
||||
className='user-select-none revision-item d-flex flex-column'
|
||||
>
|
||||
<span>
|
||||
<ForkAwesomeIcon icon={'clock-o'} className='mx-2'/>
|
||||
{moment(revision.timestamp * 1000).format('LLLL')}
|
||||
</span>
|
||||
<span>
|
||||
<ForkAwesomeIcon icon={'file-text-o'} className='mx-2'/>
|
||||
<Trans i18nKey={'editor.modal.revision.length'}/>: {revision.length}
|
||||
</span>
|
||||
<span className={'d-flex flex-row my-1 align-items-center'}>
|
||||
<ForkAwesomeIcon icon={'user-o'} className={'mx-2'}/>
|
||||
{
|
||||
revisionAuthorListMap.get(revision.timestamp)?.map((user, index) => {
|
||||
return (
|
||||
<UserAvatar name={user.name} photo={user.photo} showName={false} additionalClasses={'mx-1'} key={index}/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</span>
|
||||
</ListGroup.Item>
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
.revision-modal .row .scroll-col {
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
li.revision-item {
|
||||
cursor: pointer;
|
||||
|
||||
span > img {
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
111
src/components/editor/document-bar/revisions/revision-modal.tsx
Normal file
111
src/components/editor/document-bar/revisions/revision-modal.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Alert, Col, ListGroup, Modal, Row, Button } from 'react-bootstrap'
|
||||
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router'
|
||||
import { getAllRevisions, getRevision, Revision, RevisionListEntry } from '../../../../api/revisions'
|
||||
import { UserResponse } from '../../../../api/users/types'
|
||||
import { CommonModal, CommonModalProps } from '../../../common/modals/common-modal'
|
||||
import { ShowIf } from '../../../common/show-if/show-if'
|
||||
import { RevisionButtonProps } from './revision-button'
|
||||
import { RevisionModalListEntry } from './revision-modal-list-entry'
|
||||
import './revision-modal.scss'
|
||||
import { downloadRevision, getUserDataForRevision } from './utils'
|
||||
|
||||
export const RevisionModal: React.FC<CommonModalProps & RevisionButtonProps> = ({ show, onHide, icon, titleI18nKey, noteContent }) => {
|
||||
useTranslation()
|
||||
const [revisions, setRevisions] = useState<RevisionListEntry[]>([])
|
||||
const [selectedRevisionTimestamp, setSelectedRevisionTimestamp] = useState<number | null>(null)
|
||||
const [selectedRevision, setSelectedRevision] = useState<Revision | null>(null)
|
||||
const [error, setError] = useState(false)
|
||||
const revisionAuthorListMap = useRef(new Map<number, UserResponse[]>())
|
||||
const revisionCacheMap = useRef(new Map<number, Revision>())
|
||||
const { id } = useParams<{ id: string }>()
|
||||
|
||||
useEffect(() => {
|
||||
getAllRevisions(id).then(fetchedRevisions => {
|
||||
fetchedRevisions.forEach(revision => {
|
||||
const authorData = getUserDataForRevision(revision.authors)
|
||||
revisionAuthorListMap.current.set(revision.timestamp, authorData)
|
||||
})
|
||||
setRevisions(fetchedRevisions)
|
||||
if (fetchedRevisions.length >= 1) {
|
||||
setSelectedRevisionTimestamp(fetchedRevisions[0].timestamp)
|
||||
}
|
||||
}).catch(() => setError(true))
|
||||
}, [setRevisions, setError, id])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRevisionTimestamp === null) {
|
||||
return
|
||||
}
|
||||
const cacheEntry = revisionCacheMap.current.get(selectedRevisionTimestamp)
|
||||
if (cacheEntry) {
|
||||
setSelectedRevision(cacheEntry)
|
||||
return
|
||||
}
|
||||
getRevision(id, selectedRevisionTimestamp).then(fetchedRevision => {
|
||||
setSelectedRevision(fetchedRevision)
|
||||
revisionCacheMap.current.set(selectedRevisionTimestamp, fetchedRevision)
|
||||
}).catch(() => setError(true))
|
||||
}, [selectedRevisionTimestamp, id])
|
||||
|
||||
return (
|
||||
<CommonModal show={show} onHide={onHide} titleI18nKey={titleI18nKey} icon={icon} closeButton={true} size={'xl'} additionalClasses='revision-modal'>
|
||||
<Modal.Body>
|
||||
<Row>
|
||||
<Col lg={4} className={'scroll-col'}>
|
||||
<ListGroup as='ul'>
|
||||
{
|
||||
revisions.map((revision, revisionIndex) => (
|
||||
<RevisionModalListEntry
|
||||
key={revisionIndex}
|
||||
active={selectedRevisionTimestamp === revision.timestamp}
|
||||
revision={revision}
|
||||
revisionAuthorListMap={revisionAuthorListMap.current}
|
||||
onClick={() => setSelectedRevisionTimestamp(revision.timestamp)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Col>
|
||||
<Col lg={8} className={'scroll-col'}>
|
||||
<ShowIf condition={error}>
|
||||
<Alert variant='danger'>
|
||||
<Trans i18nKey='editor.modal.revision.error'/>
|
||||
</Alert>
|
||||
</ShowIf>
|
||||
<ShowIf condition={!error && !!selectedRevision}>
|
||||
<ReactDiffViewer
|
||||
oldValue={selectedRevision?.content}
|
||||
newValue={noteContent}
|
||||
splitView={false}
|
||||
compareMethod={DiffMethod.WORDS}
|
||||
useDarkTheme={false}
|
||||
/>
|
||||
</ShowIf>
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant='secondary'
|
||||
onClick={onHide}>
|
||||
<Trans i18nKey={'common.close'}/>
|
||||
</Button>
|
||||
<Button
|
||||
variant='danger'
|
||||
disabled={!selectedRevisionTimestamp}
|
||||
onClick={() => window.alert('Not yet implemented. Requires websocket.')}>
|
||||
<Trans i18nKey={'editor.modal.revision.revertButton'}/>
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
disabled={!selectedRevisionTimestamp}
|
||||
onClick={() => downloadRevision(id, selectedRevision)}>
|
||||
<Trans i18nKey={'editor.modal.revision.download'}/>
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</CommonModal>
|
||||
)
|
||||
}
|
39
src/components/editor/document-bar/revisions/utils.ts
Normal file
39
src/components/editor/document-bar/revisions/utils.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { Revision } from '../../../../api/revisions'
|
||||
import { getUserById } from '../../../../api/users'
|
||||
import { UserResponse } from '../../../../api/users/types'
|
||||
|
||||
const userResponseCache = new Map<string, UserResponse>()
|
||||
|
||||
export const downloadRevision = (noteId: string, revision: Revision | null): void => {
|
||||
if (!revision) {
|
||||
return
|
||||
}
|
||||
const encoded = Buffer.from(revision.content).toString('base64')
|
||||
const wrapper = document.createElement('a')
|
||||
wrapper.download = `${noteId}-${revision.timestamp}.md`
|
||||
wrapper.href = `data:text/markdown;charset=utf-8;base64,${encoded}`
|
||||
document.body.appendChild(wrapper)
|
||||
wrapper.click()
|
||||
document.body.removeChild(wrapper)
|
||||
}
|
||||
|
||||
export const getUserDataForRevision = (authors: string[]): UserResponse[] => {
|
||||
const users: UserResponse[] = []
|
||||
authors.forEach((author, index) => {
|
||||
if (index > 9) {
|
||||
return
|
||||
}
|
||||
const cacheEntry = userResponseCache.get(author)
|
||||
if (cacheEntry) {
|
||||
users.push(cacheEntry)
|
||||
return
|
||||
}
|
||||
getUserById(author)
|
||||
.then(userData => {
|
||||
users.push(userData)
|
||||
userResponseCache.set(author, userData)
|
||||
})
|
||||
.catch((error) => console.error(error))
|
||||
})
|
||||
return users
|
||||
}
|
|
@ -113,7 +113,7 @@ export const Editor: React.FC = () => {
|
|||
<DocumentTitle title={documentTitle}/>
|
||||
<div className={'d-flex flex-column vh-100'}>
|
||||
<AppBar/>
|
||||
<DocumentBar title={documentTitle}/>
|
||||
<DocumentBar title={documentTitle} noteContent={markdownContent}/>
|
||||
<Splitter
|
||||
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
|
||||
left={
|
||||
|
|
127
yarn.lock
127
yarn.lock
|
@ -188,7 +188,7 @@
|
|||
dependencies:
|
||||
"@babel/types" "^7.11.0"
|
||||
|
||||
"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.8.3":
|
||||
"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.8.3":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620"
|
||||
integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==
|
||||
|
@ -1219,6 +1219,62 @@
|
|||
debug "^3.1.0"
|
||||
lodash.once "^4.1.1"
|
||||
|
||||
"@emotion/cache@^10.0.27":
|
||||
version "10.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
|
||||
integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==
|
||||
dependencies:
|
||||
"@emotion/sheet" "0.9.4"
|
||||
"@emotion/stylis" "0.8.5"
|
||||
"@emotion/utils" "0.11.3"
|
||||
"@emotion/weak-memoize" "0.2.5"
|
||||
|
||||
"@emotion/hash@0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
|
||||
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
|
||||
|
||||
"@emotion/memoize@0.7.4":
|
||||
version "0.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
|
||||
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
|
||||
|
||||
"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16":
|
||||
version "0.11.16"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad"
|
||||
integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==
|
||||
dependencies:
|
||||
"@emotion/hash" "0.8.0"
|
||||
"@emotion/memoize" "0.7.4"
|
||||
"@emotion/unitless" "0.7.5"
|
||||
"@emotion/utils" "0.11.3"
|
||||
csstype "^2.5.7"
|
||||
|
||||
"@emotion/sheet@0.9.4":
|
||||
version "0.9.4"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5"
|
||||
integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==
|
||||
|
||||
"@emotion/stylis@0.8.5":
|
||||
version "0.8.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04"
|
||||
integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==
|
||||
|
||||
"@emotion/unitless@0.7.5":
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
|
||||
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
|
||||
|
||||
"@emotion/utils@0.11.3":
|
||||
version "0.11.3"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924"
|
||||
integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==
|
||||
|
||||
"@emotion/weak-memoize@0.2.5":
|
||||
version "0.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
|
||||
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
|
||||
|
||||
"@hapi/address@2.x.x":
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
||||
|
@ -2803,6 +2859,22 @@ babel-plugin-dynamic-import-node@^2.3.3:
|
|||
dependencies:
|
||||
object.assign "^4.1.0"
|
||||
|
||||
babel-plugin-emotion@^10.0.27:
|
||||
version "10.0.33"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz#ce1155dcd1783bbb9286051efee53f4e2be63e03"
|
||||
integrity sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.0.0"
|
||||
"@emotion/hash" "0.8.0"
|
||||
"@emotion/memoize" "0.7.4"
|
||||
"@emotion/serialize" "^0.11.16"
|
||||
babel-plugin-macros "^2.0.0"
|
||||
babel-plugin-syntax-jsx "^6.18.0"
|
||||
convert-source-map "^1.5.0"
|
||||
escape-string-regexp "^1.0.5"
|
||||
find-root "^1.1.0"
|
||||
source-map "^0.5.7"
|
||||
|
||||
babel-plugin-istanbul@^5.1.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854"
|
||||
|
@ -2820,7 +2892,7 @@ babel-plugin-jest-hoist@^24.9.0:
|
|||
dependencies:
|
||||
"@types/babel__traverse" "^7.0.6"
|
||||
|
||||
babel-plugin-macros@2.8.0:
|
||||
babel-plugin-macros@2.8.0, babel-plugin-macros@^2.0.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138"
|
||||
integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==
|
||||
|
@ -2834,6 +2906,11 @@ babel-plugin-named-asset-import@^0.3.6:
|
|||
resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.6.tgz#c9750a1b38d85112c9e166bf3ef7c5dbc605f4be"
|
||||
integrity sha512-1aGDUfL1qOOIoqk9QKGIo2lANk+C7ko/fqH0uIyC71x3PEGz0uVP8ISgfEsFuG+FKmjHTvFK/nNM8dowpmUxLA==
|
||||
|
||||
babel-plugin-syntax-jsx@^6.18.0:
|
||||
version "6.18.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
|
||||
integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=
|
||||
|
||||
babel-plugin-syntax-object-rest-spread@^6.8.0:
|
||||
version "6.13.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
|
||||
|
@ -3776,7 +3853,7 @@ content-type@~1.0.4:
|
|||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
||||
|
||||
convert-source-map@1.7.0, convert-source-map@^1.4.0, convert-source-map@^1.7.0:
|
||||
convert-source-map@1.7.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
|
||||
integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
|
||||
|
@ -3884,6 +3961,16 @@ create-ecdh@^4.0.0:
|
|||
bn.js "^4.1.0"
|
||||
elliptic "^6.5.3"
|
||||
|
||||
create-emotion@^10.0.14, create-emotion@^10.0.27:
|
||||
version "10.0.27"
|
||||
resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-10.0.27.tgz#cb4fa2db750f6ca6f9a001a33fbf1f6c46789503"
|
||||
integrity sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==
|
||||
dependencies:
|
||||
"@emotion/cache" "^10.0.27"
|
||||
"@emotion/serialize" "^0.11.15"
|
||||
"@emotion/sheet" "0.9.4"
|
||||
"@emotion/utils" "0.11.3"
|
||||
|
||||
create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
|
||||
|
@ -4207,7 +4294,7 @@ cssstyle@^1.0.0, cssstyle@^1.1.1:
|
|||
dependencies:
|
||||
cssom "0.3.x"
|
||||
|
||||
csstype@^2.5.5:
|
||||
csstype@^2.5.5, csstype@^2.5.7:
|
||||
version "2.6.13"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.13.tgz#a6893015b90e84dd6e85d0e3b442a1e84f2dbe0f"
|
||||
integrity sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==
|
||||
|
@ -4717,6 +4804,14 @@ emojis-list@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
||||
|
||||
emotion@^10.0.14:
|
||||
version "10.0.27"
|
||||
resolved "https://registry.yarnpkg.com/emotion/-/emotion-10.0.27.tgz#f9ca5df98630980a23c819a56262560562e5d75e"
|
||||
integrity sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==
|
||||
dependencies:
|
||||
babel-plugin-emotion "^10.0.27"
|
||||
create-emotion "^10.0.27"
|
||||
|
||||
encodeurl@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
|
@ -5575,6 +5670,11 @@ find-cache-dir@^3.3.1:
|
|||
make-dir "^3.0.2"
|
||||
pkg-dir "^4.1.0"
|
||||
|
||||
find-root@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
|
||||
integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
|
||||
|
||||
find-up@4.1.0, find-up@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
|
||||
|
@ -8185,6 +8285,11 @@ media-typer@0.3.0:
|
|||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||
|
||||
memoize-one@^5.0.4:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
|
||||
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
|
||||
|
||||
memory-fs@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
|
||||
|
@ -10389,6 +10494,18 @@ react-dev-utils@^10.2.1:
|
|||
strip-ansi "6.0.0"
|
||||
text-table "0.2.0"
|
||||
|
||||
react-diff-viewer@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-diff-viewer/-/react-diff-viewer-3.1.1.tgz#21ac9c891193d05a3734bfd6bd54b107ee6d46cc"
|
||||
integrity sha512-rmvwNdcClp6ZWdS11m1m01UnBA4OwYaLG/li0dB781e/bQEzsGyj+qewVd6W5ztBwseQ72pO7nwaCcq5jnlzcw==
|
||||
dependencies:
|
||||
classnames "^2.2.6"
|
||||
create-emotion "^10.0.14"
|
||||
diff "^4.0.1"
|
||||
emotion "^10.0.14"
|
||||
memoize-one "^5.0.4"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-dom@16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
|
||||
|
@ -11611,7 +11728,7 @@ source-map@^0.4.2:
|
|||
dependencies:
|
||||
amdefine ">=0.0.4"
|
||||
|
||||
source-map@^0.5.0, source-map@^0.5.6:
|
||||
source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
|
||||
version "0.5.7"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
|
||||
|
|
Loading…
Reference in a new issue