From 181938ea32cf2c11d10d56018df22586adff7860 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 11 Jan 2026 17:07:14 +0000
Subject: [PATCH] =CRUD.js cleanup, DataStore.js filtering optimization
---
assets/js/concise/DataStore.js | 273 ++++++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 250 insertions(+), 23 deletions(-)
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 95027e3..00211e2 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -82,6 +82,7 @@
endpoint: null,
apiBase: jvbSettings.api,
filters: {},
+ ignore: [], //any filters to ignore when filtering store locally
required: null,
// Cache
@@ -105,6 +106,11 @@
_initialized: false
};
+ store.ignoreFilters = new Set([
+ ... ['search', 'page', 'per_page', 'orderby', 'order'],
+ ... store.config.ignore
+ ]);
+
store.config.headers = {
'X-WP-Nonce': window.auth.getNonce(),
...store.config.headers
@@ -648,7 +654,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);
@@ -669,6 +676,12 @@
queue_stats: data.queue_stats || {}
};
+ for (let [key, value] of Object.entries(store.filters)) {
+ if (typeof value === 'string' && value.includes(',')) {
+ this.createSplitCacheEntries(name, items, key, store.filters, response);
+ }
+ }
+
// Emit events for items with status changes
changes.forEach(changeInfo => {
if (changeInfo.statusChanged) {
@@ -681,6 +694,39 @@
});
}
+ createSplitCacheEntries(name, items, key, filters, response) {
+ const store = this.stores.get(name);
+ const keys = filters[key].split(',').map(v => v.trim());
+
+ keys.forEach(value => {
+ let temp = {};
+ temp[key] = value;
+ const newFilters = {
+ ... filters,
+ [key]: value
+ };
+ const cacheKey = this.generateCacheKey(newFilters);
+ if(store.cache.has(cacheKey)) return;
+ let filteredItems = this.filterByIndex(name,temp).map(item => this.getItemKey(item, store.config.keyPath));
+
+ const entry = {
+ key: cacheKey,
+ items: filteredItems,
+ timestamp: Date.now(),
+ endpoint: store.config.endpoint,
+ filters: newFilters,
+ etag: response.headers.get('Etag'),
+ lastModified: response.headers.get('Last-Modified'),
+ has_more: filteredItems.length === 20,
+ }
+ store.cache.set(cacheKey, entry);
+ if (store.db?.objectStoreNames.contains('cache')) {
+ this.withTransaction(name, 'cache', 'readwrite', (objectStore) =>{
+ objectStore.put(entry);
+ });
+ }
+ })
+ }
/***********************************************************************
* SAVE OPERATIONS
***********************************************************************/
@@ -751,21 +797,20 @@
// Reject functions
if (type === 'function') {
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) {
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) {
- if (validate) return { valid: false, error: `FormData at ${path}` };
- console.debug(`[DataStore] Converted FormData at ${path}`);
+
return { valid: true, data: this.formDataToObject(obj) };
}
@@ -807,7 +852,7 @@
}
if (validate) return { valid: false, error: `Unknown type at ${path}` };
- console.debug(`[DataStore] Stripped unknown type at ${path}`);
+
return { valid: true, data: undefined };
}
@@ -899,15 +944,98 @@
const cacheKey = this.generateCacheKey(store.filters);
const cacheEntry = store.cache.get(cacheKey);
- if (cacheEntry && cacheEntry.items) {
- return cacheEntry.items.reduce((acc, id) => {
- const item = store.data.get(id);
- if (item) acc.push(item);
- return acc;
- }, []);
+ // First check if we have cached results for exact filters
+ if (cacheEntry?.items) {
+ return this.applyOrdering(
+ cacheEntry.items.reduce((acc, id) => {
+ const item = store.data.get(id);
+ if (item) acc.push(item);
+ return acc;
+ }, []),
+ store
+ );
}
- return this.getAll(name);
+ const allItems = Array.from(store.data.values());
+ const searchQuery = store.filters.search?.toLowerCase().trim() || '';
+
+ const filtered = allItems.filter(item => {
+ // Apply all non-ignored filters
+ for (const [key, value] of Object.entries(store.filters)) {
+ if (store.ignoreFilters.has(key)) continue;
+ if (value === null || value === undefined || value === '') continue;
+ if (value === 'all') continue;
+
+ // Comma-separated values
+ if (typeof value === 'string' && value.includes(',')) {
+ const accepted = value.split(',').map(v => v.trim());
+ if (!accepted.includes(String(item[key]))) return false;
+ continue;
+ }
+
+ if (String(item[key]) !== String(value)) return false;
+ }
+
+ // Apply search if present
+ return !(searchQuery && !this.searchObject(item, searchQuery));
+
+
+ });
+
+ return this.applyOrdering(filtered, store);
+ }
+
+ applyOrdering(items, store) {
+ if (!Array.isArray(items)) items = Array.from(items);
+ if (items.length === 0) return items;
+
+ if (store.filters.orderby || store.filters.order) {
+ const orderby = store.filters.orderby || 'date';
+ const order = (store.filters.order || 'desc').toLowerCase();
+
+ items.sort((a, b) => {
+ let aVal, bVal;
+
+ switch (orderby) {
+ case 'alphabetical':
+ case 'title':
+ aVal = (a.fields?.post_title || a.title || a.name || '').toLowerCase();
+ bVal = (b.fields?.post_title || b.title || b.name || '').toLowerCase();
+ break;
+ case 'modified':
+ aVal = new Date(a.modified || 0);
+ bVal = new Date(b.modified || 0);
+ break;
+ case 'date':
+ default:
+ aVal = new Date(a.date || 0);
+ bVal = new Date(b.date || 0);
+ }
+
+ if (aVal < bVal) return order === 'asc' ? -1 : 1;
+ if (aVal > bVal) return order === 'asc' ? 1 : -1;
+ return 0;
+ });
+ }
+ return items;
+ }
+
+ searchObject(obj, search) {
+ if (!obj || typeof obj !== 'object') return false;
+
+ for (const value of Object.values(obj)) {
+ if (value === null || value === undefined) continue;
+
+ if (typeof value === 'object') {
+ if (this.searchObject(value, search)) return true;
+ continue;
+ }
+
+ if (typeof value === 'string' && value.toLowerCase().includes(search)) {
+ return true;
+ }
+ }
+ return false;
}
async clear(name) {
@@ -931,28 +1059,125 @@
if (clearAll) {
store.filters = { ...store.config.filters };
- } else {
- // Apply updates (null/undefined/'' = delete)
- Object.entries(updates).forEach(([key, value]) => {
- if (value === null || value === undefined || value === '') {
- delete store.filters[key];
- } else {
- store.filters[key] = value;
- }
- });
}
+ Object.entries(updates).forEach(([key, value]) => {
+ if (value === null || value === undefined || value === '') {
+ delete store.filters[key];
+ } else {
+ store.filters[key] = value;
+ }
+ });
+
this.notify(name, 'filters-changed', {
oldFilters,
filters: store.filters,
updates
});
- if (store.config.endpoint) {
+ this.notify(name, 'data-loaded', {
+ cached: true,
+ items: this.getFiltered(name)
+ });
+
+ const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters);
+ 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;
+ }
+
+ if (store.lastResponse.has_more === false) {
+ // Check if new filters are a subset of what we have
+ const isSubsetFilter = Object.entries(updates).every(([key, value]) => {
+ if (store.ignoreFilters.has(key)) return true;
+ if (key === 'page') return true; // Handle pagination locally
+ return true; // We have all data, can filter locally
+ });
+
+ if (isSubsetFilter) return false;
+ }
+
+ 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 });
}
@@ -962,6 +1187,8 @@
const hasChanges = Object.keys(filters).some(
key => store.filters[key] !== filters[key]
+ ) || Object.keys(store.filters).some(
+ key => !(key in filters) && filters !== store.config.filters
);
if (!hasChanges) return;
--
Gitblit v1.10.0