diff --git a/services/web/frontend/js/features/file-tree/hooks/file-tree-socket-listener.js b/services/web/frontend/js/features/file-tree/hooks/file-tree-socket-listener.js index cc669e7dea..9217d51491 100644 --- a/services/web/frontend/js/features/file-tree/hooks/file-tree-socket-listener.js +++ b/services/web/frontend/js/features/file-tree/hooks/file-tree-socket-listener.js @@ -2,6 +2,7 @@ import { useCallback, useEffect } from 'react' import { useFileTreeMutable } from '../contexts/file-tree-mutable' import { useFileTreeSelectable } from '../contexts/file-tree-selectable' +import { findInTreeOrThrow } from '../util/find-in-tree' export function useFileTreeSocketListener() { const { @@ -10,9 +11,15 @@ export function useFileTreeSocketListener() { dispatchMove, dispatchCreateFolder, dispatchCreateDoc, - dispatchCreateFile + dispatchCreateFile, + fileTreeData } = useFileTreeMutable() - const { select, unselect } = useFileTreeSelectable() + const { + selectedEntityIds, + selectedEntityParentIds, + select, + unselect + } = useFileTreeSelectable() const socket = window._ide && window._ide.socket const selectEntityIfCreatedByUser = useCallback( @@ -38,13 +45,33 @@ export function useFileTreeSocketListener() { useEffect(() => { function handleDispatchDelete(entityId) { unselect(entityId) + if (selectedEntityParentIds.has(entityId)) { + // we're deleting a folder with a selected children so we need to + // unselect its selected children first + for (const selectedEntityId of selectedEntityIds) { + if ( + findInTreeOrThrow(fileTreeData, selectedEntityId).path.includes( + entityId + ) + ) { + unselect(selectedEntityId) + } + } + } dispatchDelete(entityId) } if (socket) socket.on('removeEntity', handleDispatchDelete) return () => { if (socket) socket.removeListener('removeEntity', handleDispatchDelete) } - }, [socket, unselect, dispatchDelete]) + }, [ + socket, + unselect, + dispatchDelete, + fileTreeData, + selectedEntityIds, + selectedEntityParentIds + ]) useEffect(() => { function handleDispatchMove(entityId, toFolderId) { diff --git a/services/web/test/frontend/features/file-tree/flows/delete-entity.test.js b/services/web/test/frontend/features/file-tree/flows/delete-entity.test.js index 88ca65bea2..639bbe4788 100644 --- a/services/web/test/frontend/features/file-tree/flows/delete-entity.test.js +++ b/services/web/test/frontend/features/file-tree/flows/delete-entity.test.js @@ -115,6 +115,61 @@ describe('FileTree Delete Entity Flow', function() { }) }) + describe('folders', function() { + beforeEach(function() { + const rootFolder = [ + { + docs: [{ _id: '456def', name: 'main.tex' }], + folders: [ + { + _id: '123abc', + name: 'folder', + docs: [], + folders: [], + fileRefs: [{ _id: '789ghi', name: 'my.bib' }] + } + ], + fileRefs: [] + } + ] + render( + + ) + + const expandButton = screen.queryByRole('button', { name: 'Expand' }) + if (expandButton) fireEvent.click(expandButton) + const treeitemDoc = screen.getByRole('treeitem', { name: 'main.tex' }) + fireEvent.click(treeitemDoc) + const treeitemFile = screen.getByRole('treeitem', { name: 'my.bib' }) + fireEvent.click(treeitemFile, { ctrlKey: true }) + + window._ide.socket.socketClient.emit('removeEntity', '123abc') + }) + + it('removes the folder', function() { + expect(screen.queryByRole('treeitem', { name: 'folder' })).to.not.exist + }) + + it('leaves the main file selected', function() { + screen.getByRole('treeitem', { name: 'main.tex', selected: true }) + }) + + it('unselect the child entity', async function() { + // as a proxy to check that the child entity has been unselect we start + // a delete and ensure the modal is displayed (the cancel button can be + // selected) This is needed to make sure the test fail. + const deleteButton = screen.getByRole('menuitem', { name: 'Delete' }) + fireEvent.click(deleteButton) + await waitFor(() => screen.getByRole('button', { name: 'Cancel' })) + }) + }) + describe('multiple entities', function() { beforeEach(function() { const rootFolder = [