From 58e8ae0759ccfa97c478ccae4e0778bdce70966f Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 22 Jan 2026 22:40:02 +0000
Subject: [PATCH] =DirectoryManager.php updates, some javascript tweaks for CRUD.js, and minor style changes
---
assets/js/concise/DataStore.js | 185 +++++++++++++++++++++++++++++++++++++++------
1 files changed, 159 insertions(+), 26 deletions(-)
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 16c17ad..58ec874 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -45,7 +45,7 @@
* @param {object|array} configs An object defining the store, or an array of objects defining the stores
* @param {number} version the database version
*/
- register(name, configs = [], version = 1.1) {
+ register(name, configs = [], version = 1.2) {
if (!Array.isArray(configs)) configs = [configs];
if (configs.length === 0) return;
@@ -144,8 +144,11 @@
// Data methods
fetch: () => this.fetch(name),
save: (item) => this.save(name, item),
+ saveMany: (items) => this.saveMany(name, items),
delete: (id) => this.delete(name, id),
+ deleteMany: (items) => this.deleteMany(name, items),
get: (id) => this.get(name, id),
+ getMany: (ids) => this.getMany(name, ids),
getAll: () => this.getAll(name),
getAllByIndex: (indexName, value) => this.getAllByIndex(name, indexName, value),
filterByIndex: (criteria) => this.filterByIndex(name, criteria),
@@ -468,13 +471,16 @@
let result;
tx.oncomplete = () => resolve(result);
- tx.onerror = () => reject(tx.error);
+ tx.onerror = () => {
+ const error = tx.error || new Error('Transaction failed with unknown error');
+ reject(error);
+ };
// Call callback immediately to queue operations
try {
result = callback(objectStore, tx);
} catch (error) {
- reject(error);
+ reject(error || new Error('Callback failed with unknown error'));
}
});
}
@@ -586,11 +592,13 @@
return data;
} catch (error) {
- if (error.name !== 'AbortError') {
+ const isAbortError = error?.name === 'AbortError';
+
+ if (!isAbortError) {
console.error(`Fetch error for store "${name}":`, error);
this.notify(name, 'fetch-error', { error });
+ throw error;
}
- throw error;
} finally {
store.isFetching = false;
@@ -625,8 +633,8 @@
*/
async processFetchedData(name, data, cacheKey, response) {
const store = this.stores.get(name);
- const items = data.items || [];
- const changes = []; // Track all changes
+ const items = (data.items || []).filter(item => item && typeof item === 'object');
+ const changes = [];
// Batch process with single transaction
if (store.db && items.length > 0) {
@@ -784,8 +792,57 @@
return changeInfo.key;
}
+ /**
+ * Save multiple items in a single transaction (batch write)
+ * @param {string} name - Store name
+ * @param {Array|Map} items - Array of items or Map of items to save
+ * @returns {Promise<Array>} - Array of saved keys
+ */
+ async saveMany(name, items) {
+ const store = this.stores.get(name);
+ if (!store) return [];
+
+ // Convert Map to array if needed
+ const itemArray = items instanceof Map
+ ? Array.from(items.values())
+ : Array.isArray(items) ? items : Object.values(items);
+
+ if (itemArray.length === 0) return [];
+
+ const changes = [];
+
+ // Process all items and update in-memory store
+ itemArray.forEach(item => {
+ const changeInfo = this._saveItem(name, item);
+ changes.push(changeInfo);
+ });
+
+ // Single transaction for all writes
+ await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+ changes.forEach(changeInfo => {
+ objectStore.put(changeInfo.processed);
+ });
+ });
+
+ // Notify once for batch
+ this.notify(name, 'items-saved', {
+ count: changes.length,
+ keys: changes.map(c => c.key)
+ });
+
+ return changes.map(c => c.key);
+ }
+
processForStorage(obj, validate = true, path = 'root') {
- if (obj === null || obj === undefined) return { valid: true, data: obj };
+ if (obj === null) {
+ return { valid: true, data: null };
+ }
+ if (obj === undefined) {
+ if (validate) {
+ return { valid: false, error: `Undefined value at ${path}` };
+ }
+ return { valid: true, data: undefined };
+ }
const type = typeof obj;
@@ -846,7 +903,10 @@
for (const [key, value] of Object.entries(obj)) {
const result = this.processForStorage(value, validate, `${path}.${key}`);
if (!result.valid) return result;
- if (result.data !== undefined) processed[key] = result.data;
+ // Include null values, skip undefined
+ if (result.data !== undefined || value === null) {
+ processed[key] = result.data;
+ }
}
return { valid: true, data: processed };
}
@@ -871,11 +931,78 @@
this.notify(name, 'item-deleted', { id });
}
+ /**
+ * Delete multiple items in a single transaction (batch delete)
+ * @param {string} name - Store name
+ * @param {Array|Set} ids - Array or Set of IDs to delete
+ * @returns {Promise<Array>} - Array of deleted IDs
+ */
+ async deleteMany(name, ids) {
+ const store = this.stores.get(name);
+ if (!store) return [];
+
+ // Convert Set to array if needed
+ const idArray = ids instanceof Set
+ ? Array.from(ids)
+ : Array.isArray(ids) ? ids : Object.keys(ids);
+
+ if (idArray.length === 0) return [];
+
+ // Update in-memory store
+ idArray.forEach(id => {
+ store.data.delete(id);
+ });
+
+ // Single transaction for all deletes
+ await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+ idArray.forEach(id => {
+ objectStore.delete(id);
+ });
+ });
+
+ // Notify once for batch
+ this.notify(name, 'items-deleted', {
+ count: idArray.length,
+ ids: idArray
+ });
+
+ return idArray;
+ }
+
get(name, id) {
const store = this.stores.get(name);
return store.data.get(id);
}
+ /**
+ * Get multiple items by IDs in a single call
+ * @param {string} name - Store name
+ * @param {Array|Set} ids - Array or Set of IDs to retrieve
+ * @param {boolean} skipMissing - If true, omit missing items; if false, include null for missing
+ * @returns {Array} - Array of items (in same order as IDs)
+ */
+ getMany(name, ids, skipMissing = true) {
+ const store = this.stores.get(name);
+ if (!store) return [];
+
+ const idArray = ids instanceof Set
+ ? Array.from(ids)
+ : Array.isArray(ids) ? ids : Object.keys(ids);
+
+ if (idArray.length === 0) return [];
+
+ if (skipMissing) {
+ return idArray.reduce((acc, id) => {
+ const item = store.data.get(id);
+ if (item) acc.push(item);
+ return acc;
+ }, []);
+ }
+
+ // Preserve order, include null for missing
+ return idArray.map(id => store.data.get(id) ?? null);
+ }
+
getAll(name) {
const store = this.stores.get(name);
return Array.from(store.data.values());
@@ -894,6 +1021,7 @@
if (!store) return [];
return Array.from(store.data.values()).filter(item => {
+ if (!item || typeof item !== 'object') return false;
return Object.entries(criteria).every(([key, value]) => {
const accepted = Array.isArray(value) ? value : [value];
return accepted.includes(item[key]);
@@ -959,27 +1087,30 @@
const allItems = Array.from(store.data.values());
const searchQuery = store.filters.search?.toLowerCase().trim() || '';
+ const filterPredicates = [];
+ 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());
+ filterPredicates.push(item => accepted.includes(String(item[key])));
+ continue;
+ }
+
+ filterPredicates.push(item => String(item[key]) === String(value));
+ }
+
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 all non-search filters
+ for (const predicate of filterPredicates) {
+ if (!predicate(item)) return false;
}
// Apply search if present
return !(searchQuery && !this.searchObject(item, searchQuery));
-
-
});
return this.applyOrdering(filtered, store);
@@ -1021,7 +1152,9 @@
}
searchObject(obj, search) {
- if (!obj || typeof obj !== 'object') return false;
+ if (!obj || typeof obj !== 'object') {
+ return typeof obj === 'string' && obj.toLowerCase().includes(search);
+ }
for (const value of Object.values(obj)) {
if (value === null || value === undefined) continue;
--
Gitblit v1.10.0