mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #3960 from overleaf/cmg-binary-file
[BinaryFile] Reopening of Binary file React migration GitOrigin-RevId: 050e66e3321bd6579d44932b669fc0a31df06d18
This commit is contained in:
parent
23c73b9bf1
commit
3dfcb95802
22 changed files with 1060 additions and 7 deletions
|
@ -866,6 +866,8 @@ const ProjectController = {
|
||||||
showReactShareModal: !wantsOldShareModalUI,
|
showReactShareModal: !wantsOldShareModalUI,
|
||||||
showReactAddFilesModal: !wantsOldAddFilesModalUI,
|
showReactAddFilesModal: !wantsOldAddFilesModalUI,
|
||||||
showReactGithubSync: !wantsOldGithubSyncUI && user.alphaProgram,
|
showReactGithubSync: !wantsOldGithubSyncUI && user.alphaProgram,
|
||||||
|
showNewBinaryFileUI:
|
||||||
|
req.query && req.query.new_binary_file === 'true',
|
||||||
})
|
})
|
||||||
timer.done()
|
timer.done()
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,11 @@ block content
|
||||||
|
|
||||||
.ui-layout-center
|
.ui-layout-center
|
||||||
include ./editor/editor
|
include ./editor/editor
|
||||||
include ./editor/binary-file
|
|
||||||
|
if showNewBinaryFileUI
|
||||||
|
include ./editor/binary-file-react
|
||||||
|
else
|
||||||
|
include ./editor/binary-file
|
||||||
include ./editor/history
|
include ./editor/history
|
||||||
|
|
||||||
if !isRestrictedTokenMember
|
if !isRestrictedTokenMember
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
div(
|
||||||
|
ng-controller="ReactBinaryFileController"
|
||||||
|
ng-show="ui.view == 'file'"
|
||||||
|
ng-if="openFile"
|
||||||
|
)
|
||||||
|
binary-file(
|
||||||
|
file='file'
|
||||||
|
store-references-keys='storeReferencesKeys'
|
||||||
|
)
|
|
@ -743,6 +743,8 @@ module.exports = settings =
|
||||||
createFileModes: []
|
createFileModes: []
|
||||||
gitBridge: []
|
gitBridge: []
|
||||||
publishModal: []
|
publishModal: []
|
||||||
|
tprLinkedFileInfo: []
|
||||||
|
tprLinkedFileRefreshError: []
|
||||||
}
|
}
|
||||||
|
|
||||||
csp: {
|
csp: {
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
"autocomplete": "",
|
"autocomplete": "",
|
||||||
"autocomplete_references": "",
|
"autocomplete_references": "",
|
||||||
"back_to_your_projects": "",
|
"back_to_your_projects": "",
|
||||||
"beta_badge_tooltip": "",
|
|
||||||
"blocked_filename": "",
|
"blocked_filename": "",
|
||||||
"can_edit": "",
|
"can_edit": "",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
|
@ -59,6 +58,7 @@
|
||||||
"dismiss": "",
|
"dismiss": "",
|
||||||
"dismiss_error_popup": "",
|
"dismiss_error_popup": "",
|
||||||
"done": "",
|
"done": "",
|
||||||
|
"download": "",
|
||||||
"download_pdf": "",
|
"download_pdf": "",
|
||||||
"drag_here": "",
|
"drag_here": "",
|
||||||
"duplicate_file": "",
|
"duplicate_file": "",
|
||||||
|
@ -103,7 +103,13 @@
|
||||||
"hide_outline": "",
|
"hide_outline": "",
|
||||||
"history": "",
|
"history": "",
|
||||||
"hotkeys": "",
|
"hotkeys": "",
|
||||||
|
"if_error_persists_try_relinking_provider": "",
|
||||||
"ignore_validation_errors": "",
|
"ignore_validation_errors": "",
|
||||||
|
"imported_from_another_project_at_date": "",
|
||||||
|
"imported_from_external_provider_at_date": "",
|
||||||
|
"imported_from_mendeley_at_date": "",
|
||||||
|
"imported_from_the_output_of_another_project_at_date": "",
|
||||||
|
"imported_from_zotero_at_date": "",
|
||||||
"importing_and_merging_changes_in_github": "",
|
"importing_and_merging_changes_in_github": "",
|
||||||
"invalid_email": "",
|
"invalid_email": "",
|
||||||
"invalid_file_name": "",
|
"invalid_file_name": "",
|
||||||
|
@ -148,6 +154,7 @@
|
||||||
"new_name": "",
|
"new_name": "",
|
||||||
"no_messages": "",
|
"no_messages": "",
|
||||||
"no_new_commits_in_github": "",
|
"no_new_commits_in_github": "",
|
||||||
|
"no_preview_available": "",
|
||||||
"normal": "",
|
"normal": "",
|
||||||
"off": "",
|
"off": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
|
@ -189,6 +196,7 @@
|
||||||
"reference_error_relink_hint": "",
|
"reference_error_relink_hint": "",
|
||||||
"refresh": "",
|
"refresh": "",
|
||||||
"refresh_page_after_starting_free_trial": "",
|
"refresh_page_after_starting_free_trial": "",
|
||||||
|
"refreshing": "",
|
||||||
"remote_service_error": "",
|
"remote_service_error": "",
|
||||||
"remove_collaborator": "",
|
"remove_collaborator": "",
|
||||||
"rename": "",
|
"rename": "",
|
||||||
|
|
|
@ -0,0 +1,268 @@
|
||||||
|
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Icon from '../../../shared/components/icon'
|
||||||
|
import { formatTime, relativeDate } from '../../utils/format-date'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
|
||||||
|
import { postJSON } from '../../../infrastructure/fetch-json'
|
||||||
|
|
||||||
|
const tprLinkedFileInfo = importOverleafModules('tprLinkedFileInfo')
|
||||||
|
const tprLinkedFileRefreshError = importOverleafModules(
|
||||||
|
'tprLinkedFileRefreshError'
|
||||||
|
)
|
||||||
|
|
||||||
|
const MAX_URL_LENGTH = 60
|
||||||
|
const FRONT_OF_URL_LENGTH = 35
|
||||||
|
const FILLER = '...'
|
||||||
|
const TAIL_OF_URL_LENGTH = MAX_URL_LENGTH - FRONT_OF_URL_LENGTH - FILLER.length
|
||||||
|
|
||||||
|
function shortenedUrl(url) {
|
||||||
|
if (!url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (url.length > MAX_URL_LENGTH) {
|
||||||
|
const front = url.slice(0, FRONT_OF_URL_LENGTH)
|
||||||
|
const tail = url.slice(url.length - TAIL_OF_URL_LENGTH)
|
||||||
|
return front + FILLER + tail
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BinaryFileHeader({ file, storeReferencesKeys }) {
|
||||||
|
const isMounted = useRef(true)
|
||||||
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [refreshError, setRefreshError] = useState(null)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// set to false on unmount to avoid unmounted component warning when refreshing
|
||||||
|
return () => (isMounted.current = false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
let fileInfo
|
||||||
|
if (file.linkedFileData) {
|
||||||
|
if (file.linkedFileData.provider === 'url') {
|
||||||
|
fileInfo = (
|
||||||
|
<div>
|
||||||
|
<UrlProvider file={file} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else if (file.linkedFileData.provider === 'project_file') {
|
||||||
|
fileInfo = (
|
||||||
|
<div>
|
||||||
|
<ProjectFilePathProvider file={file} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else if (file.linkedFileData.provider === 'project_output_file') {
|
||||||
|
fileInfo = (
|
||||||
|
<div>
|
||||||
|
<ProjectOutputFileProvider file={file} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshFile = useCallback(() => {
|
||||||
|
setRefreshing(true)
|
||||||
|
// Replacement of the file handled by the file tree
|
||||||
|
window.expectingLinkedFileRefreshedSocketFor = file.name
|
||||||
|
postJSON(`/project/${window.project_id}/linked_file/${file.id}/refresh`, {
|
||||||
|
disableAutoLoginRedirect: true,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (isMounted.current) {
|
||||||
|
setRefreshing(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (isMounted.current) {
|
||||||
|
setRefreshing(false)
|
||||||
|
setRefreshError(err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (
|
||||||
|
file.linkedFileData.provider === 'mendeley' ||
|
||||||
|
file.linkedFileData.provider === 'zotero' ||
|
||||||
|
file.name.match(/^.*\.bib$/)
|
||||||
|
) {
|
||||||
|
reindexReferences()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function reindexReferences() {
|
||||||
|
const opts = {
|
||||||
|
body: { shouldBroadcast: true },
|
||||||
|
}
|
||||||
|
|
||||||
|
postJSON(`/project/${window.project_id}/references/indexAll`, opts)
|
||||||
|
.then(response => {
|
||||||
|
// Later updated by the socket but also updated here for immediate use
|
||||||
|
storeReferencesKeys(response.keys)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [file, isMounted, storeReferencesKeys])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="binary-file-header">
|
||||||
|
{file.linkedFileData && fileInfo}
|
||||||
|
{file.linkedFileData &&
|
||||||
|
tprLinkedFileInfo.map(({ import: { LinkedFileInfo }, path }) => (
|
||||||
|
<LinkedFileInfo key={path} file={file} />
|
||||||
|
))}
|
||||||
|
{file.linkedFileData && (
|
||||||
|
<button
|
||||||
|
className="btn btn-success"
|
||||||
|
onClick={refreshFile}
|
||||||
|
disabled={refreshing}
|
||||||
|
>
|
||||||
|
<Icon type="refresh" spin={refreshing} modifier="fw" />
|
||||||
|
<span>{refreshing ? t('refreshing') + '...' : t('refresh')}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={`/project/${window.project_id}/file/${file.id}`}
|
||||||
|
className="btn btn-info"
|
||||||
|
>
|
||||||
|
<Icon type="download" modifier="fw" />
|
||||||
|
<span>{' ' + t('download')}</span>
|
||||||
|
</a>
|
||||||
|
{refreshError && (
|
||||||
|
<div className="row">
|
||||||
|
<br />
|
||||||
|
<div className="alert alert-danger col-md-6 col-md-offset-3">
|
||||||
|
Error: {refreshError}
|
||||||
|
{tprLinkedFileRefreshError.map(
|
||||||
|
({ import: { LinkedFileRefreshError }, path }) => (
|
||||||
|
<LinkedFileRefreshError key={path} file={file} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryFileHeader.propTypes = {
|
||||||
|
file: PropTypes.shape({
|
||||||
|
id: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
linkedFileData: PropTypes.object,
|
||||||
|
}).isRequired,
|
||||||
|
storeReferencesKeys: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
function UrlProvider({ file }) {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
<Icon
|
||||||
|
type="external-link-square"
|
||||||
|
modifier="rotate-180"
|
||||||
|
classes={{ icon: 'linked-file-icon' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Trans
|
||||||
|
i18nKey="imported_from_external_provider_at_date"
|
||||||
|
components={
|
||||||
|
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||||
|
[<a href={file.linkedFileData.url} />]
|
||||||
|
}
|
||||||
|
values={{
|
||||||
|
shortenedUrl: shortenedUrl(file.linkedFileData.url),
|
||||||
|
formattedDate: formatTime(file.created),
|
||||||
|
relativeDate: relativeDate(file.created),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
UrlProvider.propTypes = {
|
||||||
|
file: PropTypes.shape({
|
||||||
|
linkedFileData: PropTypes.object,
|
||||||
|
created: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectFilePathProvider({ file }) {
|
||||||
|
/* eslint-disable jsx-a11y/anchor-has-content, react/jsx-key */
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
<Icon
|
||||||
|
type="external-link-square"
|
||||||
|
modifier="rotate-180"
|
||||||
|
classes={{ icon: 'linked-file-icon' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Trans
|
||||||
|
i18nKey="imported_from_another_project_at_date"
|
||||||
|
components={
|
||||||
|
file.linkedFileData.v1_source_doc_id
|
||||||
|
? [<span />]
|
||||||
|
: [
|
||||||
|
<a
|
||||||
|
href={`/project/${file.linkedFileData.source_project_id}`}
|
||||||
|
target="_blank"
|
||||||
|
/>,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
values={{
|
||||||
|
sourceEntityPath: file.linkedFileData.source_entity_path.slice(1),
|
||||||
|
formattedDate: formatTime(file.created),
|
||||||
|
relativeDate: relativeDate(file.created),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
/* esline-enable jsx-a11y/anchor-has-content, react/jsx-key */
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectFilePathProvider.propTypes = {
|
||||||
|
file: PropTypes.shape({
|
||||||
|
linkedFileData: PropTypes.object,
|
||||||
|
created: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectOutputFileProvider({ file }) {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
<Icon
|
||||||
|
type="external-link-square"
|
||||||
|
modifier="rotate-180"
|
||||||
|
classes={{ icon: 'linked-file-icon' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Trans
|
||||||
|
i18nKey="imported_from_the_output_of_another_project_at_date"
|
||||||
|
components={
|
||||||
|
file.linkedFileData.v1_source_doc_id
|
||||||
|
? [<span />]
|
||||||
|
: [
|
||||||
|
<a
|
||||||
|
href={`/project/${file.linkedFileData.source_project_id}`}
|
||||||
|
target="_blank"
|
||||||
|
/>,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
values={{
|
||||||
|
sourceOutputFilePath: file.linkedFileData.source_output_file_path,
|
||||||
|
formattedDate: formatTime(file.created),
|
||||||
|
relativeDate: relativeDate(file.created),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectOutputFileProvider.propTypes = {
|
||||||
|
file: PropTypes.shape({
|
||||||
|
linkedFileData: PropTypes.object,
|
||||||
|
created: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default function BinaryFileImage({ fileName, fileId, onLoad, onError }) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={`/project/${window.project_id}/file/${fileId}`}
|
||||||
|
onLoad={onLoad}
|
||||||
|
onError={onError}
|
||||||
|
onAbort={onError}
|
||||||
|
alt={fileName}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryFileImage.propTypes = {
|
||||||
|
fileName: PropTypes.string.isRequired,
|
||||||
|
fileId: PropTypes.string.isRequired,
|
||||||
|
onLoad: PropTypes.func.isRequired,
|
||||||
|
onError: PropTypes.func.isRequired,
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 2 * 1024 * 1024
|
||||||
|
|
||||||
|
export default function BinaryFileText({ file, onLoad, onError }) {
|
||||||
|
const [textPreview, setTextPreview] = useState('')
|
||||||
|
const [shouldShowDots, setShouldShowDots] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let path = `/project/${window.project_id}/file/${file.id}`
|
||||||
|
fetch(path, { method: 'HEAD' })
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('HTTP Error Code: ' + response.status)
|
||||||
|
return response.headers.get('Content-Length')
|
||||||
|
})
|
||||||
|
.then(fileSize => {
|
||||||
|
let truncated = false
|
||||||
|
let maxSize = null
|
||||||
|
if (fileSize > MAX_FILE_SIZE) {
|
||||||
|
truncated = true
|
||||||
|
maxSize = MAX_FILE_SIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxSize != null) {
|
||||||
|
path += `?range=0-${maxSize}`
|
||||||
|
}
|
||||||
|
fetch(path)
|
||||||
|
.then(response => {
|
||||||
|
response.text().then(text => {
|
||||||
|
if (truncated) {
|
||||||
|
text = text.replace(/\n.*$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
setTextPreview(text)
|
||||||
|
onLoad()
|
||||||
|
setShouldShowDots(truncated)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
onError()
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
onError()
|
||||||
|
})
|
||||||
|
}, [file.id, onError, onLoad])
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{textPreview && (
|
||||||
|
<div className="text-preview">
|
||||||
|
<div className="scroll-container">
|
||||||
|
<p>{textPreview}</p>
|
||||||
|
{shouldShowDots && <p>...</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryFileText.propTypes = {
|
||||||
|
file: PropTypes.shape({ id: PropTypes.string }).isRequired,
|
||||||
|
onLoad: PropTypes.func.isRequired,
|
||||||
|
onError: PropTypes.func.isRequired,
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import BinaryFileHeader from './binary-file-header'
|
||||||
|
import BinaryFileImage from './binary-file-image'
|
||||||
|
import BinaryFileText from './binary-file-text'
|
||||||
|
import Icon from '../../../shared/components/icon'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif']
|
||||||
|
|
||||||
|
const textExtensions = [
|
||||||
|
'tex',
|
||||||
|
'latex',
|
||||||
|
'sty',
|
||||||
|
'cls',
|
||||||
|
'bst',
|
||||||
|
'bib',
|
||||||
|
'bibtex',
|
||||||
|
'txt',
|
||||||
|
'tikz',
|
||||||
|
'mtx',
|
||||||
|
'rtex',
|
||||||
|
'md',
|
||||||
|
'asy',
|
||||||
|
'latexmkrc',
|
||||||
|
'lbx',
|
||||||
|
'bbx',
|
||||||
|
'cbx',
|
||||||
|
'm',
|
||||||
|
'lco',
|
||||||
|
'dtx',
|
||||||
|
'ins',
|
||||||
|
'ist',
|
||||||
|
'def',
|
||||||
|
'clo',
|
||||||
|
'ldf',
|
||||||
|
'rmd',
|
||||||
|
'lua',
|
||||||
|
'gv',
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function BinaryFile({ file, storeReferencesKeys }) {
|
||||||
|
const extension = file.name.split('.').pop().toLowerCase()
|
||||||
|
|
||||||
|
const [contentLoading, setContentLoading] = useState(true)
|
||||||
|
const [hasError, setHasError] = useState(false)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const isUnpreviewableFile =
|
||||||
|
!imageExtensions.includes(extension) && !textExtensions.includes(extension)
|
||||||
|
|
||||||
|
function handleLoading() {
|
||||||
|
if (contentLoading) {
|
||||||
|
setContentLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError() {
|
||||||
|
if (!hasError) {
|
||||||
|
setContentLoading(false)
|
||||||
|
setHasError(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
<BinaryFileHeader file={file} storeReferencesKeys={storeReferencesKeys} />
|
||||||
|
{imageExtensions.includes(extension) && (
|
||||||
|
<BinaryFileImage
|
||||||
|
fileName={file.name}
|
||||||
|
fileId={file.id}
|
||||||
|
onLoad={handleLoading}
|
||||||
|
onError={handleError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{textExtensions.includes(extension) && (
|
||||||
|
<BinaryFileText
|
||||||
|
file={file}
|
||||||
|
onLoad={handleLoading}
|
||||||
|
onError={handleError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="binary-file full-size">
|
||||||
|
{!hasError && content}
|
||||||
|
{!isUnpreviewableFile && contentLoading && <BinaryFileLoadingIndicator />}
|
||||||
|
{(isUnpreviewableFile || hasError) && (
|
||||||
|
<p className="no-preview">{t('no_preview_available')}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BinaryFileLoadingIndicator() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<div className="loading-panel loading-panel-binary-files">
|
||||||
|
<span>
|
||||||
|
<Icon type="refresh" modifier="spin" />
|
||||||
|
{t('loading')}…
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryFile.propTypes = {
|
||||||
|
file: PropTypes.shape({
|
||||||
|
id: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
storeReferencesKeys: PropTypes.func.isRequired,
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import App from '../../../base'
|
||||||
|
import { react2angular } from 'react2angular'
|
||||||
|
import BinaryFile from '../components/binary-file'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
export default App.controller(
|
||||||
|
'ReactBinaryFileController',
|
||||||
|
function ($scope, $rootScope) {
|
||||||
|
$scope.file = $scope.openFile
|
||||||
|
|
||||||
|
$scope.storeReferencesKeys = newKeys => {
|
||||||
|
const oldKeys = $rootScope._references.keys
|
||||||
|
return ($rootScope._references.keys = _.union(oldKeys, newKeys))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
App.component(
|
||||||
|
'binaryFile',
|
||||||
|
react2angular(BinaryFile, ['storeReferencesKeys', 'file'])
|
||||||
|
)
|
20
services/web/frontend/js/features/utils/format-date.js
Normal file
20
services/web/frontend/js/features/utils/format-date.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
moment.updateLocale('en', {
|
||||||
|
calendar: {
|
||||||
|
lastDay: '[Yesterday]',
|
||||||
|
sameDay: '[Today]',
|
||||||
|
nextDay: '[Tomorrow]',
|
||||||
|
lastWeek: 'ddd, Do MMM YY',
|
||||||
|
nextWeek: 'ddd, Do MMM YY',
|
||||||
|
sameElse: 'ddd, Do MMM YY',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function formatTime(date) {
|
||||||
|
return moment(date).format('h:mm a')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function relativeDate(date) {
|
||||||
|
return moment(date).calendar()
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ import SafariScrollPatcher from './ide/SafariScrollPatcher'
|
||||||
import './ide/cobranding/CobrandingDataService'
|
import './ide/cobranding/CobrandingDataService'
|
||||||
import './ide/settings/index'
|
import './ide/settings/index'
|
||||||
import './ide/share/index'
|
import './ide/share/index'
|
||||||
|
import './ide/binary-files/index'
|
||||||
import './ide/chat/index'
|
import './ide/chat/index'
|
||||||
import './ide/clone/index'
|
import './ide/clone/index'
|
||||||
import './ide/hotkeys/index'
|
import './ide/hotkeys/index'
|
||||||
|
|
1
services/web/frontend/js/ide/binary-files/index.js
Normal file
1
services/web/frontend/js/ide/binary-files/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import '../../features/binary-file/controllers/binary-file-controller'
|
173
services/web/frontend/stories/binary-file.stories.js
Normal file
173
services/web/frontend/stories/binary-file.stories.js
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import BinaryFile from '../js/features/binary-file/components/binary-file'
|
||||||
|
import fetchMock from 'fetch-mock'
|
||||||
|
|
||||||
|
window.project_id = 'proj123'
|
||||||
|
fetchMock.restore()
|
||||||
|
fetchMock.head('express:/project/:project_id/file/:file_id', {
|
||||||
|
status: 201,
|
||||||
|
headers: { 'Content-Length': 10000 },
|
||||||
|
})
|
||||||
|
fetchMock.get('express:/project/:project_id/file/:file_id', 'Text file content')
|
||||||
|
|
||||||
|
fetchMock.post('express:/project/:project_id/linked_file/:file_id/refresh', {
|
||||||
|
status: 204,
|
||||||
|
})
|
||||||
|
|
||||||
|
fetchMock.post('express:/project/:project_id/references/indexAll', {
|
||||||
|
status: 204,
|
||||||
|
})
|
||||||
|
|
||||||
|
window.project_id = '1234'
|
||||||
|
|
||||||
|
export const FileFromUrl = args => {
|
||||||
|
return <BinaryFile {...args} />
|
||||||
|
}
|
||||||
|
FileFromUrl.args = {
|
||||||
|
file: {
|
||||||
|
linkedFileData: {
|
||||||
|
url: 'https://overleaf.com',
|
||||||
|
provider: 'url',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileFromProjectWithLinkableProjectId = args => {
|
||||||
|
return <BinaryFile {...args} />
|
||||||
|
}
|
||||||
|
FileFromProjectWithLinkableProjectId.args = {
|
||||||
|
file: {
|
||||||
|
linkedFileData: {
|
||||||
|
source_project_id: 'source-project-id',
|
||||||
|
source_entity_path: '/source-entity-path.ext',
|
||||||
|
provider: 'project_file',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileFromProjectWithoutLinkableProjectId = args => {
|
||||||
|
return <BinaryFile {...args} />
|
||||||
|
}
|
||||||
|
FileFromProjectWithoutLinkableProjectId.args = {
|
||||||
|
file: {
|
||||||
|
linkedFileData: {
|
||||||
|
v1_source_doc_id: 'v1-source-id',
|
||||||
|
source_entity_path: '/source-entity-path.ext',
|
||||||
|
provider: 'project_file',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileFromProjectOutputWithLinkableProject = args => {
|
||||||
|
return <BinaryFile {...args} />
|
||||||
|
}
|
||||||
|
FileFromProjectOutputWithLinkableProject.args = {
|
||||||
|
file: {
|
||||||
|
linkedFileData: {
|
||||||
|
source_project_id: 'source_project_id',
|
||||||
|
source_output_file_path: '/source-entity-path.ext',
|
||||||
|
provider: 'project_output_file',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileFromProjectOutputWithoutLinkableProjectId = args => {
|
||||||
|
return <BinaryFile {...args} />
|
||||||
|
}
|
||||||
|
FileFromProjectOutputWithoutLinkableProjectId.args = {
|
||||||
|
file: {
|
||||||
|
linkedFileData: {
|
||||||
|
v1_source_doc_id: 'v1-source-id',
|
||||||
|
source_output_file_path: '/source-entity-path.ext',
|
||||||
|
provider: 'project_output_file',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImageFile = args => {
|
||||||
|
return <BinaryFile {...args} />
|
||||||
|
}
|
||||||
|
ImageFile.args = {
|
||||||
|
file: {
|
||||||
|
id: '60097ca20454610027c442a8',
|
||||||
|
name: 'file.jpg',
|
||||||
|
linkedFileData: {
|
||||||
|
source_project_id: 'source_project_id',
|
||||||
|
source_entity_path: '/source-entity-path',
|
||||||
|
provider: 'project_file',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThirdPartyReferenceFile = args => {
|
||||||
|
return <BinaryFile {...args} />
|
||||||
|
}
|
||||||
|
|
||||||
|
ThirdPartyReferenceFile.args = {
|
||||||
|
file: {
|
||||||
|
name: 'example.tex',
|
||||||
|
linkedFileData: {
|
||||||
|
provider: 'zotero',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThirdPartyReferenceFileWithError = args => {
|
||||||
|
return <BinaryFile {...args} />
|
||||||
|
}
|
||||||
|
|
||||||
|
ThirdPartyReferenceFileWithError.args = {
|
||||||
|
file: {
|
||||||
|
id: '500500500500500500500500',
|
||||||
|
name: 'example.tex',
|
||||||
|
linkedFileData: {
|
||||||
|
provider: 'zotero',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextFile = args => {
|
||||||
|
return <BinaryFile {...args} />
|
||||||
|
}
|
||||||
|
TextFile.args = {
|
||||||
|
file: {
|
||||||
|
linkedFileData: {
|
||||||
|
source_project_id: 'source-project-id',
|
||||||
|
source_entity_path: '/source-entity-path.ext',
|
||||||
|
provider: 'project_file',
|
||||||
|
},
|
||||||
|
name: 'file.txt',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UploadedFile = args => {
|
||||||
|
return <BinaryFile {...args} />
|
||||||
|
}
|
||||||
|
UploadedFile.args = {
|
||||||
|
file: {
|
||||||
|
linkedFileData: null,
|
||||||
|
name: 'file.jpg',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'BinaryFile',
|
||||||
|
component: BinaryFile,
|
||||||
|
args: {
|
||||||
|
file: {
|
||||||
|
id: 'file-id',
|
||||||
|
name: 'file.tex',
|
||||||
|
created: new Date(),
|
||||||
|
},
|
||||||
|
storeReferencesKeys: () => {},
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
BinaryFile => (
|
||||||
|
<>
|
||||||
|
<style>{'html, body { height: 100%; }'}</style>
|
||||||
|
<BinaryFile />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
26
services/web/frontend/stories/linked-file.stories.js
Normal file
26
services/web/frontend/stories/linked-file.stories.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { LinkedFileInfo } from '../../modules/tpr-webmodule/frontend/js/components/linked-file-info'
|
||||||
|
|
||||||
|
export const MendeleyLinkedFile = args => {
|
||||||
|
return <LinkedFileInfo {...args} />
|
||||||
|
}
|
||||||
|
|
||||||
|
MendeleyLinkedFile.args = {
|
||||||
|
file: {
|
||||||
|
linkedFileData: {
|
||||||
|
provider: 'mendeley',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'LinkedFileInfo',
|
||||||
|
component: LinkedFileInfo,
|
||||||
|
args: {
|
||||||
|
file: {
|
||||||
|
id: 'file-id',
|
||||||
|
name: 'file.tex',
|
||||||
|
created: new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -190,6 +190,10 @@
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-panel-binary-files {
|
||||||
|
background-color: @gray-lightest;
|
||||||
|
}
|
||||||
|
|
||||||
.error-panel {
|
.error-panel {
|
||||||
.full-size;
|
.full-size;
|
||||||
padding: @line-height-computed;
|
padding: @line-height-computed;
|
||||||
|
|
|
@ -13,11 +13,6 @@
|
||||||
.box-shadow(0 2px 3px @gray;);
|
.box-shadow(0 2px 3px @gray;);
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
.img-preview {
|
|
||||||
background: url('/img/spinner.gif') no-repeat;
|
|
||||||
min-width: 200px;
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
p.no-preview {
|
p.no-preview {
|
||||||
margin-top: @line-height-computed / 2;
|
margin-top: @line-height-computed / 2;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|
|
@ -1379,6 +1379,15 @@
|
||||||
"confirm_affiliation": "Confirm Affiliation",
|
"confirm_affiliation": "Confirm Affiliation",
|
||||||
"please_check_your_inbox_to_confirm": "Please check your email inbox to confirm your <0>__institutionName__</0> affiliation.",
|
"please_check_your_inbox_to_confirm": "Please check your email inbox to confirm your <0>__institutionName__</0> affiliation.",
|
||||||
"your_affiliation_is_confirmed": "Your <0>__institutionName__</0> affiliation is confirmed.",
|
"your_affiliation_is_confirmed": "Your <0>__institutionName__</0> affiliation is confirmed.",
|
||||||
|
"thank_you": "Thank you!",
|
||||||
|
"add_email": "Add Email",
|
||||||
|
"imported_from_mendeley_at_date": "Imported from Mendeley at __formattedDate__ __relativeDate__",
|
||||||
|
"imported_from_zotero_at_date": "Imported from Zotero at __formattedDate__ __relativeDate__",
|
||||||
|
"imported_from_external_provider_at_date": "Imported from <0>__shortenedUrlHTML__</0> at __formattedDate__ __relativeDate__",
|
||||||
|
"imported_from_another_project_at_date": "Imported from <0>Another project</0>/__sourceEntityPathHTML__, at __formattedDate__ __relativeDate__",
|
||||||
|
"imported_from_the_output_of_another_project_at_date": "Imported from the output of <0>Another project</0>: __sourceOutputFilePathHTML__, at __formattedDate__ __relativeDate__",
|
||||||
|
"refreshing": "Refreshing",
|
||||||
|
"if_error_persists_try_relinking_provider": "If this error persists, try re-linking your __provider__ account here",
|
||||||
"thank_you_exclamation": "Thank you!",
|
"thank_you_exclamation": "Thank you!",
|
||||||
"add_email": "Add Email"
|
"add_email": "Add Email"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,168 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
fireEvent,
|
||||||
|
waitForElementToBeRemoved,
|
||||||
|
} from '@testing-library/react'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import fetchMock from 'fetch-mock'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
|
||||||
|
import BinaryFileHeader from '../../../../../frontend/js/features/binary-file/components/binary-file-header.js'
|
||||||
|
|
||||||
|
describe('<BinaryFileHeader/>', function () {
|
||||||
|
const urlFile = {
|
||||||
|
name: 'example.tex',
|
||||||
|
linkedFileData: {
|
||||||
|
url: 'https://overleaf.com',
|
||||||
|
provider: 'url',
|
||||||
|
},
|
||||||
|
created: new Date(2021, 1, 17, 3, 24).toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectFile = {
|
||||||
|
name: 'example.tex',
|
||||||
|
linkedFileData: {
|
||||||
|
v1_source_doc_id: 'v1-source-id',
|
||||||
|
source_project_id: 'source-project-id',
|
||||||
|
source_entity_path: '/source-entity-path.ext',
|
||||||
|
provider: 'project_file',
|
||||||
|
},
|
||||||
|
created: new Date(2021, 1, 17, 3, 24).toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectOutputFile = {
|
||||||
|
name: 'example.pdf',
|
||||||
|
linkedFileData: {
|
||||||
|
v1_source_doc_id: 'v1-source-id',
|
||||||
|
source_output_file_path: '/source-entity-path.ext',
|
||||||
|
provider: 'project_output_file',
|
||||||
|
},
|
||||||
|
created: new Date(2021, 1, 17, 3, 24).toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const thirdPartyReferenceFile = {
|
||||||
|
name: 'example.tex',
|
||||||
|
linkedFileData: {
|
||||||
|
provider: 'zotero',
|
||||||
|
},
|
||||||
|
created: new Date(2021, 1, 17, 3, 24).toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let storeReferencesKeys
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
fetchMock.reset()
|
||||||
|
storeReferencesKeys = sinon.stub()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('header text', function () {
|
||||||
|
it('Renders the correct text for a file with the url provider', function () {
|
||||||
|
render(<BinaryFileHeader file={urlFile} storeReferencesKeys={() => {}} />)
|
||||||
|
screen.getByText('Imported from', { exact: false })
|
||||||
|
screen.getByText('at 3:24 am Wed, 17th Feb 21', {
|
||||||
|
exact: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders the correct text for a file with the project_file provider', function () {
|
||||||
|
render(
|
||||||
|
<BinaryFileHeader file={projectFile} storeReferencesKeys={() => {}} />
|
||||||
|
)
|
||||||
|
screen.getByText('Imported from', { exact: false })
|
||||||
|
screen.getByText('Another project', { exact: false })
|
||||||
|
screen.getByText('/source-entity-path.ext, at 3:24 am Wed, 17th Feb 21', {
|
||||||
|
exact: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders the correct text for a file with the project_output_file provider', function () {
|
||||||
|
render(
|
||||||
|
<BinaryFileHeader
|
||||||
|
file={projectOutputFile}
|
||||||
|
storeReferencesKeys={() => {}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
screen.getByText('Imported from the output of', { exact: false })
|
||||||
|
screen.getByText('Another project', { exact: false })
|
||||||
|
screen.getByText('/source-entity-path.ext, at 3:24 am Wed, 17th Feb 21', {
|
||||||
|
exact: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('The refresh button', async function () {
|
||||||
|
let reindexResponse
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
window.project_id = '123abc'
|
||||||
|
reindexResponse = {
|
||||||
|
projectId: '123abc',
|
||||||
|
keys: ['reference1', 'reference2', 'reference3', 'reference4'],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
delete window.project_id
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Changes text when the file is refreshing', async function () {
|
||||||
|
fetchMock.post(
|
||||||
|
'express:/project/:project_id/linked_file/:file_id/refresh',
|
||||||
|
{
|
||||||
|
new_file_id: '5ff7418157b4e144321df5c4',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
render(
|
||||||
|
<BinaryFileHeader file={projectFile} storeReferencesKeys={() => {}} />
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
|
||||||
|
|
||||||
|
await waitForElementToBeRemoved(() =>
|
||||||
|
screen.getByText('Refreshing', { exact: false })
|
||||||
|
)
|
||||||
|
await screen.findByText('Refresh')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Reindexes references after refreshing a file from a third-party provider', async function () {
|
||||||
|
fetchMock.post(
|
||||||
|
'express:/project/:project_id/linked_file/:file_id/refresh',
|
||||||
|
{
|
||||||
|
new_file_id: '5ff7418157b4e144321df5c4',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
fetchMock.post(
|
||||||
|
'express:/project/:project_id/references/indexAll',
|
||||||
|
reindexResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
render(
|
||||||
|
<BinaryFileHeader
|
||||||
|
file={thirdPartyReferenceFile}
|
||||||
|
storeReferencesKeys={storeReferencesKeys}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
|
||||||
|
|
||||||
|
await waitForElementToBeRemoved(() =>
|
||||||
|
screen.getByText('Refreshing', { exact: false })
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(fetchMock.done()).to.be.true
|
||||||
|
expect(storeReferencesKeys).to.be.calledWith(reindexResponse.keys)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('The download button', function () {
|
||||||
|
it('exists', function () {
|
||||||
|
render(<BinaryFileHeader file={urlFile} storeReferencesKeys={() => {}} />)
|
||||||
|
|
||||||
|
screen.getByText('Download', { exact: false })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
|
||||||
|
import BinaryFileImage from '../../../../../frontend/js/features/binary-file/components/binary-file-image.js'
|
||||||
|
|
||||||
|
describe('<BinaryFileImage />', function () {
|
||||||
|
const file = {
|
||||||
|
id: '60097ca20454610027c442a8',
|
||||||
|
name: 'file.jpg',
|
||||||
|
linkedFileData: {
|
||||||
|
source_entity_path: '/source-entity-path',
|
||||||
|
provider: 'project_file',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
it('renders an image', function () {
|
||||||
|
render(
|
||||||
|
<BinaryFileImage
|
||||||
|
fileName={file.name}
|
||||||
|
fileId={file.id}
|
||||||
|
onError={() => {}}
|
||||||
|
onLoad={() => {}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
screen.getByRole('img')
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import fetchMock from 'fetch-mock'
|
||||||
|
|
||||||
|
import BinaryFileText from '../../../../../frontend/js/features/binary-file/components/binary-file-text.js'
|
||||||
|
|
||||||
|
describe('<BinaryFileText/>', function () {
|
||||||
|
const file = {
|
||||||
|
name: 'example.tex',
|
||||||
|
linkedFileData: {
|
||||||
|
v1_source_doc_id: 'v1-source-id',
|
||||||
|
source_project_id: 'source-project-id',
|
||||||
|
source_entity_path: '/source-entity-path.ext',
|
||||||
|
provider: 'project_file',
|
||||||
|
},
|
||||||
|
created: new Date(2021, 1, 17, 3, 24).toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
fetchMock.reset()
|
||||||
|
window.project_id = '123abc'
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
delete window.project_id
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a text view', async function () {
|
||||||
|
fetchMock.head('express:/project/:project_id/file/:file_id', {
|
||||||
|
status: 201,
|
||||||
|
headers: { 'Content-Length': 10000 },
|
||||||
|
})
|
||||||
|
fetchMock.get(
|
||||||
|
'express:/project/:project_id/file/:file_id',
|
||||||
|
'Text file content'
|
||||||
|
)
|
||||||
|
|
||||||
|
render(<BinaryFileText file={file} onError={() => {}} onLoad={() => {}} />)
|
||||||
|
|
||||||
|
await screen.findByText('Text file content', { exact: false })
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,71 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
waitForElementToBeRemoved,
|
||||||
|
fireEvent,
|
||||||
|
} from '@testing-library/react'
|
||||||
|
import fetchMock from 'fetch-mock'
|
||||||
|
|
||||||
|
import BinaryFile from '../../../../../frontend/js/features/binary-file/components/binary-file.js'
|
||||||
|
|
||||||
|
describe('<BinaryFile/>', function () {
|
||||||
|
const textFile = {
|
||||||
|
name: 'example.tex',
|
||||||
|
linkedFileData: {
|
||||||
|
v1_source_doc_id: 'v1-source-id',
|
||||||
|
source_project_id: 'source-project-id',
|
||||||
|
source_entity_path: '/source-entity-path.ext',
|
||||||
|
provider: 'project_file',
|
||||||
|
},
|
||||||
|
created: new Date(2021, 1, 17, 3, 24).toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageFile = {
|
||||||
|
id: '60097ca20454610027c442a8',
|
||||||
|
name: 'file.jpg',
|
||||||
|
linkedFileData: {
|
||||||
|
source_entity_path: '/source-entity-path',
|
||||||
|
provider: 'project_file',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
fetchMock.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('for a text file', function () {
|
||||||
|
it('it shows a loading indicator while the file is loading', async function () {
|
||||||
|
render(<BinaryFile file={textFile} storeReferencesKeys={() => {}} />)
|
||||||
|
|
||||||
|
await waitForElementToBeRemoved(() =>
|
||||||
|
screen.getByText('Loading', { exact: false })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('it shows messaging if the text view could not be loaded', async function () {
|
||||||
|
render(<BinaryFile file={textFile} storeReferencesKeys={() => {}} />)
|
||||||
|
|
||||||
|
await screen.findByText('Sorry, no preview is available', {
|
||||||
|
exact: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('for an image file', function () {
|
||||||
|
it('it shows a loading indicator while the file is loading', async function () {
|
||||||
|
render(<BinaryFile file={imageFile} storeReferencesKeys={() => {}} />)
|
||||||
|
|
||||||
|
screen.getByText('Loading', { exact: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('it shows messaging if the image could not be loaded', function () {
|
||||||
|
render(<BinaryFile file={imageFile} storeReferencesKeys={() => {}} />)
|
||||||
|
|
||||||
|
// Fake the image request failing as the request is handled by the browser
|
||||||
|
fireEvent.error(screen.getByRole('img'))
|
||||||
|
|
||||||
|
screen.findByText('Sorry, no preview is available', { exact: false })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in a new issue