file-view-header.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  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. import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
  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. const { permissionsLevel } = useEditorContext()
  45. const { fileTreeFromHistory } = useSnapshotContext()
  46. const { t } = useTranslation()
  47. const [refreshError, setRefreshError] = useState<Nullable<string>>(null)
  48. let fileInfo
  49. if (file.linkedFileData) {
  50. if (hasProvider(file, 'url')) {
  51. fileInfo = (
  52. <div>
  53. <UrlProvider file={file} />
  54. </div>
  55. )
  56. } else if (hasProvider(file, 'project_file')) {
  57. fileInfo = (
  58. <div>
  59. <ProjectFilePathProvider file={file} />
  60. </div>
  61. )
  62. } else if (hasProvider(file, 'project_output_file')) {
  63. fileInfo = (
  64. <div>
  65. <ProjectOutputFileProvider file={file} />
  66. </div>
  67. )
  68. }
  69. }
  70. return (
  71. <div>
  72. {file.linkedFileData && fileInfo}
  73. {file.linkedFileData &&
  74. tprFileViewInfo.map(({ import: { TPRFileViewInfo }, path }) => (
  75. <TPRFileViewInfo key={path} file={file} />
  76. ))}
  77. {file.linkedFileData && permissionsLevel !== 'readOnly' && (
  78. <FileViewRefreshButton file={file} setRefreshError={setRefreshError} />
  79. )}
  80. &nbsp;
  81. <a
  82. download={file.name}
  83. href={
  84. fileTreeFromHistory
  85. ? `/project/${projectId}/blob/${file.hash}`
  86. : `/project/${projectId}/file/${file.id}`
  87. }
  88. className="btn btn-secondary-info btn-secondary"
  89. >
  90. <Icon type="download" fw />
  91. &nbsp;
  92. <span>{t('download')}</span>
  93. </a>
  94. {file.linkedFileData &&
  95. tprFileViewNotOriginalImporter.map(
  96. ({ import: { TPRFileViewNotOriginalImporter }, path }) => (
  97. <TPRFileViewNotOriginalImporter key={path} file={file} />
  98. )
  99. )[0]}
  100. {refreshError && (
  101. <FileViewRefreshError file={file} refreshError={refreshError} />
  102. )}
  103. </div>
  104. )
  105. }
  106. type UrlProviderProps = {
  107. file: LinkedFile<'url'>
  108. }
  109. function UrlProvider({ file }: UrlProviderProps) {
  110. return (
  111. <p>
  112. <LinkedFileIcon />
  113. &nbsp;
  114. <Trans
  115. i18nKey="imported_from_external_provider_at_date"
  116. components={
  117. /* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
  118. [<a href={file.linkedFileData.url} />]
  119. }
  120. values={{
  121. shortenedUrl: shortenedUrl(file.linkedFileData.url),
  122. formattedDate: formatTime(file.created),
  123. relativeDate: relativeDate(file.created),
  124. }}
  125. shouldUnescape
  126. tOptions={{ interpolation: { escapeValue: true } }}
  127. />
  128. </p>
  129. )
  130. }
  131. type ProjectFilePathProviderProps = {
  132. file: LinkedFile<'project_file'>
  133. }
  134. function ProjectFilePathProvider({ file }: ProjectFilePathProviderProps) {
  135. /* eslint-disable jsx-a11y/anchor-has-content, react/jsx-key */
  136. return (
  137. <p>
  138. <LinkedFileIcon />
  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. }