fix(conflicts): the status filter, select-all, and bulk actions actually work

They never did: filterConflicts/toggleAllConflicts/updateBulkActions
were referenced in the templ but defined nowhere, and the bulk buttons
called bulkResolve()/bulkDismiss() with no arguments against functions
expecting (strategy, conflictIds).

- conflict cards carry data-status; the status dropdown filters them
  client-side (pure client state — allowed to be JS), applied on page
  init and re-applied after every partial refresh, with a
  'no conflicts match this filter' note when everything is filtered out
- select-all toggles only the visible cards; the bulk bar shows the
  selected count and resolve/dismiss now gather the checked ids and the
  chosen strategy (the bar's own buttons use resolveSelected()/
  dismissSelected() wrappers)
- dead onchange= globals became Alpine @change handlers on the server-
  rendered controls; partial swaps re-bind via Alpine.initTree (already
  in refreshConflicts) and re-apply filter + bulk state

Visible change: the dropdown's stated default (Unresolved Only) now
actually applies on load, so resolved conflicts are hidden until the
filter is changed.
This commit is contained in:
John O'Keefe committed 2026-10-18 12:42:50 -04:00
1 parent cb4e892b10
commit 4a68fe7cab
3 files changed
+195 -89

No files matched your search

+86
View File
@@ -12,11 +12,84 @@ async function refreshConflicts(): Promise<void> {
});
const list = document.getElementById("conflicts-list");
if (list) Alpine.initTree(list);
filterConflicts();
updateBulkActions();
} catch (error) {
console.error("Failed to refresh conflicts:", error);
}
}
// --- Client-side filter and bulk selection -------------------------------
// Pure functions over the server-rendered conflict cards (rendering
// ownership: JS decides what shows; templ rendered what they look like).
function filterConflicts(): void {
const select = document.getElementById(
"status-filter",
) as HTMLSelectElement | null;
const filter = select?.value ?? "unresolved";
let total = 0;
let visible = 0;
document.querySelectorAll<HTMLElement>(".conflict-item").forEach((item) => {
total++;
const status = item.dataset.status ?? "unresolved";
const show = filter === "all" || status === filter;
item.style.display = show ? "" : "none";
if (show) visible++;
});
const empty = document.getElementById("filter-empty");
if (empty) empty.classList.toggle("hidden", visible > 0 || total === 0);
// Cards hidden by the filter can't stay selected.
if (visible !== total) updateBulkActions();
}
function toggleAllConflicts(): void {
const selectAll = document.getElementById(
"select-all-conflicts",
) as HTMLInputElement | null;
if (!selectAll) return;
document.querySelectorAll<HTMLElement>(".conflict-item").forEach((item) => {
if (item.style.display === "none") return;
const box = item.querySelector<HTMLInputElement>(".conflict-checkbox");
if (box) box.checked = selectAll.checked;
});
updateBulkActions();
}
function getSelectedConflictIds(): string[] {
return Array.from(
document.querySelectorAll<HTMLInputElement>(".conflict-checkbox:checked"),
)
.map((box) => box.dataset.conflictId || "")
.filter((id) => id !== "");
}
function updateBulkActions(): void {
const selected = getSelectedConflictIds();
const bar = document.getElementById("bulk-actions");
if (bar) bar.classList.toggle("hidden", selected.length === 0);
const count = document.getElementById("selected-count");
if (count) count.textContent = String(selected.length);
}
function resolveSelected(): void {
const ids = getSelectedConflictIds();
if (ids.length === 0) return;
const strategyEl = document.getElementById(
"bulk-strategy",
) as HTMLSelectElement | null;
const strategy = (strategyEl?.value ?? "most_recent") as
| "most_recent"
| "highest_progress";
void bulkResolve(strategy, ids);
}
function dismissSelected(): void {
const ids = getSelectedConflictIds();
if (ids.length === 0) return;
void bulkDismiss(ids);
}
async function resolveConflict(
conflictId: string,
winner: string,
@@ -168,9 +241,17 @@ export {
refreshConflicts,
resolveConflict,
showResolveModal,
filterConflicts,
toggleAllConflicts,
updateBulkActions,
resolveSelected,
dismissSelected,
};
Alpine.data("conflicts", () => ({
init() {
filterConflicts();
},
bulkDismiss,
bulkResolve,
dismissAllResolved,
@@ -179,4 +260,9 @@ Alpine.data("conflicts", () => ({
refreshConflicts,
resolveConflict,
showResolveModal,
filterConflicts,
toggleAllConflicts,
updateBulkActions,
resolveSelected,
dismissSelected,
}));