review: merge #5548 (bac619e767)
This commit is contained in:
commit
f53bfb6c97
6 files changed
+162
-1
No files matched your search
@@ -484,6 +484,7 @@ const LOCALES = {
|
||||
reveal_in_finder: 'Reveal in File Manager',
|
||||
reveal_failed: 'Failed to reveal: ',
|
||||
copy_file_path: 'Copy file path',
|
||||
copy_relative_path: 'Copy relative path',
|
||||
open_in_vscode: 'Open in VS Code',
|
||||
open_in_vscode_failed: 'Failed to open in VS Code: ',
|
||||
download_folder: 'Download Folder',
|
||||
@@ -2161,6 +2162,7 @@ const LOCALES = {
|
||||
reveal_in_finder: 'Mostra nel File Manager',
|
||||
reveal_failed: 'Mostra fallito: ',
|
||||
copy_file_path: 'Copia percorso file',
|
||||
copy_relative_path: 'Copia percorso relativo',
|
||||
open_in_vscode: 'Apri in VS Code',
|
||||
open_in_vscode_failed: 'Apertura in VS Code fallita: ',
|
||||
download_folder: 'Cartella download',
|
||||
@@ -3829,6 +3831,7 @@ const LOCALES = {
|
||||
reveal_in_finder: 'ファイルマネージャーで表示',
|
||||
reveal_failed: '表示に失敗しました: ',
|
||||
copy_file_path: 'ファイルパスをコピー',
|
||||
copy_relative_path: '相対パスをコピー',
|
||||
open_in_vscode: 'VS Codeで開く',
|
||||
open_in_vscode_failed: 'VS Codeで開けませんでした: ',
|
||||
download_folder: 'フォルダをダウンロード',
|
||||
@@ -5421,6 +5424,7 @@ const LOCALES = {
|
||||
reveal_in_finder: 'Показать в файловом менеджере',
|
||||
reveal_failed: 'Не удалось открыть: ',
|
||||
copy_file_path: 'Копировать путь к файлу',
|
||||
copy_relative_path: 'Копировать относительный путь',
|
||||
open_in_vscode: 'Открыть в VS Code',
|
||||
open_in_vscode_failed: 'Не удалось открыть в VS Code: ',
|
||||
download_folder: 'Папка загрузок',
|
||||
@@ -7006,6 +7010,7 @@ const LOCALES = {
|
||||
reveal_in_finder: 'Mostrar en el gestor de archivos',
|
||||
reveal_failed: 'Error al mostrar: ',
|
||||
copy_file_path: 'Copiar ruta del archivo',
|
||||
copy_relative_path: 'Copiar ruta relativa',
|
||||
open_in_vscode: 'Abrir en VS Code',
|
||||
open_in_vscode_failed: 'Error al abrir en VS Code: ',
|
||||
download_folder: 'Carpeta de descargas',
|
||||
@@ -8589,6 +8594,7 @@ const LOCALES = {
|
||||
reveal_in_finder: 'Im Dateimanager anzeigen',
|
||||
reveal_failed: 'Anzeige fehlgeschlagen: ',
|
||||
copy_file_path: 'Dateipfad kopieren',
|
||||
copy_relative_path: 'Relativen Pfad kopieren',
|
||||
open_in_vscode: 'In VS Code öffnen',
|
||||
open_in_vscode_failed: 'In VS Code öffnen fehlgeschlagen: ',
|
||||
download_folder: 'Download-Ordner',
|
||||
@@ -10237,6 +10243,7 @@ const LOCALES = {
|
||||
reveal_in_finder: '在文件管理器中显示',
|
||||
reveal_failed: '显示失败:',
|
||||
copy_file_path: '\u590d\u5236\u6587\u4ef6\u8def\u5f84',
|
||||
copy_relative_path: '\u590d\u5236\u76f8\u5bf9\u8def\u5f84',
|
||||
open_in_vscode: '在VS Code中打开',
|
||||
open_in_vscode_failed: '在VS Code中打开失败:',
|
||||
download_folder: '下载文件夹',
|
||||
@@ -11919,6 +11926,7 @@ const LOCALES = {
|
||||
reveal_in_finder: '在檔案管理員中顯示',
|
||||
reveal_failed: '顯示失敗:',
|
||||
copy_file_path: '複製檔案路徑',
|
||||
copy_relative_path: '複製相對路徑',
|
||||
open_in_vscode: '在 VS Code 中開啟',
|
||||
open_in_vscode_failed: '在 VS Code 中開啟失敗:',
|
||||
download_folder: '下載資料夾',
|
||||
@@ -13488,6 +13496,7 @@ const LOCALES = {
|
||||
reveal_in_finder: 'Mostrar no gerenciador de arquivos',
|
||||
reveal_failed: 'Falha ao mostrar: ',
|
||||
copy_file_path: 'Copiar caminho do arquivo',
|
||||
copy_relative_path: 'Copiar caminho relativo',
|
||||
open_in_vscode: 'Abrir no VS Code',
|
||||
open_in_vscode_failed: 'Falha ao abrir no VS Code: ',
|
||||
download_folder: 'Pasta de downloads',
|
||||
@@ -15050,6 +15059,7 @@ const LOCALES = {
|
||||
reveal_in_finder: '파일 관리자에서 열기',
|
||||
reveal_failed: '표시 실패: ',
|
||||
copy_file_path: '파일 경로 복사',
|
||||
copy_relative_path: '상대 경로 복사',
|
||||
open_in_vscode: 'VS Code에서 열기',
|
||||
open_in_vscode_failed: 'VS Code에서 열기 실패: ',
|
||||
download_folder: '다운로드 폴더',
|
||||
@@ -16731,6 +16741,7 @@ const LOCALES = {
|
||||
reveal_in_finder: 'Révéler dans le gestionnaire de fichiers',
|
||||
reveal_failed: 'Échec de la révélation : ',
|
||||
copy_file_path: 'Copier le chemin du fichier',
|
||||
copy_relative_path: 'Copier le chemin relatif',
|
||||
open_in_vscode: 'Ouvrir dans VS Code',
|
||||
open_in_vscode_failed: 'Échec de l\'ouverture dans VS Code : ',
|
||||
download_folder: 'Télécharger le dossier',
|
||||
@@ -18368,6 +18379,7 @@ const LOCALES = {
|
||||
reveal_in_finder: 'Dosya Yöneticisinde Göster',
|
||||
reveal_failed: 'Açıklanamadı:',
|
||||
copy_file_path: 'Dosya yolunu kopyala',
|
||||
copy_relative_path: 'Göreli yolu kopyala',
|
||||
download_folder: 'İndirme klasörü',
|
||||
path_copied: 'Dosya yolu panoya kopyalandı',
|
||||
path_copy_failed: 'Yol kopyalanamadı:',
|
||||
@@ -20050,6 +20062,7 @@ const LOCALES = {
|
||||
reveal_in_finder: 'Pokaż w Menedżerze Plików',
|
||||
reveal_failed: 'Nie udało się pokazać: ',
|
||||
copy_file_path: 'Kopiuj ścieżkę pliku',
|
||||
copy_relative_path: 'Kopiuj ścieżkę względną',
|
||||
open_in_vscode: 'Otwórz w VS Code',
|
||||
open_in_vscode_failed: 'Nie udało się otworzyć w VS Code: ',
|
||||
download_folder: 'Pobierz folder',
|
||||
@@ -21670,6 +21683,7 @@ const LOCALES = {
|
||||
reveal_in_finder: 'Hiện trong trình quản lý tệp',
|
||||
reveal_failed: 'Hiển thị thất bại: ',
|
||||
copy_file_path: 'Sao chép đường dẫn file',
|
||||
copy_relative_path: 'Sao chép đường dẫn tương đối',
|
||||
open_in_vscode: 'Mở bằng VS Code',
|
||||
open_in_vscode_failed: 'Mở bằng VS Code thất bại: ',
|
||||
download_folder: 'Tải thư mục',
|
||||
|
||||
@@ -1645,6 +1645,7 @@
|
||||
<div class="preview-path" id="previewPath">
|
||||
<span id="previewPathText"></span>
|
||||
<span class="preview-badge" id="previewBadge"></span>
|
||||
<button id="btnCopyPreviewRelPath" class="panel-icon-btn" style="font-size:12px;width:auto;padding:2px 8px;display:inline-flex;align-items:center;gap:4px" onclick="copyPreviewRelativePath()" title="Copy relative 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_relative_path">Copy relative 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>
|
||||
|
||||
+1
-1
@@ -3384,7 +3384,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 #btnCopyPreviewRelPath,.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;}
|
||||
|
||||
@@ -17509,6 +17509,22 @@ function _showFileContextMenu(e, item){
|
||||
}
|
||||
};
|
||||
menu.appendChild(copyPathItem);
|
||||
|
||||
const copyRelPathItem=document.createElement('div');
|
||||
copyRelPathItem.textContent=t('copy_relative_path');
|
||||
copyRelPathItem.style.cssText='padding:7px 14px;cursor:pointer;font-size:13px;color:var(--text);';
|
||||
copyRelPathItem.onmouseenter=()=>copyRelPathItem.style.background='var(--hover-bg)';
|
||||
copyRelPathItem.onmouseleave=()=>copyRelPathItem.style.background='';
|
||||
copyRelPathItem.onclick=async()=>{
|
||||
menu.remove();
|
||||
try{
|
||||
const rel=_normalizeWorkspaceRelPath(item.path)||item.path;
|
||||
await _copyTextWithFallback(rel,t('path_copied'),t('path_copy_failed'));
|
||||
}catch(err){
|
||||
showToast(t('path_copy_failed')+(err.message||err));
|
||||
}
|
||||
};
|
||||
menu.appendChild(copyRelPathItem);
|
||||
}
|
||||
|
||||
if(isDirLike){
|
||||
|
||||
@@ -1230,6 +1230,39 @@ function openInBrowser(){
|
||||
}
|
||||
// openInBrowser keeps the helper-based raw path, which expands to an explicit &inline=1 URL.
|
||||
|
||||
async function copyPreviewRelativePath(){
|
||||
if(!_previewCurrentPath) return;
|
||||
const btn=$('btnCopyPreviewRelPath');
|
||||
if(btn&&btn.disabled) return;
|
||||
if(btn) btn.disabled=true;
|
||||
try{
|
||||
const rel=_normalizeWorkspaceRelPath(_previewCurrentPath)||_previewCurrentPath;
|
||||
if(typeof _copyTextWithFallback==='function'){
|
||||
await _copyTextWithFallback(rel,t('path_copied'),t('path_copy_failed'));
|
||||
return;
|
||||
}
|
||||
try{
|
||||
await navigator.clipboard.writeText(rel);
|
||||
showToast(t('path_copied'));
|
||||
}catch(clipErr){
|
||||
const ta=document.createElement('textarea');
|
||||
ta.value=rel;
|
||||
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));
|
||||
}finally{
|
||||
if(btn) btn.disabled=false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Workspace upload ──────────────────────────────────────────────────
|
||||
function triggerWorkspaceUpload() {
|
||||
if(_workspacePathIsReadOnly(S.currentDir || '.')){
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
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")
|
||||
UI_JS = (ROOT / "static" / "ui.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 _compact(src: str) -> str:
|
||||
return "".join(src.split())
|
||||
|
||||
|
||||
def test_preview_toolbar_has_copy_relative_path_button():
|
||||
assert 'id="btnCopyPreviewRelPath"' in INDEX
|
||||
assert 'onclick="copyPreviewRelativePath()"' in INDEX
|
||||
assert 'data-i18n="copy_relative_path"' in INDEX
|
||||
assert "Copy relative path" in INDEX
|
||||
|
||||
|
||||
def test_preview_copy_relative_path_uses_current_preview_path():
|
||||
body = _function_body(WORKSPACE_JS, "copyPreviewRelativePath")
|
||||
compact = _compact(body)
|
||||
|
||||
assert "_previewCurrentPath" in body
|
||||
assert "_normalizeWorkspaceRelPath(_previewCurrentPath)" in body
|
||||
assert "api('/api/file/path'" not in body
|
||||
assert "constrel=_normalizeWorkspaceRelPath(_previewCurrentPath)||_previewCurrentPath" in compact
|
||||
|
||||
|
||||
def test_preview_copy_relative_path_disables_button_while_request_is_in_flight():
|
||||
body = _function_body(WORKSPACE_JS, "copyPreviewRelativePath")
|
||||
compact = _compact(body)
|
||||
|
||||
guard = "if(btn&&btn.disabled)return;"
|
||||
disable = "if(btn)btn.disabled=true;"
|
||||
enable = "finally{if(btn)btn.disabled=false;}"
|
||||
assert "$('btnCopyPreviewRelPath')" in body
|
||||
assert guard in compact
|
||||
assert disable in compact
|
||||
assert enable in compact
|
||||
assert compact.index(guard) < compact.index(disable)
|
||||
assert compact.index(disable) < compact.index("_normalizeWorkspaceRelPath")
|
||||
|
||||
|
||||
def test_preview_copy_relative_path_reuses_clipboard_fallback_and_toasts():
|
||||
body = _function_body(WORKSPACE_JS, "copyPreviewRelativePath")
|
||||
assert "typeof _copyTextWithFallback==='function'" in body
|
||||
assert "_copyTextWithFallback(rel,t('path_copied'),t('path_copy_failed'))" in body
|
||||
assert "navigator.clipboard.writeText(rel)" in body
|
||||
assert "document.execCommand('copy')" in body
|
||||
assert "t('path_copied')" in body
|
||||
assert "t('path_copy_failed')" in body
|
||||
|
||||
|
||||
def test_tree_context_menu_keeps_absolute_copy_and_adds_relative_copy():
|
||||
assert "copyPathItem.textContent=t('copy_file_path')" in UI_JS
|
||||
assert "copyRelPathItem.textContent=t('copy_relative_path')" in UI_JS
|
||||
assert "const rel=_normalizeWorkspaceRelPath(item.path)||item.path" in UI_JS
|
||||
assert "_copyTextWithFallback(rel,t('path_copied'),t('path_copy_failed'))" in UI_JS
|
||||
|
||||
|
||||
def test_preview_toolbar_keeps_copy_button_from_shrinking_path_layout():
|
||||
assert ".preview-path #btnCopyPreviewRelPath" in STYLE
|
||||
selector_start = STYLE.index(".preview-path #btnCopyPreviewRelPath")
|
||||
selector_block = STYLE[selector_start : STYLE.index("}", selector_start) + 1]
|
||||
assert "flex-shrink:0" in selector_block
|
||||
assert "white-space:nowrap" in selector_block
|
||||
Reference in new issue
Block a user