file-view-header.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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. download
  119. href={`/project/${projectId}/file/${file.id}`}
  120. className="btn btn-secondary-info btn-secondary"
  121. >
  122. <Icon type="download" fw />
  123. &nbsp;
  124. <span>{t('download')}</span>
  125. </a>
  126. {refreshError && (
  127. <div className="row">
  128. <br />
  129. <div className="alert alert-danger col-md-6 col-md-offset-3">
  130. {t('access_denied')}: {refreshError}
  131. {tprLinkedFileRefreshError.map(
  132. ({ import: { LinkedFileRefreshError }, path }) => (
  133. <LinkedFileRefreshError key={path} file={file} />
  134. )
  135. )}
  136. </div>
  137. </div>
  138. )}
  139. </div>
  140. )
  141. }
  142. FileViewHeader.propTypes = {
  143. file: PropTypes.shape({
  144. id: PropTypes.string,
  145. name: PropTypes.string,
  146. linkedFileData: PropTypes.object,
  147. }).isRequired,
  148. storeReferencesKeys: PropTypes.func.isRequired,
  149. }
  150. function UrlProvider({ file }) {
  151. return (
  152. <p>
  153. <LinkedFileIcon />
  154. &nbsp;
  155. <Trans
  156. i18nKey="imported_from_external_provider_at_date"
  157. components={
  158. /* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
  159. [<a href={file.linkedFileData.url} />]
  160. }
  161. values={{
  162. shortenedUrl: shortenedUrl(file.linkedFileData.url),
  163. formattedDate: formatTime(file.created),
  164. relativeDate: relativeDate(file.created),
  165. }}
  166. />
  167. </p>
  168. )
  169. }
  170. UrlProvider.propTypes = {
  171. file: PropTypes.shape({
  172. linkedFileData: PropTypes.object,
  173. created: PropTypes.string,
  174. }).isRequired,
  175. }
  176. function ProjectFilePathProvider({ file }) {
  177. /* eslint-disable jsx-a11y/anchor-has-content, react/jsx-key */
  178. return (
  179. <p>
  180. <LinkedFileIcon />
  181. &nbsp;
  182. <Trans
  183. i18nKey="imported_from_another_project_at_date"
  184. components={
  185. file.linkedFileData.v1_source_doc_id
  186. ? [<span />]
  187. : [
  188. <a
  189. href={`/project/${file.linkedFileData.source_project_id}`}
  190. target="_blank"
  191. rel="noopener"
  192. />,
  193. ]
  194. }
  195. values={{
  196. sourceEntityPath: file.linkedFileData.source_entity_path.slice(1),
  197. formattedDate: formatTime(file.created),
  198. relativeDate: relativeDate(file.created),
  199. }}
  200. />
  201. </p>
  202. /* esline-enable jsx-a11y/anchor-has-content, react/jsx-key */
  203. )
  204. }
  205. ProjectFilePathProvider.propTypes = {
  206. file: PropTypes.shape({
  207. linkedFileData: PropTypes.object,
  208. created: PropTypes.string,
  209. }).isRequired,
  210. }
  211. function ProjectOutputFileProvider({ file }) {
  212. return (
  213. <p>
  214. <LinkedFileIcon />
  215. &nbsp;
  216. <Trans
  217. i18nKey="imported_from_the_output_of_another_project_at_date"
  218. components={
  219. file.linkedFileData.v1_source_doc_id
  220. ? [<span />]
  221. : [
  222. <a
  223. href={`/project/${file.linkedFileData.source_project_id}`}
  224. target="_blank"
  225. rel="noopener"
  226. />,
  227. ]
  228. }
  229. values={{
  230. sourceOutputFilePath: file.linkedFileData.source_output_file_path,
  231. formattedDate: formatTime(file.created),
  232. relativeDate: relativeDate(file.created),
  233. }}
  234. />
  235. </p>
  236. )
  237. }
  238. ProjectOutputFileProvider.propTypes = {
  239. file: PropTypes.shape({
  240. linkedFileData: PropTypes.object,
  241. created: PropTypes.string,
  242. }).isRequired,
  243. }