v0.33.7
This commit is contained in:
1 parent
162e986d37
commit
f462ca7194
26 files changed
+980
-710
No files matched your search
@@ -1,279 +1,142 @@
|
||||
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 nameInput = $("overview-preset-name");
|
||||
const saveButton = $("overview-preset-save");
|
||||
const list = $("overview-preset-list");
|
||||
const empty = $("overview-preset-empty");
|
||||
const count = $("overview-preset-count");
|
||||
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;
|
||||
const closeButton = $("overview-layout-cancel");
|
||||
const MAX_PRESETS = 20;
|
||||
let busy = false;
|
||||
let opening = false;
|
||||
|
||||
function initialDraft() {
|
||||
const prefs = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS;
|
||||
const current = () => window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS;
|
||||
const presets = () => normalizeDashboardPresets(current().dashboard_presets);
|
||||
const densityLabel = value => t({ normal: "densityNormal", compact: "densityCompact", ultra: "densityUltra" }[value] || "densityCompact");
|
||||
function snapshot(name) {
|
||||
const prefs = current();
|
||||
const order = normalizeDashboardWidgets(prefs.dashboard_widgets);
|
||||
return { order, hidden: new Set(normalizeHiddenDashboardWidgets(prefs.dashboard_hidden_widgets, prefs.dashboard_widgets)),
|
||||
sizes: normalizeDashboardWidgetSizes(prefs.dashboard_widget_sizes) };
|
||||
}
|
||||
function defaultDraft() {
|
||||
return {
|
||||
order: [...ADMIN_DASHBOARD_WIDGETS],
|
||||
hidden: new Set(ADMIN_DEFAULT_HIDDEN_WIDGETS),
|
||||
sizes: null,
|
||||
name,
|
||||
dashboard_density: normalizeAdminDensity(prefs.dashboard_density, "compact"),
|
||||
dashboard_widgets: order,
|
||||
dashboard_hidden_widgets: normalizeHiddenDashboardWidgets(prefs.dashboard_hidden_widgets, prefs.dashboard_widgets),
|
||||
dashboard_widget_sizes: normalizeDashboardWidgetSizes(prefs.dashboard_widget_sizes),
|
||||
};
|
||||
}
|
||||
function label(id) {
|
||||
const widget = ADMIN_DASHBOARD_BY_ID.get(id);
|
||||
return widget ? t(widget.label_key) : id;
|
||||
function summary(preset) {
|
||||
const visible = preset.dashboard_widgets.length - preset.dashboard_hidden_widgets.length;
|
||||
const widths = Object.keys(preset.dashboard_widget_sizes || {}).length;
|
||||
return t("dashboardPresetSummary")
|
||||
.replace("{visible}", String(visible))
|
||||
.replace("{density}", densityLabel(preset.dashboard_density))
|
||||
.replace("{widths}", String(widths));
|
||||
}
|
||||
function visibleIds() {
|
||||
return dashboardVisualOrder(draft.order, draft.sizes)
|
||||
.filter(id => !draft.hidden.has(id));
|
||||
}
|
||||
function enterFreeLayout() {
|
||||
if (draft.sizes === null) {
|
||||
draft.order = dashboardVisualOrder(draft.order, null);
|
||||
draft.sizes = {};
|
||||
}
|
||||
}
|
||||
function reorderVisible(nextVisible) {
|
||||
enterFreeLayout();
|
||||
const iter = nextVisible.values();
|
||||
draft.order = draft.order.map(id => draft.hidden.has(id) ? id : iter.next().value);
|
||||
function setBusy(value) {
|
||||
busy = value;
|
||||
if (nameInput) nameInput.disabled = value;
|
||||
if (closeButton) closeButton.disabled = value;
|
||||
render();
|
||||
}
|
||||
function move(id, target, after = false) {
|
||||
const next = visibleIds();
|
||||
if (id === target || !next.includes(id) || !next.includes(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) return;
|
||||
[next[index], next[other]] = [next[other], next[index]];
|
||||
reorderVisible(next);
|
||||
visibleRoot.querySelector(`[data-preview-widget="${id}"] .overview-layout-up`)?.focus();
|
||||
}
|
||||
function resize(id, delta) {
|
||||
if (!draft) return;
|
||||
const widths = dashboardAllowedWidths(id);
|
||||
const next = widths[widths.indexOf(dashboardWidgetWidth(id, draft.sizes)) + delta];
|
||||
if (next === undefined) return;
|
||||
enterFreeLayout();
|
||||
if (next === dashboardDefaultWidth(id)) delete draft.sizes[id];
|
||||
else draft.sizes[id] = next;
|
||||
render();
|
||||
visibleRoot.querySelector(`[data-preview-widget="${id}"] .${delta < 0 ? "overview-layout-narrow" : "overview-layout-widen"}`)?.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;
|
||||
async function saveCurrent() {
|
||||
const name = (nameInput?.value || "").trim();
|
||||
if (busy || !name) return;
|
||||
const next = presets();
|
||||
const key = name.toLowerCase();
|
||||
const index = next.findIndex(preset => preset.name.toLowerCase() === key);
|
||||
if (index < 0 && next.length >= MAX_PRESETS) {
|
||||
message.textContent = t("dashboardPresetLimitReached").replace("{max}", String(MAX_PRESETS));
|
||||
return;
|
||||
}
|
||||
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;
|
||||
const value = snapshot(name);
|
||||
if (index >= 0) next[index] = value;
|
||||
else next.unshift(value);
|
||||
setBusy(true);
|
||||
message.textContent = t("dashboardPresetSaving");
|
||||
try {
|
||||
await window.rustProxyPreferences.update({ dashboard_presets: next });
|
||||
if (nameInput) nameInput.value = "";
|
||||
message.textContent = t(index >= 0 ? "dashboardPresetUpdated" : "dashboardPresetSavedMessage").replace("{name}", name);
|
||||
toast(message.textContent);
|
||||
} catch (error) { message.textContent = error.message; }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
async function loadPreset(preset) {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
message.textContent = t("dashboardPresetLoading").replace("{name}", preset.name);
|
||||
try {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_density: preset.dashboard_density,
|
||||
dashboard_widgets: [...preset.dashboard_widgets],
|
||||
dashboard_hidden_widgets: [...preset.dashboard_hidden_widgets],
|
||||
dashboard_widget_sizes: preset.dashboard_widget_sizes === null ? null : { ...preset.dashboard_widget_sizes },
|
||||
});
|
||||
dialog.close();
|
||||
toast(t("dashboardPresetLoaded").replace("{name}", preset.name));
|
||||
} catch (error) { message.textContent = error.message; }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
async function deletePreset(preset) {
|
||||
if (busy || !confirm(t("dashboardPresetDeleteConfirm").replace("{name}", preset.name))) return;
|
||||
setBusy(true);
|
||||
message.textContent = t("dashboardPresetDeleting").replace("{name}", preset.name);
|
||||
try {
|
||||
const next = presets().filter(item => item.name.toLowerCase() !== preset.name.toLowerCase());
|
||||
await window.rustProxyPreferences.update({ dashboard_presets: next });
|
||||
message.textContent = t("dashboardPresetDeleted").replace("{name}", preset.name);
|
||||
} catch (error) { message.textContent = error.message; }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
function presetRow(preset) {
|
||||
const row = el("article", undefined, "dashboard-preset-row");
|
||||
row.setAttribute("role", "listitem");
|
||||
const info = el("div", undefined, "dashboard-preset-info");
|
||||
info.append(el("strong", preset.name), el("small", summary(preset), "fine"));
|
||||
const actions = el("div", undefined, "dashboard-preset-actions");
|
||||
const load = el("button", t("dashboardPresetLoad"), "outline");
|
||||
load.type = "button"; load.disabled = busy;
|
||||
load.setAttribute("aria-label", `${t("dashboardPresetLoad")}: ${preset.name}`);
|
||||
load.addEventListener("click", () => { void loadPreset(preset); });
|
||||
const remove = el("button", t("dashboardPresetDelete"), "quiet");
|
||||
remove.type = "button"; remove.disabled = busy;
|
||||
remove.setAttribute("aria-label", `${t("dashboardPresetDelete")}: ${preset.name}`);
|
||||
remove.addEventListener("click", () => { void deletePreset(preset); });
|
||||
actions.append(load, remove);
|
||||
row.append(info, actions);
|
||||
return row;
|
||||
}
|
||||
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;
|
||||
tile.dataset.dashboardSpan = String(dashboardWidgetWidth(id, draft.sizes));
|
||||
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;
|
||||
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;
|
||||
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 widths = dashboardAllowedWidths(id);
|
||||
const width = dashboardWidgetWidth(id, draft.sizes);
|
||||
const at = widths.indexOf(width);
|
||||
const narrow = el("button", "−", "quiet overview-layout-narrow");
|
||||
narrow.type = "button"; narrow.disabled = at <= 0;
|
||||
narrow.setAttribute("aria-label", `${t("overviewLayoutNarrow")}: ${label(id)}`);
|
||||
narrow.addEventListener("click", () => resize(id, -1));
|
||||
const widen = el("button", "+", "quiet overview-layout-widen");
|
||||
widen.type = "button"; widen.disabled = at === widths.length - 1;
|
||||
widen.setAttribute("aria-label", `${t("overviewLayoutWiden")}: ${label(id)}`);
|
||||
widen.addEventListener("click", () => resize(id, 1));
|
||||
const widthValue = el("span", `${width}/12`, "overview-layout-width-value");
|
||||
widthValue.title = t("overviewLayoutWidth");
|
||||
// The visual editor intentionally keeps uniformly sized previews.
|
||||
// Group sizing separately from ordering/hiding so both sets wrap
|
||||
// inside a narrow dialog rather than spilling into adjacent tiles.
|
||||
const sizeActions = el("span", undefined, "overview-layout-size-actions");
|
||||
sizeActions.append(narrow, widthValue, widen);
|
||||
const orderActions = el("span", undefined, "overview-layout-order-actions");
|
||||
orderActions.append(up, down, hide);
|
||||
const controls = el("span", undefined, "overview-layout-controls");
|
||||
controls.append(sizeActions, orderActions);
|
||||
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) 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")]));
|
||||
if (!dialog || !list) return;
|
||||
const values = presets();
|
||||
list.replaceChildren(...values.map(presetRow));
|
||||
if (empty) empty.hidden = values.length > 0;
|
||||
if (count) count.textContent = t("dashboardPresetCount").replace("{count}", String(values.length)).replace("{max}", String(MAX_PRESETS));
|
||||
if (saveButton) saveButton.disabled = busy || !(nameInput?.value || "").trim();
|
||||
if (nameInput) nameInput.disabled = busy;
|
||||
if (closeButton) closeButton.disabled = busy;
|
||||
}
|
||||
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; } }
|
||||
catch (error) { if (error?.status === 401) { opening = false; return; } message.textContent = error.message; }
|
||||
opening = false;
|
||||
draft = initialDraft();
|
||||
dragging = null;
|
||||
search.value = "";
|
||||
if (typeFilter) typeFilter.value = "all";
|
||||
if (nameInput) nameInput.value = "";
|
||||
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],
|
||||
dashboard_widget_sizes: null,
|
||||
});
|
||||
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],
|
||||
dashboard_widget_sizes: draft.sizes === null ? null : { ...draft.sizes },
|
||||
});
|
||||
dialog.close();
|
||||
toast(t("overviewLayoutEditorSaved"));
|
||||
} catch (error) { message.textContent = error.message; }
|
||||
finally { setSaving(false); }
|
||||
nameInput?.focus();
|
||||
}
|
||||
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();
|
||||
nameInput?.addEventListener("input", render);
|
||||
nameInput?.addEventListener("keydown", event => {
|
||||
if (event.key === "Enter" && !saveButton?.disabled) { event.preventDefault(); void saveCurrent(); }
|
||||
});
|
||||
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(); } };
|
||||
saveButton?.addEventListener("click", () => { void saveCurrent(); });
|
||||
closeButton?.addEventListener("click", () => { if (!busy) dialog.close(); });
|
||||
dialog?.addEventListener("cancel", event => { if (busy) event.preventDefault(); });
|
||||
dialog?.addEventListener("close", () => { message.textContent = ""; });
|
||||
return { open, refresh: () => { if (dialog?.open) render(); } };
|
||||
})();
|
||||
Reference in new issue
Block a user