file-view-header.js 7.5 KB

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