Excel-style column filters, full-page Custom View, card field editor

Filters (web/excel-filter.js), modeled on Excel's AutoFilter:
- Every column header gets a dropdown: Sort A to Z / Z to A, Clear Filter
  From "<column>", Search, a checkbox list of the column's values with
  (Select All) and (Blanks), OK / Cancel. Values come from rows passing
  the other columns' filters; filters on several columns combine. Tags
  and software lists are split into individual items.
- The button shows a funnel when filtered and an arrow when sorted, and a
  "Clear filters (n)" button appears beside the group's copy buttons
- Replaces the old per-column filter row (which also removes the gap
  between header and filter rows); hooks the existing filter passes so
  global search, counts and worker re-renders keep working
- Menus are fixed-position on <body>, so sticky headers and neighbouring
  tables can't cover them; on phones they open as a bottom sheet
- Workbench stacks get the same menus (sorting done client-side)
- Custom View's filter icon opens the same menu

Layout:
- Sticky headers use separated borders so no hairline gap opens
- Phones/tablets: a sticky bar of column chips (with active counts and
  "Clear all") opens the filter menus
- Custom View fills the page like the other views on desktop; it still
  scrolls sideways when wide, with its header pinned under the group header
- Custom View on phones/tablets: a "Card fields" sheet to choose, order
  and remove the fields shown on each card

Demo: first visit puts 6 PCs (one per migration stage) on the Workbench.
This commit is contained in:
Johan Sandoval committed 2026-10-06 17:24:55 +00:00
1 parent d24be4a2d6
commit b216ce43f2
5 files changed
+788 -99

No files matched your search

+29
View File
@@ -937,6 +937,35 @@
let seen = false;
try { seen = localStorage.getItem('usmt_demo_seen') === '1'; } catch (e) {}
if (!seen) this.about(true);
this.seedWorkbench();
},
// First visit: put 6 PCs on the Workbench (one per migration stage) so it isn't empty.
// Only when the visitor has no saved stacks; "Reset my changes" clears them again.
async seedWorkbench() {
try { if (localStorage.getItem('usmt_workbench_stacks')) return; } catch (e) { return; }
for (let i = 0; i < 60; i++) {
if (typeof apiData !== 'undefined' && apiData.filter(r => r["Backup Status"] && r["Backup Status"] !== "No Data").length > 20) break;
await sleep(500);
}
if (typeof apiData === 'undefined' || typeof myStacks === 'undefined') return;
try { if (localStorage.getItem('usmt_workbench_stacks')) return; } catch (e) { return; } // created meanwhile
const wanted = ["Backup In Progress", "Restore In Progress", "Backup Completed", "Completed", "Not Started", "Not Associated"];
const picks = [];
for (const status of wanted) {
const row = apiData.find(r => r["Backup Status"] === status && !picks.includes(r["Source Device"])
&& (status === "Completed" || r["Source Online"] === "Online"));
if (row) picks.push(row["Source Device"]);
}
for (const r of apiData) { // top up to 6 if a stage is empty
if (picks.length >= 6) break;
if (!["Skipped", "No Data"].includes(r["Backup Status"]) && !picks.includes(r["Source Device"])) picks.push(r["Source Device"]);
}
if (!picks.length) return;
myStacks = [{ id: 1, name: "Stack 1", list: picks.slice(0, 6).join('\n') }];
saveAndRender();
myStacks.forEach(st => refreshStackTable(st.id));
},
about(show) {
this.modal.classList.toggle('show', show);