file-view-header.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import { useState, type ElementType } 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 { useEditorContext } from '../../../shared/context/editor-context'
  7. import { useProjectContext } from '../../../shared/context/project-context'
  8. import { Nullable } from '../../../../../types/utils'
  9. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  10. import { LinkedFileIcon } from './file-view-icons'
  11. import { BinaryFile, hasProvider, LinkedFile } from '../types/binary-file'
  12. import FileViewRefreshButton from './file-view-refresh-button'
  13. import FileViewNotOriginalImporter from './file-view-not-original-importer'
  14. import FileViewRefreshError from './file-view-refresh-error'
  15. const tprLinkedFileInfo = importOverleafModules('tprLinkedFileInfo') as {
  16. import: { LinkedFileInfo: ElementType }
  17. path: string
  18. }[]
  19. const MAX_URL_LENGTH = 60
  20. const FRONT_OF_URL_LENGTH = 35
  21. const FILLER = '...'
  22. const TAIL_OF_URL_LENGTH = MAX_URL_LENGTH - FRONT_OF_URL_LENGTH - FILLER.length
  23. function shortenedUrl(url: string) {
  24. if (!url) {
  25. return
  26. }
  27. if (url.length > MAX_URL_LENGTH) {
  28. const front = url.slice(0, FRONT_OF_URL_LENGTH)
  29. const tail = url.slice(url.length - TAIL_OF_URL_LENGTH)
  30. return front + FILLER + tail
  31. }
  32. return url
  33. }
  34. type FileViewHeaderProps = {
  35. file: BinaryFile
  36. }
  37. export default function FileViewHeader({ file }: FileViewHeaderProps) {
  38. const { _id: projectId } = useProjectContext({
  39. _id: PropTypes.string.isRequired,
  40. })
  41. const { permissionsLevel } = useEditorContext({
  42. permissionsLevel: PropTypes.string,
  43. })
  44. const { t } = useTranslation()
  45. const [refreshError, setRefreshError] = useState<Nullable<string>>(null)
  46. let fileInfo
  47. if (file.linkedFileData) {
  48. if (hasProvider(file, 'url')) {
  49. fileInfo = (
  50. <div>
  51. <UrlProvider file={file} />
  52. </div>
  53. )
  54. } else if (hasProvider(file, 'project_file')) {
  55. fileInfo = (
  56. <div>
  57. <ProjectFilePathProvider file={file} />
  58. </div>
  59. )
  60. } else if (hasProvider(file, 'project_output_file')) {
  61. fileInfo = (
  62. <div>
  63. <ProjectOutputFileProvider file={file} />
  64. </div>
  65. )
  66. }
  67. }
  68. return (
  69. <div>
  70. {file.linkedFileData && fileInfo}
  71. {file.linkedFileData &&
  72. tprLinkedFileInfo.map(({ import: { LinkedFileInfo }, path }) => (
  73. <LinkedFileInfo key={path} file={file} />
  74. ))}
  75. {file.linkedFileData && permissionsLevel !== 'readOnly' && (
  76. <FileViewRefreshButton file={file} setRefreshError={setRefreshError} />
  77. )}
  78. &nbsp;
  79. <a
  80. download
  81. href={`/project/${projectId}/file/${file.id}`}
  82. className="btn btn-secondary-info btn-secondary"
  83. >
  84. <Icon type="download" fw />
  85. &nbsp;
  86. <span>{t('download')}</span>
  87. </a>
  88. {file.linkedFileData && <FileViewNotOriginalImporter file={file} />}
  89. {refreshError && (
  90. <FileViewRefreshError file={file} refreshError={refreshError} />
  91. )}
  92. </div>
  93. )
  94. }
  95. type UrlProviderProps = {
  96. file: LinkedFile<'url'>
  97. }
  98. function UrlProvider({ file }: UrlProviderProps) {
  99. return (
  100. <p>
  101. <LinkedFileIcon />
  102. &nbsp;
  103. <Trans
  104. i18nKey="imported_from_external_provider_at_date"
  105. components={
  106. /* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
  107. [<a href={file.linkedFileData.url} />]
  108. }
  109. values={{
  110. shortenedUrl: shortenedUrl(file.linkedFileData.url),
  111. formattedDate: formatTime(file.created),
  112. relativeDate: relativeDate(file.created),
  113. }}
  114. shouldUnescape
  115. tOptions={{ interpolation: { escapeValue: true } }}
  116. />
  117. </p>
  118. )
  119. }
  120. type ProjectFilePathProviderProps = {
  121. file: LinkedFile<'project_file'>
  122. }
  123. function ProjectFilePathProvider({ file }: ProjectFilePathProviderProps) {
  124. /* eslint-disable jsx-a11y/anchor-has-content, react/jsx-key */
  125. return (
  126. <p>
  127. <LinkedFileIcon />
  128. &nbsp;
  129. <Trans
  130. i18nKey="imported_from_another_project_at_date"
  131. components={
  132. file.linkedFileData.v1_source_doc_id
  133. ? [<span />]
  134. : [
  135. <a
  136. href={`/project/${file.linkedFileData.source_project_id}`}
  137. target="_blank"
  138. rel="noopener"
  139. />,
  140. ]
  141. }
  142. values={{
  143. sourceEntityPath: file.linkedFileData.source_entity_path.slice(1),
  144. formattedDate: formatTime(file.created),
  145. relativeDate: relativeDate(file.created),
  146. }}
  147. shouldUnescape
  148. tOptions={{ interpolation: { escapeValue: true } }}
  149. />
  150. </p>
  151. /* esline-enable jsx-a11y/anchor-has-content, react/jsx-key */
  152. )
  153. }
  154. type ProjectOutputFileProviderProps = {
  155. file: LinkedFile<'project_output_file'>
  156. }
  157. function ProjectOutputFileProvider({ file }: ProjectOutputFileProviderProps) {
  158. return (
  159. <p>
  160. <LinkedFileIcon />
  161. &nbsp;
  162. <Trans
  163. i18nKey="imported_from_the_output_of_another_project_at_date"
  164. components={
  165. file.linkedFileData.v1_source_doc_id
  166. ? [<span />]
  167. : [
  168. <a
  169. href={`/project/${file.linkedFileData.source_project_id}`}
  170. target="_blank"
  171. rel="noopener"
  172. />,
  173. ]
  174. }
  175. values={{
  176. sourceOutputFilePath: file.linkedFileData.source_output_file_path,
  177. formattedDate: formatTime(file.created),
  178. relativeDate: relativeDate(file.created),
  179. }}
  180. shouldUnescape
  181. tOptions={{ interpolation: { escapeValue: true } }}
  182. />
  183. </p>
  184. )
  185. }