mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
f8efc3e2ae
commit
5b6bbcb73c
14 changed files with 236 additions and 138 deletions
|
@ -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
|
||||
<OLNotification
|
||||
type="warning"
|
||||
content={<strong>{t('synctex_failed')}</strong>}
|
||||
action={
|
||||
<OLButton
|
||||
href="/learn/how-to/SyncTeX_Errors"
|
||||
target="_blank"
|
||||
id="synctex-more-info-button"
|
||||
className="alert-link-as-btn pull-right"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
bs3Props={{ className: 'alert-link-as-btn pull-right' }}
|
||||
>
|
||||
{t('more_info')}
|
||||
</a>
|
||||
</Alert>
|
||||
</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">
|
||||
<OLNotification
|
||||
type="warning"
|
||||
content={
|
||||
<strong>{t('editor_disconected_click_to_reconnect')}</strong>
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{debugging ? (
|
||||
<Alert bsStyle="warning" className="small">
|
||||
<strong>Connected: {isConnected.toString()}</strong>
|
||||
</Alert>
|
||||
<OLNotification
|
||||
type="warning"
|
||||
content={<strong>Connected: {isConnected.toString()}</strong>}
|
||||
/>
|
||||
) : null}
|
||||
</>,
|
||||
globalAlertsContainer
|
||||
|
|
|
@ -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">
|
||||
<OLNotification
|
||||
type="warning"
|
||||
content={
|
||||
<>
|
||||
<strong>{t('lost_connection')}</strong>{' '}
|
||||
{t('reconnecting_in_x_secs', { seconds: secondsUntilReconnect })}.
|
||||
<button
|
||||
</>
|
||||
}
|
||||
action={
|
||||
<OLButton
|
||||
id="try-reconnect-now-button"
|
||||
className="pull-right"
|
||||
onClick={() => tryReconnectNow()}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
bs3Props={{ className: 'pull-right' }}
|
||||
>
|
||||
{t('try_now')}
|
||||
</button>
|
||||
</Alert>
|
||||
</OLButton>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ function OLNotification(props: OLNotificationProps) {
|
|||
{bs3Props?.icon}
|
||||
{bs3Props?.icon && ' '}
|
||||
{notificationProps.content}
|
||||
{notificationProps.action}
|
||||
</Alert>
|
||||
}
|
||||
bs5={
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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,7 +13,8 @@ export const GenericErrorBoundaryFallback: FC = ({ children }) => {
|
|||
|
||||
return (
|
||||
<div className="error-boundary-container">
|
||||
<div className="icon-error-boundary-container">
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<Icon
|
||||
accessibilityLabel={`${t('generic_something_went_wrong')} ${t(
|
||||
'please_refresh'
|
||||
|
@ -19,15 +22,25 @@ export const GenericErrorBoundaryFallback: FC = ({ children }) => {
|
|||
type="exclamation-triangle fa-2x"
|
||||
fw
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
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,30 +11,16 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 211px;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
padding: 0px;
|
||||
width: 266px;
|
||||
}
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.error-message-container {
|
||||
.error-message {
|
||||
align-items: center;
|
||||
color: @neutral-90;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -28,3 +28,4 @@
|
|||
@import 'link';
|
||||
@import 'pagination';
|
||||
@import 'loading-spinner';
|
||||
@import 'error-boundary';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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%;
|
||||
|
|
Loading…
Reference in a new issue