diff --git a/html/admin.html b/html/admin.html index a0132ea..8adae4f 100644 --- a/html/admin.html +++ b/html/admin.html @@ -213,6 +213,10 @@ + + diff --git a/html/index.html b/html/index.html index c65898c..3f77fc9 100644 --- a/html/index.html +++ b/html/index.html @@ -423,6 +423,15 @@ function initProjects(){ ProjectData.list().then(list => { _projects = list || []; + // A deep link names the project explicitly, and every other page in the + // suite already honours ?project=. This one did not, so arriving here + // with a link on a browser that had nothing stored showed "Select a + // project" while the URL said otherwise. Honour it before reconciling. + const wanted = new URLSearchParams(location.search).get('project'); + if(wanted){ + const target = _projects.find(p => p.id === wanted); + if(target) ProjectData.setActive(target); + } // Reconcile the active project against the list; clear if it's gone. const active = ProjectData.getActive(); const dropped = (active && !_projects.some(p => p.id === active.id)) ? active : null; diff --git a/html/project-data.js b/html/project-data.js index 8a66578..22db885 100644 --- a/html/project-data.js +++ b/html/project-data.js @@ -24,6 +24,16 @@ } function cacheRemove(id) { writeLocal(readLocal().filter(function (x) { return x.id !== id; })); } + // Subscribers to the active project. Deliberately a plain array and a plain + // callback — this is one value with a handful of readers, not a reason for a + // state library. A throwing subscriber must not stop the others being told. + var activeSubs = []; + function notifyActive(p) { + activeSubs.slice().forEach(function (fn) { + try { fn(p); } catch (e) {} + }); + } + var SAMPLE_PROJECT = { name: 'Micron FMCS Install (sample)', number: '26-67-008', client: 'Micron Technology, Inc.', division: 'Semiconductor', @@ -78,13 +88,40 @@ // implementation of it rather than two that can disagree. // ── active project context ──────────────────────────────────────────────── + // setActive is the ONLY thing in the app that writes LS_ACTIVE / LS_ACTIVE_OBJ. + // Everything that displays the active project reads it back through getActive() + // or subscribes with onActiveChange(). Keep it that way: F1 was two readers with + // their own copies, and the global one lost. getActiveId: function () { try { return localStorage.getItem(LS_ACTIVE) || ''; } catch (e) { return ''; } }, getActive: function () { try { return JSON.parse(localStorage.getItem(LS_ACTIVE_OBJ) || 'null'); } catch (e) { return null; } }, setActive: function (p) { try { - if (p) { localStorage.setItem(LS_ACTIVE, p.id); localStorage.setItem(LS_ACTIVE_OBJ, JSON.stringify(p)); } - else { localStorage.removeItem(LS_ACTIVE); localStorage.removeItem(LS_ACTIVE_OBJ); } + if (p) { + // Several callers know only the id — a deep link resolving before the + // record arrives (field.js, wp-creation-app.js, work-package-suite-app.js + // all call setActive({id}) first and the full record second). Writing that + // stub verbatim erases the name, and the app bar then renders "(unnamed)". + // Merging keeps the fuller record; fields the caller does supply still win. + var prev = this.getActive(); + if (prev && prev.id === p.id) p = Object.assign({}, prev, p); + localStorage.setItem(LS_ACTIVE, p.id); + localStorage.setItem(LS_ACTIVE_OBJ, JSON.stringify(p)); + } else { + localStorage.removeItem(LS_ACTIVE); + localStorage.removeItem(LS_ACTIVE_OBJ); + } } catch (e) {} + notifyActive(p || null); + }, + + // Subscribe to active-project changes. Returns an unsubscribe function. + // The app bar uses this instead of holding its own copy of the value. + onActiveChange: function (fn) { + if (typeof fn !== 'function') return function () {}; + activeSubs.push(fn); + return function () { + activeSubs = activeSubs.filter(function (f) { return f !== fn; }); + }; }, // Per-project namespacing for the SOP/WP localStorage keys, e.g. @@ -399,5 +436,14 @@ } } catch (e) {} + // A second tab switching project leaves this one showing a project the user is no + // longer on. The storage event fires only in OTHER tabs, which is exactly the case + // setActive's own notification cannot cover. + try { + global.addEventListener('storage', function (e) { + if (e.key === LS_ACTIVE_OBJ || e.key === LS_ACTIVE) notifyActive(ProjectData.getActive()); + }); + } catch (e) {} + global.ProjectData = ProjectData; })(window); diff --git a/html/users.html b/html/users.html index b11c209..15293be 100644 --- a/html/users.html +++ b/html/users.html @@ -100,6 +100,10 @@ + + diff --git a/html/wp-chrome.js b/html/wp-chrome.js index 5cabc68..ae546b4 100644 --- a/html/wp-chrome.js +++ b/html/wp-chrome.js @@ -379,6 +379,16 @@ loadProjects(switcher); checkArchived(m.host); window.wpChromeRefresh = function () { switcher.wpcRefresh(); }; + + // Subscribe rather than keep our own copy of the value. Before this, the label + // was rendered once at build time and refreshed only when /api/projects came + // back, so selecting a project on the launcher updated the hero and left the bar + // reading "Select a project" — that was F1. + try { + if (window.ProjectData && ProjectData.onActiveChange) { + ProjectData.onActiveChange(function () { switcher.wpcRefresh(); }); + } + } catch (e) {} } // Wait for the auth guard: an unauthenticated page is about to redirect, and