From d597438c42357cb284404558a1af07311f94a231 Mon Sep 17 00:00:00 2001 From: Erik Michelson Date: Wed, 2 Sep 2020 22:57:44 +0200 Subject: [PATCH] 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 --- package.json | 1 + public/api/v2/notes/features/revisions-list | 12 ++ .../v2/notes/features/revisions/1598389571 | 5 + .../v2/notes/features/revisions/1598390307 | 5 + public/api/v2/users/dermolly | 7 + public/api/v2/users/emcrx | 7 + public/api/v2/users/mrdrogdrog | 7 + public/locales/en.json | 7 +- src/api/me/index.ts | 13 +- src/api/revisions/index.ts | 30 +++++ src/api/users/index.ts | 10 ++ src/api/users/types.d.ts | 8 ++ src/components/common/modals/common-modal.tsx | 6 +- .../common/user-avatar/user-avatar.tsx | 1 + .../document-bar/buttons/revision-button.tsx | 6 - .../editor/document-bar/document-bar.tsx | 7 +- .../revisions/revision-button.tsx | 18 +++ .../revisions/revision-modal-list-entry.tsx | 43 ++++++ .../revisions/revision-modal.scss | 12 ++ .../document-bar/revisions/revision-modal.tsx | 111 +++++++++++++++ .../editor/document-bar/revisions/utils.ts | 39 ++++++ src/components/editor/editor.tsx | 2 +- yarn.lock | 127 +++++++++++++++++- 23 files changed, 455 insertions(+), 29 deletions(-) create mode 100644 public/api/v2/notes/features/revisions-list create mode 100644 public/api/v2/notes/features/revisions/1598389571 create mode 100644 public/api/v2/notes/features/revisions/1598390307 create mode 100644 public/api/v2/users/dermolly create mode 100644 public/api/v2/users/emcrx create mode 100644 public/api/v2/users/mrdrogdrog create mode 100644 src/api/revisions/index.ts create mode 100644 src/api/users/index.ts create mode 100644 src/api/users/types.d.ts delete mode 100644 src/components/editor/document-bar/buttons/revision-button.tsx create mode 100644 src/components/editor/document-bar/revisions/revision-button.tsx create mode 100644 src/components/editor/document-bar/revisions/revision-modal-list-entry.tsx create mode 100644 src/components/editor/document-bar/revisions/revision-modal.scss create mode 100644 src/components/editor/document-bar/revisions/revision-modal.tsx create mode 100644 src/components/editor/document-bar/revisions/utils.ts diff --git a/package.json b/package.json index 30abb4276..d20c31003 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/api/v2/notes/features/revisions-list b/public/api/v2/notes/features/revisions-list new file mode 100644 index 000000000..e89b4e765 --- /dev/null +++ b/public/api/v2/notes/features/revisions-list @@ -0,0 +1,12 @@ +[ + { + "timestamp": 1598390307, + "length": 2788, + "authors": ["dermolly", "mrdrogdrog"] + }, + { + "timestamp": 1598389571, + "length": 2782, + "authors": ["dermolly", "mrdrogdrog", "emcrx"] + } +] diff --git a/public/api/v2/notes/features/revisions/1598389571 b/public/api/v2/notes/features/revisions/1598389571 new file mode 100644 index 000000000..6fa22b220 --- /dev/null +++ b/public/api/v2/notes/features/revisions/1598389571 @@ -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 displayed<\/size><\/back>\n __left of__ Alice.\nend note\nnote left of Bob\n This<\/u> is displayed<\/color>\n **left of<\/color> Alice<\/strike> Bob**.\nend note\nnote over Alice, Bob\n This is hosted<\/w> by \nend note\n@enduml\n```\n\n", + "timestamp": 1598389571, + "authors": ["mrdrogdrog", "dermolly", "emcrx"] +} diff --git a/public/api/v2/notes/features/revisions/1598390307 b/public/api/v2/notes/features/revisions/1598390307 new file mode 100644 index 000000000..6b814a8d5 --- /dev/null +++ b/public/api/v2/notes/features/revisions/1598390307 @@ -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 displayed<\/size><\/back>\n __left of__ Alice.\nend note\nnote left of Bob\n This<\/u> is displayed<\/color>\n **left of<\/color> Alice<\/strike> Bob**.\nend note\nnote over Alice, Bob\n This is hosted<\/w> by \nend note\n@enduml\n```\n\n", + "timestamp": 1598390307, + "authors": ["mrdrogdrog", "dermolly"] +} diff --git a/public/api/v2/users/dermolly b/public/api/v2/users/dermolly new file mode 100644 index 000000000..60dd3d4b2 --- /dev/null +++ b/public/api/v2/users/dermolly @@ -0,0 +1,7 @@ +{ + "id": "dermolly", + "photo": "/avatar.png", + "name": "Philip", + "status": "ok", + "provider": "internal" +} diff --git a/public/api/v2/users/emcrx b/public/api/v2/users/emcrx new file mode 100644 index 000000000..16d83df3d --- /dev/null +++ b/public/api/v2/users/emcrx @@ -0,0 +1,7 @@ +{ + "id": "emcrx", + "photo": "/avatar.png", + "name": "Erik", + "status": "ok", + "provider": "internal" +} diff --git a/public/api/v2/users/mrdrogdrog b/public/api/v2/users/mrdrogdrog new file mode 100644 index 000000000..05c7863c4 --- /dev/null +++ b/public/api/v2/users/mrdrogdrog @@ -0,0 +1,7 @@ +{ + "id": "mrdrogdrog", + "photo": "/avatar.png", + "name": "Tilman", + "status": "ok", + "provider": "internal" +} diff --git a/public/locales/en.json b/public/locales/en.json index d8f12925f..3086af88b 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -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", diff --git a/src/api/me/index.ts b/src/api/me/index.ts index 4b2706678..b310de105 100644 --- a/src/api/me/index.ts +++ b/src/api/me/index.ts @@ -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 => { +export const getMe = async (): Promise => { 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 => { diff --git a/src/api/revisions/index.ts b/src/api/revisions/index.ts new file mode 100644 index 000000000..746e85f17 --- /dev/null +++ b/src/api/revisions/index.ts @@ -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 => { + const response = await fetch(getApiUrl() + `/notes/${noteId}/revisions/${timestamp}`, { + ...defaultFetchConfig + }) + expectResponseCode(response) + return await response.json() as Promise +} + +export const getAllRevisions = async (noteId: string): Promise => { + // 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 +} diff --git a/src/api/users/index.ts b/src/api/users/index.ts new file mode 100644 index 000000000..98e640345 --- /dev/null +++ b/src/api/users/index.ts @@ -0,0 +1,10 @@ +import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils' +import { UserResponse } from './types' + +export const getUserById = async (userid: string): Promise => { + const response = await fetch(`${getApiUrl()}/users/${userid}`, { + ...defaultFetchConfig + }) + expectResponseCode(response) + return (await response.json()) as UserResponse +} diff --git a/src/api/users/types.d.ts b/src/api/users/types.d.ts new file mode 100644 index 000000000..c155ff711 --- /dev/null +++ b/src/api/users/types.d.ts @@ -0,0 +1,8 @@ +import { LoginProvider } from '../../redux/user/types' + +export interface UserResponse { + id: string + name: string + photo: string + provider: LoginProvider +} diff --git a/src/components/common/modals/common-modal.tsx b/src/components/common/modals/common-modal.tsx index ba5b0929e..e97388a3b 100644 --- a/src/components/common/modals/common-modal.tsx +++ b/src/components/common/modals/common-modal.tsx @@ -11,13 +11,15 @@ export interface CommonModalProps { titleI18nKey: string closeButton?: boolean icon?: IconName + size?: 'lg' | 'sm' | 'xl' + additionalClasses?: string } -export const CommonModal: React.FC = ({ show, onHide, titleI18nKey, closeButton, icon, children }) => { +export const CommonModal: React.FC = ({ show, onHide, titleI18nKey, closeButton, icon, additionalClasses, size, children }) => { useTranslation() return ( - + diff --git a/src/components/common/user-avatar/user-avatar.tsx b/src/components/common/user-avatar/user-avatar.tsx index 7c8036b38..64b4f73ce 100644 --- a/src/components/common/user-avatar/user-avatar.tsx +++ b/src/components/common/user-avatar/user-avatar.tsx @@ -19,6 +19,7 @@ const UserAvatar: React.FC = ({ name, photo, additionalClasses src={photo} className="user-avatar rounded" alt={t('common.avatarOf', { name })} + title={name} /> {name} diff --git a/src/components/editor/document-bar/buttons/revision-button.tsx b/src/components/editor/document-bar/buttons/revision-button.tsx deleted file mode 100644 index 823dd2bf7..000000000 --- a/src/components/editor/document-bar/buttons/revision-button.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' -import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button' - -export const RevisionButton: React.FC = () => { - return -} diff --git a/src/components/editor/document-bar/document-bar.tsx b/src/components/editor/document-bar/document-bar.tsx index ef85b40d0..e73f67fc9 100644 --- a/src/components/editor/document-bar/document-bar.tsx +++ b/src/components/editor/document-bar/document-bar.tsx @@ -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 = ({ title }) => { +export const DocumentBar: React.FC = ({ title, noteContent }) => { useTranslation() return ( @@ -22,7 +23,7 @@ export const DocumentBar: React.FC = ({ title }) => {
- +
diff --git a/src/components/editor/document-bar/revisions/revision-button.tsx b/src/components/editor/document-bar/revisions/revision-button.tsx new file mode 100644 index 000000000..0372509e7 --- /dev/null +++ b/src/components/editor/document-bar/revisions/revision-button.tsx @@ -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 = ({ noteContent }) => { + const [show, setShow] = useState(false) + + return ( + + setShow(true)}/> + setShow(false)} titleI18nKey={'editor.modal.revision.title'} icon={'history'} noteContent={noteContent}/> + + ) +} diff --git a/src/components/editor/document-bar/revisions/revision-modal-list-entry.tsx b/src/components/editor/document-bar/revisions/revision-modal-list-entry.tsx new file mode 100644 index 000000000..75eec4b34 --- /dev/null +++ b/src/components/editor/document-bar/revisions/revision-modal-list-entry.tsx @@ -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 +} + +export const RevisionModalListEntry: React.FC = ({ active, onClick, revision, revisionAuthorListMap }) => ( + + + + {moment(revision.timestamp * 1000).format('LLLL')} + + + + : {revision.length} + + + + { + revisionAuthorListMap.get(revision.timestamp)?.map((user, index) => { + return ( + + ) + }) + } + + +) diff --git a/src/components/editor/document-bar/revisions/revision-modal.scss b/src/components/editor/document-bar/revisions/revision-modal.scss new file mode 100644 index 000000000..9e57a1d6c --- /dev/null +++ b/src/components/editor/document-bar/revisions/revision-modal.scss @@ -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; + } +} diff --git a/src/components/editor/document-bar/revisions/revision-modal.tsx b/src/components/editor/document-bar/revisions/revision-modal.tsx new file mode 100644 index 000000000..8c9031ba4 --- /dev/null +++ b/src/components/editor/document-bar/revisions/revision-modal.tsx @@ -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 = ({ show, onHide, icon, titleI18nKey, noteContent }) => { + useTranslation() + const [revisions, setRevisions] = useState([]) + const [selectedRevisionTimestamp, setSelectedRevisionTimestamp] = useState(null) + const [selectedRevision, setSelectedRevision] = useState(null) + const [error, setError] = useState(false) + const revisionAuthorListMap = useRef(new Map()) + const revisionCacheMap = useRef(new Map()) + 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 ( + + + + + + { + revisions.map((revision, revisionIndex) => ( + setSelectedRevisionTimestamp(revision.timestamp)} + /> + )) + } + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/components/editor/document-bar/revisions/utils.ts b/src/components/editor/document-bar/revisions/utils.ts new file mode 100644 index 000000000..fc1440b1e --- /dev/null +++ b/src/components/editor/document-bar/revisions/utils.ts @@ -0,0 +1,39 @@ +import { Revision } from '../../../../api/revisions' +import { getUserById } from '../../../../api/users' +import { UserResponse } from '../../../../api/users/types' + +const userResponseCache = new Map() + +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 +} diff --git a/src/components/editor/editor.tsx b/src/components/editor/editor.tsx index 31f0429ab..6d5bbc6e0 100644 --- a/src/components/editor/editor.tsx +++ b/src/components/editor/editor.tsx @@ -113,7 +113,7 @@ export const Editor: React.FC = () => {
- + =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=