fix(csv): keep media downloads visible in previews
This commit is contained in:
1 parent
16272dd646
commit
87a9e25e41
3 files changed
+41
-7
No files matched your search
@@ -2474,6 +2474,9 @@
|
||||
|
||||
/* ── CSV table rendering ── */
|
||||
.csv-table-wrap{margin:6px 0;overflow-x:auto;border:1px solid var(--border);border-radius:8px;background:var(--surface);}
|
||||
.csv-preview-header{justify-content:space-between;flex-wrap:wrap;row-gap:6px;}
|
||||
.csv-preview-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.csv-download-link{margin-left:auto;text-transform:none;letter-spacing:0;font-size:12px;}
|
||||
.csv-table{width:100%;border-collapse:collapse;font-size:13px;}
|
||||
.csv-table thead{position:sticky;top:0;z-index:1;}
|
||||
.csv-table th{background:var(--hover-bg);font-weight:600;text-align:left;padding:8px 12px;border-bottom:2px solid var(--border2);white-space:nowrap;user-select:none;}
|
||||
|
||||
+24
-5
@@ -16894,7 +16894,18 @@ function loadDiffInline(container){
|
||||
|
||||
const CSV_MAX_SIZE=256*1024; // 256 KB cap for inline CSV rendering
|
||||
|
||||
function buildCsvTablePreview(path, text){
|
||||
function _mediaSessionQuery(){
|
||||
const mediaSessionId=(typeof S!=='undefined'&&S&&S.session&&S.session.session_id)?String(S.session.session_id):'';
|
||||
return mediaSessionId?'&session_id='+encodeURIComponent(mediaSessionId):'';
|
||||
}
|
||||
|
||||
function _csvMediaUrl(path, opts={}){
|
||||
let url='api/media?path='+encodeURIComponent(path)+_mediaSessionQuery();
|
||||
if(opts.download) url+='&download=1';
|
||||
return url;
|
||||
}
|
||||
|
||||
function buildCsvTablePreview(path, text, downloadUrl=''){
|
||||
if(typeof text!=='string') return {errorKey:'csv_error'};
|
||||
if(text.length>CSV_MAX_SIZE) return {errorKey:'csv_too_large'};
|
||||
const rows=text.replace(/\r\n/g,'\n').replace(/\r/g,'\n').split('\n').filter(r=>r.trim());
|
||||
@@ -16909,13 +16920,19 @@ function buildCsvTablePreview(path, text){
|
||||
const headers=rows[0].split(sep).map(c=>c.trim().replace(/^["']|["']$/g,''));
|
||||
const bodyRows=rows.slice(1).map(r=>'<tr>'+r.split(sep).map(c=>`<td>${esc(c.trim().replace(/^["']|["']$/g,''))}</td>`).join('')+'</tr>').join('');
|
||||
const headerRow=headers.map(h=>`<th>${esc(h)}</th>`).join('');
|
||||
const fname=path.split('/').pop()||path;
|
||||
const downloadLink=downloadUrl
|
||||
? `<a class="csv-download-link msg-media-link" href="${esc(downloadUrl)}" download="${esc(fname)}">📎 ${esc(fname)}</a>`
|
||||
: '';
|
||||
return {
|
||||
html:`<div class="csv-table-wrap"><div class="pre-header">${esc(path.split('/').pop())} <span style="opacity:.5;font-size:11px">${t('csv_header_note')}</span></div><table class="csv-table"><thead><tr>${headerRow}</tr></thead><tbody>${bodyRows}</tbody></table></div>`,
|
||||
html:`<div class="csv-table-wrap"><div class="pre-header csv-preview-header"><span class="csv-preview-title">${esc(fname)} <span style="opacity:.5;font-size:11px">${t('csv_header_note')}</span></span>${downloadLink}</div><table class="csv-table"><thead><tr>${headerRow}</tr></thead><tbody>${bodyRows}</tbody></table></div>`,
|
||||
};
|
||||
}
|
||||
|
||||
function _csvPreviewErrorHtml(path, errorKey){
|
||||
return `<div class="diff-inline-error">${esc(path.split('/').pop())}<br><span style="color:var(--muted);font-size:12px">${t(errorKey)}</span></div>`;
|
||||
const fname=path.split('/').pop()||path;
|
||||
const downloadUrl=_csvMediaUrl(path,{download:true});
|
||||
return `<div class="diff-inline-error">${esc(fname)}<br><a class="msg-media-link" href="${esc(downloadUrl)}" download="${esc(fname)}">📎 ${esc(fname)}</a><br><span style="color:var(--muted);font-size:12px">${t(errorKey)}</span></div>`;
|
||||
}
|
||||
|
||||
function loadCsvInline(container){
|
||||
@@ -16923,10 +16940,12 @@ function loadCsvInline(container){
|
||||
root.querySelectorAll('.csv-inline-load:not([data-loaded])').forEach(el=>{
|
||||
el.setAttribute('data-loaded','1');
|
||||
const path=el.dataset.path;
|
||||
fetch('api/media?path='+encodeURIComponent(path))
|
||||
const mediaUrl=_csvMediaUrl(path);
|
||||
const downloadUrl=_csvMediaUrl(path,{download:true});
|
||||
fetch(mediaUrl)
|
||||
.then(r=>{if(!r.ok) throw new Error(r.status);return r.text();})
|
||||
.then(text=>{
|
||||
const preview=buildCsvTablePreview(path, text);
|
||||
const preview=buildCsvTablePreview(path, text, downloadUrl);
|
||||
el.outerHTML=preview.html||_csvPreviewErrorHtml(path, preview.errorKey||'csv_error');
|
||||
})
|
||||
.catch(()=>{
|
||||
|
||||
@@ -65,7 +65,19 @@ def test_loadCsvInline_function():
|
||||
with open('static/ui.js', encoding="utf-8") as f:
|
||||
src = f.read()
|
||||
assert 'function loadCsvInline' in src, "Missing loadCsvInline function"
|
||||
assert 'function buildCsvTablePreview(path, text)' in src, "Missing shared CSV preview helper"
|
||||
assert 'function buildCsvTablePreview(path, text, downloadUrl' in src, "Missing shared CSV preview helper"
|
||||
assert 'function _csvMediaUrl(path, opts={})' in src, "Missing CSV media URL helper"
|
||||
|
||||
|
||||
def test_csv_media_file_keeps_download_affordance():
|
||||
"""MEDIA: CSV preview must keep a visible downloadable attachment link."""
|
||||
with open('static/ui.js', encoding="utf-8") as f:
|
||||
src = f.read()
|
||||
csv_section = src[src.find('function buildCsvTablePreview'):src.find('function loadCsvInline') + 1600]
|
||||
assert 'csv-download-link msg-media-link' in csv_section
|
||||
assert '_csvMediaUrl(path,{download:true})' in src
|
||||
assert 'download="${esc(fname)}"' in csv_section
|
||||
assert 'buildCsvTablePreview(path, text, downloadUrl)' in src
|
||||
|
||||
|
||||
def test_csv_inline_max_size():
|
||||
@@ -126,7 +138,7 @@ def test_csv_loadCsvInline_called_after_render():
|
||||
body = src[idx:idx + 500]
|
||||
assert 'loadCsvInline(container)' in body, "post-process should call loadCsvInline once per render"
|
||||
load_section = src[src.find('function loadCsvInline'):src.find('function loadCsvInline') + 1200]
|
||||
assert 'buildCsvTablePreview(path, text)' in load_section, "Inline loader should reuse the shared helper"
|
||||
assert 'buildCsvTablePreview(path, text, downloadUrl)' in load_section, "Inline loader should reuse the shared helper"
|
||||
open_file = WORKSPACE_JS[WORKSPACE_JS.index("async function openFile(path, opts={}){"):WORKSPACE_JS.index("\nfunction downloadFile")]
|
||||
csv_pos = open_file.find("} else if(ext==='.csv'){")
|
||||
generic_pos = open_file.find("} else {\n // Plain code / text -- but fall back to download if server signals binary")
|
||||
|
||||
Reference in new issue
Block a user