file-view-header.js 7.4 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. import { debugConsole } from '@/utils/debugging'
  13. const tprLinkedFileInfo = importOverleafModules('tprLinkedFileInfo')
  14. const tprLinkedFileRefreshError = importOverleafModules(
  15. 'tprLinkedFileRefreshError'
  16. )
  17. const MAX_URL_LENGTH = 60
  18. const FRONT_OF_URL_LENGTH = 35
  19. const FILLER = '...'
  20. const TAIL_OF_URL_LENGTH = MAX_URL_LENGTH - FRONT_OF_URL_LENGTH - FILLER.length
  21. function shortenedUrl(url) {
  22. if (!url) {
  23. return
  24. }
  25. if (url.length > MAX_URL_LENGTH) {
  26. const front = url.slice(0, FRONT_OF_URL_LENGTH)
  27. const tail = url.slice(url.length - TAIL_OF_URL_LENGTH)
  28. return front + FILLER + tail
  29. }
  30. return url
  31. }
  32. export default function FileViewHeader({ file, storeReferencesKeys }) {
  33. const { _id: projectId } = useProjectContext({
  34. _id: PropTypes.string.isRequired,
  35. })
  36. const { permissionsLevel } = useEditorContext({
  37. permissionsLevel: PropTypes.string,
  38. })
  39. const { t } = useTranslation()
  40. const [refreshing, setRefreshing] = useState(false)
  41. const [refreshError, setRefreshError] = useState(null)
  42. const { signal } = useAbortController()
  43. let fileInfo
  44. if (file.linkedFileData) {
  45. if (file.linkedFileData.provider === 'url') {
  46. fileInfo = (
  47. <div>
  48. <UrlProvider file={file} />
  49. </div>
  50. )
  51. } else if (file.linkedFileData.provider === 'project_file') {
  52. fileInfo = (
  53. <div>
  54. <ProjectFilePathProvider file={file} />
  55. </div>
  56. )
  57. } else if (file.linkedFileData.provider === 'project_output_file') {
  58. fileInfo = (
  59. <div>
  60. <ProjectOutputFileProvider file={file} />
  61. </div>
  62. )
  63. }
  64. }
  65. const refreshFile = useCallback(() => {
  66. setRefreshing(true)
  67. // Replacement of the file handled by the file tree
  68. window.expectingLinkedFileRefreshedSocketFor = file.name
  69. postJSON(`/project/${projectId}/linked_file/${file.id}/refresh`, { signal })
  70. .then(() => {
  71. setRefreshing(false)
  72. })
  73. .catch(err => {
  74. setRefreshing(false)
  75. setRefreshError(err.data?.message || err.message)
  76. })
  77. .finally(() => {
  78. if (
  79. file.linkedFileData.provider === 'mendeley' ||
  80. file.linkedFileData.provider === 'zotero' ||
  81. file.name.match(/^.*\.bib$/)
  82. ) {
  83. reindexReferences()
  84. }
  85. })
  86. function reindexReferences() {
  87. const opts = {
  88. body: { shouldBroadcast: true },
  89. }
  90. postJSON(`/project/${projectId}/references/indexAll`, opts)
  91. .then(response => {
  92. // Later updated by the socket but also updated here for immediate use
  93. storeReferencesKeys(response.keys)
  94. })
  95. .catch(debugConsole.error)
  96. }
  97. }, [file, projectId, signal, storeReferencesKeys])
  98. return (
  99. <div>
  100. {file.linkedFileData && fileInfo}
  101. {file.linkedFileData &&
  102. tprLinkedFileInfo.map(({ import: { LinkedFileInfo }, path }) => (
  103. <LinkedFileInfo key={path} file={file} />
  104. ))}
  105. {file.linkedFileData && permissionsLevel !== 'readOnly' && (
  106. <button
  107. className="btn btn-primary"
  108. onClick={refreshFile}
  109. disabled={refreshing}
  110. >
  111. <Icon type="refresh" spin={refreshing} fw />
  112. <span>{refreshing ? t('refreshing') + '...' : t('refresh')}</span>
  113. </button>
  114. )}
  115. &nbsp;
  116. <a
  117. download
  118. href={`/project/${projectId}/file/${file.id}`}
  119. className="btn btn-secondary-info btn-secondary"
  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. }