feat(async-table): history api

Fixes #426
This commit is contained in:
Gregor Kleen 2020-05-05 15:09:33 +02:00
parent 9e47a7cd28
commit c348b7cb03
6 changed files with 215 additions and 61 deletions

View File

@ -9,9 +9,10 @@ export const LOCATION = {
LOCAL: 'local', LOCAL: 'local',
SESSION: 'session', SESSION: 'session',
WINDOW: 'window', WINDOW: 'window',
HISTORY: 'history',
}; };
const LOCATION_SHADOWING = [ LOCATION.WINDOW, LOCATION.SESSION, LOCATION.LOCAL ]; const LOCATION_SHADOWING = [ LOCATION.HISTORY, LOCATION.WINDOW, LOCATION.SESSION, LOCATION.LOCAL ];
export class StorageManager { export class StorageManager {
@ -26,7 +27,16 @@ export class StorageManager {
constructor(namespace, version, options) { constructor(namespace, version, options) {
this._debugLog('constructor', namespace, version, options); this._debugLog('constructor', namespace, version, options);
this.namespace = namespace; if (typeof namespace === 'object') {
let sep = '_';
const namespace_arr = Array.from(namespace);
while (namespace_arr.some(str => str.includes(sep)))
sep = sep + '_';
this.namespace = Array.from(namespace).join(sep);
} else {
this.namespace = namespace;
}
this.version = semver.valid(version); this.version = semver.valid(version);
if (!namespace) { if (!namespace) {
@ -48,7 +58,7 @@ export class StorageManager {
throw new Error('Cannot setup StorageManager without window or global'); throw new Error('Cannot setup StorageManager without window or global');
if (this._options.encryption) { if (this._options.encryption) {
[LOCATION.LOCAL, LOCATION.SESSION].forEach((location) => { [LOCATION.LOCAL, LOCATION.SESSION, LOCATION.HISTORY].forEach((location) => {
const encryption = this._options.encryption.all || this._options.encryption[location]; const encryption = this._options.encryption.all || this._options.encryption[location];
if (encryption) this._requestStorageKey({ location: location, encryption: encryption }); if (encryption) this._requestStorageKey({ location: location, encryption: encryption });
}); });
@ -70,17 +80,21 @@ export class StorageManager {
switch (location) { switch (location) {
case LOCATION.LOCAL: { case LOCATION.LOCAL: {
this._saveToLocalStorage(this._updateStorage(this._getFromLocalStorage(options), { [key]: value }, LOCATION.LOCAL, options)); this._saveToLocalStorage({ ...this._getFromLocalStorage(options), [key]: value}, options);
break; break;
} }
case LOCATION.SESSION: { case LOCATION.SESSION: {
this._saveToSessionStorage(this._updateStorage(this._getFromSessionStorage(options), { [key]: value }, LOCATION.SESSION, options)); this._saveToSessionStorage({ ...this._getFromSessionStorage(options), [key]: value}, options);
break; break;
} }
case LOCATION.WINDOW: { case LOCATION.WINDOW: {
this._saveToWindow({ ...this._getFromWindow(), [key]: value }); this._saveToWindow({ ...this._getFromWindow(), [key]: value });
break; break;
} }
case LOCATION.HISTORY: {
this._saveToHistory({ ...this._getFromHistory(), [key]: value }, options);
break;
}
default: default:
console.error('StorageManager.save cannot save item with unsupported location'); console.error('StorageManager.save cannot save item with unsupported location');
} }
@ -112,6 +126,10 @@ export class StorageManager {
val = this._getFromWindow()[key]; val = this._getFromWindow()[key];
break; break;
} }
case LOCATION.HISTORY: {
val = this._getFromHistory(options)[key];
break;
}
default: default:
console.error('StorageManager.load cannot load item with unsupported location'); console.error('StorageManager.load cannot load item with unsupported location');
} }
@ -138,14 +156,14 @@ export class StorageManager {
delete val[key]; delete val[key];
return this._saveToLocalStorage(val); return this._saveToLocalStorage(val, options);
} }
case LOCATION.SESSION: { case LOCATION.SESSION: {
let val = this._getFromSessionStorage(options); let val = this._getFromSessionStorage(options);
delete val[key]; delete val[key];
return this._saveToSessionStorage(val); return this._saveToSessionStorage(val, options);
} }
case LOCATION.WINDOW: { case LOCATION.WINDOW: {
let val = this._getFromWindow(); let val = this._getFromWindow();
@ -154,6 +172,14 @@ export class StorageManager {
return this._saveToWindow(val); return this._saveToWindow(val);
} }
case LOCATION.HISTORY: {
let val = this._getFromHistory(options);
delete val[key];
return this._saveToHistory(val, options);
}
default: default:
console.error('StorageManager.load cannot load item with unsupported location'); console.error('StorageManager.load cannot load item with unsupported location');
} }
@ -177,6 +203,8 @@ export class StorageManager {
return this._clearSessionStorage(); return this._clearSessionStorage();
case LOCATION.WINDOW: case LOCATION.WINDOW:
return this._clearWindow(); return this._clearWindow();
case LOCATION.HISTORY:
return this._clearHistory(options && options.history);
default: default:
console.error('StorageManager.clear cannot clear with unsupported location'); console.error('StorageManager.clear cannot clear with unsupported location');
} }
@ -185,7 +213,7 @@ export class StorageManager {
} }
_getFromLocalStorage(options=this._options) { _getFromLocalStorage(options) {
this._debugLog('_getFromLocalStorage', options); this._debugLog('_getFromLocalStorage', options);
let state; let state;
@ -210,7 +238,7 @@ export class StorageManager {
} }
} }
_saveToLocalStorage(state) { _saveToLocalStorage(state, options) {
this._debugLog('_saveToLocalStorage', state); this._debugLog('_saveToLocalStorage', state);
if (!state) if (!state)
@ -223,8 +251,8 @@ export class StorageManager {
} else { } else {
versionedState = { version: this.version, ...state }; versionedState = { version: this.version, ...state };
} }
window.localStorage.setItem(this.namespace, JSON.stringify(versionedState)); window.localStorage.setItem(this.namespace, JSON.stringify(this._updateStorage({}, versionedState, LOCATION.LOCAL, options)));
} }
_clearLocalStorage() { _clearLocalStorage() {
@ -240,10 +268,10 @@ export class StorageManager {
if (!this._global || !this._global.App) if (!this._global || !this._global.App)
return {}; return {};
if (!this._global.App.Storage) if (!this._global.App.Storage || !this._global.App.Storage[this.namespace])
this._global.App.Storage = {}; return {};
return this._global.App.Storage; return this._global.App.Storage[this.namespace];
} }
_saveToWindow(value) { _saveToWindow(value) {
@ -274,8 +302,71 @@ export class StorageManager {
} }
} }
_getFromHistory(options) {
this._debugLog('_getFromHistory');
_getFromSessionStorage(options=this._options) { if (!this._global || !this._global.history)
return {};
if (!this._global.history.state || !this._global.history.state[this.namespace])
return {};
return this._getFromStorage(this._global.history.state[this.namespace], LOCATION.HISTORY, options);
}
_saveToHistory(value, options) {
this._debugLog('_saveToHistory', options);
if (!this._global || !this._global.history) {
throw new Error('StorageManager._saveToHistory called when window.history is not available');
}
const push = (options.history && typeof options.history.push !== 'undefined') ? !!options.history.push : true;
const title = (options.history && options.history.title) || (this._global.document && this._global.document.title) || '';
const url = (options.history && options.history.url) || (this._global.document && this._global.document.location);
const state = this._global.history.state || {};
state[this.namespace] = this._updateStorage({}, value, LOCATION.HISTORY, options);
this._debugLog('_saveToHistory', { state: state, push: push, title: title, url: url});
if (push)
this._global.history.pushState(state, title, url);
else
this._global.history.replaceState(state, title, url);
}
_clearHistory(options) {
this._debugLog('_clearHistory', options);
if (!this._global || !this._global.history) {
throw new Error('StorageManager._clearHistory called when window.history is not available');
}
const push = (options.history && typeof options.history.push !== 'undefined' ? !!options.history.push : true) || true;
const title = (options.history && options.history.title) || (this._global.document && this._global.document.title) || '';
const url = (options.history && options.history.url) || (this._global.document && this._global.document.location);
const state = this._global.history.state || {};
delete state[this.namespace];
if (push)
this._global.history.pushState(state, title, url);
else
this._global.history.replaceState(state, title, url);
}
addHistoryListener(listener, options=this._options, ...args) {
const modified_listener = (function(event, ...listener_args) { // eslint-disable-line no-unused-vars
this._global.setTimeout(() => listener(this._getFromHistory(options), ...listener_args));
}).bind(this);
this._global.addEventListener('popstate', modified_listener, args);
}
_getFromSessionStorage(options) {
this._debugLog('_getFromSessionStorage', options); this._debugLog('_getFromSessionStorage', options);
let state; let state;
@ -300,7 +391,7 @@ export class StorageManager {
} }
} }
_saveToSessionStorage(state) { _saveToSessionStorage(state, options) {
this._debugLog('_saveToSessionStorage', state); this._debugLog('_saveToSessionStorage', state);
if (!state) if (!state)
@ -314,7 +405,7 @@ export class StorageManager {
versionedState = { version: this.version, ...state }; versionedState = { version: this.version, ...state };
} }
window.sessionStorage.setItem(this.namespace, JSON.stringify(versionedState)); window.sessionStorage.setItem(this.namespace, JSON.stringify(this._updateStorage({}, versionedState, LOCATION.SESSION, options)));
} }
_clearSessionStorage() { _clearSessionStorage() {
@ -324,10 +415,10 @@ export class StorageManager {
} }
_getFromStorage(storage, location, options=this._options) { _getFromStorage(storage, location, options) {
this._debugLog('_getFromStorage', storage, location, options); this._debugLog('_getFromStorage', storage, location, options);
const encryption = options.encryption && (options.encryption.all || options.encryption[location]); const encryption = options && options.encryption && (options.encryption.all || options.encryption[location]);
if (encryption && storage.encryption) { if (encryption && storage.encryption) {
return { ...storage, ...JSON.parse(decrypt(storage.encryption.ciphertext, this._encryptionKey[location]) || null) }; return { ...storage, ...JSON.parse(decrypt(storage.encryption.ciphertext, this._encryptionKey[location]) || null) };
} else { } else {
@ -335,10 +426,10 @@ export class StorageManager {
} }
} }
_updateStorage(storage, update, location, options=this._options) { _updateStorage(storage, update, location, options) {
this._debugLog('_updateStorage', storage, update, location, options); this._debugLog('_updateStorage', storage, update, location, options);
const encryption = options.encryption && (options.encryption.all || options.encryption[location]); const encryption = options && options.encryption && (options.encryption.all || options.encryption[location]);
if (encryption && storage.encryption) { if (encryption && storage.encryption) {
const updatedDecryptedStorage = { ...JSON.parse(decrypt(storage.encryption.ciphertext, this._encryptionKey[location]) || null), ...update }; const updatedDecryptedStorage = { ...JSON.parse(decrypt(storage.encryption.ciphertext, this._encryptionKey[location]) || null), ...update };
console.log('updatedDecryptedStorage', updatedDecryptedStorage); console.log('updatedDecryptedStorage', updatedDecryptedStorage);
@ -357,13 +448,13 @@ export class StorageManager {
const enc = this.load('encryption', { ...options, encryption: false }); const enc = this.load('encryption', { ...options, encryption: false });
const requestBody = { const requestBody = {
type : options.encryption, type : options.encryption,
length : 42, length : sodium.crypto_secretbox_KEYBYTES,
salt : enc.salt, salt : enc.salt,
timestamp : enc.timestamp, timestamp : enc.timestamp,
}; };
this._global.App.httpClient.post({ this._global.App.httpClient.post({
url: '../../../../../../user/storage-key', // TODO use APPROOT instead url: '/user/storage-key',
headers: { headers: {
'Content-Type' : HttpClient.ACCEPT.JSON, 'Content-Type' : HttpClient.ACCEPT.JSON,
'Accept' : HttpClient.ACCEPT.JSON, 'Accept' : HttpClient.ACCEPT.JSON,
@ -381,11 +472,10 @@ export class StorageManager {
}).catch(console.error); }).catch(console.error);
} }
_debugLog() { _debugLog() {}
// _debugLog(fName, ...args) { // _debugLog(fName, ...args) {
// console.log(`[DEBUGLOG] StorageManager.${fName}`, { args: args, instance: this }); // console.log(`[DEBUGLOG] StorageManager.${fName}`, { args: args, instance: this });
} // }
} }

View File

@ -17,7 +17,7 @@ export class HtmlHelpers {
idPrefix = this._getIdPrefix(); idPrefix = this._getIdPrefix();
this._prefixIds(element, idPrefix); this._prefixIds(element, idPrefix);
} }
return Promise.resolve({ idPrefix, element }); return Promise.resolve({ idPrefix, element, headers: response.headers });
}, },
Promise.reject, Promise.reject,
).catch(console.error); ).catch(console.error);

View File

@ -13,7 +13,9 @@ const INPUT_DEBOUNCE = 600;
const FILTER_DEBOUNCE = 100; const FILTER_DEBOUNCE = 100;
const HEADER_HEIGHT = 80; const HEADER_HEIGHT = 80;
const ASYNC_TABLE_LOCAL_STORAGE_KEY = 'ASYNC_TABLE'; const ASYNC_TABLE_STORAGE_KEY = 'ASYNC_TABLE';
const ASYNC_TABLE_STORAGE_VERSION = '2.0.0';
const ASYNC_TABLE_SCROLLTABLE_SELECTOR = '.scrolltable'; const ASYNC_TABLE_SCROLLTABLE_SELECTOR = '.scrolltable';
const ASYNC_TABLE_INITIALIZED_CLASS = 'async-table--initialized'; const ASYNC_TABLE_INITIALIZED_CLASS = 'async-table--initialized';
const ASYNC_TABLE_LOADING_CLASS = 'async-table--loading'; const ASYNC_TABLE_LOADING_CLASS = 'async-table--loading';
@ -31,6 +33,8 @@ export class AsyncTable {
_asyncTableHeader; _asyncTableHeader;
_asyncTableId; _asyncTableId;
_asyncTableIdent;
_ths = []; _ths = [];
_pageLinks = []; _pageLinks = [];
_pagesizeForm; _pagesizeForm;
@ -47,7 +51,10 @@ export class AsyncTable {
}; };
_ignoreRequest = false; _ignoreRequest = false;
_storageManager = new StorageManager(ASYNC_TABLE_LOCAL_STORAGE_KEY, '1.0.0', { location: LOCATION.WINDOW }); _windowStorage;
_historyStorage;
_active = true;
constructor(element, app) { constructor(element, app) {
if (!element) { if (!element) {
@ -79,21 +86,58 @@ export class AsyncTable {
this._cssIdPrefix = findCssIdPrefix(rawTableId); this._cssIdPrefix = findCssIdPrefix(rawTableId);
this._asyncTableId = rawTableId.replace(this._cssIdPrefix, ''); this._asyncTableId = rawTableId.replace(this._cssIdPrefix, '');
this._asyncTableIdent = this._asyncTableId.replace(/-table-wrapper$/, '');
if (!this._asyncTableIdent) {
throw new Error('Async Table cannot be set up without an ident!');
}
this._windowStorage = new StorageManager([ASYNC_TABLE_STORAGE_KEY, this._asyncTableIdent], ASYNC_TABLE_STORAGE_VERSION, { location: LOCATION.WINDOW });
this._historyStorage = new StorageManager([ASYNC_TABLE_STORAGE_KEY, this._asyncTableIdent], ASYNC_TABLE_STORAGE_VERSION, { location: LOCATION.HISTORY });
// find scrolltable wrapper // find scrolltable wrapper
this._scrollTable = this._element.querySelector(ASYNC_TABLE_SCROLLTABLE_SELECTOR); this._scrollTable = this._element.querySelector(ASYNC_TABLE_SCROLLTABLE_SELECTOR);
if (!this._scrollTable) { if (!this._scrollTable) {
throw new Error('Async Table cannot be set up without a scrolltable element!'); throw new Error('Async Table cannot be set up without a scrolltable element!');
} }
this._setupTableFilter();
this._processStorage(); this._processStorage();
// clear currentTableUrl from previous requests this._setupTableFilter();
this._storageManager.remove('currentTableUrl');
this._windowStorage.remove('currentTableUrl');
if (!('currentTableUrl' in this._element.dataset)) {
this._element.dataset['currentTableUrl'] = document.location.href;
this._historyStorage.save('currentTableUrl', document.location.href, { location: LOCATION.HISTORY, history: { push: false } });
}
this._historyListener();
this._historyStorage.addHistoryListener(this._historyListener.bind(this));
// mark initialized // mark initialized
this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS); if (this._active)
this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
}
_historyListener(historyState) {
if (!this._active)
return;
const windowUrl = this._element.dataset['currentTableUrl'];
const historyUrl = historyState ? historyState['currentTableUrl'] : this._historyStorage.load('currentTableUrl');
this._debugLog('_historyListener', historyState, windowUrl, historyUrl);
if (this._isEquivalentUrl(windowUrl, historyUrl))
return;
this._debugLog('_historyListener', historyUrl);
this._updateTableFrom(historyUrl || document.location.href, undefined, true);
}
_isEquivalentUrl(a, b) {
return a === b;
} }
start() { start() {
@ -113,7 +157,7 @@ export class AsyncTable {
this._ths.forEach((th) => { this._ths.forEach((th) => {
th.clickHandler = (event) => { th.clickHandler = (event) => {
this._storageManager.save('horizPos', (this._scrollTable || {}).scrollLeft); this._windowStorage.save('horizPos', (this._scrollTable || {}).scrollLeft);
this._linkClickHandler(event); this._linkClickHandler(event);
}; };
th.element.addEventListener('click', th.clickHandler); th.element.addEventListener('click', th.clickHandler);
@ -135,7 +179,7 @@ export class AsyncTable {
left: this._scrollTable.offsetLeft || 0, left: this._scrollTable.offsetLeft || 0,
behavior: 'smooth', behavior: 'smooth',
}; };
this._storageManager.save('scrollTo', scrollTo); this._windowStorage.save('scrollTo', scrollTo);
} }
this._linkClickHandler(event); this._linkClickHandler(event);
}; };
@ -256,7 +300,7 @@ export class AsyncTable {
const prefix = findCssIdPrefix(focusedInput.id); const prefix = findCssIdPrefix(focusedInput.id);
const focusId = focusedInput.id.replace(prefix, ''); const focusId = focusedInput.id.replace(prefix, '');
callback = function(wrapper) { callback = function(wrapper) {
const idPrefix = this._storageManager.load('cssIdPrefix'); const idPrefix = this._windowStorage.load('cssIdPrefix');
const toBeFocused = wrapper.querySelector('#' + idPrefix + focusId); const toBeFocused = wrapper.querySelector('#' + idPrefix + focusId);
if (toBeFocused) { if (toBeFocused) {
toBeFocused.focus(); toBeFocused.focus();
@ -268,34 +312,33 @@ export class AsyncTable {
} }
_serializeTableFilterToURL(tableFilterForm) { _serializeTableFilterToURL(tableFilterForm) {
const url = new URL(this._storageManager.load('currentTableUrl') || window.location.href); const url = new URL(this._windowStorage.load('currentTableUrl') || window.location.href);
// create new FormData and format any date values // create new FormData and format any date values
const formData = Datepicker.unformatAll(tableFilterForm, new FormData(tableFilterForm)); const formData = Datepicker.unformatAll(tableFilterForm, new FormData(tableFilterForm));
for (var k of url.searchParams.keys()) { this._debugLog('_serializeTableFilterToURL', Array.from(formData.entries()), url.href);
url.searchParams.delete(k);
}
for (var kv of formData.entries()) { const searchParams = new URLSearchParams(Array.from(formData.entries()));
url.searchParams.append(kv[0], kv[1]); url.search = searchParams.toString();
}
this._debugLog('_serializeTableFilterToURL', url.href);
return url; return url;
} }
_processStorage() { _processStorage() {
const scrollTo = this._storageManager.load('scrollTo'); const scrollTo = this._windowStorage.load('scrollTo');
if (scrollTo && this._scrollTable) { if (scrollTo && this._scrollTable) {
window.scrollTo(scrollTo); window.scrollTo(scrollTo);
} }
this._storageManager.remove('scrollTo'); this._windowStorage.remove('scrollTo');
const horizPos = this._storageManager.load('horizPos'); const horizPos = this._windowStorage.load('horizPos');
if (horizPos && this._scrollTable) { if (horizPos && this._scrollTable) {
this._scrollTable.scrollLeft = horizPos; this._scrollTable.scrollLeft = horizPos;
} }
this._storageManager.remove('horizPos'); this._windowStorage.remove('horizPos');
} }
_removeListeners() { _removeListeners() {
@ -330,7 +373,7 @@ export class AsyncTable {
} }
_changePagesizeHandler = () => { _changePagesizeHandler = () => {
const url = new URL(this._storageManager.load('currentTableUrl') || window.location.href); const url = new URL(this._windowStorage.load('currentTableUrl') || window.location.href);
// create new FormData and format any date values // create new FormData and format any date values
const formData = Datepicker.unformatAll(this._pagesizeForm, new FormData(this._pagesizeForm)); const formData = Datepicker.unformatAll(this._pagesizeForm, new FormData(this._pagesizeForm));
@ -347,7 +390,9 @@ export class AsyncTable {
} }
// fetches new sorted element from url with params and replaces contents of current element // fetches new sorted element from url with params and replaces contents of current element
_updateTableFrom(url, callback) { _updateTableFrom(url, callback, isPopState) {
url = new URL(url);
const cancelPendingUpdates = (() => { const cancelPendingUpdates = (() => {
this._cancelPendingUpdates.forEach(f => f()); this._cancelPendingUpdates.forEach(f => f());
}).bind(this); }).bind(this);
@ -372,23 +417,33 @@ export class AsyncTable {
return false; return false;
} }
this._storageManager.save('currentTableUrl', url.href); if (!isPopState)
this._historyStorage.save('currentTableUrl', url.href, { location: LOCATION.HISTORY, history: { push: true, url: response.headers.get('DB-Table-Canonical-URL') || url.href } });
this._windowStorage.save('currentTableUrl', url.href);
// reset table // reset table
this._removeListeners(); this._removeListeners();
this._active = false;
this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS); this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS);
this._element.dataset['currentTableUrl'] = url.href;
// update table with new // update table with new
this._element.innerHTML = response.element.innerHTML; this._element.innerHTML = response.element.innerHTML;
this._app.utilRegistry.initAll(this._element); this._app.utilRegistry.initAll(this._element);
if (callback && typeof callback === 'function') { if (callback && typeof callback === 'function') {
this._storageManager.save('cssIdPrefix', response.idPrefix); this._windowStorage.save('cssIdPrefix', response.idPrefix);
callback(this._element); callback(this._element);
this._storageManager.remove('cssIdPrefix'); this._windowStorage.remove('cssIdPrefix');
} }
}).catch((err) => console.error(err) }).catch((err) => console.error(err)
).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS)); ).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS));
} }
_debugLog() {}
// _debugLog(fName, ...args) {
// console.log(`[DEBUGLOG] AsyncTable.${fName}`, { args: args, instance: this });
// }
} }

