Ctrl K
ComunidadGuías…

web/admin/views.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/* web/admin/views.js — the admin panel's grouped views.
 *
 * The panel had grown to thirteen sections and a summary rail on one page, about 6,800 px
 * tall on a laptop screen: an operator looking for the keys card scrolled
 * past the audit tables, the quality tables twice and the vocabulary
 * table first. The sections now belong to four views, and the sticky nav
 * starts with one button per view. Only the chosen view's sections are
 * shown; the summary rail above them is always shown. «همه» shows every
 * section, which is the page as it was.
 *
 * Nothing is removed or moved in the DOM: a section outside the view gets
 * the `hidden` attribute, a two-column wrapper whose sections are all
 * hidden is hidden too, and the jump links of hidden sections are hidden
 * with them. A link to any `#sec-*` (a bookmark, the old jump nav, a
 * summary-rail item) still works: it first switches to the view that holds
 * the section. Every card keeps loading and refreshing as before, hidden
 * or not, so switching views never shows a stale or empty card.
 *
 * A pure DOM module with no fetch or state of its own, like
 * access-requests.js, so it is testable under Node without main.js's
 * bootstrap. The chosen view is remembered in localStorage (a per-browser
 * convenience; every read and write is guarded).
 */

"use strict";

/** The views, in nav order. `sections` lists the ids of the `<section>`s a
 * view shows; `null` means every section. Every section id of the page is
 * in exactly one of the four named views (pinned by
 * tests/web_ui/test_web_ui_admin_views.py). */
export const VIEWS = [
  { id: "status", label: "وضعیت", sections: ["sec-maintenance", "sec-health"] },
  { id: "quality", label: "کیفیت و استفاده", sections: ["sec-stats", "sec-feedback", "sec-quality", "sec-usage"] },
  { id: "access", label: "دسترسی و کلیدها", sections: ["sec-access-requests", "sec-keys", "sec-auth"] },
  { id: "data", label: "داده و پیکربندی", sections: ["sec-cache", "sec-config", "sec-schema", "sec-vocab"] },
  { id: "all", label: "همه", sections: null },
];

export const DEFAULT_VIEW = "status";
export const STORAGE_KEY = "lsa-admin-view";

/**
 * The id of the named view that holds section *sectionId*, or null when
 * no named view does (the summary rail, `#overview`, is in every view).
 *
 * @param {string} sectionId
 * @returns {string|null}
 */
export function viewForSection(sectionId) {
  for (const v of VIEWS) {
    if (v.sections && v.sections.includes(sectionId)) return v.id;
  }
  return null;
}

/**
 * True when *viewId* names a view.
 *
 * @param {unknown} viewId
 * @returns {boolean}
 */
export function isView(viewId) {
  return VIEWS.some((v) => v.id === viewId);
}

/**
 * Show the sections of *viewId* and hide the others.
 *
 * @param {ParentNode} root — the document (or any node holding the page).
 * @param {string} viewId — an id from VIEWS; an unknown id shows every
 *   section, so a bad value can never leave the page empty.
 * @returns {string} the view actually applied.
 */
export function applyView(root, viewId) {
  const view = VIEWS.find((v) => v.id === viewId) || VIEWS.find((v) => v.id === "all");
  const shown = view.sections ? new Set(view.sections) : null;

  for (const sec of root.querySelectorAll("section.admin-section")) {
    sec.hidden = shown ? !shown.has(sec.id) : false;
  }
  // A two-column wrapper with nothing left to show would still take its
  // gap in the page; with one section left, the grid's auto-fit gives that
  // section the full width.
  for (const wrap of root.querySelectorAll(".admin-columns")) {
    const kids = wrap.querySelectorAll("section.admin-section");
    wrap.hidden = kids.length > 0 && [...kids].every((k) => k.hidden);
  }
  for (const a of root.querySelectorAll(".admin-jump a[href^='#']")) {
    const id = a.getAttribute("href").slice(1);
    const owner = viewForSection(id);
    a.hidden = Boolean(shown) && owner !== null && !shown.has(id);
  }
  for (const b of root.querySelectorAll(".admin-view-btn")) {
    const on = b.dataset.view === view.id;
    b.setAttribute("aria-pressed", String(on));
    b.classList.toggle("on", on);
  }
  return view.id;
}

/**
 * Mark the view buttons that need attention (a failed check, maintenance
 * on, open feedback). A mark is a dot plus a visually hidden word, so it
 * is not carried by colour alone and a screen reader hears it.
 *
 * @param {ParentNode} root
 * @param {Record<string, boolean>} flags — view id → needs attention.
 */
export function markAttention(root, flags) {
  for (const b of root.querySelectorAll(".admin-view-btn")) {
    const on = Boolean(flags[b.dataset.view]);
    b.classList.toggle("attn", on);
    const sr = b.querySelector(".admin-view-attn");
    if (sr) sr.textContent = on ? " (نیاز به توجه)" : "";
  }
}

/**
 * The view to open with: the one holding the section in *hash* when it
 * names one, else the remembered view, else DEFAULT_VIEW.
 *
 * @param {string} hash — location.hash, with or without "#".
 * @param {string|null} remembered — the value read from localStorage.
 * @returns {string}
 */
export function initialView(hash, remembered) {
  const id = String(hash || "").replace(/^#/, "");
  const owner = id ? viewForSection(id) : null;
  if (owner) return owner;
  if (isView(remembered)) return remembered;
  return DEFAULT_VIEW;
}

/** Read the remembered view; null when storage is unavailable. */
export function readRememberedView(storage) {
  try { return storage ? storage.getItem(STORAGE_KEY) : null; } catch { return null; }
}

/** Remember *viewId*; storage that throws (private mode) is ignored. */
export function rememberView(storage, viewId) {
  try { if (storage) storage.setItem(STORAGE_KEY, viewId); } catch { /* per-browser convenience only */ }
}

/**
 * Build the view buttons and put them at the start of *nav*.
 *
 * @param {HTMLElement} nav — the sticky `.admin-jump` nav.
 * @param {(viewId: string) => void} onSelect
 * @returns {HTMLElement} the group of buttons.
 */
export function renderViewSwitch(nav, onSelect) {
  const group = document.createElement("div");
  group.className = "admin-views";
  group.setAttribute("role", "group");
  group.setAttribute("aria-label", "نما");
  for (const v of VIEWS) {
    const b = document.createElement("button");
    b.type = "button";
    b.className = "admin-view-btn";
    b.dataset.view = v.id;
    b.setAttribute("aria-pressed", "false");
    b.textContent = v.label;
    const sr = document.createElement("span");
    sr.className = "admin-view-attn sr-only";
    b.appendChild(sr);
    b.addEventListener("click", () => onSelect(v.id));
    group.appendChild(b);
  }
  nav.insertBefore(group, nav.firstChild);
  return group;
}