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:
Erik Michelson 2020-09-02 22:57:44 +02:00 committed by GitHub
parent 0fecda027c
commit d597438c42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 455 additions and 29 deletions

View file

@ -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",

View file

@ -0,0 +1,12 @@
[
{
"timestamp": 1598390307,
"length": 2788,
"authors": ["dermolly", "mrdrogdrog"]
},
{
"timestamp": 1598389571,
"length": 2782,
"authors": ["dermolly", "mrdrogdrog", "emcrx"]
}
]

View 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"]
}

View 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"]
}

View file

@ -0,0 +1,7 @@
{
"id": "dermolly",
"photo": "/avatar.png",
"name": "Philip",
"status": "ok",
"provider": "internal"
}

View file

@ -0,0 +1,7 @@
{
"id": "emcrx",
"photo": "/avatar.png",
"name": "Erik",
"status": "ok",
"provider": "internal"
}

View file

@ -0,0 +1,7 @@
{
"id": "mrdrogdrog",
"photo": "/avatar.png",
"name": "Tilman",
"status": "ok",
"provider": "internal"
}

View file

@ -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",

View file

@ -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> => {

View 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
View 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
View file

@ -0,0 +1,8 @@
import { LoginProvider } from '../../redux/user/types'
export interface UserResponse {
id: string
name: string
photo: string
provider: LoginProvider
}

View file

@ -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}>

View file

@ -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>

View file

@ -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'}/>
}

View file

@ -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>

View file

@ -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>
)
}

View file

@ -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>
)

View file

@ -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;
}
}

View 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>
)
}

View 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
}

View file

@ -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
View file

@ -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=