T1.1 - F1: one source of truth for the active project; the app bar subscribes
The hero, the picker and the create-user card showed the active project while
the app bar still read "Select a project". Three separate causes, all of them
the same shape - a reader with its own copy of the value.
1. Nothing told the bar. wp-chrome.js rendered projectLabel() once at build
time and refreshed it only when /api/projects came back, so selecting a
project updated the hero and left the bar behind. ProjectData.setActive now
notifies, and the bar subscribes through ProjectData.onActiveChange instead
of holding a copy. A plain array of callbacks - this is one value with a
handful of readers, not a reason for a state library.
2. admin.html and users.html load wp-chrome.js but never loaded
project-data.js, so window.ProjectData was undefined and their bar could
NEVER show a project - it read "Select a project" permanently, whatever was
selected. Both now load it, ahead of wp-chrome.js.
3. setActive({id}) erased the name. field.js, wp-creation-app.js and
work-package-suite-app.js all set the id first and the full record second;
writing that stub verbatim left the bar rendering "(unnamed)". setActive now
merges onto the stored record when the id matches, so a partial write cannot
lose fields it did not mean to touch.
Also: index.html never honoured ?project=<id>, though every other page does, so
a deep link on a browser with nothing stored showed "Select a project" while
the URL said otherwise. It now resolves the parameter before reconciling.
setActive is the only code path that writes wp_active_project /
wp_active_project_obj - project-data.js:83-105, noted there in a comment so it
stays that way. A storage listener keeps a second tab from showing a project
the user has since switched away from.
Verified, all at 1440px and against the wave 0 baseline:
- bar shows the project on launcher, SOP wizard, admin, field, users
- survives a hard refresh on each of them
- selecting a project updates hero and bar in one interaction, no reload
- with nothing selected the bar reads "Select a project" and both the
launcher picker and the bar's own switcher are reachable
- deep link ?project= works on a cold browser, hero and bar agree
- setActive({id}) after a full record keeps the name
The creator is the one page with no app bar to fix: it loads neither
wp-chrome.js nor wp-chrome.css, because it renders as the iframe child of the
SOP wizard. Giving it chrome is T7.1's work once B7 dissolves that boundary -
adding it here would put a second app bar inside the embedded view. This is the
"all 6 pages" wording in the plan meeting the 7 pages that exist; see file-map
D1.
tests/f_items.py F1 now reports FIXED. F2-F6 still reproduce, untouched.
browser_check.py 71/71.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -213,6 +213,10 @@
|
||||
|
||||
<script src="console-util.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
<!-- The app bar's project switcher reads ProjectData; without this the bar on this
|
||||
page could never show a project and always read "Select a project" (F1). Must
|
||||
parse before wp-chrome.js, which reads it as it mounts. -->
|
||||
<script src="project-data.js"></script>
|
||||
<script src="wp-chrome.js"></script>
|
||||
<script src="wp-sidenav.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -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=<id>. 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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -100,6 +100,10 @@
|
||||
|
||||
<script src="console-util.js"></script>
|
||||
<script src="users.js"></script>
|
||||
<!-- The app bar's project switcher reads ProjectData; without this the bar on this
|
||||
page could never show a project and always read "Select a project" (F1). Must
|
||||
parse before wp-chrome.js, which reads it as it mounts. -->
|
||||
<script src="project-data.js"></script>
|
||||
<script src="wp-chrome.js"></script>
|
||||
<script src="wp-sidenav.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user