[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 { formatTime, relativeDate } from '../../utils/format-date'
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 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 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 }
@ -58,50 +61,44 @@ export default function FileViewHeader({ file }: FileViewHeaderProps) {
let fileInfo
if (file.linkedFileData) {
if (hasProvider(file, 'url')) {
fileInfo = (
<div>
<UrlProvider file={file} />
</div>
)
fileInfo = <UrlProvider file={file} />
} else if (hasProvider(file, 'project_file')) {
fileInfo = (
<div>
<ProjectFilePathProvider file={file} />
</div>
)
fileInfo = <ProjectFilePathProvider file={file} />
} else if (hasProvider(file, 'project_output_file')) {
fileInfo = (
<div>
<ProjectOutputFileProvider file={file} />
</div>
)
fileInfo = <ProjectOutputFileProvider file={file} />
}
}
return (
<div>
<>
{file.linkedFileData && fileInfo}
{file.linkedFileData &&
tprFileViewInfo.map(({ import: { TPRFileViewInfo }, path }) => (
<TPRFileViewInfo key={path} file={file} />
))}
{file.linkedFileData && !fileTreeReadOnly && (
<FileViewRefreshButton file={file} setRefreshError={setRefreshError} />
)}
&nbsp;
<a
download={file.name}
href={
fileTreeFromHistory
? `/project/${projectId}/blob/${file.hash}`
: `/project/${projectId}/file/${file.id}`
}
className="btn btn-secondary-info btn-secondary"
>
<Icon type="download" fw />
&nbsp;
<span>{t('download')}</span>
</a>
<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 }) => (
@ -111,7 +108,7 @@ export default function FileViewHeader({ file }: FileViewHeaderProps) {
{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 */
return (
<p>
<LinkedFileIcon />
<LinkedFileIcon />{' '}
<Trans
i18nKey="imported_from_another_project_at_date"
components={

View file

@ -1,12 +1,26 @@
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 => {
return (
<Icon
type="external-link-square"
modifier="rotate-180"
className="linked-file-icon"
{...props}
<BootstrapVersionSwitcher
bs3={
<Icon
type="external-link-square"
modifier="rotate-180"
className="linked-file-icon"
{...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 { Nullable } from '../../../../../types/utils'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import OLButton from '@/features/ui/components/ol/ol-button'
type FileViewRefreshButtonProps = {
setRefreshError: Dispatch<SetStateAction<Nullable<string>>>
@ -90,13 +91,21 @@ function FileViewRefreshButtonDefault({
const { t } = useTranslation()
return (
<button
className="btn btn-primary"
<OLButton
variant="primary"
onClick={() => refreshFile(null)}
disabled={refreshing}
isLoading={refreshing}
bs3Props={{
loading: (
<>
<Icon type="refresh" spin={refreshing} fw />{' '}
<span>{refreshing ? `${t('refreshing')}` : t('refresh')}</span>
</>
),
}}
>
<Icon type="refresh" spin={refreshing} fw />
<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 importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { BinaryFile } from '../types/binary-file'
import OLNotification from '@/features/ui/components/ol/ol-notification'
type FileViewRefreshErrorProps = {
file: BinaryFile
@ -33,11 +34,15 @@ export default function FileViewRefreshError({
)[0]
} else {
return (
<div className="row">
<br />
<div className="alert alert-danger col-md-6 col-md-offset-3">
{t('access_denied')}: {refreshError}
</div>
<div className="file-view-error">
<OLNotification
type="error"
content={
<span>
{t('access_denied')}: {refreshError}
</span>
}
/>
</div>
)
}

View file

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

View file

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

View file

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

View file

@ -4,6 +4,33 @@
background-color: @gray-lightest;
text-align: center;
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,
.file-view-pdf {
max-width: 100%;
@ -43,10 +70,7 @@
line-height: 1.1em;
overflow: auto;
border: 1px solid @gray-lighter;
padding-left: 12px;
padding-right: 12px;
padding-top: 8px;
padding-bottom: 8px;
padding: 8px 12px;
text-align: left;
white-space: pre;
font-family: monospace;

View file

@ -10,6 +10,7 @@
@import 'editor/loading-screen';
@import 'editor/outline';
@import 'editor/file-tree';
@import 'editor/file-view';
@import 'editor/figure-modal';
@import 'editor/chat';
@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} />)
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 () {
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 () {