From 6538c00742c121e3410ae9589def2d0ddfa1be06 Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Thu, 14 May 2026 11:40:18 +0100 Subject: [PATCH] Merge pull request #33690 from overleaf/mj-prune-deleted-tabs [web] Prune non-existent tabs when file tree changes GitOrigin-RevId: 97e68a88a201acc2d1e582911ca64e1f72f9bfe1 --- .../ide-react/context/tabs-context.tsx | 18 +++++++++++++ .../source-editor/components/tabs.spec.tsx | 27 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/services/web/frontend/js/features/ide-react/context/tabs-context.tsx b/services/web/frontend/js/features/ide-react/context/tabs-context.tsx index b7b95eb92c..57cd1bb299 100644 --- a/services/web/frontend/js/features/ide-react/context/tabs-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/tabs-context.tsx @@ -241,6 +241,24 @@ export const TabsProvider: FC = ({ children }) => { }) }, [openEntity, previewTabs, setOpenTabs, tabsEnabled]) + useEffect(() => { + if (!tabsEnabled) { + return + } + // Make sure file tree is ready for lookup before pruning tabs, to avoid + // accidentally closing tabs that are still valid but not yet available + if (!fileTreeData?._id) { + return + } + setOpenTabs(current => { + const pruned = current.filter(tab => findInTree(fileTreeData, tab.id)) + if (pruned.length === current.length) { + return current + } + return pruned + }) + }, [fileTreeData, setOpenTabs, tabsEnabled]) + const value = useMemo( () => ({ tabs, diff --git a/services/web/test/frontend/features/source-editor/components/tabs.spec.tsx b/services/web/test/frontend/features/source-editor/components/tabs.spec.tsx index 00aeeae826..6fbb40c2b4 100644 --- a/services/web/test/frontend/features/source-editor/components/tabs.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/tabs.spec.tsx @@ -947,6 +947,33 @@ describe('File Tabs', function () { }) }) + describe('Pruning deleted files', function () { + it('prunes persisted tabs whose files are no longer in the tree', function () { + cy.then(() => selectDoc(DOC_IDS.main)) + cy.then(() => selectDoc(DOC_IDS.intro)) + cy.then(() => selectDoc(DOC_IDS.appendix)) + + cy.findAllByRole('tab').should('have.length', 3) + + // Re-mount with a tree containing only appendix.tex, then verify + // the last remaining tab cannot be closed + const trimmedRootFolder = makeRootFolder([ + { _id: DOC_IDS.appendix, name: 'appendix.tex' }, + ]) + mountTabs({ rootFolder: trimmedRootFolder }) + + cy.findAllByRole('tab').should('have.length', 1) + cy.findByRole('tab', { name: /appendix\.tex/ }).should('exist') + + cy.findByRole('tab', { name: /appendix\.tex/ }).within(() => { + cy.findByRole('button', { name: 'Close tab' }).click() + }) + + cy.findAllByRole('tab').should('have.length', 1) + cy.findByRole('tab', { name: /appendix\.tex/ }).should('exist') + }) + }) + describe('SplitTestBadge', function () { it('renders the labs badge icon in the tabs container', function () { cy.window().then(win => {