diff --git a/static/index.html b/static/index.html index 4d40fed8..7e24f0fc 100644 --- a/static/index.html +++ b/static/index.html @@ -1645,6 +1645,7 @@
+ diff --git a/static/style.css b/static/style.css index 70509695..d7943d8c 100644 --- a/static/style.css +++ b/static/style.css @@ -3377,7 +3377,7 @@ body.tts-enabled .msg-tts-btn{display:inline-flex;align-items:center;} .preview-path{display:flex;align-items:center;gap:6px;flex-wrap:nowrap;overflow:hidden;min-width:0;} .preview-path #previewPathText{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .preview-path #previewBadge{flex-shrink:0;white-space:nowrap;} -.preview-path #btnDownloadFile,.preview-path #btnEditFile,.preview-path #btnRenderMarkdownAnyway{flex-shrink:0;white-space:nowrap;} +.preview-path #btnCopyPreviewPath,.preview-path #btnDownloadFile,.preview-path #btnEditFile,.preview-path #btnRenderMarkdownAnyway{flex-shrink:0;white-space:nowrap;} /* Preview badge typography */ .preview-badge{font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;} diff --git a/static/workspace.js b/static/workspace.js index 296d6be7..f3c60a8e 100644 --- a/static/workspace.js +++ b/static/workspace.js @@ -1216,6 +1216,35 @@ function openInBrowser(){ } // openInBrowser keeps the helper-based raw path, which expands to an explicit &inline=1 URL. +async function copyPreviewFilePath(){ + if(!_previewCurrentPath||!S.session) return; + try{ + const r=await api('/api/file/path',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:_previewCurrentPath})}); + const abs=(r&&r.path)||_previewCurrentPath; + if(typeof _copyTextWithFallback==='function'){ + await _copyTextWithFallback(abs,t('path_copied'),t('path_copy_failed')); + return; + } + try{ + await navigator.clipboard.writeText(abs); + showToast(t('path_copied')); + }catch(clipErr){ + const ta=document.createElement('textarea'); + ta.value=abs; + ta.style.cssText='position:fixed;left:-9999px;top:-9999px;'; + document.body.appendChild(ta); + ta.select(); + let copied=false; + try{copied=document.execCommand('copy');}catch(_){} + ta.remove(); + if(copied) showToast(t('path_copied')); + else showToast(t('path_copy_failed')+(clipErr&&clipErr.message?clipErr.message:String(clipErr))); + } + }catch(err){ + showToast(t('path_copy_failed')+(err.message||err)); + } +} + // ── Workspace upload ────────────────────────────────────────────────── function triggerWorkspaceUpload() { if(_workspacePathIsReadOnly(S.currentDir || '.')){ diff --git a/tests/test_issue5533_preview_copy_file_path.py b/tests/test_issue5533_preview_copy_file_path.py new file mode 100644 index 00000000..9c1999c1 --- /dev/null +++ b/tests/test_issue5533_preview_copy_file_path.py @@ -0,0 +1,69 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +INDEX = (ROOT / "static" / "index.html").read_text(encoding="utf-8") +WORKSPACE_JS = (ROOT / "static" / "workspace.js").read_text(encoding="utf-8") +STYLE = (ROOT / "static" / "style.css").read_text(encoding="utf-8") + + +def _function_body(src: str, name: str) -> str: + start = src.index(f"function {name}(") + brace = src.index("{", start) + depth = 0 + in_string = "" + escape = False + for idx in range(brace, len(src)): + ch = src[idx] + if in_string: + if escape: + escape = False + elif ch == "\\": + escape = True + elif ch == in_string: + in_string = "" + continue + if ch in "'\"`": + in_string = ch + continue + 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_preview_toolbar_has_copy_file_path_button(): + assert 'id="btnCopyPreviewPath"' in INDEX + assert 'onclick="copyPreviewFilePath()"' in INDEX + assert 'data-i18n="copy_file_path"' in INDEX + assert "Copy file path" in INDEX + + +def test_preview_copy_file_path_uses_server_resolved_current_preview_path(): + body = _function_body(WORKSPACE_JS, "copyPreviewFilePath") + assert "if(!_previewCurrentPath||!S.session) return;" in body + assert "api('/api/file/path'" in body + assert "session_id:S.session.session_id" in body + assert "path:_previewCurrentPath" in body + assert "const abs=(r&&r.path)||_previewCurrentPath" in body + + +def test_preview_copy_file_path_reuses_clipboard_fallback_and_toasts(): + body = _function_body(WORKSPACE_JS, "copyPreviewFilePath") + assert "typeof _copyTextWithFallback==='function'" in body + assert "_copyTextWithFallback(abs,t('path_copied'),t('path_copy_failed'))" in body + assert "navigator.clipboard.writeText(abs)" in body + assert "document.execCommand('copy')" in body + assert "t('path_copied')" in body + assert "t('path_copy_failed')" in body + + +def test_preview_toolbar_keeps_copy_button_from_shrinking_path_layout(): + assert ".preview-path #btnCopyPreviewPath" in STYLE + selector_start = STYLE.index(".preview-path #btnCopyPreviewPath") + selector_block = STYLE[selector_start : STYLE.index("}", selector_start) + 1] + assert "flex-shrink:0" in selector_block + assert "white-space:nowrap" in selector_block