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 | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 201 insertions(+), 20 deletions(-)
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index d4cb8bc..fd5d3cb 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -6,6 +6,7 @@
* this.store = window.jvbStore.register('feed', { config });
*/
class DataStore {
+
constructor() {
// Singleton pattern
if (DataStore.instance) {
@@ -140,6 +141,8 @@
delete: (id) => this.delete(name, id),
get: (id) => this.get(name, id),
getAll: () => this.getAll(name),
+ getAllByIndex: (indexName, value) => this.getAllByIndex(name, indexName, value),
+ filterByIndex: (criteria) => this.filterByIndex(name, criteria),
getFiltered: () => this.getFiltered(name),
clear: () => this.clear(name),
@@ -645,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);
@@ -747,30 +751,33 @@
// Reject functions
if (type === 'function') {
- return validate ? { valid: false, error: `Function at ${path}` } : { valid: true, data: null };
+ if (validate) return { valid: false, error: `Function at ${path}` };
+ console.debug(`[DataStore] Stripped function at ${path}`);
+ return { valid: true, data: undefined };
}
// DOM elements
if (obj instanceof HTMLElement || obj.nodeType !== undefined) {
- return validate ? { valid: false, error: `DOM element at ${path}` } : { valid: true, data: null };
+ if (validate) return { valid: false, error: `DOM element at ${path}` };
+ console.debug(`[DataStore] Stripped DOM element at ${path}`);
+ return { valid: true, data: undefined };
}
// FormData - convert and continue
if (obj instanceof FormData) {
- return validate
- ? { valid: false, error: `FormData at ${path}` }
- : { valid: true, data: this.formDataToObject(obj) };
+ if (validate) return { valid: false, error: `FormData at ${path}` };
+ console.debug(`[DataStore] Converted FormData at ${path}`);
+ return { valid: true, data: this.formDataToObject(obj) };
}
// Preserve safe types
- if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
+ if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj) || obj instanceof Blob) {
return { valid: true, data: obj };
}
// Convert Sets to Arrays
if (obj instanceof Set) {
- const arr = Array.from(obj);
- return this.processForStorage(arr, validate, path);
+ return this.processForStorage(Array.from(obj), validate, path);
}
// Convert Maps to Objects
@@ -784,7 +791,7 @@
for (let i = 0; i < obj.length; i++) {
const result = this.processForStorage(obj[i], validate, `${path}[${i}]`);
if (!result.valid) return result;
- if (result.data !== null) processed.push(result.data);
+ if (result.data !== undefined) processed.push(result.data);
}
return { valid: true, data: processed };
}
@@ -795,14 +802,14 @@
for (const [key, value] of Object.entries(obj)) {
const result = this.processForStorage(value, validate, `${path}.${key}`);
if (!result.valid) return result;
- if (result.data !== null) processed[key] = result.data;
+ if (result.data !== undefined) processed[key] = result.data;
}
return { valid: true, data: processed };
}
- return validate
- ? { valid: false, error: `Unknown type at ${path}` }
- : { valid: true, data: null };
+ if (validate) return { valid: false, error: `Unknown type at ${path}` };
+ console.debug(`[DataStore] Stripped unknown type at ${path}`);
+ return { valid: true, data: undefined };
}
/***********************************************************************
@@ -829,12 +836,71 @@
const store = this.stores.get(name);
return Array.from(store.data.values());
}
+ /**
+ * Filter in-memory data by multiple index/value pairs
+ * @param {string} name - Store name
+ * @param {Object} criteria - Object of { indexName: acceptedValue(s) }
+ * @returns {Array} - Items matching ALL criteria
+ *
+ * @example
+ * filterByIndex(name, { field: 'upload_123', status: ['queued', 'uploading'] })
+ */
+ filterByIndex(name, criteria) {
+ const store = this.stores.get(name);
+ if (!store) return [];
+
+ return Array.from(store.data.values()).filter(item => {
+ return Object.entries(criteria).every(([key, value]) => {
+ const accepted = Array.isArray(value) ? value : [value];
+ return accepted.includes(item[key]);
+ });
+ });
+ }
+ /**
+ * Get all items matching an index value
+ * @param {string} name - Store name
+ * @param {string} indexName - Name of the index to query
+ * @param {*} value - Value to match
+ * @returns {Promise<Array>} - Matching items
+ */
+ async getAllByIndex(name, indexName, value) {
+ const store = this.stores.get(name);
+ const values = Array.isArray(value) ? value : [value];
+
+ // Try IndexedDB index query first (more efficient for large datasets)
+ if (store.db && store.db.objectStoreNames.contains(store.config.storeName)) {
+ try {
+ const tx = store.db.transaction([store.config.storeName], 'readonly');
+ const objectStore = tx.objectStore(store.config.storeName);
+
+ if (objectStore.indexNames.contains(indexName)) {
+ const index = objectStore.index(indexName);
+
+ const results = await Promise.all(
+ values.map(v => new Promise((resolve, reject) => {
+ const request = index.getAll(v);
+ request.onsuccess = () => resolve(request.result || []);
+ request.onerror = () => reject(request.error);
+ }))
+ );
+
+ return results.flat();
+ }
+ } catch (error) {
+ console.warn(`Index query failed for "${indexName}", falling back to filter:`, error);
+ }
+ }
+
+ // Fallback: filter in-memory data
+ return Array.from(store.data.values()).filter(item => values.includes(item[indexName]));
+ }
getFiltered(name) {
const store = this.stores.get(name);
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);
@@ -843,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);
}
@@ -859,12 +961,8 @@
}
/***********************************************************************
- * FILTER OPERATIONS (UNIFIED)
+ * FILTER OPERATIONS
***********************************************************************/
-
- /**
- * Unified filter update - handles all filter operations
- */
async updateFilters(name, updates, clearAll = false) {
const store = this.stores.get(name);
const oldFilters = { ...store.filters };
@@ -881,6 +979,7 @@
}
});
}
+ const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters);
this.notify(name, 'filters-changed', {
oldFilters,
@@ -888,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