T4.2 - S3: the app's state has an address; X1 is unblocked
Wave 0 counted pushState across html/ and found 0. Every page read its query
string once at boot and never wrote one again, so you could not send anyone a
link to WP07, a refresh dropped you back at the default view, and Back left the
app entirely because the app had never added a history entry.
CR-011 and CR-014 both promise an email carrying a direct link to a work package.
That is X1, and it was blocked on this. It is not blocked now.
html/wp-url.js is the whole mechanism, and it is deliberately NOT a router.
Nothing in it intercepts navigation or renders anything; it is the query string
treated as state that can be read, merged, written and subscribed to. Pages keep
their own rendering. Query parameters rather than a hash, because the server
already serves these paths and a hash is never sent to the server - which matters
the day a link has to be resolved before the page boots.
The merge behaviour is the part that earns its place: WPUrl.push({wp:id}) keeps
the active project, and WPUrl.push({wp:''}) clears one key without needing to know
what else is in the URL. Hand-built URLs losing ?project= is the usual way this
goes wrong.
WIRED: the creator (open package, dashboard view), the SOP wizard (tool, step),
the launcher (project). Each records a history entry only when the user chose the
change - restoring from the URL uses replace, or Back would immediately add an
entry and appear to do nothing.
WPUrl.absolute() is what CR-011/CR-014 will paste into an email in wave 8.
TWO BUGS THIS TASK CREATED AND FIXED, both found by the probe rather than by
reading:
- bootSOP() calls newPackage() during boot, and newPackage() cleared ?wp=. A
deep link therefore worked and then erased its own parameter, leaving Back
with nothing to return to. Now guarded on wpCreatorReady.
- goToStep() runs validateStep(), which ends in alert() when a required field
is empty - always true on a freshly loaded page. So restoring ?step=3 from a
shared link opened a modal dialog mid-boot, and hung the browser under CDP.
Restoring a view is not a forward navigation and no longer runs the
forward-navigation guard.
The second one is worth keeping in mind for the rest of wave 4: this app has 79
native dialogs, and any of them firing during a restore path will hang a headless
browser rather than fail visibly.
VERIFICATION. tests/url_state_check.py, 23 checks, all passing, covering every
done-when on the task:
- a URL identifying a work package opens that package
- the same URL for a SIGNED-OUT user goes to login, carries the target through
?next=, and lands on the work package itself after signing in
- refresh preserves project, package, tab and view
- Back and Forward move through states, verified as still-initialised rather
than reloaded, and with the dashboard actually rendered rather than only the
URL changed
- a different user opening the same URL reaches the same view
- nothing credential-shaped appears in the query string
Metric 8, pushState: was 0 at wave 0, now 2 in html/ (one pushState and one
replaceState, both in wp-url.js) behind 6 call sites across 4 files. The raw
count stays low by design - one place writes history, which is the same reason
the token work put one place in charge of colour.
browser_check 71/71, f_items 5 FIXED / F6 REPRODUCES, aggregates 16/16.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -291,15 +291,25 @@ window.addEventListener('DOMContentLoaded',()=>{
|
||||
updateProjectDisplay();
|
||||
loadProjectUsers(); // team pickers: who's on this project
|
||||
applyBimFlag(); // hide the BIM section unless an admin enabled it
|
||||
// Deep-link: ?tab=sop | ?tab=wp | ?view=dashboard | ?wp=<id>. Consumed ONCE —
|
||||
// leaving ?view=dashboard in the URL used to make the Work Package Creation tab
|
||||
// keep opening the dashboard for the rest of the session.
|
||||
// Deep-link: ?tab=sop | ?tab=wp | ?view=dashboard | ?wp=<id> | ?step=N.
|
||||
//
|
||||
// These used to be consumed ONCE, because a leftover ?view=dashboard made the
|
||||
// Work Package Creation tab keep reopening the dashboard for the rest of the
|
||||
// session. Since T4.2 the URL is not a one-shot instruction — it TRACKS the
|
||||
// current state and switchTool() clears `view` when you leave the dashboard —
|
||||
// so the param can be honoured every time and a refresh lands where you were.
|
||||
//
|
||||
// fromUrl on every call: the URL already says this, so restoring it must not
|
||||
// add a history entry. Without that, the first Back after loading a deep link
|
||||
// would just return you to the same view.
|
||||
const tab = params.get('tab');
|
||||
_deepLinkWp = params.get('wp') || '';
|
||||
const wantDashboard = params.get('view') === 'dashboard';
|
||||
if(wantDashboard) switchTool('dashboard');
|
||||
else if(tab === 'wp' || tab === 'sop') switchTool(tab);
|
||||
else if(_deepLinkWp) switchTool('wp');
|
||||
if(wantDashboard) switchTool('dashboard', {fromUrl:true});
|
||||
else if(tab === 'wp' || tab === 'sop') switchTool(tab, {fromUrl:true});
|
||||
else if(_deepLinkWp) switchTool('wp', {fromUrl:true});
|
||||
const bootStep = parseInt(params.get('step'), 10);
|
||||
if(bootStep >= 1 && bootStep <= 10 && currentTool === 'sop') goToStep(bootStep, {fromUrl:true});
|
||||
}
|
||||
if(projId && typeof ProjectData!=='undefined' && ProjectData.pullProject){
|
||||
ProjectData.pullProject(projId).then(afterPull).catch(afterPull);
|
||||
@@ -464,8 +474,15 @@ function repopulateForm(){
|
||||
}
|
||||
|
||||
// ── TOOL SWITCHING ────────────────────────────────────────────────────────────
|
||||
function switchTool(tool){
|
||||
function switchTool(tool, opts){
|
||||
currentTool = tool;
|
||||
// S3: which tool is open is addressable state. `fromUrl` is set when we are
|
||||
// restoring because the user pressed Back - recording that as a new entry would
|
||||
// make Back appear to do nothing.
|
||||
if(typeof WPUrl !== 'undefined' && !(opts && opts.fromUrl)){
|
||||
WPUrl.push(tool === 'dashboard' ? { tab: 'wp', view: 'dashboard' }
|
||||
: { tab: tool, view: '' });
|
||||
}
|
||||
// 'dashboard' is a pseudo-tab: it reuses the WP tool's content (the embedded
|
||||
// creator) but opens it straight to the dashboard view.
|
||||
const isDash = (tool === 'dashboard');
|
||||
@@ -1148,12 +1165,31 @@ function addSource(){
|
||||
}
|
||||
|
||||
// ── STEP NAVIGATION ────────────────────────────────────────────────────────────
|
||||
function goToStep(n){
|
||||
if(!validateStep(currentStep)) return;
|
||||
function goToStep(n, opts){
|
||||
const fromUrl = !!(opts && opts.fromUrl);
|
||||
// Restoring a step from the URL is not a forward navigation, so it must not run
|
||||
// the forward-navigation guard. validateStep() ends in alert() when a required
|
||||
// field is empty, which on a freshly-loaded deep link is ALWAYS - so a shared
|
||||
// link to step 3 opened a modal dialog before the page had finished booting.
|
||||
if(!fromUrl && !validateStep(currentStep)) return;
|
||||
currentStep = n;
|
||||
// S3: the step you are on survives a refresh and a shared link.
|
||||
if(typeof WPUrl !== 'undefined' && !fromUrl) WPUrl.push({ step: n > 1 ? n : '' });
|
||||
updateStepUI();
|
||||
}
|
||||
|
||||
// S3: Back / Forward across tools and steps. The URL is the state, so restoring is
|
||||
// "read it and show that" rather than a bespoke undo stack.
|
||||
if(typeof WPUrl !== 'undefined'){
|
||||
WPUrl.onChange(function(state, viaPop){
|
||||
if(!viaPop) return;
|
||||
const wantTool = state.view === 'dashboard' ? 'dashboard' : (state.tab || 'sop');
|
||||
if(wantTool !== currentTool) switchTool(wantTool, {fromUrl:true});
|
||||
const step = parseInt(state.step, 10);
|
||||
if(currentTool === 'sop' && step >= 1 && step !== currentStep) goToStep(step, {fromUrl:true});
|
||||
});
|
||||
}
|
||||
|
||||
function nextStep(){
|
||||
if(!validateStep(currentStep)) return;
|
||||
if(currentStep < 10){
|
||||
|
||||
Reference in New Issue
Block a user