table optimization (filtering, resizing and settings saved in user settings

This commit is contained in:
2026-08-12 23:14:21 +02:00
parent e0175ab48a
commit 0fb18ae4f7
24 changed files with 991 additions and 189 deletions
+156 -25
View File
@@ -1,24 +1,165 @@
(() => {
const perf = window.AssetPerf;
perf?.start('browser-state:gesamtes Modul');
const persistent = document.body?.dataset.persistBrowserState === 'true';
const storage = persistent ? window.localStorage : window.sessionStorage;
const fallbackStorage = persistent ? window.localStorage : window.sessionStorage;
const alternateStorage = persistent ? window.sessionStorage : window.localStorage;
const prefix = 'assetmanager:';
let serverBacked = false;
let serverState = {};
let pendingUpdates = {};
let pendingRemovals = new Set();
let saveTimer = null;
let saveChain = Promise.resolve();
function browserGet(storage, key) {
try { return storage.getItem(prefix + key); } catch (_) { return null; }
}
function browserSet(storage, key, value) {
try { storage.setItem(prefix + key, value); } catch (_) {}
}
function browserRemove(storage, key) {
try { storage.removeItem(prefix + key); } catch (_) {}
}
function collectBrowserState() {
const result = {};
const collect = storage => {
try {
for (let index = 0; index < storage.length; index += 1) {
const fullKey = storage.key(index);
if (!fullKey || !fullKey.startsWith(prefix)) continue;
const key = fullKey.slice(prefix.length);
if (!key || Object.prototype.hasOwnProperty.call(result, key)) continue;
const value = storage.getItem(fullKey);
if (value !== null) result[key] = value;
}
} catch (_) {}
};
// Prefer the storage mode that was active for this user before the update.
collect(fallbackStorage);
collect(alternateStorage);
return result;
}
async function postState(updates = {}, removals = []) {
const response = await fetch('/api/user-ui-state', {
method: 'POST',
credentials: 'same-origin',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({updates, remove: removals}),
keepalive: true,
});
if (!response.ok) throw new Error(`UI state save failed (${response.status})`);
}
function flushPending() {
if (!serverBacked) return Promise.resolve();
if (saveTimer !== null) {
clearTimeout(saveTimer);
saveTimer = null;
}
const updates = pendingUpdates;
const removals = [...pendingRemovals];
pendingUpdates = {};
pendingRemovals = new Set();
if (!Object.keys(updates).length && !removals.length) return saveChain;
saveChain = saveChain
.catch(() => {})
.then(() => postState(updates, removals))
.catch(error => console.warn('[AssetManager] Benutzerbezogener Tabellenzustand konnte nicht gespeichert werden.', error));
return saveChain;
}
function scheduleSave() {
if (!serverBacked) return;
if (saveTimer !== null) clearTimeout(saveTimer);
saveTimer = window.setTimeout(flushPending, 350);
}
const api = {
persistent,
serverBacked: false,
ready: Promise.resolve(),
get(key) {
try { return storage.getItem(prefix + key); } catch (_) { return null; }
const normalized = String(key || '');
if (!normalized) return null;
if (serverBacked) {
const value = serverState[normalized];
return value === undefined || value === null ? null : String(value);
}
return browserGet(fallbackStorage, normalized);
},
set(key, value) {
try { storage.setItem(prefix + key, value); } catch (_) {}
const normalized = String(key || '');
if (!normalized) return;
const serialized = String(value);
if (serverBacked) {
serverState[normalized] = serialized;
pendingUpdates[normalized] = serialized;
pendingRemovals.delete(normalized);
scheduleSave();
return;
}
browserSet(fallbackStorage, normalized, serialized);
},
remove(key) {
try { storage.removeItem(prefix + key); } catch (_) {}
}
const normalized = String(key || '');
if (!normalized) return;
if (serverBacked) {
delete serverState[normalized];
delete pendingUpdates[normalized];
pendingRemovals.add(normalized);
scheduleSave();
return;
}
browserRemove(fallbackStorage, normalized);
},
flush: flushPending,
};
window.AssetBrowserState = api;
async function loadUserState() {
try {
const response = await fetch('/api/user-ui-state', {credentials: 'same-origin', cache: 'no-store'});
if (!response.ok) return;
const payload = await response.json();
if (!payload?.authenticated) return;
serverBacked = true;
api.serverBacked = true;
serverState = payload.state && typeof payload.state === 'object' && !Array.isArray(payload.state)
? {...payload.state}
: {};
// One-time/non-destructive migration: existing values from this browser are
// copied only when the account does not already have a value for the key.
const browserState = collectBrowserState();
const migration = {};
Object.entries(browserState).forEach(([key, value]) => {
if (!Object.prototype.hasOwnProperty.call(serverState, key)) {
serverState[key] = value;
migration[key] = value;
}
});
if (Object.keys(migration).length) {
try { await postState(migration, []); }
catch (error) { console.warn('[AssetManager] Vorhandener Browserzustand konnte nicht in das Benutzerkonto übernommen werden.', error); }
}
} catch (error) {
// Browser storage remains the safe fallback if the account-state endpoint
// is temporarily unavailable.
console.warn('[AssetManager] Benutzerbezogener Tabellenzustand nicht verfügbar; Browser-Fallback wird verwendet.', error);
}
}
api.ready = loadUserState();
function tableKey(table, index) {
return table.dataset.storageKey || table.id || `table-${location.pathname}-${index}`;
}
@@ -58,11 +199,6 @@
function restoreTable(table, index) {
if (table.dataset.browserStateReady === '1') return;
if (table.dataset.serverFilters === '1') {
table.dataset.browserStateReady = '1';
restoreSort(table, index);
return;
}
const restoreStarted = performance.now();
const controls = filterControls(table);
if (!controls.length) {
@@ -76,10 +212,11 @@
let values = {};
try { values = JSON.parse(api.get(key) || '{}') || {}; } catch (_) {}
let serverValuesChanged = false;
controls.forEach((control, controlIndex) => {
const field = controlKey(control, controlIndex);
if (Object.prototype.hasOwnProperty.call(values, field)) {
// Restore without dispatching one input/change pair per column.
if (control.value !== values[field]) serverValuesChanged = true;
control.value = values[field];
}
const save = () => {
@@ -93,9 +230,9 @@
control.addEventListener('change', save);
});
// One filtering pass after every stored value is in place.
if (typeof table.assetApplyFilters === 'function') {
table.assetApplyFilters();
if (typeof table.assetApplyFilters === 'function') table.assetApplyFilters();
if (table.dataset.serverFilters === '1' && serverValuesChanged && typeof table.assetSubmitServerFilters === 'function') {
table.assetSubmitServerFilters(true);
}
restoreSort(table, index);
table.dataset.browserStateRestored = '1';
@@ -109,7 +246,8 @@
});
}
function initializeTables() {
async function initializeTables() {
await api.ready;
document.querySelectorAll('table').forEach(restoreTable);
}
@@ -119,21 +257,14 @@
initializeTables();
}
// Filter controls are added by table-tools.js. Initialize only on its explicit
// completion event instead of observing every DOM mutation in the document.
window.addEventListener('asset-table-structure-ready', initializeTables);
// A programmatic reset does not emit input/change events. Remove the
// corresponding persisted filter state explicitly so it cannot return after
// a page refresh.
window.addEventListener('asset-table-filters-cleared', event => {
const table = event.detail?.table;
if (!(table instanceof HTMLTableElement)) return;
const tables = [...document.querySelectorAll('table')];
const index = tables.indexOf(table);
const key = `${tableKey(table, index >= 0 ? index : 0)}:filters`;
api.remove(key);
api.remove(`${tableKey(table, index >= 0 ? index : 0)}:filters`);
});
window.addEventListener('asset-table-sort-changed', event => {
@@ -141,8 +272,7 @@
if (!(table instanceof HTMLTableElement)) return;
const tables = [...document.querySelectorAll('table')];
const index = tables.indexOf(table);
const key = `${tableKey(table, index >= 0 ? index : 0)}:sort`;
api.set(key, JSON.stringify({
api.set(`${tableKey(table, index >= 0 ? index : 0)}:sort`, JSON.stringify({
field: event.detail?.field || '',
direction: event.detail?.direction || 'asc'
}));
@@ -156,5 +286,6 @@
restoreSort(table, index >= 0 ? index : 0, true);
});
window.addEventListener('pagehide', () => { flushPending(); });
perf?.end('browser-state:gesamtes Modul');
})();