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

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

GitOrigin-RevId: c195ecf0dd9e38ec8326c823174e559e1f192ce1
This commit is contained in:
Rebeka Dekany 2024-10-11 11:23:33 +02:00 committed by Copybot
parent f8efc3e2ae
commit 5b6bbcb73c
14 changed files with 236 additions and 138 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 { .ide-react-editor-sidebar {
background-color: var(--file-tree-bg); background-color: var(--file-tree-bg);
height: 100%; height: 100%;
@ -57,16 +70,6 @@
color: var(--neutral-20); 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 { .ide-react-editor-panel {
display: flex; display: flex;
flex-direction: column; 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 { .horizontal-resize-handle {
width: 7px !important; width: 7px !important;
height: 100%; height: 100%;