overleaf/services/web/frontend/js/features/file-view/components/file-view-header.js
Tyna William 3297506021 Revert "Revert "Tw reference manager access"" (#13825)
* Revert "Revert "Tw reference manager access""

This reverts commit 55f3512f254e136d8a239bb5d9ca39f0d04720fd.

* added conditional to check whether file linked data exists

* created new translation key for refresh error

* Fix for misleading reference manager access error to ensure users are aware only original importer of reference file can refresh it. If original importer, then user is prompted to relink provider account. Previous PR and conversation #13618.

GitOrigin-RevId: 85232c1559c6d2068effec50e31b8fa30bf53c89
2023-07-17 11:06:48 +00:00

292 lines
8.4 KiB
JavaScript

import { useState, useCallback } from 'react'
import PropTypes from 'prop-types'
import { Trans, useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
import { formatTime, relativeDate } from '../../utils/format-date'
import { postJSON } from '../../../infrastructure/fetch-json'
import { useEditorContext } from '../../../shared/context/editor-context'
import { useProjectContext } from '../../../shared/context/project-context'
import { useUserContext } from '../../../shared/context/user-context'
import { capitalize } from 'lodash'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import useAbortController from '../../../shared/hooks/use-abort-controller'
import { LinkedFileIcon } from './file-view-icons'
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 FileViewHeader({ file, storeReferencesKeys }) {
const { _id: projectId } = useProjectContext({
_id: PropTypes.string.isRequired,
})
const { permissionsLevel } = useEditorContext({
permissionsLevel: PropTypes.string,
})
const { id: userId } = useUserContext()
const { t } = useTranslation()
const [refreshing, setRefreshing] = useState(false)
const [refreshError, setRefreshError] = useState(null)
const { signal } = useAbortController()
let fileInfo
let isImporter
if (file.linkedFileData) {
isImporter = file.linkedFileData.importer_id === userId
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/${projectId}/linked_file/${file.id}/refresh`, { signal })
.then(() => {
setRefreshing(false)
})
.catch(err => {
setRefreshing(false)
setRefreshError(err.data?.message || 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/${projectId}/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, projectId, signal, storeReferencesKeys])
return (
<div>
{file.linkedFileData && fileInfo}
{file.linkedFileData &&
tprLinkedFileInfo.map(({ import: { LinkedFileInfo }, path }) => (
<LinkedFileInfo key={path} file={file} />
))}
{file.linkedFileData && permissionsLevel !== 'readOnly' && (
<button
className={`btn ${isImporter ? 'btn-primary' : 'btn-secondary'}`}
onClick={refreshFile}
disabled={refreshing || !isImporter}
>
<Icon type="refresh" spin={refreshing} fw />
<span>{refreshing ? t('refreshing') + '...' : t('refresh')}</span>
</button>
)}
&nbsp;
<a
download
href={`/project/${projectId}/file/${file.id}`}
className="btn btn-secondary-info btn-secondary"
>
<Icon type="download" fw />
&nbsp;
<span>{t('download')}</span>
</a>
{file.linkedFileData && !isImporter && (
<div className="row">
<div className="alert">
{t('only_importer_can_refresh', {
provider: capitalize(file.linkedFileData.provider),
})}
</div>
</div>
)}
{refreshError && (
<div className="row">
<br />
<div
className="alert alert-danger col-md-10 col-md-offset-1"
style={{ display: 'flex', alignItems: 'center', gap: '10px' }}
>
<div>
{t('something_not_right')}!&nbsp;
{tprLinkedFileRefreshError.map(
({ import: { LinkedFileRefreshError }, path }) => (
<LinkedFileRefreshError key={path} file={file} />
)
)}
</div>
<div className="text-center">
<button className="btn btn-danger">
<a
href="/user/settings"
target="_blank"
style={{ fontWeight: 'bold', textDecoration: 'none' }}
>
{t('go_to_settings')}
</a>
</button>
</div>
</div>
</div>
)}
</div>
)
}
FileViewHeader.propTypes = {
file: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
linkedFileData: PropTypes.object,
}).isRequired,
storeReferencesKeys: PropTypes.func.isRequired,
}
function UrlProvider({ file }) {
return (
<p>
<LinkedFileIcon />
&nbsp;
<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>
<LinkedFileIcon />
&nbsp;
<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"
rel="noopener"
/>,
]
}
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>
<LinkedFileIcon />
&nbsp;
<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"
rel="noopener"
/>,
]
}
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,
}