View File

@ -552,7 +552,7 @@ defaultDBSFilterLayout filterWdgt filterEnctype filterAction scrolltable
{ formMethod = GET { formMethod = GET
, formAction = Just filterAction , formAction = Just filterAction
, formEncoding = filterEnctype , formEncoding = filterEnctype
, formAttrs = [("class", "table-filter-form")] , formAttrs = [("class", "table-filter-form"), ("autocomplete", "off")]
, formSubmit = FormAutoSubmit , formSubmit = FormAutoSubmit
, formAnchor = Nothing :: Maybe Text , formAnchor = Nothing :: Maybe Text
} }
@ -933,7 +933,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
getParams <- liftHandler $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest getParams <- liftHandler $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest
let let
tblLink :: (QueryText -> QueryText) -> SomeRoute UniWorX tblLink :: (QueryText -> QueryText) -> SomeRoute UniWorX
tblLink f = SomeRoute . (currentRoute, ) . over (mapped . _2) (fromMaybe Text.empty) $ (f . substPi . setParam "_hasdata" Nothing) getParams tblLink f = SomeRoute . (currentRoute, ) . over (mapped . _2) (fromMaybe Text.empty) $ (f . substPi . setParam "_hasdata" Nothing . setParam (toPathPiece PostFormIdentifier) Nothing) getParams
substPi = foldr (.) id substPi = foldr (.) id
[ setParams (wIdent "sorting") . map toPathPiece $ fromMaybe [] piSorting [ setParams (wIdent "sorting") . map toPathPiece $ fromMaybe [] piSorting
, foldr (.) id . map (\k -> setParams (dbFilterKey dbtIdent' k) . fromMaybe [] . join $ traverse (Map.lookup k) piFilter) $ Map.keys dbtFilter , foldr (.) id . map (\k -> setParams (dbFilterKey dbtIdent' k) . fromMaybe [] . join $ traverse (Map.lookup k) piFilter) $ Map.keys dbtFilter
@ -1306,7 +1306,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
{ formMethod = GET { formMethod = GET
, formAction = Just . SomeRoute $ rawAction :#: wIdent "table-wrapper" , formAction = Just . SomeRoute $ rawAction :#: wIdent "table-wrapper"
, formEncoding = pagesizeEnc , formEncoding = pagesizeEnc
, formAttrs = [("class", "pagesize")] , formAttrs = [("class", "pagesize"), ("autocomplete", "off")]
, formSubmit = FormAutoSubmit , formSubmit = FormAutoSubmit
, formAnchor = Just $ wIdent "pagesize-form" , formAnchor = Just $ wIdent "pagesize-form"
} }
@ -1315,6 +1315,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
csvWdgt = $(widgetFile "table/csv-transcode") csvWdgt = $(widgetFile "table/csv-transcode")
uiLayout :: Widget -> Widget
uiLayout table = dbsFilterLayout filterWdgt filterEnc (SomeRoute $ rawAction :#: wIdent "table-wrapper") $(widgetFile "table/layout") uiLayout table = dbsFilterLayout filterWdgt filterEnc (SomeRoute $ rawAction :#: wIdent "table-wrapper") $(widgetFile "table/layout")
dbInvalidateResult' = foldr (<=<) return . catMaybes $ dbInvalidateResult' = foldr (<=<) return . catMaybes $
@ -1344,7 +1345,14 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
redirect $ tblLink id redirect $ tblLink id
(act, _) -> act (act, _) -> act
dbInvalidateResult' <=< bool (dbHandler (Proxy @m) (Proxy @x) $ (\table -> $(widgetFile "table/layout-wrapper")) . uiLayout) (sendResponse <=< tblLayout . uiLayout <=< dbWidget (Proxy @m) (Proxy @x)) psShortcircuit <=< runDBTable dbtable paginationInput currentKeys . fmap swap $ runWriterT table' let
wrapLayout :: DBResult m x -> DB (DBResult m x)
wrapLayout = dbHandler (Proxy @m) (Proxy @x) $ (\table -> $(widgetFile "table/layout-wrapper")) . uiLayout
shortcircuit :: forall void. DBResult m x -> DB void
shortcircuit res = do
addCustomHeader HeaderDBTableCanonicalURL =<< toTextUrl (tblLink substPi)
sendResponse =<< tblLayout . uiLayout =<< dbWidget (Proxy @m) (Proxy @x) res
dbInvalidateResult' <=< bool wrapLayout shortcircuit psShortcircuit <=< runDBTable dbtable paginationInput currentKeys . fmap swap $ runWriterT table'
where where
tblLayout :: forall m'. (MonadHandler m', HandlerSite m' ~ UniWorX) => Widget -> m' Html tblLayout :: forall m'. (MonadHandler m', HandlerSite m' ~ UniWorX) => Widget -> m' Html
tblLayout tbl' = do tblLayout tbl' = do
@ -1356,6 +1364,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
setParam :: Text -> Maybe Text -> QueryText -> QueryText setParam :: Text -> Maybe Text -> QueryText -> QueryText
setParam key = setParams key . maybeToList setParam key = setParams key . maybeToList
dbTableWidget :: Monoid x dbTableWidget :: Monoid x
=> PSValidator (HandlerFor UniWorX) x => PSValidator (HandlerFor UniWorX) x

View File

@ -841,7 +841,7 @@ choice = foldr (<|>) empty
-- Custom HTTP Headers -- -- Custom HTTP Headers --
--------------------------------- ---------------------------------
data CustomHeader = HeaderIsModal | HeaderDBTableShortcircuit | HeaderMassInputShortcircuit | HeaderAlerts data CustomHeader = HeaderIsModal | HeaderDBTableShortcircuit | HeaderMassInputShortcircuit | HeaderAlerts | HeaderDBTableCanonicalURL
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
instance Universe CustomHeader instance Universe CustomHeader

View File

@ -3,10 +3,10 @@ $newline never
$maybe flag <- sortableKey $maybe flag <- sortableKey
$case directions $case directions
$of [SortAsc] $of [SortAsc]
<a .table__th-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortDesc : piSorting'))}> <a .table__th-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortDesc : piSorting')) . substPi}>
^{widget} ^{widget}
$of _ $of _
<a .table__th-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortAsc : piSorting'))}> <a .table__th-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortAsc : piSorting')) . substPi}>
^{widget} ^{widget}
$nothing $nothing
^{widget} ^{widget}