file-view-header.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import { useState, useCallback } from 'react'
  2. import PropTypes from 'prop-types'
  3. import { Trans, useTranslation } from 'react-i18next'
  4. import Icon from '../../../shared/components/icon'
  5. import { formatTime, relativeDate } from '../../utils/format-date'
  6. import { postJSON } from '../../../infrastructure/fetch-json'
  7. import { useEditorContext } from '../../../shared/context/editor-context'
  8. import { useProjectContext } from '../../../shared/context/project-context'
  9. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  10. import useAbortController from '../../../shared/hooks/use-abort-controller'
  11. import { LinkedFileIcon } from './file-view-icons'
  12. const tprLinkedFileInfo = importOverleafModules('tprLinkedFileInfo')
  13. const tprLinkedFileRefreshError = importOverleafModules(
  14. 'tprLinkedFileRefreshError'
  15. )
  16. const MAX_URL_LENGTH = 60
  17. const FRONT_OF_URL_LENGTH = 35
  18. const FILLER = '...'
  19. const TAIL_OF_URL_LENGTH = MAX_URL_LENGTH - FRONT_OF_URL_LENGTH - FILLER.length
  20. function shortenedUrl(url) {
  21. if (!url) {
  22. return
  23. }
  24. if (url.length > MAX_URL_LENGTH) {
  25. const front = url.slice(0, FRONT_OF_URL_LENGTH)
  26. const tail = url.slice(url.length - TAIL_OF_URL_LENGTH)
  27. return front + FILLER + tail
  28. }
  29. return url
  30. }
  31. export default function FileViewHeader({ file, storeReferencesKeys }) {
  32. const { _id: projectId } = useProjectContext({
  33. _id: PropTypes.string.isRequired,
  34. })
  35. const { permissionsLevel } = useEditorContext({
  36. permissionsLevel: PropTypes.string,
  37. })
  38. const { t } = useTranslation()
  39. const [refreshing, setRefreshing] = useState(false)
  40. const [refreshError, setRefreshError] = useState(null)
  41. const { signal } = useAbortController()
  42. let fileInfo
  43. if (file.linkedFileData) {
  44. if (file.linkedFileData.provider === 'url') {
  45. fileInfo = (
  46. <div>
  47. <UrlProvider file={file} />
  48. </div>
  49. )
  50. } else if (file.linkedFileData.provider === 'project_file') {
  51. fileInfo = (
  52. <div>
  53. <ProjectFilePathProvider file={file} />
  54. </div>
  55. )
  56. } else if (file.linkedFileData.provider === 'project_output_file') {
  57. fileInfo = (
  58. <div>
  59. <ProjectOutputFileProvider file={file} />
  60. </div>
  61. )
  62. }
  63. }
  64. const refreshFile = useCallback(() => {
  65. setRefreshing(true)
  66. // Replacement of the file handled by the file tree
  67. window.expectingLinkedFileRefreshedSocketFor = file.name
  68. postJSON(`/project/${projectId}/linked_file/${file.id}/refresh`, { signal })
  69. .then(() => {
  70. setRefreshing(false)
  71. })
  72. .catch(err => {
  73. setRefreshing(false)
  74. setRefreshError(err.data?.message || err.message)
  75. })
  76. .finally(() => {
  77. if (
  78. file.linkedFileData.provider === 'mendeley' ||
  79. file.linkedFileData.provider === 'zotero' ||
  80. file.name.match(/^.*\.bib$/)
  81. ) {
  82. reindexReferences()
  83. }
  84. })
  85. function reindexReferences() {
  86. const opts = {
  87. body: { shouldBroadcast: true },
  88. }
  89. postJSON(`/project/${projectId}/references/indexAll`, opts)
  90. .then(response => {
  91. // Later updated by the socket but also updated here for immediate use
  92. storeReferencesKeys(response.keys)
  93. })
  94. .catch(error => {
  95. console.log(error)
  96. })
  97. }
  98. }, [file, projectId, signal, storeReferencesKeys])
  99. return (
  100. <div>
  101. {file.linkedFileData && fileInfo}
  102. {file.linkedFileData &&
  103. tprLinkedFileInfo.map(({ import: { LinkedFileInfo }, path }) => (
  104. <LinkedFileInfo key={path} file={file} />
  105. ))}
  106. {file.linkedFileData && permissionsLevel !== 'readOnly' && (
  107. <button
  108. className="btn btn-primary"
  109. onClick={refreshFile}
  110. disabled={refreshing}
  111. >
  112. <Icon type="refresh" spin={refreshing} fw />
  113. <span>{refreshing ? t('refreshing') + '...' : t('refresh')}</span>
  114. </button>
  115. )}
  116. &nbsp;
  117. <a
  118. href={`/project/${projectId}/file/${file.id}`}
  119. className="btn btn-info"
  120. >
  121. <Icon type="download" fw />
  122. &nbsp;
  123. <span>{t('download')}</span>
  124. </a>
  125. {refreshError && (
  126. <div className="row">
  127. <br />
  128. <div className="alert alert-danger col-md-6 col-md-offset-3">
  129. {t('access_denied')}: {refreshError}
  130. {tprLinkedFileRefreshError.map(
  131. ({ import: { LinkedFileRefreshError }, path }) => (
  132. <LinkedFileRefreshError key={path} file={file} />
  133. )
  134. )}
  135. </div>
  136. </div>
  137. )}
  138. </div>
  139. )
  140. }
  141. FileViewHeader.propTypes = {
  142. file: PropTypes.shape({
  143. id: PropTypes.string,
  144. name: PropTypes.string,
  145. linkedFileData: PropTypes.object,
  146. }).isRequired,
  147. storeReferencesKeys: PropTypes.func.isRequired,
  148. }
  149. function UrlProvider({ file }) {
  150. return (
  151. <p>
  152. <LinkedFileIcon />
  153. &nbsp;
  154. <Trans
  155. i18nKey="imported_from_external_provider_at_date"
  156. components={
  157. /* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
  158. [<a href={file.linkedFileData.url} />]
  159. }
  160. values={{
  161. shortenedUrl: shortenedUrl(file.linkedFileData.url),
  162. formattedDate: formatTime(file.created),
  163. relativeDate: relativeDate(file.created),
  164. }}
  165. />
  166. </p>
  167. )
  168. }
  169. UrlProvider.propTypes = {
  170. file: PropTypes.shape({
  171. linkedFileData: PropTypes.object,
  172. created: PropTypes.string,
  173. }).isRequired,
  174. }
  175. function ProjectFilePathProvider({ file }) {
  176. /* eslint-disable jsx-a11y/anchor-has-content, react/jsx-key */
  177. return (
  178. <p>
  179. <LinkedFileIcon />
  180. &nbsp;
  181. <Trans
  182. i18nKey="imported_from_another_project_at_date"
  183. components={
  184. file.linkedFileData.v1_source_doc_id
  185. ? [<span />]
  186. : [
  187. <a
  188. href={`/project/${file.linkedFileData.source_project_id}`}
  189. target="_blank"
  190. rel="noopener"
  191. />,
  192. ]
  193. }
  194. values={{
  195. sourceEntityPath: file.linkedFileData.source_entity_path.slice(1),
  196. formattedDate: formatTime(file.created),
  197. relativeDate: relativeDate(file.created),
  198. }}
  199. />
  200. </p>
  201. /* esline-enable jsx-a11y/anchor-has-content, react/jsx-key */
  202. )
  203. }
  204. ProjectFilePathProvider.propTypes = {
  205. file: PropTypes.shape({
  206. linkedFileData: PropTypes.object,
  207. created: PropTypes.string,
  208. }).isRequired,
  209. }
  210. function ProjectOutputFileProvider({ file }) {
  211. return (
  212. <p>
  213. <LinkedFileIcon />
  214. &nbsp;
  215. <Trans
  216. i18nKey="imported_from_the_output_of_another_project_at_date"
  217. components={
  218. file.linkedFileData.v1_source_doc_id
  219. ? [<span />]
  220. : [
  221. <a
  222. href={`/project/${file.linkedFileData.source_project_id}`}
  223. target="_blank"
  224. rel="noopener"
  225. />,
  226. ]
  227. }
  228. values={{
  229. sourceOutputFilePath: file.linkedFileData.source_output_file_path,
  230. formattedDate: formatTime(file.created),
  231. relativeDate: relativeDate(file.created),
  232. }}
  233. />
  234. </p>
  235. )
  236. }
  237. ProjectOutputFileProvider.propTypes = {
  238. file: PropTypes.shape({
  239. linkedFileData: PropTypes.object,
  240. created: PropTypes.string,
  241. }).isRequired,
  242. }