file-view-header.tsx 6.0 KB

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