mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-12 20:54:47 +00:00
8b924cd217
## Summary Encodes view + filter state in the URL hash so deep links restore the exact page state (issue #749). ## Changes New shared helper `public/url-state.js` exposing `URLState`: - `parseSort('col:asc')` → `{column, direction}` (defaults to `desc`) - `serializeSort('col', 'desc')` → `'col'` (omits default direction) - `parseHash('#/nodes/abc?tab=x')` → `{route: 'nodes/abc', params: {tab:'x'}}` - `buildHash(route, params)` and `updateHashParams(updates, currentHash)` for round-tripping while preserving subpaths. Wired into: - **packets.js** — sort column/direction now in `#/packets?sort=col[:asc]`, restored on init (overrides localStorage). Subpath `#/packets/<hash>` preserved. - **nodes.js** — sort encoded as `#/nodes?sort=col[:asc]`, restored on init. Subpath `#/nodes/<pubkey>` preserved. - **analytics.js** — both selected tab (`tab=topology`) AND time-window picker value (`window=7d`) now round-trip via URL. Subview keys used by rf-health (`range/observer/from/to`) cleared when switching tabs to keep URLs clean. Existing deep links (`#/nodes/<pubkey>`, `#/packets/<hash>`, `?filter=…`, `?node=…`, `?observer=…`, `?channel=…`, `?timeWindow=…`, `?region=…`) all keep working — additive change only. ## Tests TDD red→green: - Red: `5e1482e` (stub throws "not implemented"; 18/18 tests fail on assertions) - Green: `512940e` (helper implemented; 18/18 pass) Wired `test-url-state.js` into `test-all.sh`. Fixes #749 --------- Co-authored-by: clawbot <clawbot@users.noreply.github.com>
124 lines
4.1 KiB
JavaScript
124 lines
4.1 KiB
JavaScript
/* === CoreScope — url-state.js ===
|
|
*
|
|
* Shared helpers for encoding/decoding view & filter state in the URL hash.
|
|
* Pages use these so deep links restore the exact view (issue #749).
|
|
*
|
|
* Hash format: "#/<route>?key1=val1&key2=val2"
|
|
*
|
|
* Existing deep links remain intact:
|
|
* #/nodes/<pubkey> (path segment after route)
|
|
* #/packets/<hash> (path segment after route)
|
|
* #/packets?filter=... (query after route)
|
|
*
|
|
* This module ONLY parses/serializes — it never mutates location.
|
|
*/
|
|
'use strict';
|
|
|
|
(function (root) {
|
|
/**
|
|
* Parse a sort token "column[:direction]" into { column, direction }.
|
|
* Direction defaults to 'desc'. Anything other than 'asc'/'desc' falls back to 'desc'.
|
|
* Empty/null input returns null.
|
|
*/
|
|
function parseSort(s) {
|
|
if (s == null || s === '') return null;
|
|
var str = String(s);
|
|
var idx = str.indexOf(':');
|
|
var column = idx >= 0 ? str.slice(0, idx) : str;
|
|
var dir = idx >= 0 ? str.slice(idx + 1) : 'desc';
|
|
if (dir !== 'asc' && dir !== 'desc') dir = 'desc';
|
|
return { column: column, direction: dir };
|
|
}
|
|
|
|
/**
|
|
* Serialize a sort state to a token. 'desc' is the default and omitted.
|
|
* Empty/null column returns ''.
|
|
*/
|
|
function serializeSort(column, direction) {
|
|
if (!column) return '';
|
|
if (direction === 'asc') return column + ':asc';
|
|
return String(column);
|
|
}
|
|
|
|
/**
|
|
* Parse a location.hash string into { route, params }.
|
|
* - Strips leading '#' and '/'.
|
|
* - Splits on first '?'; left = route (may include subpath like 'nodes/abc'),
|
|
* right = querystring parsed via URLSearchParams.
|
|
*/
|
|
function parseHash(hash) {
|
|
var h = String(hash || '');
|
|
if (h.charAt(0) === '#') h = h.slice(1);
|
|
if (h.charAt(0) === '/') h = h.slice(1);
|
|
if (h === '') return { route: '', params: {} };
|
|
var qi = h.indexOf('?');
|
|
var route = qi >= 0 ? h.slice(0, qi) : h;
|
|
var qs = qi >= 0 ? h.slice(qi + 1) : '';
|
|
var params = {};
|
|
if (qs) {
|
|
var sp = new URLSearchParams(qs);
|
|
sp.forEach(function (v, k) { params[k] = v; });
|
|
}
|
|
return { route: route, params: params };
|
|
}
|
|
|
|
/**
|
|
* Build a hash string '#/<route>?k=v&...'. Skips keys with null/undefined/'' values.
|
|
* 'route' may be passed as '#/foo', '/foo' or 'foo'.
|
|
*/
|
|
function buildHash(route, params) {
|
|
var r = String(route || '');
|
|
if (r.charAt(0) === '#') r = r.slice(1);
|
|
if (r.charAt(0) === '/') r = r.slice(1);
|
|
var sp = new URLSearchParams();
|
|
if (params && typeof params === 'object') {
|
|
for (var k in params) {
|
|
if (!Object.prototype.hasOwnProperty.call(params, k)) continue;
|
|
var v = params[k];
|
|
if (v == null || v === '') continue;
|
|
sp.set(k, String(v));
|
|
}
|
|
}
|
|
var qs = sp.toString();
|
|
return '#/' + r + (qs ? '?' + qs : '');
|
|
}
|
|
|
|
/**
|
|
* Apply a partial-update to the params of an existing hash, preserving the route
|
|
* (including any subpath like 'nodes/<pubkey>'). Returns the new hash string —
|
|
* caller decides whether to history.replaceState() it.
|
|
*
|
|
* Setting a key to '' / null / undefined removes it.
|
|
*/
|
|
function updateHashParams(updates, currentHash) {
|
|
var src = currentHash != null ? currentHash :
|
|
(typeof location !== 'undefined' ? location.hash : '');
|
|
var parsed = parseHash(src);
|
|
var merged = {};
|
|
var k;
|
|
for (k in parsed.params) {
|
|
if (Object.prototype.hasOwnProperty.call(parsed.params, k)) merged[k] = parsed.params[k];
|
|
}
|
|
if (updates && typeof updates === 'object') {
|
|
for (k in updates) {
|
|
if (!Object.prototype.hasOwnProperty.call(updates, k)) continue;
|
|
var v = updates[k];
|
|
if (v == null || v === '') delete merged[k];
|
|
else merged[k] = v;
|
|
}
|
|
}
|
|
return buildHash(parsed.route, merged);
|
|
}
|
|
|
|
var api = {
|
|
parseSort: parseSort,
|
|
serializeSort: serializeSort,
|
|
parseHash: parseHash,
|
|
buildHash: buildHash,
|
|
updateHashParams: updateHashParams,
|
|
};
|
|
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
root.URLState = api;
|
|
})(typeof window !== 'undefined' ? window : globalThis);
|