Jake Vanderwerf
2026-01-06 2c955cebb5f1e01fbdb866b50d296fe9fbd852b8
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 });
   }