mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
[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:
parent
2e080a3a34
commit
d6de6da781
11 changed files with 223 additions and 74 deletions
|
@ -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} />
|
||||
)}
|
||||
|
||||
<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 />
|
||||
|
||||
<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={
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
{t('loading')}…
|
||||
</span>
|
||||
<div
|
||||
className="loading-panel loading-panel-file-view"
|
||||
data-testid="loading-panel-file-view"
|
||||
>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 () {
|
||||
|
|
Loading…
Reference in a new issue