diff --git a/docs/pr-media/1785/workspace-preview-breadcrumb-before.png b/docs/pr-media/1785/workspace-preview-breadcrumb-before.png new file mode 100644 index 00000000..12d18944 Binary files /dev/null and b/docs/pr-media/1785/workspace-preview-breadcrumb-before.png differ diff --git a/docs/pr-media/1785/workspace-root-breadcrumb-fixed.png b/docs/pr-media/1785/workspace-root-breadcrumb-fixed.png new file mode 100644 index 00000000..6167e6ee Binary files /dev/null and b/docs/pr-media/1785/workspace-root-breadcrumb-fixed.png differ diff --git a/static/boot.js b/static/boot.js index 7c4a66f9..3fa54c94 100644 --- a/static/boot.js +++ b/static/boot.js @@ -820,10 +820,11 @@ $('importFileInput').onchange=async(e)=>{ } }; // btnRefreshFiles is now panel-icon-btn in header (see HTML) -function clearPreview(){ +function clearPreview(opts={}){ + const keepPanelOpen=!!(opts&&opts.keepPanelOpen); // Restore directory breadcrumb after closing file preview if(typeof renderBreadcrumb==='function') renderBreadcrumb(); - const closePanelAfter=_workspacePanelMode==='preview'; + const closePanelAfter=_workspacePanelMode==='preview'&&!keepPanelOpen; const pa=$('previewArea');if(pa)pa.classList.remove('visible'); const pi=$('previewImg');if(pi){pi.onerror=null;pi.src='';} const pdf=$('previewPdfFrame');if(pdf)pdf.src=''; @@ -834,6 +835,7 @@ function clearPreview(){ const ft=$('fileTree');if(ft)ft.style.display=''; _previewCurrentPath='';_previewCurrentMode='';_previewDirty=false; if(closePanelAfter)closeWorkspacePanel(); + else if(keepPanelOpen&&_workspacePanelMode==='preview')openWorkspacePanel('browse'); else syncWorkspacePanelUI(); } $('btnClearPreview').onclick=handleWorkspaceClose; diff --git a/static/workspace.js b/static/workspace.js index bf26f1e4..1511a70a 100644 --- a/static/workspace.js +++ b/static/workspace.js @@ -85,9 +85,9 @@ async function loadDir(path){ } if(typeof clearPreview==='function'){ if(typeof _previewDirty!=='undefined'&&_previewDirty){ - showConfirmDialog({title:t('unsaved_confirm'),message:'',confirmLabel:'Discard',danger:true,focusCancel:true}).then(ok=>{if(ok)clearPreview();}); + showConfirmDialog({title:t('unsaved_confirm'),message:'',confirmLabel:'Discard',danger:true,focusCancel:true}).then(ok=>{if(ok)clearPreview({keepPanelOpen:true});}); }else{ - clearPreview(); + clearPreview({keepPanelOpen:true}); } } // Fetch git info for workspace root (non-blocking) @@ -337,7 +337,7 @@ function renderFileBreadcrumb(filePath) { const root = document.createElement('span'); root.className = 'breadcrumb-seg breadcrumb-link'; root.textContent = '~'; - root.onclick = () => { clearPreview(); loadDir('.'); }; + root.onclick = () => { loadDir('.'); }; bar.appendChild(root); const parts = filePath.split('/'); @@ -354,7 +354,7 @@ function renderFileBreadcrumb(filePath) { if (i < parts.length - 1) { seg.className = 'breadcrumb-seg breadcrumb-link'; const target = accumulated; - seg.onclick = () => { clearPreview(); loadDir(target); }; + seg.onclick = () => { loadDir(target); }; } else { seg.className = 'breadcrumb-seg breadcrumb-current'; } diff --git a/tests/test_issue1785_workspace_preview_breadcrumb.py b/tests/test_issue1785_workspace_preview_breadcrumb.py new file mode 100644 index 00000000..8c75b254 --- /dev/null +++ b/tests/test_issue1785_workspace_preview_breadcrumb.py @@ -0,0 +1,59 @@ +from pathlib import Path + + +BOOT_JS = Path("static/boot.js").read_text(encoding="utf-8") +WORKSPACE_JS = Path("static/workspace.js").read_text(encoding="utf-8") + + +def _function_block(src: str, name: str) -> str: + marker = f"function {name}(" + start = src.find(marker) + assert start != -1, f"{name}() not found" + params_end = src.find("){", start) + assert params_end != -1, f"{name}() body not found" + brace = params_end + 1 + depth = 0 + for idx in range(brace, len(src)): + ch = src[idx] + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + return src[start : idx + 1] + raise AssertionError(f"{name}() body did not close") + + +def test_clear_preview_can_keep_preview_only_panel_open_for_directory_navigation(): + """#1785: leaving preview via a directory breadcrumb should switch to browse mode, not close.""" + block = _function_block(BOOT_JS, "clearPreview") + assert "keepPanelOpen" in block, ( + "clearPreview() needs an explicit keep-open option so breadcrumb/directory " + "navigation can leave preview-only mode without closing the workspace panel." + ) + assert "_workspacePanelMode==='preview'&&!keepPanelOpen" in block.replace(" ", ""), ( + "Preview-only close behavior should remain for the X button, but must be gated " + "off when directory navigation requests keepPanelOpen." + ) + assert "openWorkspacePanel('browse')" in block or '_setWorkspacePanelMode("browse")' in block, ( + "When keepPanelOpen is requested from preview-only mode, clearPreview() should " + "transition the workspace panel to browse mode so the root listing remains visible." + ) + + +def test_load_dir_keeps_workspace_panel_open_when_clearing_preview(): + """#1785: loadDir('.') from the ~ breadcrumb should reveal the listing, not collapse the panel.""" + block = _function_block(WORKSPACE_JS, "loadDir") + assert "clearPreview({keepPanelOpen:true})" in block.replace(" ", ""), ( + "Directory navigation clears previews as part of showing the file tree; that clear " + "must keep the workspace panel open for breadcrumb navigation from preview mode." + ) + + +def test_file_preview_breadcrumb_uses_directory_navigation_for_root(): + block = _function_block(WORKSPACE_JS, "renderFileBreadcrumb") + assert "loadDir('.')" in block, "The preview root breadcrumb should navigate to the workspace root." + assert "clearPreview(); loadDir('.')" not in block, ( + "The preview root breadcrumb should not do a close-style preview clear before " + "directory navigation; loadDir() owns the keep-open preview clear." + )