diff --git a/services/web/frontend/js/features/file-view/components/file-view-header.tsx b/services/web/frontend/js/features/file-view/components/file-view-header.tsx
index 0e0a8ab4ea..04ff106db7 100644
--- a/services/web/frontend/js/features/file-view/components/file-view-header.tsx
+++ b/services/web/frontend/js/features/file-view/components/file-view-header.tsx
@@ -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 = (
-
-
-
- )
+ fileInfo =
} else if (hasProvider(file, 'project_file')) {
- fileInfo = (
-
- )
+ fileInfo =
} else if (hasProvider(file, 'project_output_file')) {
- fileInfo = (
-
- )
+ fileInfo =
}
}
return (
-
+ <>
{file.linkedFileData && fileInfo}
{file.linkedFileData &&
tprFileViewInfo.map(({ import: { TPRFileViewInfo }, path }) => (
))}
- {file.linkedFileData && !fileTreeReadOnly && (
-
- )}
-
-
-
-
- {t('download')}
-
+
+ {file.linkedFileData && !fileTreeReadOnly && (
+
+ )}
+
+ }
+ bs5={}
+ />{' '}
+ {t('download')}
+
+
{file.linkedFileData &&
tprFileViewNotOriginalImporter.map(
({ import: { TPRFileViewNotOriginalImporter }, path }) => (
@@ -111,7 +108,7 @@ export default function FileViewHeader({ file }: FileViewHeaderProps) {
{refreshError && (
)}
-
+ >
)
}
@@ -150,7 +147,7 @@ function ProjectFilePathProvider({ file }: ProjectFilePathProviderProps) {
/* eslint-disable jsx-a11y/anchor-has-content, react/jsx-key */
return (
-
+ {' '}
{
return (
-
+ }
+ bs5={
+
+ }
/>
)
}
diff --git a/services/web/frontend/js/features/file-view/components/file-view-refresh-button.tsx b/services/web/frontend/js/features/file-view/components/file-view-refresh-button.tsx
index be6e68ccc0..46721c7520 100644
--- a/services/web/frontend/js/features/file-view/components/file-view-refresh-button.tsx
+++ b/services/web/frontend/js/features/file-view/components/file-view-refresh-button.tsx
@@ -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>>
@@ -90,13 +91,21 @@ function FileViewRefreshButtonDefault({
const { t } = useTranslation()
return (
-
+ {t('refresh')}
+
)
}
diff --git a/services/web/frontend/js/features/file-view/components/file-view-refresh-error.tsx b/services/web/frontend/js/features/file-view/components/file-view-refresh-error.tsx
index ebc126ae19..363c663988 100644
--- a/services/web/frontend/js/features/file-view/components/file-view-refresh-error.tsx
+++ b/services/web/frontend/js/features/file-view/components/file-view-refresh-error.tsx
@@ -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 (
-
-
-
- {t('access_denied')}: {refreshError}
-
+
+
+ {t('access_denied')}: {refreshError}
+
+ }
+ />
)
}
diff --git a/services/web/frontend/js/features/file-view/components/file-view-text.tsx b/services/web/frontend/js/features/file-view/components/file-view-text.tsx
index cf3f2263dd..ddc5a216ef 100644
--- a/services/web/frontend/js/features/file-view/components/file-view-text.tsx
+++ b/services/web/frontend/js/features/file-view/components/file-view-text.tsx
@@ -93,15 +93,13 @@ export default function FileViewText({
fetchDataController,
])
return (
-
- {textPreview && (
-
-
-
{textPreview}
- {shouldShowDots &&
...
}
-
+ Boolean(textPreview) && (
+
+
+
{textPreview}
+ {shouldShowDots &&
...
}
- )}
-
+
+ )
)
}
diff --git a/services/web/frontend/js/features/file-view/components/file-view.jsx b/services/web/frontend/js/features/file-view/components/file-view.jsx
index bb567bb8c5..711d125fe9 100644
--- a/services/web/frontend/js/features/file-view/components/file-view.jsx
+++ b/services/web/frontend/js/features/file-view/components/file-view.jsx
@@ -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 (
-
-
-
- {t('loading')}…
-
+
+
)
}
diff --git a/services/web/frontend/js/features/ui/components/types/button-props.ts b/services/web/frontend/js/features/ui/components/types/button-props.ts
index 07ff9c2a9c..433655ae8a 100644
--- a/services/web/frontend/js/features/ui/components/types/button-props.ts
+++ b/services/web/frontend/js/features/ui/components/types/button-props.ts
@@ -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
diff --git a/services/web/frontend/stylesheets/app/editor/file-view.less b/services/web/frontend/stylesheets/app/editor/file-view.less
index 573d86da41..552305870e 100644
--- a/services/web/frontend/stylesheets/app/editor/file-view.less
+++ b/services/web/frontend/stylesheets/app/editor/file-view.less
@@ -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;
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss
index 6668a9ba9a..ca015ca871 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss
@@ -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';
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/file-view.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/file-view.scss
new file mode 100644
index 0000000000..4a65914c4a
--- /dev/null
+++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/file-view.scss
@@ -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;
+ }
+}
diff --git a/services/web/test/frontend/features/file-view/components/file-view.test.jsx b/services/web/test/frontend/features/file-view/components/file-view.test.jsx
index 5a97e36629..638b5d40ea 100644
--- a/services/web/test/frontend/features/file-view/components/file-view.test.jsx
+++ b/services/web/test/frontend/features/file-view/components/file-view.test.jsx
@@ -48,7 +48,7 @@ describe('
', function () {
renderWithEditorContext(
)
await waitForElementToBeRemoved(() =>
- screen.getByText('Loading', { exact: false })
+ screen.getByTestId('loading-panel-file-view')
)
})
@@ -70,7 +70,7 @@ describe('
', function () {
it('shows a loading indicator while the file is loading', async function () {
renderWithEditorContext(
)
- screen.getByText('Loading', { exact: false })
+ screen.getByTestId('loading-panel-file-view')
})
it('shows messaging if the image could not be loaded', async function () {