Преглед изворни кода

[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
Antoine Clausse пре 1 година
родитељ
комит
d6de6da781

+ 33 - 36
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 = (
-        <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={

+ 19 - 5
services/web/frontend/js/features/file-view/components/file-view-icons.jsx

@@ -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}
+        />
+      }
     />
   )
 }

+ 14 - 5
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<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>
   )
 }

+ 10 - 5
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 (
-      <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>
     )
   }

+ 7 - 9
services/web/frontend/js/features/file-view/components/file-view-text.tsx

@@ -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
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 (
-    <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>
   )
 }

+ 1 - 1
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

+ 28 - 4
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;

+ 1 - 0
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';

+ 102 - 0
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;
+  }
+}

+ 2 - 2
services/web/test/frontend/features/file-view/components/file-view.test.jsx

@@ -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 () {