fix: refactor filter loading and clearing to prevent stale field data
Major refactoring of bookshelf filter logic: Filter loading improvements: - Add clearFormWithoutSubmit() helper to reset form without submission - Refactor clearFilters() to reuse clearFormWithoutSubmit() helper Reduces code duplication from 38 lines to 8 lines - Update loadFilter() to call clearFormWithoutSubmit() before populating This ensures all stale data from previous filter is cleared - Move has_cover handling before empty value check Fixes issue where has_cover=false was being skipped - Remove automatic HTMX trigger note from cycleHasCover() Fixed issues: - Author field staying populated when switching to filter without author - has_cover tristate button not updating when switching between filters - has_cover button not updating from "Has Cover" to "Any" when loading filter without has_cover - General stale data retention when loading different saved filters HTMX event handling: - Add event listener in initBookshelf() for htmx:afterSwap events - Listens on #saved-filters-list element (the swap target) - Calls afterFilterSave() to close modal and show success toast - Properly handles Alpine component state access All filter operations now work correctly with proper state management and no visual artifacts from previous filters.
This commit is contained in:
+143
-84
@@ -4,11 +4,65 @@ import { Alpine } from "./alpine";
|
||||
import { showToast } from "./toast";
|
||||
|
||||
function clearFilters(): void {
|
||||
// Clear all form state (reuses helper)
|
||||
clearFormWithoutSubmit();
|
||||
|
||||
// Trigger form submit with cleared filters
|
||||
const filterForm = document.getElementById("filter-form") as HTMLFormElement;
|
||||
if (!filterForm) return;
|
||||
// Reset all form fields
|
||||
window.htmx.trigger(filterForm, "submit");
|
||||
}
|
||||
|
||||
// Remove dynamically created hidden inputs from filter form
|
||||
// Preserves: limit, offset, has_cover (static/managed inputs)
|
||||
function cleanupDynamicHiddenInputs(): void {
|
||||
const filterForm = document.getElementById("filter-form") as HTMLFormElement;
|
||||
if (!filterForm) return;
|
||||
|
||||
// Names of inputs to preserve (static or Alpine-managed)
|
||||
const preserveNames = ["limit", "offset", "has_cover"];
|
||||
|
||||
// Find all hidden inputs
|
||||
const hiddenInputs = filterForm.querySelectorAll('input[type="hidden"]');
|
||||
hiddenInputs.forEach((input) => {
|
||||
const name = (input as HTMLInputElement).name;
|
||||
// Remove if it's not in our preserve list and has a name
|
||||
if (name && !preserveNames.includes(name)) {
|
||||
input.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear form fields WITHOUT submitting
|
||||
// Used by loadFilter to reset state before applying new filter
|
||||
function clearFormWithoutSubmit(): void {
|
||||
const filterForm = document.getElementById("filter-form") as HTMLFormElement;
|
||||
if (!filterForm) return;
|
||||
|
||||
// Reset all form fields to their HTML defaults
|
||||
filterForm.reset();
|
||||
// Manually reset pagination values
|
||||
|
||||
// Remove has_cover input entirely
|
||||
const hasCoverInput = document.querySelector(
|
||||
'[name="has_cover"]',
|
||||
) as HTMLInputElement;
|
||||
if (hasCoverInput) {
|
||||
hasCoverInput.remove();
|
||||
}
|
||||
|
||||
// Reset Alpine hasCoverState to null
|
||||
const bookshelfComponent = document.querySelector('[x-data*="bookshelf"]');
|
||||
if (bookshelfComponent && (bookshelfComponent as any)._x_dataStack) {
|
||||
const data = (bookshelfComponent as any)._x_dataStack[0];
|
||||
if (data && typeof data.hasCoverState !== "undefined") {
|
||||
data.hasCoverState = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any hidden inputs from previous filter loads
|
||||
cleanupDynamicHiddenInputs();
|
||||
|
||||
// Reset pagination to defaults
|
||||
const limitInput = filterForm.querySelector(
|
||||
'input[name="limit"]',
|
||||
) as HTMLInputElement;
|
||||
@@ -17,16 +71,15 @@ function clearFilters(): void {
|
||||
) as HTMLInputElement;
|
||||
if (limitInput) limitInput.value = "50";
|
||||
if (offsetInput) offsetInput.value = "0";
|
||||
// Trigger form submit with cleared filters
|
||||
window.htmx.trigger(filterForm, "submit");
|
||||
}
|
||||
|
||||
// NOTE: No form submit here!
|
||||
}
|
||||
// Alpine.js component
|
||||
Alpine.data("bookshelf", () => ({
|
||||
// Component state
|
||||
showSaveModal: false,
|
||||
filterName: "",
|
||||
showFiltersDropdown: false,
|
||||
hasCoverState: null as boolean | null,
|
||||
|
||||
// Standalone function references (don't access component state)
|
||||
clearFilters,
|
||||
@@ -40,6 +93,33 @@ Alpine.data("bookshelf", () => ({
|
||||
toggleFiltersDropdown() {
|
||||
this.showFiltersDropdown = !this.showFiltersDropdown;
|
||||
},
|
||||
// Cycle through has_cover states: null -> true -> false -> null
|
||||
cycleHasCover(): void {
|
||||
if (this.hasCoverState === null) {
|
||||
this.hasCoverState = true;
|
||||
} else if (this.hasCoverState === true) {
|
||||
this.hasCoverState = false;
|
||||
} else {
|
||||
this.hasCoverState = null;
|
||||
}
|
||||
// NOTE: We don't trigger form submission here
|
||||
// The user must click "Search" or press Enter to apply the filter
|
||||
},
|
||||
// Handle post-HTMX swap cleanup for saved filters
|
||||
afterFilterSave(): void {
|
||||
// Hide empty state if it exists
|
||||
const filtersList = document.getElementById("saved-filters-list");
|
||||
if (filtersList) {
|
||||
const emptyState = filtersList.nextElementSibling;
|
||||
if (emptyState && emptyState.textContent?.includes("No saved filters")) {
|
||||
emptyState.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
// Close modal
|
||||
this.showSaveModal = false;
|
||||
// Show success toast
|
||||
showToast("Filter saved successfully", "success");
|
||||
},
|
||||
// Load a saved filter into the form
|
||||
async loadFilter(event: Event) {
|
||||
const button = event.target as HTMLElement;
|
||||
@@ -91,29 +171,55 @@ Alpine.data("bookshelf", () => ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up any hidden inputs from previous filter loads
|
||||
cleanupDynamicHiddenInputs();
|
||||
|
||||
// Clear all form state before applying new filter
|
||||
clearFormWithoutSubmit();
|
||||
|
||||
// Don't clear form - just update values
|
||||
// Populate form fields from filter data
|
||||
Object.entries(filterData).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
// Only set non-empty values
|
||||
// Update visible form fields if they exist
|
||||
const visibleField = filterForm.querySelector(
|
||||
`[name="${key}"]`,
|
||||
) as HTMLInputElement;
|
||||
if (visibleField) {
|
||||
if (visibleField.type === "checkbox") {
|
||||
visibleField.checked = value === "true";
|
||||
} else {
|
||||
visibleField.value = value;
|
||||
if (key === "has_cover") {
|
||||
const bookshelfComponent = document.querySelector(
|
||||
'[x-data*="bookshelf"]',
|
||||
);
|
||||
if (bookshelfComponent && (bookshelfComponent as any)._x_dataStack) {
|
||||
const data = (bookshelfComponent as any)._x_dataStack[0];
|
||||
if (data && typeof data.hasCoverState !== "undefined") {
|
||||
// Convert string value to boolean|null
|
||||
if (value === "true") {
|
||||
data.hasCoverState = true;
|
||||
} else if (value === "false") {
|
||||
data.hasCoverState = false;
|
||||
} else {
|
||||
data.hasCoverState = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create hidden input if visible field doesn't exist
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = key;
|
||||
input.value = value;
|
||||
filterForm.appendChild(input);
|
||||
}
|
||||
return; // Don't process has_cover like other fields
|
||||
}
|
||||
|
||||
if (!value) return; // Skip empty values
|
||||
// Special handling for has_cover - update Alpine state directly
|
||||
|
||||
// Handle all other fields normally
|
||||
const visibleField = filterForm.querySelector(
|
||||
`[name="${key}"]`,
|
||||
) as HTMLInputElement;
|
||||
if (visibleField) {
|
||||
if (visibleField.type === "checkbox") {
|
||||
visibleField.checked = value === "true";
|
||||
} else {
|
||||
visibleField.value = value;
|
||||
}
|
||||
} else {
|
||||
// Create hidden input if visible field doesn't exist
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = key;
|
||||
input.value = value;
|
||||
filterForm.appendChild(input);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -164,65 +270,6 @@ Alpine.data("bookshelf", () => ({
|
||||
showToast("Error deleting filter", "error");
|
||||
}
|
||||
},
|
||||
// Inline method - saves the filter and closes modal
|
||||
async saveFilter(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
showToast("Not authenticated", "error");
|
||||
return;
|
||||
}
|
||||
// Get filter name from component state
|
||||
const filterName = this.filterName;
|
||||
if (!filterName) {
|
||||
showToast("Please enter a filter name", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect filter form data
|
||||
const filterForm = document.getElementById(
|
||||
"filter-form",
|
||||
) as HTMLFormElement;
|
||||
if (!filterForm) {
|
||||
showToast("Filter form not found", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(filterForm);
|
||||
const filterData: Record<string, string> = {};
|
||||
formData.forEach((value, key) => {
|
||||
filterData[key] = value.toString();
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/saved-filters", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: filterName,
|
||||
resource_type: "media-items",
|
||||
filters: filterData,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast("Filter saved successfully", "success");
|
||||
// Clear the filter name input
|
||||
this.filterName = "";
|
||||
// Close the modal
|
||||
this.showSaveModal = false;
|
||||
} else {
|
||||
showToast("Failed to save filter", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save filter:", error);
|
||||
showToast("Error saving filter", "error");
|
||||
}
|
||||
},
|
||||
|
||||
// Autocomplete helper function
|
||||
async fetchFieldValues(
|
||||
@@ -333,6 +380,18 @@ function initBookshelf(): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Setup listener for HTMX swap events on the filters list
|
||||
const filtersList = document.getElementById("saved-filters-list");
|
||||
if (filtersList) {
|
||||
filtersList.addEventListener("htmx:afterSwap", () => {
|
||||
const bookshelfElem = document.querySelector("[x-data*='bookshelf']");
|
||||
if (!bookshelfElem) return;
|
||||
const bookshelf = (bookshelfElem as any)._x_dataStack?.[0];
|
||||
if (!bookshelf) return;
|
||||
|
||||
bookshelf.afterFilterSave();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { clearFilters };
|
||||
export { clearFilters, cleanupDynamicHiddenInputs, clearFormWithoutSubmit };
|
||||
|
||||
Reference in New Issue
Block a user