fix(dashboard): F-U-009 add download link + copy-id button per file row

Listing showed name/content-type/size/created/id; only mutation was
Delete. No download link, no copy-id button (the UUID was rendered
unclickable), no preview, no metadata refresh. Operators had to use
the admin API directly.

- Add api.files.downloadUrl(slug, collection, id) helper that builds
  the admin GET URL. The anchor uses HTML `download` so the browser
  saves with the original filename.
- Add a ⧉ copy-id button next to the UUID column using
  navigator.clipboard.writeText. Silently no-ops where the browser
  doesn't expose clipboard (plain-http LAN).
- Preview / metadata-refresh are deferred to a UI overhaul.

AUDIT.md anchor: F-U-009 (partial — download + copy; preview deferred).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-07 21:07:36 +02:00
parent cd0cd8464c
commit 6298c7d21c
2 changed files with 29 additions and 2 deletions

View File

@@ -907,7 +907,11 @@ export const api = {
adminRequest<null>(
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/files/${encodeURIComponent(collection)}/${fileId}`,
{ method: 'DELETE' }
)
),
/// F-U-009: build the admin download URL. GET returns the file
/// bytes inline; the browser handles content-disposition.
downloadUrl: (idOrSlug: string, collection: string, fileId: string): string =>
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/files/${encodeURIComponent(collection)}/${fileId}`
},
secrets: {

View File

@@ -91,6 +91,12 @@
return new Date(iso).toLocaleString();
}
// F-U-009: best-effort clipboard copy; silently no-ops where the
// browser doesn't expose navigator.clipboard (e.g. plain-http LAN).
function copyToClipboard(text: string) {
void navigator.clipboard?.writeText(text);
}
function fmtSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -180,8 +186,25 @@
<td><code>{f.content_type}</code></td>
<td>{fmtSize(f.size)}</td>
<td>{fmtTime(f.created_at)}</td>
<td class="mono small">{f.id}</td>
<td class="mono small">
{f.id}
<button
type="button"
class="copy-btn"
title="Copy ID"
onclick={() => copyToClipboard(f.id)}
>
</button>
</td>
<td>
<a
class="secondary"
href={api.files.downloadUrl(slug, activeCollection, f.id)}
download={f.name}
>
Download
</a>
<button type="button" class="danger" onclick={() => (fileToRemove = f)}>
Delete
</button>