mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
[web] Add offline indicator to toolbar (#35368)
GitOrigin-RevId: 81127bff21afb6aadcf175ba1767a2629f01d747
This commit is contained in:
@@ -277,6 +277,7 @@
|
||||
"change_to_group_plan": "",
|
||||
"change_to_this_plan": "",
|
||||
"changes_applied": "",
|
||||
"changes_saved_in_browser_sync_when_online": "",
|
||||
"changes_to_these_settings_will_be_applied_to_all_new_projects": "",
|
||||
"changing_the_position_of_your_figure": "",
|
||||
"changing_the_position_of_your_table": "",
|
||||
@@ -2499,6 +2500,7 @@
|
||||
"you_have_x_licenses_and_your_plan_supports_up_to_y": "",
|
||||
"you_have_x_licenses_on_your_subscription": "",
|
||||
"you_need_to_configure_your_sso_settings": "",
|
||||
"you_re_offline": "",
|
||||
"you_unpaused_your_subscription": "",
|
||||
"you_will_be_able_to_reassign_subscription": "",
|
||||
"youll_get_best_results_in_visual_but_can_be_used_in_source": "",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
import { useUnsavedDocsContext } from '@/features/ide-react/context/unsaved-docs-context'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
export function OfflineIndicatorContent() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<OLTooltip
|
||||
id="tooltip-offline-indicator"
|
||||
description={t('changes_saved_in_browser_sync_when_online')}
|
||||
overlayProps={{ delay: 0, placement: 'bottom' }}
|
||||
>
|
||||
<div className="ide-redesign-toolbar-offline-indicator">
|
||||
<MaterialIcon type="wifi_off" />
|
||||
<span>{t('you_re_offline')}</span>
|
||||
</div>
|
||||
</OLTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default function OfflineIndicator() {
|
||||
const { t } = useTranslation()
|
||||
const { unsavedDocs } = useUnsavedDocsContext()
|
||||
const improvedFlakyConnections = useFeatureFlag(
|
||||
'intermittent-connection-improvements'
|
||||
)
|
||||
|
||||
if (!improvedFlakyConnections) {
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: use shared hook for this when merged
|
||||
const MAX_UNSAVED_ALERT_SECONDS = 15
|
||||
const isOffline = [...unsavedDocs.values()].some(
|
||||
seconds => seconds >= MAX_UNSAVED_ALERT_SECONDS
|
||||
)
|
||||
return (
|
||||
<>
|
||||
{isOffline && <OfflineIndicatorContent />}
|
||||
<div className="visually-hidden" role="status">
|
||||
{isOffline && (
|
||||
<>
|
||||
<span>{t('you_re_offline')}</span>
|
||||
<span>{t('changes_saved_in_browser_sync_when_online')}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -19,6 +19,8 @@ export const OnlineUsers = () => {
|
||||
[openDoc]
|
||||
)
|
||||
|
||||
if (!onlineUsersArray.length) return null
|
||||
|
||||
return (
|
||||
<div className="ide-redesign-online-users">
|
||||
<OnlineUsersWidget onlineUsers={onlineUsersArray} goToUser={goToUser} />
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
import OLIconButton from '@/shared/components/ol/ol-icon-button'
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
import SplitTestBadge from '@/shared/components/split-test-badge'
|
||||
import OfflineIndicator from './offline-indicator'
|
||||
|
||||
const [publishModalModules] = importOverleafModules('publishModalToolbarButton')
|
||||
const SubmitProjectButton = publishModalModules?.import.default
|
||||
@@ -128,15 +129,18 @@ export const Toolbar = () => {
|
||||
{showUpgradePrompt && upgradeButtonRelocation && <UpgradeButton />}
|
||||
</div>
|
||||
<ToolbarProjectTitle />
|
||||
<div className="ide-redesign-toolbar-actions">
|
||||
<OnlineUsers />
|
||||
{!isRestrictedTokenMember && <ShowHistoryButton />}
|
||||
<ChangeLayoutButton />
|
||||
{shouldDisplaySubmitButton && cobranding && (
|
||||
<SubmitProjectButton cobranding={cobranding} />
|
||||
)}
|
||||
<ShareProjectButton />
|
||||
{showUpgradePrompt && !upgradeButtonRelocation && <UpgradeButton />}
|
||||
<div className="ide-redesign-toolbar-actions-wrapper">
|
||||
<OfflineIndicator />
|
||||
<div className="ide-redesign-toolbar-actions">
|
||||
<OnlineUsers />
|
||||
{!isRestrictedTokenMember && <ShowHistoryButton />}
|
||||
<ChangeLayoutButton />
|
||||
{shouldDisplaySubmitButton && cobranding && (
|
||||
<SubmitProjectButton cobranding={cobranding} />
|
||||
)}
|
||||
<ShareProjectButton />
|
||||
{showUpgradePrompt && !upgradeButtonRelocation && <UpgradeButton />}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
|
||||
+6
-1
@@ -4,10 +4,14 @@ import { UnsavedDocsAlert } from '@/features/ide-react/components/unsaved-docs/u
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useGlobalAlertsContainer } from '@/features/ide-react/context/global-alerts-context'
|
||||
import { useUnsavedDocsContext } from '@/features/ide-react/context/unsaved-docs-context'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
export const UnsavedDocs: FC = () => {
|
||||
const { unsavedDocs, isLocked } = useUnsavedDocsContext()
|
||||
const globalAlertsContainer = useGlobalAlertsContainer()
|
||||
const improvedFlakyConnections = useFeatureFlag(
|
||||
'intermittent-connection-improvements'
|
||||
)
|
||||
|
||||
if (!globalAlertsContainer) {
|
||||
return null
|
||||
@@ -18,7 +22,8 @@ export const UnsavedDocs: FC = () => {
|
||||
{isLocked &&
|
||||
createPortal(<UnsavedDocsLockedAlert />, globalAlertsContainer)}
|
||||
|
||||
{unsavedDocs.size > 0 &&
|
||||
{!improvedFlakyConnections &&
|
||||
unsavedDocs.size > 0 &&
|
||||
createPortal(
|
||||
<UnsavedDocsAlert unsavedDocs={unsavedDocs} />,
|
||||
globalAlertsContainer
|
||||
|
||||
@@ -26,9 +26,9 @@ type UnsavedDocsContextValue = {
|
||||
isLocked: boolean
|
||||
}
|
||||
|
||||
const UnsavedDocsContext = createContext<UnsavedDocsContextValue | undefined>(
|
||||
undefined
|
||||
)
|
||||
export const UnsavedDocsContext = createContext<
|
||||
UnsavedDocsContextValue | undefined
|
||||
>(undefined)
|
||||
|
||||
export const UnsavedDocsProvider: FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
}
|
||||
|
||||
.ide-redesign-toolbar-menu,
|
||||
.ide-redesign-toolbar-actions {
|
||||
.ide-redesign-toolbar-actions-wrapper {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -187,10 +187,16 @@
|
||||
gap: var(--spacing-05);
|
||||
}
|
||||
|
||||
.ide-redesign-toolbar-actions-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-06);
|
||||
padding-left: var(--spacing-05);
|
||||
}
|
||||
|
||||
.ide-redesign-toolbar-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-04);
|
||||
padding-left: var(--spacing-05);
|
||||
}
|
||||
|
||||
.ide-redesign-toolbar-button-container {
|
||||
@@ -199,6 +205,22 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ide-redesign-toolbar-offline-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-03);
|
||||
color: var(--content-secondary-themed);
|
||||
font-size: var(--font-size-02);
|
||||
line-height: var(--line-height-02);
|
||||
cursor: default;
|
||||
|
||||
.material-symbols {
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-redesign-online-users {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -357,6 +357,7 @@
|
||||
"change_to_group_plan": "Change to a group plan",
|
||||
"change_to_this_plan": "Change to this plan",
|
||||
"changes_applied": "Changes applied",
|
||||
"changes_saved_in_browser_sync_when_online": "Your changes are saved in the browser. We’ll sync your work when you’re back online.",
|
||||
"changes_to_these_settings_will_be_applied_to_all_new_projects": "Changes to these settings will be applied to all new projects. Existing projects will not be affected.",
|
||||
"changing_the_position_of_your_figure": "Changing the position of your figure",
|
||||
"changing_the_position_of_your_table": "Changing the position of your table",
|
||||
@@ -3155,6 +3156,7 @@
|
||||
"you_have_x_licenses_and_your_plan_supports_up_to_y": "You have allocated __addedUsersSize__ licenses and your plan supports up to __groupSize__.",
|
||||
"you_have_x_licenses_on_your_subscription": "You have __groupSize__ licenses on your subscription.",
|
||||
"you_need_to_configure_your_sso_settings": "You need to configure and test your SSO settings before enabling SSO",
|
||||
"you_re_offline": "You’re offline",
|
||||
"you_unpaused_your_subscription": "You unpaused your subscription.",
|
||||
"you_will_be_able_to_contact_us_any_time_to_share_your_feedback": "<0>You will be able to contact us</0> any time to share your feedback",
|
||||
"you_will_be_able_to_reassign_subscription": "You will be able to reassign their subscription membership to another person in your organization",
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import OfflineIndicator from '@/features/ide-react/components/toolbar/offline-indicator'
|
||||
import { UnsavedDocsContext } from '@/features/ide-react/context/unsaved-docs-context'
|
||||
import { IdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||
import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
|
||||
import { SplitTestProvider } from '@/shared/context/split-test-context'
|
||||
|
||||
const defaultIdeReactContextValue = {
|
||||
projectId: 'test-project',
|
||||
eventEmitter: new IdeEventEmitter(),
|
||||
startedFreeTrial: false,
|
||||
setStartedFreeTrial: () => {},
|
||||
reportError: () => {},
|
||||
projectJoined: true,
|
||||
permissionsLevel: 'owner' as const,
|
||||
setPermissionsLevel: () => {},
|
||||
outOfSync: false,
|
||||
setOutOfSync: () => {},
|
||||
}
|
||||
|
||||
const defaultUnsavedDocsContextValue = {
|
||||
unsavedDocs: new Map<string, number>(),
|
||||
isLocked: false,
|
||||
}
|
||||
|
||||
const mount = ({
|
||||
unsavedDocs = new Map<string, number>(),
|
||||
}: {
|
||||
unsavedDocs?: Map<string, number>
|
||||
} = {}) => {
|
||||
cy.mount(
|
||||
<SplitTestProvider>
|
||||
<IdeReactContext.Provider value={defaultIdeReactContextValue}>
|
||||
<UnsavedDocsContext.Provider
|
||||
value={{ ...defaultUnsavedDocsContextValue, unsavedDocs }}
|
||||
>
|
||||
<OfflineIndicator />
|
||||
</UnsavedDocsContext.Provider>
|
||||
</IdeReactContext.Provider>
|
||||
</SplitTestProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const enableFlag = () => {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-splitTestVariants', {
|
||||
'intermittent-connection-improvements': 'enabled',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const unsavedDoc = () => new Map<string, number>([['doc-1', 20]])
|
||||
|
||||
describe('<OfflineIndicator />', function () {
|
||||
it('is hidden when the feature flag is disabled', function () {
|
||||
mount({ unsavedDocs: unsavedDoc() })
|
||||
cy.findByText('You’re offline').should('not.exist')
|
||||
cy.findByRole('status').should('not.exist')
|
||||
})
|
||||
|
||||
it('is hidden when the flag is enabled but there are no unsaved docs', function () {
|
||||
enableFlag()
|
||||
mount()
|
||||
cy.findByText('You’re offline').should('not.exist')
|
||||
cy.findByRole('status').should('be.empty')
|
||||
})
|
||||
|
||||
it('is shown when the flag is enabled and there are unsaved docs', function () {
|
||||
enableFlag()
|
||||
mount({ unsavedDocs: unsavedDoc() })
|
||||
cy.findAllByText('You’re offline').first().should('be.visible')
|
||||
})
|
||||
|
||||
it('announces the offline status to screen readers via a live region', function () {
|
||||
enableFlag()
|
||||
mount({ unsavedDocs: unsavedDoc() })
|
||||
cy.findByRole('status').within(() => {
|
||||
cy.findByText('You’re offline').should('exist')
|
||||
cy.findByText(
|
||||
'Your changes are saved in the browser. We’ll sync your work when you’re back online.'
|
||||
).should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { UnsavedDocs } from '@/features/ide-react/components/unsaved-docs/unsaved-docs'
|
||||
import { UnsavedDocsContext } from '@/features/ide-react/context/unsaved-docs-context'
|
||||
import { GlobalAlertsProvider } from '@/features/ide-react/context/global-alerts-context'
|
||||
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'
|
||||
|
||||
const defaultUnsavedDocsContextValue = {
|
||||
unsavedDocs: new Map<string, number>([['doc1', 16]]),
|
||||
isLocked: false,
|
||||
}
|
||||
|
||||
const mockSocket = {
|
||||
socket: { transport: { name: 'websocket' } },
|
||||
} as any
|
||||
|
||||
const mockConnectionContextValue = {
|
||||
socket: mockSocket,
|
||||
connectionState: { readyState: WebSocket.OPEN },
|
||||
isConnected: true,
|
||||
isStillReconnecting: false,
|
||||
secondsUntilReconnect: () => 0,
|
||||
tryReconnectNow: () => {},
|
||||
registerUserActivity: () => {},
|
||||
closeConnection: () => {},
|
||||
getSocketDebuggingInfo: () => ({ id: '' }),
|
||||
} as any
|
||||
|
||||
const mockFileTreePathContextValue = {
|
||||
dirname: () => '',
|
||||
pathInFolder: (docId: string) => (docId === 'doc1' ? 'main.tex' : null),
|
||||
findEntityByPath: (path: string) =>
|
||||
path === 'main.tex'
|
||||
? { entity: { _id: 'doc1', name: 'main.tex' }, type: 'doc' as const }
|
||||
: null,
|
||||
previewByPath: () => null,
|
||||
}
|
||||
|
||||
const mount = () => {
|
||||
cy.mount(
|
||||
<SplitTestProvider>
|
||||
<GlobalAlertsProvider>
|
||||
<ConnectionContext.Provider value={mockConnectionContextValue}>
|
||||
<FileTreePathContext.Provider value={mockFileTreePathContextValue}>
|
||||
<UnsavedDocsContext.Provider value={defaultUnsavedDocsContextValue}>
|
||||
<UnsavedDocs />
|
||||
</UnsavedDocsContext.Provider>
|
||||
</FileTreePathContext.Provider>
|
||||
</ConnectionContext.Provider>
|
||||
</GlobalAlertsProvider>
|
||||
</SplitTestProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const enableFlag = () => {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-splitTestVariants', {
|
||||
'intermittent-connection-improvements': 'enabled',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe('<UnsavedDocs />', function () {
|
||||
it('shows UnsavedDocsAlert when flag is disabled', function () {
|
||||
mount()
|
||||
cy.findByRole('alert').should('exist')
|
||||
})
|
||||
|
||||
it('suppresses UnsavedDocsAlert when flag is enabled', function () {
|
||||
enableFlag()
|
||||
mount()
|
||||
cy.findByRole('alert').should('not.exist')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user