From 2c955cebb5f1e01fbdb866b50d296fe9fbd852b8 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 06 Jan 2026 20:40:03 +0000
Subject: [PATCH] =TaxonomySelector.js and creator refactor complete

---
 assets/js/concise/DataStore.js |  125 +++++++++++++++++++++++++++++++++++++++++
 1 files changed, 123 insertions(+), 2 deletions(-)

diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 95027e3..fd5d3cb 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -648,7 +648,8 @@
 			endpoint: store.config.endpoint,
 			filters: { ...store.filters },
 			etag: response.headers.get('ETag'),
-			lastModified: response.headers.get('Last-Modified')
+			lastModified: response.headers.get('Last-Modified'),
+			has_more: data.has_more || false
 		};
 
 		store.cache.set(cacheKey, cacheEntry);
@@ -899,6 +900,7 @@
 		const cacheKey = this.generateCacheKey(store.filters);
 		const cacheEntry = store.cache.get(cacheKey);
 
+		// First check if we have cached results for exact filters
 		if (cacheEntry && cacheEntry.items) {
 			return cacheEntry.items.reduce((acc, id) => {
 				const item = store.data.get(id);
@@ -907,6 +909,42 @@
 			}, []);
 		}
 
+		// If we have a search filter and complete base data, filter locally
+		if (store.filters.search && store.filters.search.trim()) {
+			const searchQuery = store.filters.search.toLowerCase().trim();
+
+			// Get all items and filter them locally
+			const allItems = Array.from(store.data.values());
+
+			// Filter by current filters (excluding search and page)
+			let filtered = allItems.filter(item => {
+				// Apply all filters except search and page
+				for (const [key, value] of Object.entries(store.filters)) {
+					if (key === 'search' || key === 'page') continue;
+
+					if (value !== null && value !== undefined && value !== '') {
+						if (item[key] !== value) return false;
+					}
+				}
+				return true;
+			});
+
+			// Apply search filter to common searchable fields
+			filtered = filtered.filter(item => {
+				// Search in common fields: name, title, path, description
+				const searchableFields = ['name', 'title', 'path', 'description', 'slug'];
+
+				return searchableFields.some(field => {
+					const value = item[field];
+					if (!value) return false;
+					return value.toLowerCase().includes(searchQuery);
+				});
+			});
+
+			return filtered;
+		}
+
+		// Fallback to all data
 		return this.getAll(name);
 	}
 
@@ -941,6 +979,7 @@
 				}
 			});
 		}
+		const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters);
 
 		this.notify(name, 'filters-changed', {
 			oldFilters,
@@ -948,11 +987,93 @@
 			updates
 		});
 
-		if (store.config.endpoint) {
+		if (store.config.endpoint && shouldFetch) {
 			await this.fetch(name);
+		} else if (store.config.endpoint) {
+			this.notify(name, 'data-loaded');
 		}
 	}
 
+	/**
+	 * Determine if we need to fetch or can use local data
+	 * @param {string} name - Store name
+	 * @param {object} updates - Filter updates being applied
+	 * @param {object} oldFilters - Previous filter state
+	 * @returns {Promise<boolean>} - True if fetch is needed, false if local filtering suffices
+	 */
+	async shouldFetchWithFilters(name, updates, oldFilters) {
+		const store = this.stores.get(name);
+
+		// If no endpoint or no lastResponse, always fetch
+		if (!store.config.endpoint || !store.lastResponse) {
+			return true;
+		}
+
+		// PAGE OPTIMIZATION: Don't fetch if trying to go beyond available pages
+		if ('page' in updates) {
+			const newPage = updates.page;
+			const oldPage = oldFilters.page || 1;
+
+			// If trying to go to a higher page but no more data available
+			if (newPage > oldPage && !store.lastResponse.has_more) {
+				// Reset page to last valid page
+				store.filters.page = oldPage;
+				return false;
+			}
+		}
+
+		// SEARCH OPTIMIZATION: Check if we need to fetch for search
+		if ('search' in updates) {
+			const searchQuery = updates.search?.trim() || '';
+			const oldSearch = oldFilters.search?.trim() || '';
+
+			// If search is being cleared, we might already have the data
+			if (!searchQuery && oldSearch) {
+				// Check if we have all base data (without search)
+				const baseFilters = { ...store.filters };
+				delete baseFilters.search;
+				baseFilters.page = 1;
+
+				// If we have complete base data, no need to fetch
+				if (this.hasCompleteData(store, baseFilters)) {
+					return false;
+				}
+			}
+
+			// If search is new or changed, check if we have all data to filter locally
+			if (searchQuery && searchQuery !== oldSearch) {
+				// Check: do we have all data for base filters (no search, page 1)?
+				const baseFilters = { ...store.filters };
+				delete baseFilters.search;
+				baseFilters.page = 1;
+
+				// If we have complete base data, we can filter locally
+				if (this.hasCompleteData(store, baseFilters)) {
+					return false;
+				}
+			}
+		}
+
+		// Default: fetch is needed
+		return true;
+	}
+
+	/**
+	 * Check if we have complete data for given filters
+	 * @param {object} store - Store instance
+	 * @param {object} filters - Filters to check
+	 * @returns {boolean} - True if we have all data
+	 */
+	hasCompleteData(store, filters) {
+		const cacheKey = this.generateCacheKey(filters);
+		const cached = store.cache.get(cacheKey);
+
+		if (!cached) return false;
+
+		// Check if cache indicates no more data
+		return cached.has_more === false || store.lastResponse?.has_more === false;
+	}
+
 	setFilter(name, key, value) {
 		return this.updateFilters(name, { [key]: value });
 	}

--
Gitblit v1.10.0