This commit is contained in:
Mateusz Gruszczyński committed 2026-10-13 21:29:57 +02:00
1 parent bb025dcf44
commit b0f211555a
74 files changed
+6089 -556

No files matched your search

@@ -0,0 +1,239 @@
window.rustProxyDashboardLayout = (() => {
const dialog = $("overview-layout-dialog");
const visibleRoot = $("overview-layout-visible");
const hiddenRoot = $("overview-layout-hidden");
const saveButton = $("overview-layout-save");
const cancelButton = $("overview-layout-cancel");
const search = $("overview-layout-search");
const typeFilter = $("overview-layout-type-filter");
const message = $("overview-layout-message");
const defaultButton = $("overview-layout-default");
const resetButton = $("overview-layout-reset");
const resetConfirm = $("overview-reset-confirm");
const resetCancel = $("overview-reset-cancel");
const resetApply = $("overview-reset-apply");
let draft = null;
let dragging = null;
let saving = false;
let opening = false;
function initialDraft() {
const prefs = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS;
const order = normalizeDashboardWidgets(prefs.dashboard_widgets);
return { order, hidden: new Set(normalizeHiddenDashboardWidgets(prefs.dashboard_hidden_widgets, prefs.dashboard_widgets)) };
}
function defaultDraft() {
return {
order: [...ADMIN_DASHBOARD_WIDGETS],
hidden: new Set(ADMIN_DEFAULT_HIDDEN_WIDGETS),
};
}
function label(id) {
const widget = ADMIN_DASHBOARD_BY_ID.get(id);
return widget ? t(widget.label_key) : id;
}
const groupFor = id => window.rustProxyMasonry?.groupFor?.(id) ?? 0;
function visibleIds() {
return draft.order.filter(id => !draft.hidden.has(id))
.sort((a, b) => groupFor(a) - groupFor(b));
}
function reorderVisible(nextVisible) {
const iter = nextVisible.values();
draft.order = draft.order.map(id => draft.hidden.has(id) ? id : iter.next().value);
render();
}
function move(id, target, after = false) {
const next = visibleIds();
if (id === target || !next.includes(id) || !next.includes(target) || groupFor(id) !== groupFor(target)) return;
next.splice(next.indexOf(id), 1);
next.splice(next.indexOf(target) + (after ? 1 : 0), 0, id);
reorderVisible(next);
}
function nudge(id, direction) {
const next = visibleIds(), index = next.indexOf(id), other = index + direction;
if (index < 0 || other < 0 || other >= next.length || groupFor(id) !== groupFor(next[other])) return;
[next[index], next[other]] = [next[other], next[index]];
reorderVisible(next);
visibleRoot.querySelectorAll(".overview-layout-tile").forEach(node => {
if (node.dataset.previewWidget === id) node.querySelector(".overview-layout-up")?.focus();
});
}
function preview(id) {
const original = document.querySelector(`#panel-overview [data-dashboard-widget="${id}"]`);
if (!original) return el("article", label(id), "stat");
if (ADMIN_DASHBOARD_BY_ID.get(id)?.category === "panels") {
const card = el("article", undefined, "overview-layout-panel-preview");
card.append(el("strong", label(id)));
const chart = id === "backend_table" ? null : original.querySelector?.("svg");
if (chart) {
const graphic = chart.cloneNode(true);
for (const node of [graphic, ...graphic.querySelectorAll("[id]")]) node.removeAttribute("id");
graphic.removeAttribute("data-i18n-aria");
graphic.setAttribute("aria-hidden", "true");
card.append(graphic);
}
const detailKey = ADMIN_DASHBOARD_BY_ID.get(id)?.description_key;
if (detailKey) card.append(el("small", t(detailKey)));
card.setAttribute("aria-hidden", "true");
return card;
}
const copy = original.cloneNode(true);
copy.hidden = false;
copy.removeAttribute("data-dashboard-widget");
for (const node of [copy, ...copy.querySelectorAll("[id]")]) node.removeAttribute("id");
for (const node of copy.querySelectorAll("[aria-labelledby], [aria-describedby]")) {
node.removeAttribute("aria-labelledby"); node.removeAttribute("aria-describedby");
}
copy.classList.add("overview-layout-card-preview");
copy.setAttribute("aria-hidden", "true");
return copy;
}
function render() {
if (!draft || !dialog) return;
const visible = visibleIds();
const tiles = visible.map((id, index) => {
const tile = el("div", undefined, "overview-layout-tile");
tile.dataset.previewWidget = id;
if (ADMIN_DASHBOARD_BY_ID.get(id)?.category === "panels") tile.classList.add("is-panel");
tile.draggable = true;
tile.setAttribute("role", "listitem");
tile.setAttribute("aria-label", label(id));
const toolbar = el("div", undefined, "overview-layout-tile-toolbar");
const grip = el("span", "⠿", "overview-layout-grip");
grip.setAttribute("aria-hidden", "true");
const up = el("button", "←", "quiet overview-layout-up");
up.type = "button"; up.disabled = index === 0 || groupFor(id) !== groupFor(visible[index - 1]);
up.setAttribute("aria-label", `${t("moveUp")}: ${label(id)}`);
up.addEventListener("click", () => nudge(id, -1));
const down = el("button", "→", "quiet overview-layout-down");
down.type = "button"; down.disabled = index === visible.length - 1 || groupFor(id) !== groupFor(visible[index + 1]);
down.setAttribute("aria-label", `${t("moveDown")}: ${label(id)}`);
down.addEventListener("click", () => nudge(id, 1));
const hide = el("button", t("overviewLayoutEditorHide"), "quiet overview-layout-hide");
hide.type = "button";
hide.setAttribute("aria-label", `${t("overviewLayoutEditorHide")}: ${label(id)}`);
hide.addEventListener("click", () => { draft.hidden.add(id); render(); });
const controls = el("span", undefined, "overview-layout-controls");
controls.append(up, down, hide);
toolbar.append(grip, dashboardWidgetTypeBadge(id), controls);
tile.append(toolbar, preview(id));
tile.addEventListener("dragstart", event => {
if (event.target !== tile) return;
dragging = id;
event.dataTransfer?.setData("text/plain", id);
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
tile.classList.add("is-dragging");
});
tile.addEventListener("dragover", event => {
if (!dragging || dragging === id || groupFor(dragging) !== groupFor(id)) return;
event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
tile.classList.add("is-drop-target");
});
tile.addEventListener("dragleave", () => tile.classList.remove("is-drop-target"));
tile.addEventListener("drop", event => {
event.preventDefault();
tile.classList.remove("is-drop-target");
if (dragging && dragging !== id) {
const startIndex = visibleIds().indexOf(dragging);
const targetIndex = visibleIds().indexOf(id);
move(dragging, id, startIndex < targetIndex);
}
dragging = null;
});
tile.addEventListener("dragend", () => { dragging = null; tile.classList.remove("is-dragging", "is-drop-target"); });
return tile;
});
visibleRoot.replaceChildren(...(tiles.length ? tiles : [el("p", t("overviewLayoutEditorEmpty"), "fine")]));
const query = (search?.value || "").trim().toLocaleLowerCase();
const hidden = draft.order.filter(id => draft.hidden.has(id)
&& (typeFilter?.value === "all" || !typeFilter?.value || dashboardWidgetType(id) === typeFilter.value)
&& (`${label(id)} ${dashboardWidgetTypeLabel(id)}`).toLocaleLowerCase().includes(query));
const hiddenItems = hidden.map(id => {
const item = el("div", undefined, "overview-layout-hidden-item");
item.setAttribute("role", "listitem");
const name = el("span", label(id));
const meta = ADMIN_DASHBOARD_BY_ID.get(id)?.description_key;
if (meta) name.title = t(meta);
const add = el("button", t("overviewLayoutEditorShow"), "outline");
add.type = "button";
add.setAttribute("aria-label", `${t("overviewLayoutEditorShow")}: ${label(id)}`);
add.addEventListener("click", () => { draft.hidden.delete(id); render(); });
item.append(name, dashboardWidgetTypeBadge(id), add);
return item;
});
hiddenRoot.replaceChildren(...(hiddenItems.length || query ? hiddenItems : [el("p", t("overviewLayoutEditorHiddenEmpty"), "fine")]));
}
async function open() {
if (dialog?.open || opening || window.rustProxyInlineLayout?.isEditing?.()) return;
opening = true;
try { await window.rustProxyPreferences?.load?.(); }
catch (error) { if (error?.status === 401) { opening = false; return; } }
opening = false;
draft = initialDraft();
dragging = null;
search.value = "";
if (typeFilter) typeFilter.value = "all";
message.textContent = "";
render();
dialog.showModal();
cancelButton.focus();
}
function setSaving(value) {
saving = value;
for (const control of [saveButton, cancelButton, defaultButton, resetButton, resetCancel, resetApply])
if (control) control.disabled = value;
}
function restoreDefaultPreview() {
if (saving || !draft) return;
draft = defaultDraft();
dragging = null;
if (search) search.value = "";
if (typeFilter) typeFilter.value = "all";
message.textContent = t("overviewLayoutDefaultLoaded");
render();
}
async function resetToDefault() {
if (saving || !draft) return;
setSaving(true);
message.textContent = t("overviewLayoutEditorSaving");
const defaults = defaultDraft();
try {
await window.rustProxyPreferences.update({
dashboard_widgets: [...defaults.order], dashboard_hidden_widgets: [...defaults.hidden],
});
resetConfirm.close();
dialog.close();
toast(t("overviewLayoutResetSaved"));
} catch (error) { message.textContent = error.message; }
finally { setSaving(false); }
}
async function save() {
if (saving || !draft) return;
setSaving(true);
message.textContent = t("overviewLayoutEditorSaving");
try {
await window.rustProxyPreferences.update({
dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden],
});
dialog.close();
toast(t("overviewLayoutEditorSaved"));
} catch (error) { message.textContent = error.message; }
finally { setSaving(false); }
}
for (const id of ["overview-layout-open", "overview-layout-open-settings"]) $(id)?.addEventListener("click", () => { void open(); });
cancelButton.addEventListener("click", () => { if (!saving) dialog.close(); });
saveButton.addEventListener("click", () => { void save(); });
defaultButton?.addEventListener("click", restoreDefaultPreview);
resetButton?.addEventListener("click", () => {
if (!saving && draft && !resetConfirm.open) resetConfirm.showModal();
});
resetCancel?.addEventListener("click", () => { if (!saving) resetConfirm.close(); });
resetApply?.addEventListener("click", () => { void resetToDefault(); });
resetConfirm?.addEventListener("cancel", event => { if (saving) event.preventDefault(); });
search.addEventListener("input", render);
typeFilter?.addEventListener("change", render);
dialog.addEventListener("cancel", event => { if (saving || resetConfirm.open) event.preventDefault(); });
dialog.addEventListener("close", () => { draft = null; dragging = null; message.textContent = ""; });
return { open, refresh: () => { if (dialog.open) render(); } };
})();