feat(#5533): make preview file paths copyable
This commit is contained in:
parent
980e98dc7e
commit
7ebd898c5e
4 changed files with 100 additions and 1 deletions
|
|
@ -1645,6 +1645,7 @@
|
|||
<div class="preview-path" id="previewPath">
|
||||
<span id="previewPathText"></span>
|
||||
<span class="preview-badge" id="previewBadge"></span>
|
||||
<button id="btnCopyPreviewPath" class="panel-icon-btn" style="font-size:12px;width:auto;padding:2px 8px;display:inline-flex;align-items:center;gap:4px" onclick="copyPreviewFilePath()" title="Copy file path"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> <span data-i18n="copy_file_path">Copy file path</span></button>
|
||||
<button id="btnOpenInBrowser" class="panel-icon-btn" style="margin-left:auto;font-size:12px;width:auto;padding:2px 8px;display:none;align-items:center;gap:4px" onclick="openInBrowser()" title="Open in new browser tab"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg> <span data-i18n="open_in_browser">Open in browser</span></button>
|
||||
<button id="btnRenderMarkdownAnyway" class="panel-icon-btn" style="font-size:12px;width:auto;padding:2px 8px;display:none;align-items:center;gap:4px" onclick="forceRenderMarkdownPreview()" title="Render this large file as rich Markdown"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M16 3h3a2 2 0 0 1 2 2v3"/><path d="M8 21H5a2 2 0 0 1-2-2v-3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/><path d="M7 8h10"/><path d="M7 12h10"/><path d="M7 16h6"/></svg> Render as markdown anyway</button>
|
||||
<button id="btnDownloadFile" class="panel-icon-btn" style="margin-left:auto;font-size:12px;width:auto;padding:2px 8px;display:inline-flex;align-items:center;gap:4px" onclick="downloadFile(_previewCurrentPath)" title="Download file to your computer"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> Download</button>
|
||||
|
|
|
|||
|
|
@ -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;}
|
||||
|
|
|
|||
|
|
@ -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 || '.')){
|
||||
|
|
|
|||
69
tests/test_issue5533_preview_copy_file_path.py
Normal file
69
tests/test_issue5533_preview_copy_file_path.py
Normal file
|
|
@ -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
|
||||
Loading…
Reference in a new issue