[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
This commit is contained in:
Antoine Clausse 2024-10-10 09:38:28 +02:00 committed by Copybot
parent 2e080a3a34
commit d6de6da781
11 changed files with 223 additions and 74 deletions

View file

@ -4,7 +4,7 @@ import { Trans, useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { formatTime, relativeDate } from '../../utils/format-date' import { formatTime, relativeDate } from '../../utils/format-date'
import { useFileTreeData } from '@/shared/context/file-tree-data-context' import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { useProjectContext } from '../../../shared/context/project-context' import { useProjectContext } from '@/shared/context/project-context'
import { Nullable } from '../../../../../types/utils' import { Nullable } from '../../../../../types/utils'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro' import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
@ -13,6 +13,9 @@ import { BinaryFile, hasProvider, LinkedFile } from '../types/binary-file'
import FileViewRefreshButton from './file-view-refresh-button' import FileViewRefreshButton from './file-view-refresh-button'
import FileViewRefreshError from './file-view-refresh-error' import FileViewRefreshError from './file-view-refresh-error'
import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context' 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 { const tprFileViewInfo = importOverleafModules('tprFileViewInfo') as {
import: { TPRFileViewInfo: ElementType } import: { TPRFileViewInfo: ElementType }
@ -58,50 +61,44 @@ export default function FileViewHeader({ file }: FileViewHeaderProps) {
let fileInfo let fileInfo
if (file.linkedFileData) { if (file.linkedFileData) {
if (hasProvider(file, 'url')) { if (hasProvider(file, 'url')) {
fileInfo = ( fileInfo = <UrlProvider file={file} />
<div>
<UrlProvider file={file} />
</div>
)
} else if (hasProvider(file, 'project_file')) { } else if (hasProvider(file, 'project_file')) {
fileInfo = ( fileInfo = <ProjectFilePathProvider file={file} />
<div>
<ProjectFilePathProvider file={file} />
</div>
)
} else if (hasProvider(file, 'project_output_file')) { } else if (hasProvider(file, 'project_output_file')) {
fileInfo = ( fileInfo = <ProjectOutputFileProvider file={file} />
<div>
<ProjectOutputFileProvider file={file} />
</div>
)
} }
} }
return ( return (
<div> <>
{file.linkedFileData && fileInfo} {file.linkedFileData && fileInfo}
{file.linkedFileData && {file.linkedFileData &&
tprFileViewInfo.map(({ import: { TPRFileViewInfo }, path }) => ( tprFileViewInfo.map(({ import: { TPRFileViewInfo }, path }) => (
<TPRFileViewInfo key={path} file={file} /> <TPRFileViewInfo key={path} file={file} />
))} ))}
<div className="file-view-buttons">
{file.linkedFileData && !fileTreeReadOnly && ( {file.linkedFileData && !fileTreeReadOnly && (
<FileViewRefreshButton file={file} setRefreshError={setRefreshError} /> <FileViewRefreshButton
file={file}
setRefreshError={setRefreshError}
/>
)} )}
&nbsp; <OLButton
<a variant="secondary"
download={file.name} download={file.name}
href={ href={
fileTreeFromHistory fileTreeFromHistory
? `/project/${projectId}/blob/${file.hash}` ? `/project/${projectId}/blob/${file.hash}`
: `/project/${projectId}/file/${file.id}` : `/project/${projectId}/file/${file.id}`
} }
className="btn btn-secondary-info btn-secondary"
> >
<Icon type="download" fw /> <BootstrapVersionSwitcher
&nbsp; bs3={<Icon type="download" fw />}
bs5={<MaterialIcon type="download" className="align-middle" />}
/>{' '}
<span>{t('download')}</span> <span>{t('download')}</span>
</a> </OLButton>
</div>
{file.linkedFileData && {file.linkedFileData &&
tprFileViewNotOriginalImporter.map( tprFileViewNotOriginalImporter.map(
({ import: { TPRFileViewNotOriginalImporter }, path }) => ( ({ import: { TPRFileViewNotOriginalImporter }, path }) => (
@ -111,7 +108,7 @@ export default function FileViewHeader({ file }: FileViewHeaderProps) {
{refreshError && ( {refreshError && (
<FileViewRefreshError file={file} refreshError={refreshError} /> <FileViewRefreshError file={file} refreshError={refreshError} />
)} )}
</div> </>
) )
} }
@ -150,7 +147,7 @@ function ProjectFilePathProvider({ file }: ProjectFilePathProviderProps) {
/* eslint-disable jsx-a11y/anchor-has-content, react/jsx-key */ /* eslint-disable jsx-a11y/anchor-has-content, react/jsx-key */
return ( return (
<p> <p>
<LinkedFileIcon /> <LinkedFileIcon />{' '}
<Trans <Trans
i18nKey="imported_from_another_project_at_date" i18nKey="imported_from_another_project_at_date"
components={ components={

View file

@ -1,12 +1,26 @@
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
export const LinkedFileIcon = props => { export const LinkedFileIcon = props => {
return ( return (
<BootstrapVersionSwitcher
bs3={
<Icon <Icon
type="external-link-square" type="external-link-square"
modifier="rotate-180" modifier="rotate-180"
className="linked-file-icon" className="linked-file-icon"
{...props} {...props}
/> />
}
bs5={
<MaterialIcon
type="open_in_new"
modifier="rotate-180"
className="align-middle linked-file-icon"
{...props}
/>
}
/>
) )
} }

View file

@ -13,6 +13,7 @@ import useAbortController from '@/shared/hooks/use-abort-controller'
import type { BinaryFile } from '../types/binary-file' import type { BinaryFile } from '../types/binary-file'
import { Nullable } from '../../../../../types/utils' import { Nullable } from '../../../../../types/utils'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro' import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import OLButton from '@/features/ui/components/ol/ol-button'
type FileViewRefreshButtonProps = { type FileViewRefreshButtonProps = {
setRefreshError: Dispatch<SetStateAction<Nullable<string>>> setRefreshError: Dispatch<SetStateAction<Nullable<string>>>
@ -90,13 +91,21 @@ function FileViewRefreshButtonDefault({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<button <OLButton
className="btn btn-primary" variant="primary"
onClick={() => refreshFile(null)} onClick={() => refreshFile(null)}
disabled={refreshing} disabled={refreshing}
> isLoading={refreshing}
<Icon type="refresh" spin={refreshing} fw /> bs3Props={{
loading: (
<>
<Icon type="refresh" spin={refreshing} fw />{' '}
<span>{refreshing ? `${t('refreshing')}` : t('refresh')}</span> <span>{refreshing ? `${t('refreshing')}` : t('refresh')}</span>
</button> </>
),
}}
>
{t('refresh')}
</OLButton>
) )
} }

View file

@ -2,6 +2,7 @@ import type { ElementType } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro' import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { BinaryFile } from '../types/binary-file' import { BinaryFile } from '../types/binary-file'
import OLNotification from '@/features/ui/components/ol/ol-notification'
type FileViewRefreshErrorProps = { type FileViewRefreshErrorProps = {
file: BinaryFile file: BinaryFile
@ -33,11 +34,15 @@ export default function FileViewRefreshError({
)[0] )[0]
} else { } else {
return ( return (
<div className="row"> <div className="file-view-error">
<br /> <OLNotification
<div className="alert alert-danger col-md-6 col-md-offset-3"> type="error"
content={
<span>
{t('access_denied')}: {refreshError} {t('access_denied')}: {refreshError}
</div> </span>
}
/>
</div> </div>
) )
} }

View file

@ -93,15 +93,13 @@ export default function FileViewText({
fetchDataController, fetchDataController,
]) ])
return ( return (
<div> Boolean(textPreview) && (
{textPreview && (
<div className="text-preview"> <div className="text-preview">
<div className="scroll-container"> <div className="scroll-container">
<p>{textPreview}</p> <p>{textPreview}</p>
{shouldShowDots && <p>...</p>} {shouldShowDots && <p>...</p>}
</div> </div>
</div> </div>
)} )
</div>
) )
} }

View file

@ -6,7 +6,7 @@ import FileViewHeader from './file-view-header'
import FileViewImage from './file-view-image' import FileViewImage from './file-view-image'
import FileViewPdf from './file-view-pdf' import FileViewPdf from './file-view-pdf'
import FileViewText from './file-view-text' import FileViewText from './file-view-text'
import Icon from '../../../shared/components/icon' import LoadingSpinner from '@/shared/components/loading-spinner'
import getMeta from '@/utils/meta' import getMeta from '@/utils/meta'
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif'] const imageExtensions = ['png', 'jpg', 'jpeg', 'gif']
@ -71,13 +71,12 @@ export default function FileView({ file }) {
} }
function FileViewLoadingIndicator() { function FileViewLoadingIndicator() {
const { t } = useTranslation()
return ( return (
<div className="loading-panel loading-panel-file-view"> <div
<span> className="loading-panel loading-panel-file-view"
<Icon type="refresh" spin /> data-testid="loading-panel-file-view"
&nbsp;&nbsp;{t('loading')} >
</span> <LoadingSpinner />
</div> </div>
) )
} }

View file

@ -4,7 +4,7 @@ export type ButtonProps = {
children?: ReactNode children?: ReactNode
className?: string className?: string
disabled?: boolean disabled?: boolean
download?: boolean download?: boolean | string
draggable?: boolean draggable?: boolean
form?: string form?: string
leadingIcon?: string | React.ReactNode leadingIcon?: string | React.ReactNode

View file

@ -4,6 +4,33 @@
background-color: @gray-lightest; background-color: @gray-lightest;
text-align: center; text-align: center;
overflow: auto; overflow: auto;
.file-view-buttons {
display: flex;
flex-wrap: wrap;
gap: @spacing-03;
justify-content: center;
}
.file-view-error {
margin: @spacing-08 -15px auto;
> .alert {
max-width: 400px;
margin: 0 auto;
}
}
.tpr-refresh-error {
.btn {
color: @neutral-90;
background-color: @white;
&:hover {
background-color: @neutral-20;
}
}
}
img, img,
.file-view-pdf { .file-view-pdf {
max-width: 100%; max-width: 100%;
@ -43,10 +70,7 @@
line-height: 1.1em; line-height: 1.1em;
overflow: auto; overflow: auto;
border: 1px solid @gray-lighter; border: 1px solid @gray-lighter;
padding-left: 12px; padding: 8px 12px;
padding-right: 12px;
padding-top: 8px;
padding-bottom: 8px;
text-align: left; text-align: left;
white-space: pre; white-space: pre;
font-family: monospace; font-family: monospace;

View file

@ -10,6 +10,7 @@
@import 'editor/loading-screen'; @import 'editor/loading-screen';
@import 'editor/outline'; @import 'editor/outline';
@import 'editor/file-tree'; @import 'editor/file-tree';
@import 'editor/file-view';
@import 'editor/figure-modal'; @import 'editor/figure-modal';
@import 'editor/chat'; @import 'editor/chat';
@import 'subscription'; @import 'subscription';

View file

@ -0,0 +1,102 @@
.file-view {
padding: var(--spacing-05);
text-align: center;
overflow: auto;
.loading-panel {
padding-top: 8rem;
background: var(--neutral-10);
}
.file-view-buttons {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-03);
justify-content: center;
}
.file-view-error {
margin: var(--spacing-08) auto auto;
max-width: 400px;
text-align: left;
}
img,
.file-view-pdf {
max-width: 100%;
max-height: 90%;
display: block;
margin: var(--spacing-05) auto auto;
border: 1px solid var(--neutral-60);
box-shadow: 0 2px 3px var(--neutral-60);
background-color: var(--white);
}
.file-view-pdf {
overflow: auto;
width: max-content;
display: flex;
flex-direction: column;
align-items: center;
.pdf-page:not(:last-of-type) {
border-bottom: 1px solid var(--neutral-60);
}
}
.linked-file-icon {
color: var(--blue-50);
}
.no-preview {
color: var(--neutral-60);
font-size: var(--font-size-06);
margin-top: var(--spacing-06);
}
.text-preview {
margin-top: var(--spacing-05);
.scroll-container {
background-color: var(--white);
font-size: 0.8em;
line-height: 1.1em;
overflow: auto;
border: 1px solid var(--neutral-30);
padding: var(--spacing-04) var(--spacing-05);
text-align: left;
white-space: pre;
font-family: monospace;
}
}
}
.full-size,
.loading-panel {
position: absolute;
inset: 0;
}
.no-history-available,
.no-file-selection-message,
.multi-selection-message {
width: 50%;
margin: var(--spacing-10) auto;
text-align: center;
}
.pdf-empty,
.no-history-available,
.no-file-selection,
.multi-selection-ongoing {
&::before {
@extend .full-size;
left: 20px;
content: '';
background: url(../../../../../public/img/ol-brand/overleaf-o-grey.svg)
center / 200px no-repeat;
opacity: 0.2;
pointer-events: none;
}
}

View file

@ -48,7 +48,7 @@ describe('<FileView/>', function () {
renderWithEditorContext(<FileView file={textFile} />) renderWithEditorContext(<FileView file={textFile} />)
await waitForElementToBeRemoved(() => await waitForElementToBeRemoved(() =>
screen.getByText('Loading', { exact: false }) screen.getByTestId('loading-panel-file-view')
) )
}) })
@ -70,7 +70,7 @@ describe('<FileView/>', function () {
it('shows a loading indicator while the file is loading', async function () { it('shows a loading indicator while the file is loading', async function () {
renderWithEditorContext(<FileView file={imageFile} />) renderWithEditorContext(<FileView file={imageFile} />)
screen.getByText('Loading', { exact: false }) screen.getByTestId('loading-panel-file-view')
}) })
it('shows messaging if the image could not be loaded', async function () { it('shows messaging if the image could not be loaded', async function () {