Bundles a stretch of in-progress work across the SPA tools so the
tree returns to a coherent shippable state ahead of cutting a new
zddc-server stable image:
- landing: substantial rework of the project picker (sortable/filterable
table, presets refactor, ?projects= filter, ?v= channel propagation,
loading/error states)
- archive: presets cleanup, source.js refactor, filtering/url-state
alignment with the landing page
- mdedit: file-system module split, resizer, file-tree improvements,
base/toc styling tweaks
- transmittal/classifier: small template touch-ups for shared chrome
- shared: build-lib.sh helpers, new favicon.svg
- bootstrap, build.sh: pick up the channel-aware install/track zip
generation
- tests: new landing.spec.js, expanded archive/mdedit/build-label specs
- docs: CLAUDE.md picks up the zddc-server section and freshens the
alpha-build exception note
- regenerated artifacts: install.zip, track-{alpha,beta,stable}.zip,
*_alpha.html — these are produced by `sh build.sh` and per project
convention are committed alongside the source changes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
224 lines
8 KiB
JavaScript
224 lines
8 KiB
JavaScript
(function() {
|
|
'use strict';
|
|
// URL state sync module for ZDDC Archive
|
|
|
|
// Default values for URL params
|
|
var DEFAULT_SORT_FIELD = 'trackingNumber';
|
|
var DEFAULT_SORT_DIRECTION = 'asc';
|
|
var DEFAULT_ENABLED_TYPES = ['issued', 'received'];
|
|
|
|
// Map URL param names to state paths
|
|
var PARAM_MAP = {
|
|
sort: 'sortField',
|
|
dir: 'sortDirection',
|
|
tn: 'columnFilters.trackingNumber',
|
|
ti: 'columnFilters.title',
|
|
rv: 'columnFilters.revisions',
|
|
types: 'enabledFolderTypes',
|
|
gf: 'groupingFilter',
|
|
tf: 'transmittalFilter',
|
|
projects: 'projectFilter',
|
|
show: 'visibleProjects'
|
|
};
|
|
|
|
// Serialize current state to URL query string
|
|
function serialize() {
|
|
var params = new URLSearchParams();
|
|
|
|
// Sort field
|
|
if (window.app.sortField !== DEFAULT_SORT_FIELD) {
|
|
params.set('sort', window.app.sortField);
|
|
}
|
|
|
|
// Sort direction
|
|
if (window.app.sortDirection !== DEFAULT_SORT_DIRECTION) {
|
|
params.set('dir', window.app.sortDirection);
|
|
}
|
|
|
|
// Column filters
|
|
if (window.app.columnFilters.trackingNumber !== '') {
|
|
params.set('tn', window.app.columnFilters.trackingNumber);
|
|
}
|
|
if (window.app.columnFilters.title !== '') {
|
|
params.set('ti', window.app.columnFilters.title);
|
|
}
|
|
if (window.app.columnFilters.revisions !== '') {
|
|
params.set('rv', window.app.columnFilters.revisions);
|
|
}
|
|
|
|
// Folder types (only if different from default [issued, received])
|
|
var enabledTypes = Array.from(window.app.enabledFolderTypes).sort();
|
|
var defaultTypes = DEFAULT_ENABLED_TYPES.slice().sort();
|
|
if (JSON.stringify(enabledTypes) !== JSON.stringify(defaultTypes)) {
|
|
params.set('types', enabledTypes.join(','));
|
|
}
|
|
|
|
// Grouping filter
|
|
if (window.app.groupingFilter !== '') {
|
|
params.set('gf', window.app.groupingFilter);
|
|
}
|
|
|
|
// Transmittal filter
|
|
if (window.app.transmittalFilter !== '') {
|
|
params.set('tf', window.app.transmittalFilter);
|
|
}
|
|
|
|
// Project filter — always preserved if set (for shareable URLs)
|
|
if (window.app.projectFilter && window.app.projectFilter.size > 0) {
|
|
params.set('projects', Array.from(window.app.projectFilter).join(','));
|
|
}
|
|
|
|
// Visibility filter (project picker). Emit only when it's a strict subset
|
|
// of projectFilter — the common "everything visible" case keeps URLs clean.
|
|
if (window.app.visibleProjects && window.app.projectFilter
|
|
&& window.app.projectFilter.size > 0) {
|
|
var pfSize = window.app.projectFilter.size;
|
|
var vp = Array.from(window.app.visibleProjects).filter(function(n) {
|
|
return window.app.projectFilter.has(n);
|
|
});
|
|
if (vp.length < pfSize) {
|
|
params.set('show', vp.slice().sort().join(','));
|
|
}
|
|
}
|
|
|
|
// Build query string
|
|
var qs = params.toString();
|
|
return qs ? '?' + qs : '';
|
|
}
|
|
|
|
// Push state to URL without triggering popstate
|
|
function push() {
|
|
var result = serialize();
|
|
if (result === location.search) {
|
|
return;
|
|
}
|
|
try {
|
|
history.replaceState(null, '', location.pathname + result);
|
|
} catch (e) {
|
|
// Silently swallow errors (e.g., file:// protocol restrictions)
|
|
}
|
|
}
|
|
|
|
// Restore state from URL query string
|
|
function restore() {
|
|
var params = new URLSearchParams(location.search);
|
|
|
|
// Restore sort field
|
|
if (params.has('sort')) {
|
|
var sortValue = params.get('sort');
|
|
if (sortValue === 'trackingNumber' || sortValue === 'title') {
|
|
window.app.sortField = sortValue;
|
|
}
|
|
}
|
|
|
|
// Restore sort direction
|
|
if (params.has('dir')) {
|
|
var dirValue = params.get('dir');
|
|
if (dirValue === 'asc' || dirValue === 'desc') {
|
|
window.app.sortDirection = dirValue;
|
|
}
|
|
}
|
|
|
|
// Restore column filters with AST parsing
|
|
if (params.has('tn')) {
|
|
var tnValue = params.get('tn');
|
|
window.app.columnFilters.trackingNumber = tnValue;
|
|
window.app.columnFilterASTs.trackingNumber = zddc.filter.parse(tnValue);
|
|
}
|
|
if (params.has('ti')) {
|
|
var tiValue = params.get('ti');
|
|
window.app.columnFilters.title = tiValue;
|
|
window.app.columnFilterASTs.title = zddc.filter.parse(tiValue);
|
|
}
|
|
if (params.has('rv')) {
|
|
var rvValue = params.get('rv');
|
|
window.app.columnFilters.revisions = rvValue;
|
|
window.app.columnFilterASTs.revisions = zddc.filter.parse(rvValue);
|
|
}
|
|
|
|
// Restore folder types
|
|
if (params.has('types')) {
|
|
var typesValue = params.get('types');
|
|
var typeValues = typesValue.split(',').map(function(t) { return t.trim(); });
|
|
// Validate against app.FOLDER_TYPE_NAMES
|
|
var validTypes = typeValues.filter(function(t) {
|
|
return window.app.FOLDER_TYPE_NAMES.indexOf(t) !== -1;
|
|
});
|
|
window.app.enabledFolderTypes = new Set(validTypes);
|
|
}
|
|
|
|
// Restore grouping filter
|
|
if (params.has('gf')) {
|
|
window.app.groupingFilter = params.get('gf');
|
|
}
|
|
|
|
// Restore transmittal filter
|
|
if (params.has('tf')) {
|
|
window.app.transmittalFilter = params.get('tf');
|
|
}
|
|
|
|
// Restore project filter
|
|
if (params.has('projects')) {
|
|
var projValue = params.get('projects');
|
|
var projNames = projValue.split(',').map(function(p) { return p.trim(); }).filter(Boolean);
|
|
window.app.projectFilter = new Set(projNames);
|
|
}
|
|
|
|
// Restore visibility filter. autoConnectHttpSource will intersect against
|
|
// projectFilter / availableProjects after the project list resolves, so
|
|
// dropping bogus names is handled there. We just parse here.
|
|
if (params.has('show')) {
|
|
var showValue = params.get('show');
|
|
var showNames = showValue.split(',').map(function(p) { return p.trim(); }).filter(Boolean);
|
|
window.app.visibleProjects = new Set(showNames);
|
|
}
|
|
|
|
// Update DOM inputs to reflect restored values
|
|
updateFilterInputs();
|
|
}
|
|
|
|
// Update DOM filter inputs to match restored state
|
|
function updateFilterInputs() {
|
|
// Column filter inputs
|
|
document.querySelectorAll('.column-filter[data-filter-field]').forEach(function(input) {
|
|
var field = input.getAttribute('data-filter-field');
|
|
var filterValue = window.app.columnFilters[field] || '';
|
|
input.value = filterValue;
|
|
if (filterValue !== '') {
|
|
input.classList.add('filter-active');
|
|
} else {
|
|
input.classList.remove('filter-active');
|
|
}
|
|
});
|
|
|
|
// Grouping filter
|
|
var groupingFilterEl = document.getElementById('groupingFilter');
|
|
if (groupingFilterEl) {
|
|
groupingFilterEl.value = window.app.groupingFilter;
|
|
if (window.app.groupingFilter !== '') {
|
|
groupingFilterEl.classList.add('filter-active');
|
|
} else {
|
|
groupingFilterEl.classList.remove('filter-active');
|
|
}
|
|
}
|
|
|
|
// Transmittal filter
|
|
var transmittalFilterEl = document.getElementById('transmittalFilter');
|
|
if (transmittalFilterEl) {
|
|
transmittalFilterEl.value = window.app.transmittalFilter;
|
|
if (window.app.transmittalFilter !== '') {
|
|
transmittalFilterEl.classList.add('filter-active');
|
|
} else {
|
|
transmittalFilterEl.classList.remove('filter-active');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Register module
|
|
window.app.modules.urlState = {
|
|
serialize: serialize,
|
|
push: push,
|
|
restore: restore
|
|
};
|
|
|
|
})();
|