mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
[web] Disable various editor buttons during network stall (#34247)
Co-authored-by: Davinder Singh <davibaweja@gmail.com> GitOrigin-RevId: 9e169f3591cfff2fdae63b461b946fde4611f2d7
This commit is contained in:
committed by
Copybot
co-authored by
Davinder Singh
parent
8741846df8
commit
1acb00407e
@@ -9,11 +9,13 @@ export default function FileTreeActionButton({
|
||||
description,
|
||||
onClick,
|
||||
iconType,
|
||||
disabled,
|
||||
}: {
|
||||
id: string
|
||||
description: string
|
||||
onClick: () => void
|
||||
iconType: AvailableUnfilledIcon
|
||||
disabled?: boolean
|
||||
}) {
|
||||
return (
|
||||
<OLTooltip
|
||||
@@ -21,7 +23,11 @@ export default function FileTreeActionButton({
|
||||
description={description}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
<button className="btn file-tree-toolbar-action-button" onClick={onClick}>
|
||||
<button
|
||||
className="btn file-tree-toolbar-action-button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<MaterialIcon
|
||||
unfilled
|
||||
type={iconType}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provi
|
||||
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
|
||||
import FileTreeActionButton from './file-tree-action-button'
|
||||
import { useRailContext } from '../../ide-react/context/rail-context'
|
||||
import useIsNetworkStalled from '@/features/ide-react/hooks/use-is-network-stalled'
|
||||
|
||||
export default function FileTreeActionButtons({
|
||||
fileTreeExpanded,
|
||||
@@ -17,6 +18,7 @@ export default function FileTreeActionButtons({
|
||||
const { fileTreeReadOnly } = useFileTreeData()
|
||||
const { write } = usePermissionsContext()
|
||||
const { handlePaneCollapse } = useRailContext()
|
||||
const isDisabledDueToNetworkStall = useIsNetworkStalled()
|
||||
|
||||
const {
|
||||
canCreate,
|
||||
@@ -37,11 +39,13 @@ export default function FileTreeActionButtons({
|
||||
eventTracking.sendMB('new-file-click', { location })
|
||||
startCreatingDocOrFile()
|
||||
},
|
||||
disabled: isDisabledDueToNetworkStall,
|
||||
},
|
||||
{
|
||||
label: t('new_folder'),
|
||||
id: 'new_folder',
|
||||
handler: startCreatingFolder,
|
||||
disabled: isDisabledDueToNetworkStall,
|
||||
},
|
||||
{
|
||||
label: t('upload_file'),
|
||||
@@ -50,6 +54,7 @@ export default function FileTreeActionButtons({
|
||||
eventTracking.sendMB('upload-click', { location })
|
||||
startUploadingDocOrFile()
|
||||
},
|
||||
disabled: isDisabledDueToNetworkStall,
|
||||
},
|
||||
]
|
||||
}, [
|
||||
@@ -60,6 +65,7 @@ export default function FileTreeActionButtons({
|
||||
startCreatingFolder,
|
||||
startUploadingDocOrFile,
|
||||
write,
|
||||
isDisabledDueToNetworkStall,
|
||||
])
|
||||
|
||||
if (fileTreeReadOnly) return null
|
||||
@@ -84,6 +90,7 @@ export default function FileTreeActionButtons({
|
||||
description={t('new_file')}
|
||||
onClick={createWithAnalytics}
|
||||
iconType="note_add"
|
||||
disabled={isDisabledDueToNetworkStall}
|
||||
/>
|
||||
)}
|
||||
{canCreate && (
|
||||
@@ -92,6 +99,7 @@ export default function FileTreeActionButtons({
|
||||
description={t('new_folder')}
|
||||
onClick={startCreatingFolder}
|
||||
iconType="create_new_folder"
|
||||
disabled={isDisabledDueToNetworkStall}
|
||||
/>
|
||||
)}
|
||||
{canCreate && (
|
||||
@@ -100,6 +108,7 @@ export default function FileTreeActionButtons({
|
||||
description={t('upload')}
|
||||
onClick={uploadWithAnalytics}
|
||||
iconType="upload"
|
||||
disabled={isDisabledDueToNetworkStall}
|
||||
/>
|
||||
)}
|
||||
{canBulkDelete && (
|
||||
|
||||
+30
-6
@@ -8,6 +8,7 @@ import {
|
||||
OLDropdownItem,
|
||||
} from '@/shared/components/ol/ol-dropdown-menu'
|
||||
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
|
||||
import useIsNetworkStalled from '@/features/ide-react/hooks/use-is-network-stalled'
|
||||
|
||||
function FileTreeItemMenuItems() {
|
||||
const { t } = useTranslation()
|
||||
@@ -47,11 +48,18 @@ function FileTreeItemMenuItems() {
|
||||
startUploadingDocOrFile()
|
||||
}, [startUploadingDocOrFile])
|
||||
|
||||
const isDisabledDueToNetworkStall = useIsNetworkStalled()
|
||||
|
||||
return (
|
||||
<>
|
||||
{canRename ? (
|
||||
<li role="none">
|
||||
<OLDropdownItem onClick={startRenaming}>{t('rename')}</OLDropdownItem>
|
||||
<OLDropdownItem
|
||||
onClick={startRenaming}
|
||||
disabled={isDisabledDueToNetworkStall}
|
||||
>
|
||||
{t('rename')}
|
||||
</OLDropdownItem>
|
||||
</li>
|
||||
) : null}
|
||||
{downloadPath ? (
|
||||
@@ -60,6 +68,7 @@ function FileTreeItemMenuItems() {
|
||||
href={downloadPath}
|
||||
onClick={downloadWithAnalytics}
|
||||
download={selectedFileName ?? undefined}
|
||||
disabled={isDisabledDueToNetworkStall}
|
||||
>
|
||||
{t('download')}
|
||||
</OLDropdownItem>
|
||||
@@ -69,7 +78,10 @@ function FileTreeItemMenuItems() {
|
||||
<>
|
||||
<OLDropdownDivider />
|
||||
<li role="none">
|
||||
<OLDropdownItem onClick={setRootDocId}>
|
||||
<OLDropdownItem
|
||||
onClick={setRootDocId}
|
||||
disabled={isDisabledDueToNetworkStall}
|
||||
>
|
||||
{t('set_as_main_document')}
|
||||
</OLDropdownItem>
|
||||
</li>
|
||||
@@ -79,7 +91,10 @@ function FileTreeItemMenuItems() {
|
||||
<>
|
||||
<OLDropdownDivider />
|
||||
<li role="none">
|
||||
<OLDropdownItem onClick={startDeleting}>
|
||||
<OLDropdownItem
|
||||
onClick={startDeleting}
|
||||
disabled={isDisabledDueToNetworkStall}
|
||||
>
|
||||
{t('delete')}
|
||||
</OLDropdownItem>
|
||||
</li>
|
||||
@@ -89,17 +104,26 @@ function FileTreeItemMenuItems() {
|
||||
<>
|
||||
<OLDropdownDivider />
|
||||
<li role="none">
|
||||
<OLDropdownItem onClick={createWithAnalytics}>
|
||||
<OLDropdownItem
|
||||
onClick={createWithAnalytics}
|
||||
disabled={isDisabledDueToNetworkStall}
|
||||
>
|
||||
{t('new_file')}
|
||||
</OLDropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<OLDropdownItem onClick={startCreatingFolder}>
|
||||
<OLDropdownItem
|
||||
onClick={startCreatingFolder}
|
||||
disabled={isDisabledDueToNetworkStall}
|
||||
>
|
||||
{t('new_folder')}
|
||||
</OLDropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<OLDropdownItem onClick={uploadWithAnalytics}>
|
||||
<OLDropdownItem
|
||||
onClick={uploadWithAnalytics}
|
||||
disabled={isDisabledDueToNetworkStall}
|
||||
>
|
||||
{t('upload')}
|
||||
</OLDropdownItem>
|
||||
</li>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { Folder } from '../../../../../types/folder'
|
||||
import { FileTreeEntity } from '../../../../../types/file-tree-entity'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import useIsNetworkStalled from '@/features/ide-react/hooks/use-is-network-stalled'
|
||||
|
||||
const FileTreeSelectableContext = createContext<
|
||||
| {
|
||||
@@ -253,6 +254,7 @@ export function useSelectableEntity(id: string, type: string) {
|
||||
const { view, setView } = useLayoutContext()
|
||||
const { setContextMenuCoords } = useFileTreeMainContext()
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
const isDisabledDueToNetworkStall = useIsNetworkStalled()
|
||||
const {
|
||||
selectedEntityIds,
|
||||
selectOrMultiSelectEntity,
|
||||
@@ -262,6 +264,11 @@ export function useSelectableEntity(id: string, type: string) {
|
||||
|
||||
const isSelected = selectedEntityIds.has(id)
|
||||
|
||||
// block opening a different doc/file while saving is stalled. Folders only
|
||||
// expand or keep the current view, so they stay interactive.
|
||||
const isDisabled =
|
||||
isDisabledDueToNetworkStall && (type === 'doc' || type === 'file')
|
||||
|
||||
const buildSelectedRange = useCallback(
|
||||
(id: string) => {
|
||||
const selected = []
|
||||
@@ -352,44 +359,63 @@ export function useSelectableEntity(id: string, type: string) {
|
||||
|
||||
const handleClick = useCallback(
|
||||
(ev: any) => {
|
||||
if (isDisabled) {
|
||||
return
|
||||
}
|
||||
handleEvent(ev)
|
||||
if (!ev.ctrlKey && !ev.metaKey) {
|
||||
setContextMenuCoords(null)
|
||||
}
|
||||
},
|
||||
[handleEvent, setContextMenuCoords]
|
||||
[isDisabled, handleEvent, setContextMenuCoords]
|
||||
)
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
(ev: any) => {
|
||||
if (isDisabled) {
|
||||
return
|
||||
}
|
||||
if (ev.key === 'Enter' || ev.key === ' ') {
|
||||
handleEvent(ev)
|
||||
}
|
||||
},
|
||||
[handleEvent]
|
||||
[isDisabled, handleEvent]
|
||||
)
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(ev: any) => {
|
||||
// make sure the right-clicked entity gets selected
|
||||
if (isDisabled) {
|
||||
return
|
||||
}
|
||||
if (!selectedEntityIds.has(id)) {
|
||||
handleEvent(ev)
|
||||
}
|
||||
},
|
||||
[id, handleEvent, selectedEntityIds]
|
||||
[id, handleEvent, selectedEntityIds, isDisabled]
|
||||
)
|
||||
|
||||
const isVisuallySelected =
|
||||
!isRootFolderSelected && isSelected && view !== 'pdf'
|
||||
const props = useMemo(
|
||||
() => ({
|
||||
className: classNames({ selected: isVisuallySelected }),
|
||||
className: classNames({
|
||||
selected: isVisuallySelected,
|
||||
'entity-disabled': isDisabled,
|
||||
}),
|
||||
'aria-selected': isVisuallySelected,
|
||||
'aria-disabled': isDisabled || undefined,
|
||||
onClick: handleClick,
|
||||
onContextMenu: handleContextMenu,
|
||||
onKeyPress: handleKeyPress,
|
||||
}),
|
||||
[handleClick, handleContextMenu, handleKeyPress, isVisuallySelected]
|
||||
[
|
||||
handleClick,
|
||||
handleContextMenu,
|
||||
handleKeyPress,
|
||||
isVisuallySelected,
|
||||
isDisabled,
|
||||
]
|
||||
)
|
||||
|
||||
return { isSelected, props }
|
||||
|
||||
@@ -17,6 +17,7 @@ import CommandDropdown, {
|
||||
MenuStructure,
|
||||
} from './command-dropdown'
|
||||
import { useRailContext } from '../../context/rail-context'
|
||||
import useIsNetworkStalled from '@/features/ide-react/hooks/use-is-network-stalled'
|
||||
import WordCountModal from '@/features/word-count-modal/components/word-count-modal'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
|
||||
@@ -44,6 +45,7 @@ export const ToolbarMenuBar = () => {
|
||||
const { setView, view } = useLayoutContext()
|
||||
const { pdfUrl } = useCompileContext()
|
||||
const wordCountEnabled = pdfUrl || isSplitTestEnabled('word-count-client')
|
||||
const isDisabledDueToNetworkStall = useIsNetworkStalled()
|
||||
const [showWordCountModal, setShowWordCountModal] = useState(false)
|
||||
const [showCloneProjectModal, setShowCloneProjectModal] = useState(false)
|
||||
const openProject = useOpenProject()
|
||||
@@ -59,6 +61,7 @@ export const ToolbarMenuBar = () => {
|
||||
{
|
||||
type: 'command',
|
||||
label: t('show_version_history'),
|
||||
disabled: isDisabledDueToNetworkStall,
|
||||
handler: () => {
|
||||
setView(view === 'history' ? 'editor' : 'history')
|
||||
},
|
||||
@@ -76,14 +79,14 @@ export const ToolbarMenuBar = () => {
|
||||
{
|
||||
type: 'command',
|
||||
label: t('make_a_copy'),
|
||||
disabled: anonymous,
|
||||
disabled: anonymous || isDisabledDueToNetworkStall,
|
||||
handler: () => {
|
||||
setShowCloneProjectModal(true)
|
||||
},
|
||||
id: 'copy_project',
|
||||
},
|
||||
],
|
||||
[t, setView, view, wordCountEnabled, anonymous]
|
||||
[t, setView, view, wordCountEnabled, anonymous, isDisabledDueToNetworkStall]
|
||||
)
|
||||
const fileMenuStructure: MenuStructure = useMemo(
|
||||
() => [
|
||||
|
||||
@@ -4,11 +4,13 @@ import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import { useCallback } from 'react'
|
||||
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
||||
import useIsNetworkStalled from '@/features/ide-react/hooks/use-is-network-stalled'
|
||||
|
||||
export default function ShowHistoryButton() {
|
||||
const { t } = useTranslation()
|
||||
const { view, setView, restoreView } = useLayoutContext()
|
||||
const { sendEvent } = useEditorAnalytics()
|
||||
const isDisabledDueToNetworkStall = useIsNetworkStalled()
|
||||
|
||||
const toggleHistoryOpen = useCallback(() => {
|
||||
const action = view === 'history' ? 'close' : 'open'
|
||||
@@ -32,6 +34,7 @@ export default function ShowHistoryButton() {
|
||||
className="ide-redesign-toolbar-button-subdued ide-redesign-toolbar-button-icon"
|
||||
onClick={toggleHistoryOpen}
|
||||
accessibilityLabel={t('history')}
|
||||
disabled={isDisabledDueToNetworkStall}
|
||||
/>
|
||||
</OLTooltip>
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,7 @@ type IdeReactContextValue = {
|
||||
projectJoined: boolean
|
||||
permissionsLevel: PermissionsLevel
|
||||
setPermissionsLevel: (permissionsLevel: PermissionsLevel) => void
|
||||
outOfSync: boolean
|
||||
setOutOfSync: (value: boolean) => void
|
||||
}
|
||||
|
||||
@@ -143,6 +144,7 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
setStartedFreeTrial,
|
||||
permissionsLevel: outOfSync ? 'readOnly' : permissionsLevel,
|
||||
setPermissionsLevel,
|
||||
outOfSync,
|
||||
setOutOfSync,
|
||||
projectId,
|
||||
reportError,
|
||||
|
||||
@@ -18,11 +18,14 @@ const intermittentConnectionImprovementsEnabled = isSplitTestEnabled(
|
||||
'intermittent-connection-improvements'
|
||||
)
|
||||
|
||||
const STALL_AFTER_SECONDS = 10 // treat saving as stalled after this time
|
||||
|
||||
// lock the editor after this time if unsaved
|
||||
const MAX_UNSAVED_SECONDS = intermittentConnectionImprovementsEnabled ? 600 : 30
|
||||
|
||||
type UnsavedDocsContextValue = {
|
||||
unsavedDocs: Map<string, number>
|
||||
isSavingStalled: boolean
|
||||
isLocked: boolean
|
||||
}
|
||||
|
||||
@@ -94,6 +97,7 @@ export const UnsavedDocsProvider: FC<React.PropsWithChildren> = ({
|
||||
}, [openDocs, debugTimers])
|
||||
|
||||
const maxUnsavedSeconds = Math.max(0, ...unsavedDocs.values())
|
||||
const isSavingStalled = maxUnsavedSeconds > STALL_AFTER_SECONDS
|
||||
|
||||
// lock the editor if at least one doc has been unsaved for too long
|
||||
useEffect(() => {
|
||||
@@ -123,8 +127,8 @@ export const UnsavedDocsProvider: FC<React.PropsWithChildren> = ({
|
||||
}, [unsavedDocs])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ unsavedDocs, isLocked }),
|
||||
[unsavedDocs, isLocked]
|
||||
() => ({ unsavedDocs, isSavingStalled, isLocked }),
|
||||
[unsavedDocs, isSavingStalled, isLocked]
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
import { useUnsavedDocsContext } from '@/features/ide-react/context/unsaved-docs-context'
|
||||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||
|
||||
export default function useIsNetworkStalled(): boolean {
|
||||
const enabled = useFeatureFlag('intermittent-connection-improvements')
|
||||
const { isSavingStalled } = useUnsavedDocsContext()
|
||||
const { outOfSync } = useIdeReactContext()
|
||||
return enabled && (isSavingStalled || outOfSync)
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
TAB_USER_EDIT_EVENT,
|
||||
tabsEvents,
|
||||
} from '@/features/source-editor/extensions/tabs-listener'
|
||||
import useIsNetworkStalled from '@/features/ide-react/hooks/use-is-network-stalled'
|
||||
|
||||
type TabProps = {
|
||||
tab: EditorFileTab
|
||||
@@ -65,6 +66,7 @@ export const Tab = memo(function Tab({
|
||||
onTabDrop,
|
||||
}: TabProps) {
|
||||
const { t } = useTranslation()
|
||||
const isDisabledDueToNetworkStall = useIsNetworkStalled()
|
||||
const tabRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [dropTargetPosition, setDropTargetPosition] = useState<
|
||||
@@ -135,20 +137,20 @@ export const Tab = memo(function Tab({
|
||||
|
||||
const onClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (e.button === 0) {
|
||||
if (e.button === 0 && !isDisabledDueToNetworkStall) {
|
||||
openTab(tab.id)
|
||||
}
|
||||
},
|
||||
[openTab, tab]
|
||||
[openTab, tab, isDisabledDueToNetworkStall]
|
||||
)
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === 'Enter' && !isDisabledDueToNetworkStall) {
|
||||
openTab(tab.id)
|
||||
}
|
||||
},
|
||||
[openTab, tab]
|
||||
[openTab, tab, isDisabledDueToNetworkStall]
|
||||
)
|
||||
|
||||
const onCloseClick = useCallback(
|
||||
@@ -213,6 +215,7 @@ export const Tab = memo(function Tab({
|
||||
onDoubleClick={onDoubleClick}
|
||||
role="tab"
|
||||
aria-selected={isSelected}
|
||||
aria-disabled={isDisabledDueToNetworkStall}
|
||||
key={tab.id}
|
||||
onClick={onClick}
|
||||
onMouseUp={onMouseUp}
|
||||
@@ -225,6 +228,7 @@ export const Tab = memo(function Tab({
|
||||
'tab-drop-left': dropTargetPosition === 'left',
|
||||
'tab-drop-right': dropTargetPosition === 'right',
|
||||
'tab-temporary': tab.lifetime === 'temporary',
|
||||
'tab-disabled': isDisabledDueToNetworkStall,
|
||||
})}
|
||||
>
|
||||
<div className="editor-file-tab-content">
|
||||
|
||||
@@ -160,6 +160,12 @@ $dropdown-item-min-height: 36px;
|
||||
color: var(--dropdown-text-active);
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&[aria-disabled='true'] {
|
||||
color: var(--content-disabled);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.btn-link {
|
||||
text-decoration: none;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
--file-tree-item-color: var(--content-primary-dark);
|
||||
--file-tree-bg: var(--bg-dark-primary);
|
||||
--file-tree-item-selected-color: var(--green-10);
|
||||
--file-tree-item-disabled-color: var(--content-disabled-dark);
|
||||
--file-tree-item-dragging-bg: var(--green-40);
|
||||
--file-tree-item-dragging-color: var(--content-primary-dark);
|
||||
--file-tree-item-icon-dragging-color: var(--file-tree-item-dragging-color);
|
||||
@@ -20,6 +21,7 @@
|
||||
--file-tree-item-color: var(--content-primary);
|
||||
--file-tree-bg: var(--white);
|
||||
--file-tree-item-selected-color: var(--green-70);
|
||||
--file-tree-item-disabled-color: var(--content-disabled);
|
||||
--file-tree-item-dragging-bg: var(--green-60);
|
||||
--file-tree-item-dragging-color: var(--white);
|
||||
--file-tree-item-icon-dragging-color: var(--file-tree-item-dragging-color);
|
||||
@@ -91,6 +93,11 @@
|
||||
&:active {
|
||||
color: var(--file-tree-expand-button-color);
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&:disabled {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.file-tree-folder-list {
|
||||
@@ -503,6 +510,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul.file-tree-list li.entity-disabled > .entity > .entity-name {
|
||||
&,
|
||||
& .file-tree-icon {
|
||||
color: var(--file-tree-item-disabled-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// while dragging, the previously selected item gets no highlight
|
||||
|
||||
@@ -162,6 +162,14 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.tab-disabled {
|
||||
.editor-file-tab-content,
|
||||
.editor-file-tab-icon .file-tree-icon,
|
||||
.editor-file-tab-icon .linked-file-highlight {
|
||||
color: var(--content-disabled-themed);
|
||||
}
|
||||
}
|
||||
|
||||
.editor-file-tab-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import FileTreeDoc from '../../../../../frontend/js/features/file-tree/components/file-tree-doc'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import { makeEditorManagerProviderWithStaleDocs } from '../../ide-react/helpers/editor-manager-provider-with-stale-docs'
|
||||
import { FileTreeProvider } from '../helpers/file-tree-provider'
|
||||
|
||||
describe('<FileTreeDoc/>', function () {
|
||||
@@ -74,4 +75,47 @@ describe('<FileTreeDoc/>', function () {
|
||||
cy.findByRole('treeitem').click({ ctrlKey: true, cmdKey: true })
|
||||
cy.findByRole('treeitem', { selected: true })
|
||||
})
|
||||
|
||||
describe('Network stall', function () {
|
||||
const rootFolder = [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [{ _id: '123abc' }],
|
||||
fileRefs: [],
|
||||
folders: [],
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(function () {
|
||||
cy.clock()
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-splitTestVariants', {
|
||||
'intermittent-connection-improvements': 'enabled',
|
||||
})
|
||||
})
|
||||
cy.then(() => {
|
||||
const now = performance.now()
|
||||
cy.mount(
|
||||
<EditorProviders
|
||||
rootFolder={rootFolder as any}
|
||||
providers={{
|
||||
EditorManagerProvider: makeEditorManagerProviderWithStaleDocs(
|
||||
now - 11_000
|
||||
),
|
||||
}}
|
||||
>
|
||||
<FileTreeProvider>
|
||||
<FileTreeDoc name="foo.tex" id="123abc" />
|
||||
</FileTreeProvider>
|
||||
</EditorProviders>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('disables doc when saving is stalled', function () {
|
||||
cy.tick(1000)
|
||||
cy.findByRole('treeitem').should('have.attr', 'aria-disabled', 'true')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import FileTreeToolbar from '../../../../../frontend/js/features/file-tree/components/file-tree-toolbar'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import { makeEditorManagerProviderWithStaleDocs } from '../../ide-react/helpers/editor-manager-provider-with-stale-docs'
|
||||
import { FileTreeProvider } from '../helpers/file-tree-provider'
|
||||
|
||||
describe('<FileTreeToolbar/>', function () {
|
||||
@@ -55,4 +56,39 @@ describe('<FileTreeToolbar/>', function () {
|
||||
cy.findAllByRole('button', { name: 'Upload' })
|
||||
cy.findAllByRole('button', { name: 'Close' })
|
||||
})
|
||||
|
||||
describe('Network stall', function () {
|
||||
beforeEach(function () {
|
||||
cy.clock()
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-splitTestVariants', {
|
||||
'intermittent-connection-improvements': 'enabled',
|
||||
})
|
||||
})
|
||||
cy.then(() => {
|
||||
const now = performance.now()
|
||||
cy.mount(
|
||||
<EditorProviders
|
||||
rootDocId=""
|
||||
providers={{
|
||||
EditorManagerProvider: makeEditorManagerProviderWithStaleDocs(
|
||||
now - 11_000
|
||||
),
|
||||
}}
|
||||
>
|
||||
<FileTreeProvider>
|
||||
<FileTreeToolbar />
|
||||
</FileTreeProvider>
|
||||
</EditorProviders>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('disables action buttons when saving is stalled', function () {
|
||||
cy.tick(1000)
|
||||
cy.findByRole('button', { name: 'New file' }).should('be.disabled')
|
||||
cy.findByRole('button', { name: 'New folder' }).should('be.disabled')
|
||||
cy.findByRole('button', { name: 'Upload' }).should('be.disabled')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ const defaultIdeReactContextValue = {
|
||||
const defaultUnsavedDocsContextValue = {
|
||||
unsavedDocs: new Map<string, number>(),
|
||||
isLocked: false,
|
||||
isSavingStalled: false,
|
||||
}
|
||||
|
||||
const mount = ({
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ import { SplitTestProvider } from '@/shared/context/split-test-context'
|
||||
const defaultUnsavedDocsContextValue = {
|
||||
unsavedDocs: new Map<string, number>([['doc1', 16]]),
|
||||
isLocked: false,
|
||||
isSavingStalled: false,
|
||||
}
|
||||
|
||||
const mockSocket = {
|
||||
|
||||
@@ -48,6 +48,7 @@ function mount(
|
||||
projectJoined: true,
|
||||
permissionsLevel: 'owner' as PermissionsLevel,
|
||||
setPermissionsLevel: onSetPermissionsLevel,
|
||||
outOfSync: false,
|
||||
setOutOfSync: () => {},
|
||||
}))
|
||||
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import React, { type FC, type PropsWithChildren } from 'react'
|
||||
import {
|
||||
EditorManager,
|
||||
EditorManagerContext,
|
||||
} from '@/features/ide-react/context/editor-manager-context'
|
||||
|
||||
export function makeEditorManagerProviderWithStaleDocs(
|
||||
pendingAt: number,
|
||||
stubs?: {
|
||||
openDocWithId?: any
|
||||
openDoc?: any
|
||||
openFileWithId?: any
|
||||
}
|
||||
) {
|
||||
const staleDocs = [
|
||||
{
|
||||
doc_id: 'stale-doc',
|
||||
getInflightOpCreatedAt: () => undefined,
|
||||
getPendingOpCreatedAt: () => pendingAt,
|
||||
},
|
||||
]
|
||||
const EditorManagerProviderWithStaleDocs: FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const value = {
|
||||
getEditorType: () => null,
|
||||
getCurrentDocValue: () => null,
|
||||
getCurrentDocumentId: () => null,
|
||||
setIgnoringExternalUpdates: () => {},
|
||||
openDocWithId: stubs?.openDocWithId ?? cy.stub().resolves(),
|
||||
openDoc: stubs?.openDoc ?? cy.stub().resolves(),
|
||||
openDocs: {
|
||||
awaitBufferedOps: cy.stub().resolves(),
|
||||
unsavedDocs: () => staleDocs,
|
||||
hasUnsavedChanges: () => true,
|
||||
getUnsavedOpsSize: () => ({
|
||||
pendingOpsLength: 0,
|
||||
inflightOpsLength: 0,
|
||||
}),
|
||||
} as any,
|
||||
openFileWithId: stubs?.openFileWithId ?? cy.stub(),
|
||||
openInitialDoc: cy.stub().resolves(),
|
||||
isLoading: false,
|
||||
jumpToLine: () => {},
|
||||
debugTimers: { current: {} },
|
||||
} as unknown as EditorManager
|
||||
return (
|
||||
<EditorManagerContext.Provider value={value}>
|
||||
{children}
|
||||
</EditorManagerContext.Provider>
|
||||
)
|
||||
}
|
||||
return EditorManagerProviderWithStaleDocs
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { FC, useEffect, useRef, useState } from 'react'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import { makeEditorManagerProviderWithStaleDocs } from '../../ide-react/helpers/editor-manager-provider-with-stale-docs'
|
||||
import { TabsContainer } from '../../../../../frontend/js/features/source-editor/components/tabs/tabs-container'
|
||||
import {
|
||||
FileTreeDocumentFindResult,
|
||||
@@ -1177,6 +1178,79 @@ describe('File Tabs', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Network stall', function () {
|
||||
function mountWithStaleDocs() {
|
||||
cy.then(() => {
|
||||
const now = performance.now()
|
||||
const openDocWithId = cy.stub().as('openDocWithId').resolves()
|
||||
const openDoc = cy.stub().as('openDoc').resolves()
|
||||
const openFileWithId = cy.stub().as('openFileWithId')
|
||||
cy.mount(
|
||||
<EditorProviders
|
||||
rootFolder={defaultRootFolder as any}
|
||||
rootDocId={DOC_IDS.main}
|
||||
providers={{
|
||||
EditorManagerProvider: makeEditorManagerProviderWithStaleDocs(
|
||||
now - 11_000,
|
||||
{ openDocWithId, openDoc, openFileWithId }
|
||||
),
|
||||
EditorViewProvider: makeEditorViewProvider(),
|
||||
}}
|
||||
>
|
||||
<FileTree />
|
||||
<TabsContainer />
|
||||
</EditorProviders>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.clock()
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-splitTestVariants', {
|
||||
'editor-tabs': 'enabled',
|
||||
'intermittent-connection-improvements': 'enabled',
|
||||
})
|
||||
})
|
||||
mountWithStaleDocs()
|
||||
})
|
||||
|
||||
it('disables tabs when saving is stalled', function () {
|
||||
cy.then(() => selectDoc(DOC_IDS.main))
|
||||
cy.tick(1000)
|
||||
cy.findByRole('tab', { name: /main\.tex/ }).should(
|
||||
'have.attr',
|
||||
'aria-disabled',
|
||||
'true'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not switch the active tab when clicking a disabled tab', function () {
|
||||
cy.then(() => selectDoc(DOC_IDS.main))
|
||||
cy.then(() => selectDoc(DOC_IDS.intro))
|
||||
cy.tick(1000)
|
||||
cy.get('@openDocWithId').invoke('resetHistory')
|
||||
cy.findByRole('tab', { name: /main\.tex/ }).click()
|
||||
cy.get('@openDocWithId').should('not.have.been.called')
|
||||
})
|
||||
|
||||
it('does not disable tabs when the feature flag is off', function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-splitTestVariants', {
|
||||
'editor-tabs': 'enabled',
|
||||
})
|
||||
})
|
||||
mountWithStaleDocs()
|
||||
cy.then(() => selectDoc(DOC_IDS.main))
|
||||
cy.tick(1000)
|
||||
cy.findByRole('tab', { name: /main\.tex/ }).should(
|
||||
'not.have.attr',
|
||||
'aria-disabled',
|
||||
'true'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SplitTestBadge', function () {
|
||||
it('renders the labs badge icon in the tabs container', function () {
|
||||
cy.window().then(win => {
|
||||
|
||||
@@ -393,6 +393,7 @@ const makeIdeReactProvider = (
|
||||
projectJoined: true,
|
||||
permissionsLevel: scope.permissionsLevel as PermissionsLevel,
|
||||
setPermissionsLevel: () => {},
|
||||
outOfSync: false,
|
||||
setOutOfSync: () => {},
|
||||
}))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user