Просмотр исходного кода

Merge pull request #21008 from overleaf/rd-editor-errors

[web] Migrate notifications and error boundaries on the editor page

GitOrigin-RevId: c195ecf0dd9e38ec8326c823174e559e1f192ce1
Rebeka Dekany 1 год назад
Родитель
Сommit
5b6bbcb73c

+ 36 - 24
services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx

@@ -2,10 +2,11 @@ import { useTranslation } from 'react-i18next'
 import { LostConnectionAlert } from './lost-connection-alert'
 import { useConnectionContext } from '@/features/ide-react/context/connection-context'
 import { debugging } from '@/utils/debugging'
-import { Alert } from 'react-bootstrap'
 import useScopeValue from '@/shared/hooks/use-scope-value'
 import { createPortal } from 'react-dom'
 import { useGlobalAlertsContainer } from '@/features/ide-react/context/global-alerts-context'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 export function Alerts() {
   const { t } = useTranslation()
@@ -27,9 +28,10 @@ export function Alerts() {
   return createPortal(
     <>
       {connectionState.forceDisconnected ? (
-        <Alert bsStyle="danger" className="small">
-          <strong>{t('disconnected')}</strong>
-        </Alert>
+        <OLNotification
+          type="error"
+          content={<strong>{t('disconnected')}</strong>}
+        />
       ) : null}
 
       {connectionState.reconnectAt ? (
@@ -40,23 +42,29 @@ export function Alerts() {
       ) : null}
 
       {isStillReconnecting ? (
-        <Alert bsStyle="warning" className="small">
-          <strong>{t('reconnecting')}…</strong>
-        </Alert>
+        <OLNotification
+          type="warning"
+          content={<strong>{t('reconnecting')}…</strong>}
+        />
       ) : null}
 
       {synctexError ? (
-        <Alert bsStyle="warning" className="small">
-          <strong>{t('synctex_failed')}</strong>
-          <a
-            href="/learn/how-to/SyncTeX_Errors"
-            target="_blank"
-            id="synctex-more-info-button"
-            className="alert-link-as-btn pull-right"
-          >
-            {t('more_info')}
-          </a>
-        </Alert>
+        <OLNotification
+          type="warning"
+          content={<strong>{t('synctex_failed')}</strong>}
+          action={
+            <OLButton
+              href="/learn/how-to/SyncTeX_Errors"
+              target="_blank"
+              id="synctex-more-info-button"
+              variant="secondary"
+              size="sm"
+              bs3Props={{ className: 'alert-link-as-btn pull-right' }}
+            >
+              {t('more_info')}
+            </OLButton>
+          }
+        />
       ) : null}
 
       {connectionState.inactiveDisconnect ||
@@ -64,15 +72,19 @@ export function Alerts() {
         (connectionState.error === 'rate-limited' ||
           connectionState.error === 'unable-to-connect') &&
         !secondsUntilReconnect()) ? (
-        <Alert bsStyle="warning" className="small">
-          <strong>{t('editor_disconected_click_to_reconnect')}</strong>
-        </Alert>
+        <OLNotification
+          type="warning"
+          content={
+            <strong>{t('editor_disconected_click_to_reconnect')}</strong>
+          }
+        />
       ) : null}
 
       {debugging ? (
-        <Alert bsStyle="warning" className="small">
-          <strong>Connected: {isConnected.toString()}</strong>
-        </Alert>
+        <OLNotification
+          type="warning"
+          content={<strong>Connected: {isConnected.toString()}</strong>}
+        />
       ) : null}
     </>,
     globalAlertsContainer

+ 22 - 12
services/web/frontend/js/features/ide-react/components/alerts/lost-connection-alert.tsx

@@ -1,7 +1,8 @@
 import { useTranslation } from 'react-i18next'
 import { useEffect, useState } from 'react'
 import { secondsUntil } from '@/features/ide-react/connection/utils'
-import { Alert } from 'react-bootstrap'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 type LostConnectionAlertProps = {
   reconnectAt: number
@@ -25,16 +26,25 @@ export function LostConnectionAlert({
   }, [reconnectAt])
 
   return (
-    <Alert bsStyle="warning" className="small">
-      <strong>{t('lost_connection')}</strong>{' '}
-      {t('reconnecting_in_x_secs', { seconds: secondsUntilReconnect })}.
-      <button
-        id="try-reconnect-now-button"
-        className="pull-right"
-        onClick={() => tryReconnectNow()}
-      >
-        {t('try_now')}
-      </button>
-    </Alert>
+    <OLNotification
+      type="warning"
+      content={
+        <>
+          <strong>{t('lost_connection')}</strong>{' '}
+          {t('reconnecting_in_x_secs', { seconds: secondsUntilReconnect })}.
+        </>
+      }
+      action={
+        <OLButton
+          id="try-reconnect-now-button"
+          onClick={() => tryReconnectNow()}
+          size="sm"
+          variant="secondary"
+          bs3Props={{ className: 'pull-right' }}
+        >
+          {t('try_now')}
+        </OLButton>
+      }
+    />
   )
 }

+ 12 - 9
services/web/frontend/js/features/ide-react/components/modals/force-disconnected.tsx

@@ -1,8 +1,11 @@
 import { useTranslation } from 'react-i18next'
-import { Modal } from 'react-bootstrap'
-import AccessibleModal from '@/shared/components/accessible-modal'
 import { memo, useEffect, useState } from 'react'
 import { useConnectionContext } from '@/features/ide-react/context/connection-context'
+import OLModal, {
+  OLModalBody,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
 
 // show modal when editor is forcefully disconnected
 function ForceDisconnected() {
@@ -40,7 +43,7 @@ function ForceDisconnected() {
   }
 
   return (
-    <AccessibleModal
+    <OLModal
       show
       // It's not possible to hide this modal, but it's a required prop
       onHide={() => {}}
@@ -48,13 +51,13 @@ function ForceDisconnected() {
       backdrop={false}
       keyboard={false}
     >
-      <Modal.Header>
-        <Modal.Title>{t('please_wait')}</Modal.Title>
-      </Modal.Header>
-      <Modal.Body>
+      <OLModalHeader>
+        <OLModalTitle>{t('please_wait')}</OLModalTitle>
+      </OLModalHeader>
+      <OLModalBody>
         {t('were_performing_maintenance', { seconds: secondsUntilRefresh })}
-      </Modal.Body>
-    </AccessibleModal>
+      </OLModalBody>
+    </OLModal>
   )
 }
 

+ 18 - 13
services/web/frontend/js/features/ide-react/components/modals/generic-message-modal.tsx

@@ -1,14 +1,19 @@
 import { useTranslation } from 'react-i18next'
-import { Modal } from 'react-bootstrap'
-import AccessibleModal from '@/shared/components/accessible-modal'
 import { memo } from 'react'
+import OLModal, {
+  OLModalBody,
+  OLModalFooter,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 export type GenericMessageModalOwnProps = {
   title: string
   message: string
 }
 
-type GenericMessageModalProps = React.ComponentProps<typeof AccessibleModal> &
+type GenericMessageModalProps = React.ComponentProps<typeof OLModal> &
   GenericMessageModalOwnProps
 
 function GenericMessageModal({
@@ -19,19 +24,19 @@ function GenericMessageModal({
   const { t } = useTranslation()
 
   return (
-    <AccessibleModal {...modalProps}>
-      <Modal.Header closeButton>
-        <Modal.Title>{title}</Modal.Title>
-      </Modal.Header>
+    <OLModal {...modalProps}>
+      <OLModalHeader closeButton>
+        <OLModalTitle>{title}</OLModalTitle>
+      </OLModalHeader>
 
-      <Modal.Body className="modal-body-share">{message}</Modal.Body>
+      <OLModalBody className="modal-body-share">{message}</OLModalBody>
 
-      <Modal.Footer>
-        <button className="btn btn-info" onClick={() => modalProps.onHide()}>
+      <OLModalFooter>
+        <OLButton variant="secondary" onClick={() => modalProps.onHide()}>
           {t('ok')}
-        </button>
-      </Modal.Footer>
-    </AccessibleModal>
+        </OLButton>
+      </OLModalFooter>
+    </OLModal>
   )
 }
 

+ 23 - 18
services/web/frontend/js/features/ide-react/components/modals/out-of-sync-modal.tsx

@@ -1,8 +1,13 @@
 import { Trans, useTranslation } from 'react-i18next'
-import { Button, Modal } from 'react-bootstrap'
-import AccessibleModal from '@/shared/components/accessible-modal'
 import { memo, useState } from 'react'
 import { useLocation } from '@/shared/hooks/use-location'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import OLModal, {
+  OLModalBody,
+  OLModalFooter,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
 
 export type OutOfSyncModalProps = {
   editorContent: string
@@ -24,17 +29,17 @@ function OutOfSyncModal({ editorContent, show, onHide }: OutOfSyncModalProps) {
   }
 
   return (
-    <AccessibleModal
+    <OLModal
       show={show}
       onHide={done}
       className="out-of-sync-modal"
       backdrop={false}
       keyboard={false}
     >
-      <Modal.Header closeButton>
-        <Modal.Title>{t('out_of_sync')}</Modal.Title>
-      </Modal.Header>
-      <Modal.Body className="modal-body-share">
+      <OLModalHeader closeButton>
+        <OLModalTitle>{t('out_of_sync')}</OLModalTitle>
+      </OLModalHeader>
+      <OLModalBody className="modal-body-share">
         <Trans
           i18nKey="out_of_sync_detail"
           components={[
@@ -48,16 +53,16 @@ function OutOfSyncModal({ editorContent, show, onHide }: OutOfSyncModalProps) {
             />,
           ]}
         />
-      </Modal.Body>
-      <Modal.Body>
-        <Button
-          bsStyle="info"
+      </OLModalBody>
+      <OLModalBody>
+        <OLButton
+          variant="secondary"
           onClick={() => setEditorContentShown(shown => !shown)}
         >
           {editorContentShown
             ? t('hide_local_file_contents')
             : t('show_local_file_contents')}
-        </Button>
+        </OLButton>
         {editorContentShown ? (
           <div className="text-preview">
             <textarea
@@ -68,13 +73,13 @@ function OutOfSyncModal({ editorContent, show, onHide }: OutOfSyncModalProps) {
             />
           </div>
         ) : null}
-      </Modal.Body>
-      <Modal.Footer>
-        <Button bsStyle="info" onClick={done}>
+      </OLModalBody>
+      <OLModalFooter>
+        <OLButton variant="secondary" onClick={done}>
           {t('reload_editor')}
-        </Button>
-      </Modal.Footer>
-    </AccessibleModal>
+        </OLButton>
+      </OLModalFooter>
+    </OLModal>
   )
 }
 

+ 5 - 4
services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-alert.tsx

@@ -1,7 +1,7 @@
 import { FC, useMemo } from 'react'
 import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
-import { Alert } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
 
 export const UnsavedDocsAlert: FC<{ unsavedDocs: Map<string, number> }> = ({
   unsavedDocs,
@@ -33,11 +33,12 @@ const UnsavedDocAlert: FC<{ docId: string; seconds: number }> = ({
   }
 
   return (
-    <Alert bsStyle="warning" bsSize="small">
-      {t('saving_notification_with_seconds', {
+    <OLNotification
+      type="warning"
+      content={t('saving_notification_with_seconds', {
         docname: doc.entity.name,
         seconds,
       })}
-    </Alert>
+    />
   )
 }

+ 13 - 8
services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal.tsx

@@ -1,23 +1,28 @@
 import { FC } from 'react'
-import { Modal } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
-import AccessibleModal from '@/shared/components/accessible-modal'
+import OLModal, {
+  OLModalBody,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
 
 export const UnsavedDocsLockedModal: FC = () => {
   const { t } = useTranslation()
 
   return (
-    <AccessibleModal
+    <OLModal
       show
       onHide={() => {}} // It's not possible to hide this modal, but it's a required prop
       className="lock-editor-modal"
       backdrop={false}
       keyboard={false}
     >
-      <Modal.Header>
-        <Modal.Title>{t('connection_lost')}</Modal.Title>
-      </Modal.Header>
-      <Modal.Body>{t('sorry_the_connection_to_the_server_is_down')}</Modal.Body>
-    </AccessibleModal>
+      <OLModalHeader>
+        <OLModalTitle>{t('connection_lost')}</OLModalTitle>
+      </OLModalHeader>
+      <OLModalBody>
+        {t('sorry_the_connection_to_the_server_is_down')}
+      </OLModalBody>
+    </OLModal>
   )
 }

+ 1 - 0
services/web/frontend/js/features/ui/components/ol/ol-notification.tsx

@@ -28,6 +28,7 @@ function OLNotification(props: OLNotificationProps) {
           {bs3Props?.icon}
           {bs3Props?.icon && ' '}
           {notificationProps.content}
+          {notificationProps.action}
         </Alert>
       }
       bs5={

+ 2 - 2
services/web/frontend/js/shared/components/error-boundary-fallback.tsx

@@ -1,6 +1,6 @@
 import { FC, ReactNode } from 'react'
-import { Alert } from 'react-bootstrap'
 import { DefaultMessage } from './default-message'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
 
 export const ErrorBoundaryFallback: FC<{ modal?: ReactNode }> = ({
   children,
@@ -8,7 +8,7 @@ export const ErrorBoundaryFallback: FC<{ modal?: ReactNode }> = ({
 }) => {
   return (
     <div className="error-boundary-alert">
-      <Alert bsStyle="danger">{children || <DefaultMessage />}</Alert>
+      <OLNotification type="error" content={children || <DefaultMessage />} />
       {modal}
     </div>
   )

+ 26 - 13
services/web/frontend/js/shared/components/generic-error-boundary-fallback.tsx

@@ -1,9 +1,11 @@
 import { FC } from 'react'
 import { useTranslation } from 'react-i18next'
 import Icon from './icon'
-import { Button } from 'react-bootstrap'
 import { useLocation } from '../hooks/use-location'
 import { DefaultMessage } from './default-message'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import MaterialIcon from './material-icon'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 export const GenericErrorBoundaryFallback: FC = ({ children }) => {
   const { t } = useTranslation()
@@ -11,23 +13,34 @@ export const GenericErrorBoundaryFallback: FC = ({ children }) => {
 
   return (
     <div className="error-boundary-container">
-      <div className="icon-error-boundary-container">
-        <Icon
-          accessibilityLabel={`${t('generic_something_went_wrong')} ${t(
-            'please_refresh'
-          )}`}
-          type="exclamation-triangle fa-2x"
-          fw
-        />
-      </div>
+      <BootstrapVersionSwitcher
+        bs3={
+          <Icon
+            accessibilityLabel={`${t('generic_something_went_wrong')} ${t(
+              'please_refresh'
+            )}`}
+            type="exclamation-triangle fa-2x"
+            fw
+          />
+        }
+        bs5={
+          <MaterialIcon
+            accessibilityLabel={`${t('generic_something_went_wrong')} ${t(
+              'please_refresh'
+            )}`}
+            type="warning"
+            size="2x"
+          />
+        }
+      />
       {children || (
-        <div className="error-message-container">
+        <div className="error-message">
           <DefaultMessage className="small" style={{ fontWeight: 'bold' }} />
         </div>
       )}
-      <Button bsStyle="primary" onClick={handleClick}>
+      <OLButton variant="primary" onClick={handleClick}>
         {t('refresh')}
-      </Button>
+      </OLButton>
     </div>
   )
 }

+ 11 - 25
services/web/frontend/stylesheets/app/editor/error-boundary.less

@@ -11,30 +11,16 @@
   display: flex;
   flex-direction: column;
   gap: 16px;
-  height: 211px;
-  justify-content: center;
-  margin: auto;
-  padding: 0px;
-  width: 266px;
-}
-
-.error-message-container {
-  align-items: center;
-  color: @neutral-90;
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-  height: 56px;
-  padding: 0px;
-  width: 266px;
-}
+  position: absolute;
+  top: 40%;
+  left: 50%;
+  transform: translate(-50%, -50%);
 
-.icon-error-boundary-container {
-  align-items: center;
-  background: #ffffff;
-  border-radius: 999px;
-  display: grid;
-  height: 88px;
-  padding: 24px;
-  width: 88px;
+  .error-message {
+    align-items: center;
+    color: @neutral-90;
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+  }
 }

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/components/all.scss

@@ -28,3 +28,4 @@
 @import 'link';
 @import 'pagination';
 @import 'loading-spinner';
+@import 'error-boundary';

+ 22 - 0
services/web/frontend/stylesheets/bootstrap-5/components/error-boundary.scss

@@ -0,0 +1,22 @@
+.error-boundary-alert {
+  padding: var(--spacing-05);
+}
+
+.error-boundary-container {
+  align-items: center;
+  display: flex;
+  flex-direction: column;
+  gap: var(--spacing-06);
+  position: absolute;
+  top: 40%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+
+  .error-message {
+    align-items: center;
+    color: var(--content-primary);
+    display: flex;
+    flex-direction: column;
+    gap: var(--spacing-02);
+  }
+}

+ 44 - 10
services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss

@@ -26,6 +26,19 @@
   }
 }
 
+.global-alerts {
+  height: 0;
+  margin-top: var(--spacing-01);
+  text-align: center;
+
+  [role='alert'] {
+    text-align: left;
+    min-width: 400px;
+    position: relative;
+    z-index: 20;
+  }
+}
+
 .ide-react-editor-sidebar {
   background-color: var(--file-tree-bg);
   height: 100%;
@@ -57,16 +70,6 @@
   color: var(--neutral-20);
 }
 
-.ide-react-file-tree-panel {
-  display: flex;
-  flex-direction: column;
-
-  // Prevent the file tree expanding beyond the boundary of the panel
-  .file-tree {
-    width: 100%;
-  }
-}
-
 .ide-react-editor-panel {
   display: flex;
   flex-direction: column;
@@ -88,6 +91,37 @@
   }
 }
 
+.modal.lock-editor-modal {
+  display: flex !important;
+  background-color: rgba($bg-dark-primary, 0.3);
+  overflow-y: hidden;
+  pointer-events: none;
+
+  .modal-dialog {
+    top: 25px;
+  }
+}
+
+.out-of-sync-modal {
+  .text-preview {
+    margin-top: var(--spacing-05);
+
+    .scroll-container {
+      @include body-sm;
+
+      max-height: 360px;
+      width: 100%;
+      background-color: var(--bg-light-primary);
+      overflow: auto;
+      border: 1px solid var(--border-primary-dark);
+      padding: var(--spacing-04) var(--spacing-05);
+      text-align: left;
+      white-space: pre;
+      font-family: monospace;
+    }
+  }
+}
+
 .horizontal-resize-handle {
   width: 7px !important;
   height: 100%;