refactoring

This commit is contained in:
2026-09-02 08:38:03 +02:00
parent 649ff0d7c5
commit 38f255c1a4
48 changed files with 6998 additions and 5577 deletions
@@ -0,0 +1,124 @@
import { pageRoute } from "../routes.js";
/* Orphaned-upload administration view. */
/** Format a byte count for the administrative upload list. */
function formatFileSize(size) {
const bytes = Number(size || 0);
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
class OrphanedUploadsAdmin {
/**
* goal : Create the controller for uploads without current references.
* pre : api performs authenticated requests and translate resolves UI keys.
* post : The controller is connected to the orphaned-upload list.
* result : A directly usable orphaned-upload administration controller.
*/
constructor(api, translate) {
this.api = api;
this.translate = translate;
this.list = document.getElementById("orphaned-uploads-list");
}
/**
* goal : Load orphaned uploads and their last historical uses.
* pre : The current session has administrator permission.
* post : The upload list represents the current server state.
* result : A promise that resolves when all upload rows have been rendered.
*/
async load() {
const result = await this.api("/api/admin/uploads/orphaned");
const uploads = result.uploads || [];
this.list.replaceChildren();
if (uploads.length === 0) {
const empty = document.createElement("p");
empty.className = "muted";
empty.textContent = this.translate("no-orphaned-uploads", "No orphaned uploads.");
this.list.append(empty);
return;
}
for (const upload of uploads) this.list.append(this.#renderUpload(upload));
}
/** Build one upload row with history, view link and deletion action. */
#renderUpload(upload) {
const row = document.createElement("article");
row.className = "orphaned-upload-row";
const title = document.createElement("strong");
title.textContent = upload.originalName;
const meta = document.createElement("div");
meta.className = "muted";
meta.textContent = `${upload.mimeType} · ${formatFileSize(upload.size)} · ` +
`${this.translate("uploaded-by", "uploaded by")} ${upload.uploadedBy}`;
const history = this.#uploadHistory(upload);
const actions = document.createElement("div");
actions.className = "orphaned-upload-actions";
const view = document.createElement("a");
view.href = `/uploads/${encodeURIComponent(upload.ownerSlug)}/` +
encodeURIComponent(upload.storedName);
view.target = "_blank";
view.rel = "noopener";
view.textContent = this.translate("view", "View");
const remove = document.createElement("button");
remove.className = "danger";
remove.textContent = this.translate("delete", "Delete");
remove.addEventListener("click", () => {
this.#deleteUpload(upload).catch((error) => console.error(error));
});
actions.append(view, remove);
row.append(title, meta, history, actions);
return row;
}
/** Build the historical-use portion of one upload row. */
#uploadHistory(upload) {
const history = document.createElement("div");
const lastUses = Array.isArray(upload.lastUses) ? upload.lastUses : [];
if (lastUses.length === 0) {
history.textContent = this.translate(
"never-referenced", "Never referenced by a saved page.");
return history;
}
const label = document.createElement("div");
label.textContent = `${this.translate("last-used", "Last used")}:`;
const uses = document.createElement("ul");
uses.className = "orphaned-upload-uses";
for (const use of lastUses) {
const item = document.createElement("li");
const link = document.createElement("a");
link.href = pageRoute(use.slug);
link.textContent = use.title || use.slug;
item.append(link);
if (use.version) {
item.append(document.createTextNode(
` · ${this.translate("version", "Version")} ${use.version}`));
}
uses.append(item);
}
history.append(label, uses);
return history;
}
/** Confirm and remove one still-orphaned upload, then refresh the list. */
async #deleteUpload(upload) {
const question = this.translate(
"delete-orphaned-upload-confirm", "Delete this orphaned upload?");
if (!window.confirm(question)) return;
await this.api(`/api/admin/uploads/orphaned/${upload.id}`, { method: "DELETE" });
await this.load();
}
}
export { OrphanedUploadsAdmin };