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 rendering ── */
|
||||||
.csv-table-wrap{margin:6px 0;overflow-x:auto;border:1px solid var(--border);border-radius:8px;background:var(--surface);}
|
.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{width:100%;border-collapse:collapse;font-size:13px;}
|
||||||
.csv-table thead{position:sticky;top:0;z-index:1;}
|
.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;}
|
.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
|
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(typeof text!=='string') return {errorKey:'csv_error'};
|
||||||
if(text.length>CSV_MAX_SIZE) return {errorKey:'csv_too_large'};
|
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());
|
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 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 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 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 {
|
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){
|
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){
|
function loadCsvInline(container){
|
||||||
@@ -16923,10 +16940,12 @@ function loadCsvInline(container){
|
|||||||
root.querySelectorAll('.csv-inline-load:not([data-loaded])').forEach(el=>{
|
root.querySelectorAll('.csv-inline-load:not([data-loaded])').forEach(el=>{
|
||||||
el.setAttribute('data-loaded','1');
|
el.setAttribute('data-loaded','1');
|
||||||
const path=el.dataset.path;
|
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(r=>{if(!r.ok) throw new Error(r.status);return r.text();})
|
||||||
.then(text=>{
|
.then(text=>{
|
||||||
const preview=buildCsvTablePreview(path, text);
|
const preview=buildCsvTablePreview(path, text, downloadUrl);
|
||||||
el.outerHTML=preview.html||_csvPreviewErrorHtml(path, preview.errorKey||'csv_error');
|
el.outerHTML=preview.html||_csvPreviewErrorHtml(path, preview.errorKey||'csv_error');
|
||||||
})
|
})
|
||||||
.catch(()=>{
|
.catch(()=>{
|
||||||
|
|||||||
@@ -65,7 +65,19 @@ def test_loadCsvInline_function():
|
|||||||
with open('static/ui.js', encoding="utf-8") as f:
|
with open('static/ui.js', encoding="utf-8") as f:
|
||||||
src = f.read()
|
src = f.read()
|
||||||
assert 'function loadCsvInline' in src, "Missing loadCsvInline function"
|
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():
|
def test_csv_inline_max_size():
|
||||||
@@ -126,7 +138,7 @@ def test_csv_loadCsvInline_called_after_render():
|
|||||||
body = src[idx:idx + 500]
|
body = src[idx:idx + 500]
|
||||||
assert 'loadCsvInline(container)' in body, "post-process should call loadCsvInline once per render"
|
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]
|
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")]
|
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'){")
|
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")
|
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