overleaf/services/web/frontend/js/features/file-view/components/file-view-header.tsx
Antoine Clausse d6de6da781 [web] Migrate the file view to Bootstrap 5 (#20765)
* [web] Remove unnecessary divs around `fileInfo`

* [web] Add file-view SCSS style

* [web] Simplify `TPRFileViewInfo`

* [web] Add div for action buttons

* [web] Misc. simplifications

* [web] Add Overleaf logo in bg when selecting multiple files

* [web] Add message when multiple files are selected

* [web] Add .full-size class

* [web] Import styles from LESS file

* [web] Update icons, use MaterialIcon

* [web] Use OLButton

* [web] Add missing space between icons and text

* [web] Adjust margins

* [web] Fix alert button

* [web] Update Alerts

* [web] Update `FileViewLoadingIndicator`

* [web] Fix test "shows a loading indicator..."

This was failing because `LoadingSpinner` is shown after a setTimeout.
Maybe we can skip this setTimeout when delay==0 ?

* [web] Remove Row/Col around error notifications

* [web] Replace `!!` by `Boolean`

Co-authored-by: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com>

* [web] Use `alert` class in BS3 only

Co-authored-by: Ilkin Ismailov <ilkin.ismailov@overleaf.com>

* [web] Update "Go to settings" to OLButton

Co-authored-by: Ilkin Ismailov <ilkin.ismailov@overleaf.com>

* [web] Use `BootstrapVersionSwitcher` instead of `isBootstrap5`

Co-authored-by: Ilkin Ismailov <ilkin.ismailov@overleaf.com>

* [web] Align Alert content to the left in BS5

* [web] Remove `leadingIcon` on Refresh buttons

* [web] Make the download link be an OLButton

* [web] Set `tpr-refresh-error` in BS3 only

Co-authored-by: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com>

* [web] Use `var(--white);` instead of `white`

Co-authored-by: Rebeka <rebeka.dekany@overleaf.com>

* [web] Update OLButton size (small -> sm)

---------

Co-authored-by: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com>
Co-authored-by: Ilkin Ismailov <ilkin.ismailov@overleaf.com>
Co-authored-by: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com>
Co-authored-by: Rebeka <rebeka.dekany@overleaf.com>
GitOrigin-RevId: 04f369c0f1a53d47619a1570648ee58de5050751
2024-10-14 11:07:55 +00:00

209 lines
6.4 KiB
TypeScript

import { useState, type ElementType } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
import { formatTime, relativeDate } from '../../utils/format-date'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { useProjectContext } from '@/shared/context/project-context'
import { Nullable } from '../../../../../types/utils'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { LinkedFileIcon } from './file-view-icons'
import { BinaryFile, hasProvider, LinkedFile } from '../types/binary-file'
import FileViewRefreshButton from './file-view-refresh-button'
import FileViewRefreshError from './file-view-refresh-error'
import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import OLButton from '@/features/ui/components/ol/ol-button'
const tprFileViewInfo = importOverleafModules('tprFileViewInfo') as {
import: { TPRFileViewInfo: ElementType }
path: string
}[]
const tprFileViewNotOriginalImporter = importOverleafModules(
'tprFileViewNotOriginalImporter'
) as {
import: { TPRFileViewNotOriginalImporter: ElementType }
path: string
}[]
const MAX_URL_LENGTH = 60
const FRONT_OF_URL_LENGTH = 35
const FILLER = '...'
const TAIL_OF_URL_LENGTH = MAX_URL_LENGTH - FRONT_OF_URL_LENGTH - FILLER.length
function shortenedUrl(url: string) {
if (!url) {
return
}
if (url.length > MAX_URL_LENGTH) {
const front = url.slice(0, FRONT_OF_URL_LENGTH)
const tail = url.slice(url.length - TAIL_OF_URL_LENGTH)
return front + FILLER + tail
}
return url
}
type FileViewHeaderProps = {
file: BinaryFile
}
export default function FileViewHeader({ file }: FileViewHeaderProps) {
const { _id: projectId } = useProjectContext()
const { fileTreeReadOnly } = useFileTreeData()
const { fileTreeFromHistory } = useSnapshotContext()
const { t } = useTranslation()
const [refreshError, setRefreshError] = useState<Nullable<string>>(null)
let fileInfo
if (file.linkedFileData) {
if (hasProvider(file, 'url')) {
fileInfo = <UrlProvider file={file} />
} else if (hasProvider(file, 'project_file')) {
fileInfo = <ProjectFilePathProvider file={file} />
} else if (hasProvider(file, 'project_output_file')) {
fileInfo = <ProjectOutputFileProvider file={file} />
}
}
return (
<>
{file.linkedFileData && fileInfo}
{file.linkedFileData &&
tprFileViewInfo.map(({ import: { TPRFileViewInfo }, path }) => (
<TPRFileViewInfo key={path} file={file} />
))}
<div className="file-view-buttons">
{file.linkedFileData && !fileTreeReadOnly && (
<FileViewRefreshButton
file={file}
setRefreshError={setRefreshError}
/>
)}
<OLButton
variant="secondary"
download={file.name}
href={
fileTreeFromHistory
? `/project/${projectId}/blob/${file.hash}`
: `/project/${projectId}/file/${file.id}`
}
>
<BootstrapVersionSwitcher
bs3={<Icon type="download" fw />}
bs5={<MaterialIcon type="download" className="align-middle" />}
/>{' '}
<span>{t('download')}</span>
</OLButton>
</div>
{file.linkedFileData &&
tprFileViewNotOriginalImporter.map(
({ import: { TPRFileViewNotOriginalImporter }, path }) => (
<TPRFileViewNotOriginalImporter key={path} file={file} />
)
)[0]}
{refreshError && (
<FileViewRefreshError file={file} refreshError={refreshError} />
)}
</>
)
}
type UrlProviderProps = {
file: LinkedFile<'url'>
}
function UrlProvider({ file }: UrlProviderProps) {
return (
<p>
<LinkedFileIcon />
&nbsp;
<Trans
i18nKey="imported_from_external_provider_at_date"
components={
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
[<a href={file.linkedFileData.url} />]
}
values={{
shortenedUrl: shortenedUrl(file.linkedFileData.url),
formattedDate: formatTime(file.created),
relativeDate: relativeDate(file.created),
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</p>
)
}
type ProjectFilePathProviderProps = {
file: LinkedFile<'project_file'>
}
function ProjectFilePathProvider({ file }: ProjectFilePathProviderProps) {
/* eslint-disable jsx-a11y/anchor-has-content, react/jsx-key */
return (
<p>
<LinkedFileIcon />{' '}
<Trans
i18nKey="imported_from_another_project_at_date"
components={
file.linkedFileData.v1_source_doc_id
? [<span />]
: [
<a
href={`/project/${file.linkedFileData.source_project_id}`}
target="_blank"
rel="noopener"
/>,
]
}
values={{
sourceEntityPath: file.linkedFileData.source_entity_path.slice(1),
formattedDate: formatTime(file.created),
relativeDate: relativeDate(file.created),
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</p>
/* esline-enable jsx-a11y/anchor-has-content, react/jsx-key */
)
}
type ProjectOutputFileProviderProps = {
file: LinkedFile<'project_output_file'>
}
function ProjectOutputFileProvider({ file }: ProjectOutputFileProviderProps) {
return (
<p>
<LinkedFileIcon />
&nbsp;
<Trans
i18nKey="imported_from_the_output_of_another_project_at_date"
components={
file.linkedFileData.v1_source_doc_id
? [<span />]
: [
<a
href={`/project/${file.linkedFileData.source_project_id}`}
target="_blank"
rel="noopener"
/>,
]
}
values={{
sourceOutputFilePath: file.linkedFileData.source_output_file_path,
formattedDate: formatTime(file.created),
relativeDate: relativeDate(file.created),
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</p>
)
}