file-view-header.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import { useState, type ElementType } from 'react'
  2. import { Trans, useTranslation } from 'react-i18next'
  3. import Icon from '../../../shared/components/icon'
  4. import { formatTime, relativeDate } from '../../utils/format-date'
  5. import { useEditorContext } from '../../../shared/context/editor-context'
  6. import { useProjectContext } from '../../../shared/context/project-context'
  7. import { Nullable } from '../../../../../types/utils'
  8. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  9. import { LinkedFileIcon } from './file-view-icons'
  10. import { BinaryFile, hasProvider, LinkedFile } from '../types/binary-file'
  11. import FileViewRefreshButton from './file-view-refresh-button'
  12. import FileViewRefreshError from './file-view-refresh-error'
  13. const tprFileViewInfo = importOverleafModules('tprFileViewInfo') as {
  14. import: { TPRFileViewInfo: ElementType }
  15. path: string
  16. }[]
  17. const tprFileViewNotOriginalImporter = importOverleafModules(
  18. 'tprFileViewNotOriginalImporter'
  19. ) as {
  20. import: { TPRFileViewNotOriginalImporter: ElementType }
  21. path: string
  22. }[]
  23. const MAX_URL_LENGTH = 60
  24. const FRONT_OF_URL_LENGTH = 35
  25. const FILLER = '...'
  26. const TAIL_OF_URL_LENGTH = MAX_URL_LENGTH - FRONT_OF_URL_LENGTH - FILLER.length
  27. function shortenedUrl(url: string) {
  28. if (!url) {
  29. return
  30. }
  31. if (url.length > MAX_URL_LENGTH) {
  32. const front = url.slice(0, FRONT_OF_URL_LENGTH)
  33. const tail = url.slice(url.length - TAIL_OF_URL_LENGTH)
  34. return front + FILLER + tail
  35. }
  36. return url
  37. }
  38. type FileViewHeaderProps = {
  39. file: BinaryFile
  40. }
  41. export default function FileViewHeader({ file }: FileViewHeaderProps) {
  42. const { _id: projectId } = useProjectContext()
  43. const { permissionsLevel } = useEditorContext()
  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. tprFileViewInfo.map(({ import: { TPRFileViewInfo }, path }) => (
  73. <TPRFileViewInfo 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 &&
  89. tprFileViewNotOriginalImporter.map(
  90. ({ import: { TPRFileViewNotOriginalImporter }, path }) => (
  91. <TPRFileViewNotOriginalImporter key={path} file={file} />
  92. )
  93. )[0]}
  94. {refreshError && (
  95. <FileViewRefreshError file={file} refreshError={refreshError} />
  96. )}
  97. </div>
  98. )
  99. }
  100. type UrlProviderProps = {
  101. file: LinkedFile<'url'>
  102. }
  103. function UrlProvider({ file }: UrlProviderProps) {
  104. return (
  105. <p>
  106. <LinkedFileIcon />
  107. &nbsp;
  108. <Trans
  109. i18nKey="imported_from_external_provider_at_date"
  110. components={
  111. /* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
  112. [<a href={file.linkedFileData.url} />]
  113. }
  114. values={{
  115. shortenedUrl: shortenedUrl(file.linkedFileData.url),
  116. formattedDate: formatTime(file.created),
  117. relativeDate: relativeDate(file.created),
  118. }}
  119. shouldUnescape
  120. tOptions={{ interpolation: { escapeValue: true } }}
  121. />
  122. </p>
  123. )
  124. }
  125. type ProjectFilePathProviderProps = {
  126. file: LinkedFile<'project_file'>
  127. }
  128. function ProjectFilePathProvider({ file }: ProjectFilePathProviderProps) {
  129. /* eslint-disable jsx-a11y/anchor-has-content, react/jsx-key */
  130. return (
  131. <p>
  132. <LinkedFileIcon />
  133. <Trans
  134. i18nKey="imported_from_another_project_at_date"
  135. components={
  136. file.linkedFileData.v1_source_doc_id
  137. ? [<span />]
  138. : [
  139. <a
  140. href={`/project/${file.linkedFileData.source_project_id}`}
  141. target="_blank"
  142. rel="noopener"
  143. />,
  144. ]
  145. }
  146. values={{
  147. sourceEntityPath: file.linkedFileData.source_entity_path.slice(1),
  148. formattedDate: formatTime(file.created),
  149. relativeDate: relativeDate(file.created),
  150. }}
  151. shouldUnescape
  152. tOptions={{ interpolation: { escapeValue: true } }}
  153. />
  154. </p>
  155. /* esline-enable jsx-a11y/anchor-has-content, react/jsx-key */
  156. )
  157. }
  158. type ProjectOutputFileProviderProps = {
  159. file: LinkedFile<'project_output_file'>
  160. }
  161. function ProjectOutputFileProvider({ file }: ProjectOutputFileProviderProps) {
  162. return (
  163. <p>
  164. <LinkedFileIcon />
  165. &nbsp;
  166. <Trans
  167. i18nKey="imported_from_the_output_of_another_project_at_date"
  168. components={
  169. file.linkedFileData.v1_source_doc_id
  170. ? [<span />]
  171. : [
  172. <a
  173. href={`/project/${file.linkedFileData.source_project_id}`}
  174. target="_blank"
  175. rel="noopener"
  176. />,
  177. ]
  178. }
  179. values={{
  180. sourceOutputFilePath: file.linkedFileData.source_output_file_path,
  181. formattedDate: formatTime(file.created),
  182. relativeDate: relativeDate(file.created),
  183. }}
  184. shouldUnescape
  185. tOptions={{ interpolation: { escapeValue: true } }}
  186. />
  187. </p>
  188. )
  189. }