app/tabs/components.mjs (237 lines of code) (raw):

import { _, __, debounce } from "util"; import * as Dialog from "dialog"; import * as Global from "global"; import * as Tooltips from "tooltips"; const g = { $table: undefined, lastQuery: undefined, }; const onSelectedChanged = debounce(() => { const selected = Global.selectedComponents(); // counter in tab _("#selected-components-count").textContent = selected.length === 0 ? "" : `(${selected.length})`; // disabled tabs for (const $tab of __("#components-tab-group .tab")) { if ($tab.dataset.tab !== "components") { if (selected.length === 0) { $tab.classList.add("disabled"); } else { $tab.classList.remove("disabled"); } } } // tab tooltip const tooltip = []; if (selected.length === 0) { tooltip.push("No components selected."); } else { for (const c of selected) { tooltip.push(c.title); } } if (selected.length > 50) { Tooltips.set( _("#selected-components-title"), "More than 50 components selected", ); } else { Tooltips.set(_("#selected-components-title"), tooltip.join("\n")); } // "selected only" checkbox if (!_("#filter-selected").checked) { _("#filter-selected").disabled = selected.length === 0; if (selected.length === 0) { _("label[for=filter-selected]").classList.add("disabled"); } else { _("label[for=filter-selected]").classList.remove("disabled"); } } saveToURL(); document.dispatchEvent(new Event("components.changed")); }, 10); function onFilterKeyUp(event) { if (event.key === "Escape") { // escape to clear the filter and show all _("#component-filter").value = ""; for (const $tr of __("#components tr")) { $tr.classList.remove("hidden"); } _("#tab-components").classList.remove("no-matching-components"); g.lastQuery = undefined; return; } applyFilter(); } function applyFilter() { // no need to filter if unchanged const queryOptions = [ _("#component-filter").value.trim().toLowerCase(), _("#filter-scope").value, _("#filter-selected").checked.toString(), ].join("\n"); if (queryOptions === g.lastQuery) { return; } g.lastQuery = queryOptions; const query = _("#component-filter").value.trim().toLowerCase(); // component title or team that contain all of the filter words const queryWords = query.split(/\s+/); let matches = 0; const field = _("#filter-scope").value; for (const c of Global.allComponents()) { if (queryWords.every((w) => c[field].toLowerCase().includes(w))) { _(`#c${c.id}-row`).classList.remove("hidden"); matches++; } else { _(`#c${c.id}-row`).classList.add("hidden"); } } if (matches === 0) { _("#tab-components").classList.add("no-matching-components"); } else { _("#tab-components").classList.remove("no-matching-components"); } onSelectedChanged(); } export async function initUI() { g.$table = _("#components tbody"); _("#components").addEventListener("click", (event) => { if (event.target.nodeName === "TD") { // clicking anywhere on a row should toggle the checkbox const $row = event.target.closest("tr"); if ($row?.classList.contains("row")) { _($row, "input[type=checkbox]").click(); } } if (event.target.nodeName === "INPUT") { onSelectedChanged(); } }); _("#component-filter").addEventListener("keyup", debounce(onFilterKeyUp, 100)); _("#filter-scope").addEventListener("change", applyFilter); _("#filter-all").addEventListener("click", async () => { const components = __("#components tr:not(.hidden) input:not(:checked)"); if (components.length > 50) { await Dialog.alert( "Too many visible components. Please filter to show fewer than 50.", ); return; } for (const $cb of components) { $cb.click(); } }); _("#filter-none-visible").addEventListener("click", () => { for (const $cb of __("#components tr:not(.hidden) input:checked")) { $cb.click(); } }); _("#filter-none").addEventListener("click", () => { for (const $cb of __("#components input:checked")) { $cb.click(); } }); _("#filter-selected").addEventListener("click", () => { if (_("#filter-selected").checked) { _("#component-filter").disabled = true; _("#filter-scope").disabled = true; for (const $cb of __("#components input")) { const $tr = $cb.closest("tr"); if ($cb.checked) { $tr.classList.remove("hidden"); } else { $tr.classList.add("hidden"); } } g.lastQuery = undefined; } else { _("#component-filter").disabled = false; _("#filter-scope").disabled = false; applyFilter(); } }); // always start with an empty filter, even if the browser restored the input _("#component-filter").value = ""; refreshTable(); loadFromURL(); onSelectedChanged(); document.addEventListener("tab.components", () => { document.body.classList.remove("component-warning"); saveToURL(); _("#component-filter").focus(); }); } function loadFromURL() { const searchParams = new URLSearchParams(window.location.search); if (searchParams.has("team")) { const team = searchParams.get("team"); for (const c of Global.allComponents()) { if (c.team === team) { _(`#c${c.id}`).checked = true; } } } else { const selectedComponents = new Set(searchParams.getAll("component")); for (const c of Global.allComponents()) { const key = `${c.product}:${c.component}`; if (selectedComponents.has(key)) { _(`#c${c.id}`).checked = true; selectedComponents.delete(key); } } if (selectedComponents.size > 0) { document.body.classList.add("component-warning"); } } } function saveToURL() { const url = new URL(window.location.href); const searchParams = url.searchParams; searchParams.delete("component"); searchParams.delete("team"); const selected = Global.selectedComponents(); // if the filter scope is a team and all components in that team are selected // then use that team as the search params if (selected.length > 0 && _("#filter-scope").value === "team") { const team = selected[0].team; const teamComponents = Global.allComponents().filter((c) => c.team === team); if ( selected.every((c) => c.team === team) && selected.length === teamComponents.length ) { searchParams.append("team", team); } } // otherwise use individual components if (!searchParams.has("team")) { for (const c of selected) { searchParams.append("component", `${c.product}:${c.component}`); } } if (url.href.length < 2048) { window.history.replaceState(undefined, undefined, url.href); } } function refreshTable() { for (const $tr of __(g.$table, ".row")) { $tr.remove(); } const $template = _("#components-row"); for (const component of Global.allComponents()) { // create <tr> from <template> const $row = document.importNode($template.content, true).querySelector("tr"); // set row id from component id $row.dataset.component = component.id; const id = `c${component.id}`; $row.id = `c${component.id}-row`; _($row, "input[type=checkbox]").id = id; for (const $label of __($row, "label")) { // associate <label> with this row's checkbox $label.setAttribute("for", id); // set cell content $label.textContent = component[$label.dataset.field]; } g.$table.append($row); } }