[web] hide all toasts when offline (#35400)

GitOrigin-RevId: cf179bf7a761ddaae56f3e5bab6327b0f3625fb5
This commit is contained in:
Kristina
2026-09-17 11:26:23 +00:00
committed by Copybot
parent 6f55a97262
commit 26f12a6366
4 changed files with 188 additions and 52 deletions
@@ -7,6 +7,7 @@ import { createPortal } from 'react-dom'
import { useGlobalAlertsContainer } from '@/features/ide-react/context/global-alerts-context'
import Notification from '@/shared/components/notification'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import { useFeatureFlag } from '@/shared/context/split-test-context'
const rollingBuildsUpdatedAlert: Array<{
import: { default: ElementType }
@@ -23,6 +24,9 @@ export function Alerts() {
secondsUntilReconnect,
} = useConnectionContext()
const globalAlertsContainer = useGlobalAlertsContainer()
const improvedFlakyConnections = useFeatureFlag(
'intermittent-connection-improvements'
)
if (!globalAlertsContainer) {
return null
@@ -36,56 +40,60 @@ export function Alerts() {
)
)}
{connectionState.forceDisconnected &&
// hide "disconnected" banner when displaying out of sync modal
connectionState.error !== 'out-of-sync' ? (
<div className="notification-list">
<Notification
type="error"
content={<strong>{t('disconnected')}</strong>}
/>
</div>
) : null}
{!improvedFlakyConnections && (
<>
{connectionState.forceDisconnected &&
// hide "disconnected" banner when displaying out of sync modal
connectionState.error !== 'out-of-sync' ? (
<div className="notification-list">
<Notification
type="error"
content={<strong>{t('disconnected')}</strong>}
/>
</div>
) : null}
{connectionState.reconnectAt ? (
<LostConnectionAlert
reconnectAt={connectionState.reconnectAt}
tryReconnectNow={tryReconnectNow}
/>
) : null}
{connectionState.reconnectAt ? (
<LostConnectionAlert
reconnectAt={connectionState.reconnectAt}
tryReconnectNow={tryReconnectNow}
/>
) : null}
{isStillReconnecting ? (
<div className="notification-list">
<Notification
type="warning"
content={<strong>{t('reconnecting')}…</strong>}
/>
</div>
) : null}
{isStillReconnecting ? (
<div className="notification-list">
<Notification
type="warning"
content={<strong>{t('reconnecting')}…</strong>}
/>
</div>
) : null}
{connectionState.inactiveDisconnect ||
(connectionState.readyState === WebSocket.CLOSED &&
(connectionState.error === 'rate-limited' ||
connectionState.error === 'unable-to-connect') &&
!secondsUntilReconnect()) ? (
<div className="notification-list">
<Notification
type="warning"
content={
<strong>{t('editor_disconected_click_to_reconnect')}</strong>
}
/>
</div>
) : null}
{connectionState.inactiveDisconnect ||
(connectionState.readyState === WebSocket.CLOSED &&
(connectionState.error === 'rate-limited' ||
connectionState.error === 'unable-to-connect') &&
!secondsUntilReconnect()) ? (
<div className="notification-list">
<Notification
type="warning"
content={
<strong>{t('editor_disconected_click_to_reconnect')}</strong>
}
/>
</div>
) : null}
{debugging ? (
<div className="notification-list">
<Notification
type="warning"
content={<strong>Connected: {isConnected.toString()}</strong>}
/>
</div>
) : null}
{debugging ? (
<div className="notification-list">
<Notification
type="warning"
content={<strong>Connected: {isConnected.toString()}</strong>}
/>
</div>
) : null}
</>
)}
</>,
globalAlertsContainer
)
@@ -3,11 +3,15 @@ import { useTranslation } from 'react-i18next'
import Notification from '@/shared/components/notification'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { useFeatureFlag } from '@/shared/context/split-test-context'
export const UnsavedDocsLockedAlert: FC = () => {
const { t } = useTranslation()
const { openDocs } = useEditorManagerContext()
const { reportError } = useIdeReactContext()
const improvedFlakyConnections = useFeatureFlag(
'intermittent-connection-improvements'
)
useEffect(() => {
const { pendingOpsLength, inflightOpsLength } = openDocs.getUnsavedOpsSize()
@@ -17,6 +21,10 @@ export const UnsavedDocsLockedAlert: FC = () => {
})
}, [reportError, openDocs])
if (improvedFlakyConnections) {
return null
}
return (
<div className="notification-list">
<Notification
@@ -0,0 +1,67 @@
import { Alerts } from '@/features/ide-react/components/alerts/alerts'
import { ConnectionContext } from '@/features/ide-react/context/connection-context'
import { GlobalAlertsProvider } from '@/features/ide-react/context/global-alerts-context'
import { ProjectContext } from '@/shared/context/project-context'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import { makeTutorialProvider } from '../../../../helpers/make-tutorial-provider'
const TutorialProvider = makeTutorialProvider()
const disconnectedConnectionContextValue = {
socket: { socket: {} },
connectionState: {
forceDisconnected: true,
readyState: WebSocket.CLOSED,
error: '',
},
isConnected: false,
isStillReconnecting: false,
secondsUntilReconnect: () => 0,
tryReconnectNow: () => {},
registerUserActivity: () => {},
closeConnection: () => {},
getSocketDebuggingInfo: () => ({ id: '' }),
} as any
const projectContextValue = {
project: { imageName: 'texlive-full:2023.1' },
} as any
const mount = () => {
cy.mount(
<SplitTestProvider>
<TutorialProvider>
<ProjectContext.Provider value={projectContextValue}>
<GlobalAlertsProvider>
<ConnectionContext.Provider
value={disconnectedConnectionContextValue}
>
<Alerts />
</ConnectionContext.Provider>
</GlobalAlertsProvider>
</ProjectContext.Provider>
</TutorialProvider>
</SplitTestProvider>
)
}
const enableFlag = () => {
cy.window().then(win => {
win.metaAttributesCache.set('ol-splitTestVariants', {
'intermittent-connection-improvements': 'enabled',
})
})
}
describe('<Alerts />', function () {
it('shows the connection alert when flag is disabled', function () {
mount()
cy.findByRole('alert').should('exist')
})
it('suppresses the connection alert when flag is enabled', function () {
enableFlag()
mount()
cy.findByRole('alert').should('not.exist')
})
})
@@ -4,6 +4,8 @@ import { GlobalAlertsProvider } from '@/features/ide-react/context/global-alerts
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import { ConnectionContext } from '@/features/ide-react/context/connection-context'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import { EditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
import { IdeReactContext } from '@/features/ide-react/context/ide-react-context'
const defaultUnsavedDocsContextValue = {
unsavedDocs: new Map<string, number>([['doc1', 16]]),
@@ -37,16 +39,37 @@ const mockFileTreePathContextValue = {
previewByPath: () => null,
}
const mount = () => {
const mockEditorManagerContextValue = {
openDocs: {
getUnsavedOpsSize: () => ({ pendingOpsLength: 2, inflightOpsLength: 1 }),
},
} as any
const mount = ({
unsavedDocsContextValue = defaultUnsavedDocsContextValue,
reportError = cy.stub(),
}: {
unsavedDocsContextValue?: typeof defaultUnsavedDocsContextValue
reportError?: any
} = {}) => {
const ideReactContextValue = { reportError } as any
cy.mount(
<SplitTestProvider>
<GlobalAlertsProvider>
<ConnectionContext.Provider value={mockConnectionContextValue}>
<FileTreePathContext.Provider value={mockFileTreePathContextValue}>
<UnsavedDocsContext.Provider value={defaultUnsavedDocsContextValue}>
<UnsavedDocs />
</UnsavedDocsContext.Provider>
</FileTreePathContext.Provider>
<IdeReactContext.Provider value={ideReactContextValue}>
<EditorManagerContext.Provider
value={mockEditorManagerContextValue}
>
<FileTreePathContext.Provider
value={mockFileTreePathContextValue}
>
<UnsavedDocsContext.Provider value={unsavedDocsContextValue}>
<UnsavedDocs />
</UnsavedDocsContext.Provider>
</FileTreePathContext.Provider>
</EditorManagerContext.Provider>
</IdeReactContext.Provider>
</ConnectionContext.Provider>
</GlobalAlertsProvider>
</SplitTestProvider>
@@ -72,4 +95,34 @@ describe('<UnsavedDocs />', function () {
mount()
cy.findByRole('alert').should('not.exist')
})
describe('when locked', function () {
const lockedContextValue = {
unsavedDocs: new Map<string, number>(),
isLocked: true,
}
it('shows the locked alert and reports the error when flag is disabled', function () {
const reportError = cy.stub().as('reportError')
mount({ unsavedDocsContextValue: lockedContextValue, reportError })
cy.findByRole('alert').should('exist')
cy.get('@reportError').should(
'have.been.calledOnceWith',
'connection-lost-with-unsaved-changes',
{ pendingOpsLength: 2, inflightOpsLength: 1 }
)
})
it('hides the locked alert but still reports the error when flag is enabled', function () {
const reportError = cy.stub().as('reportError')
enableFlag()
mount({ unsavedDocsContextValue: lockedContextValue, reportError })
cy.findByRole('alert').should('not.exist')
cy.get('@reportError').should(
'have.been.calledOnceWith',
'connection-lost-with-unsaved-changes',
{ pendingOpsLength: 2, inflightOpsLength: 1 }
)
})
})
})