From 8a68f89a03ffdc4889c3690976a791805495953e Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Tue, 9 Jun 2026 10:35:48 +0100 Subject: [PATCH] Merge pull request #34415 from overleaf/mj-file-tree-permanent-tabs [web] Make tab permanent on file tree double click GitOrigin-RevId: 8fe725eb3235e35d2070d806ad4f57e7f475d188 --- .../file-tree/components/file-tree-doc.tsx | 13 +++ .../source-editor/components/tabs.spec.tsx | 104 ++++++++---------- 2 files changed, 56 insertions(+), 61 deletions(-) diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-doc.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-doc.tsx index 48006b0dee..4043feffdd 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-doc.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-doc.tsx @@ -1,6 +1,9 @@ +import { useCallback } from 'react' import { useSelectableEntity } from '../contexts/file-tree-selectable' import FileTreeIcon from './file-tree-icon' import FileTreeItemInner from './file-tree-item/file-tree-item-inner' +import { useTabsContext } from '@/features/ide-react/context/tabs-context' +import { useAreTabsEnabled } from '@/features/ide-react/hooks/use-are-tabs-enabled' function FileTreeDoc({ name, @@ -20,6 +23,15 @@ function FileTreeDoc({ type ) + const { makeTabPermanent } = useTabsContext() + const areTabsEnabled = useAreTabsEnabled() + + const onDoubleClick = useCallback(() => { + if (areTabsEnabled) { + makeTabPermanent(id) + } + }, [makeTabPermanent, areTabsEnabled, id]) + return (
  • { - if (!initDone.current) { - initDone.current = true - handleFileTreeInit() - if (autoSelectEntity) { - handleFileTreeSelect([autoSelectEntity]) - } - } - }, [handleFileTreeInit, handleFileTreeSelect, autoSelectEntity]) - - useEffect(() => { - const handler = (e: Event) => { - const { detail } = e as CustomEvent - handleFileTreeSelect([detail]) - } - document.addEventListener('test:selectEntity', handler) - return () => { - document.removeEventListener('test:selectEntity', handler) - } - }, [handleFileTreeSelect]) - - return null +function expandFolders(path: string[] = []) { + // path[0] is the root folder, so skip that one + path.slice(1).forEach(folderId => { + cy.get(`[data-file-id="${folderId}"] .file-tree-entity-button`).click() + }) } -// Dispatch a custom event to select an entity (simulates file-tree click). -// Must be wrapped in cy.then() so it runs in the Cypress command queue. +// Target rows by entity id rather than accessible name so that entities sharing +// a name stay unambiguous. function selectEntity( entity: FileTreeDocumentFindResult | FileTreeFileRefFindResult ) { - document.dispatchEvent( - new CustomEvent('test:selectEntity', { detail: entity }) - ) - cy.findByRole('tab', { name: new RegExp(entity.entity.name) }).should('exist') + expandFolders(entity.path) + cy.get(`[data-file-id="${entity.entity._id}"]`).click() + cy.get(`[data-tab-id="${entity.entity._id}"]`).should('exist') } function selectDoc(id: string, path?: string[]) { @@ -246,8 +219,8 @@ describe('File Tabs', function () { EditorViewProvider: makeEditorViewProvider(), }} > - + @@ -264,10 +237,17 @@ describe('File Tabs', function () { // Clear persisted tab state from localStorage cy.window().then(win => { Object.keys(win.localStorage).forEach(key => { - if (key.startsWith('open-tabs:')) { + if (key.startsWith('open-tabs:') || key.startsWith('folder.')) { win.localStorage.removeItem(key) } }) + + // Pointing doc.open_id at a non-existent entity stops the file tree + // auto-selecting + win.localStorage.setItem( + `doc.open_id.${PROJECT_ID}`, + JSON.stringify('no-open-doc') + ) }) cy.window().then(win => { win.metaAttributesCache.set('ol-user', { id: 'user1' }) @@ -280,26 +260,13 @@ describe('File Tabs', function () { describe('Initial file selection', function () { it('automatically creates a tab for the initially opened file', function () { - // Re-mount with auto-selection of the root doc to simulate - // the file tree opening the initial document on startup - cy.mount( - - - - - ) + cy.window().then(win => { + win.localStorage.setItem( + `doc.open_id.${PROJECT_ID}`, + JSON.stringify(DOC_IDS.intro) + ) + }) + mountTabs() cy.findByRole('tab', { name: /intro\.tex/ }).should('exist') }) @@ -429,6 +396,21 @@ describe('File Tabs', function () { ) }) + it('makes a temporary tab permanent on double-clicking its file tree entry', function () { + cy.then(() => selectDoc(DOC_IDS.main)) + cy.findByRole('tab', { name: /main\.tex/ }).should( + 'have.class', + 'tab-temporary' + ) + + cy.findByRole('treeitem', { name: 'main.tex' }).dblclick() + + cy.findByRole('tab', { name: /main\.tex/ }).should( + 'not.have.class', + 'tab-temporary' + ) + }) + it('opens a new tab as permanent when previewTabs is disabled', function () { mountTabs({ userSettings: { previewTabs: false } })