Isolate SOP/WP data per project (namespaced localStorage)
SOP and Work Package localStorage keys are now namespaced by the active project via ProjectData.key(base) -> base+'__'+<projectId> (SK() in the suite, wpKey() in the creator), so switching projects shows that project's own SOP and packages. project-data.js runs a one-time discard of the legacy un-namespaced keys (guarded by wp_ns_migrated_v1), per the chosen approach. Active project is resolved before the store loads in both the suite and the creator so namespaced keys resolve correctly, including standalone deep-links. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -158,13 +158,20 @@ SOP and Work Package belongs to one.
|
|||||||
empty. Passes `&project` into the WP-creator iframe.
|
empty. Passes `&project` into the WP-creator iframe.
|
||||||
- **WP creator:** stamps `projectId` onto every saved package (for API sync).
|
- **WP creator:** stamps `projectId` onto every saved package (for API sync).
|
||||||
|
|
||||||
**Not yet done (the next fork):** SOP/WP *localStorage* is still global, not
|
**Per-project isolation (done, local):** SOP/WP localStorage keys are now
|
||||||
namespaced per project — selecting a different project locally still shows the
|
namespaced per active project via `ProjectData.key(base)` →
|
||||||
same `wp_suite_sop` / `wp_iwp_v1` data. The intended end state is per-project
|
`base + '__' + <projectId>` (`SK()` in the suite, `wpKey()` in the creator).
|
||||||
data via the API (`GET /api/sops/latest?project_id=…`, `GET /api/wps?project_id=…`).
|
So each project keeps its own `wp_suite_sop` / `wp_suite_state` /
|
||||||
Decide whether to (a) namespace the local keys by project id with a migration of
|
`wp_suite_sop_complete` / `wp_iwp_v1`. On first load after this change,
|
||||||
existing un-namespaced data into a "default" project, or (b) jump straight to the
|
`project-data.js` runs a **one-time discard** of the legacy un-namespaced keys
|
||||||
API for SOP/WP reads. See the open question below.
|
(guarded by `wp_ns_migrated_v1`) — chosen over migrating, since the local data
|
||||||
|
was throwaway demo content.
|
||||||
|
|
||||||
|
**Still ahead (true Phase 2):** move SOP/WP reads+writes to the API filtered by
|
||||||
|
`project_id` (`GET /api/sops/latest?project_id=…`, `GET /api/wps?project_id=…`)
|
||||||
|
so projects are shared across users, not just isolated per browser. The
|
||||||
|
endpoints already accept the `project_id` filter; the front end still reads
|
||||||
|
localStorage.
|
||||||
|
|
||||||
**Pending — Phase 2: wire the front end to the API**
|
**Pending — Phase 2: wire the front end to the API**
|
||||||
- SOP: on *SOP Complete*, `POST /api/sops`; on load, `GET /api/sops/latest` to hydrate the Creator (currently uses `localStorage` key `wp_suite_sop`).
|
- SOP: on *SOP Complete*, `POST /api/sops`; on load, `GET /api/sops/latest` to hydrate the Creator (currently uses `localStorage` key `wp_suite_sop`).
|
||||||
|
|||||||
@@ -601,11 +601,10 @@
|
|||||||
function reflectSOPStatus(active){
|
function reflectSOPStatus(active){
|
||||||
let complete = false, projName = '';
|
let complete = false, projName = '';
|
||||||
try {
|
try {
|
||||||
complete = localStorage.getItem('wp_suite_sop_complete') === '1';
|
// Storage is namespaced per project, so these already scope to `active`.
|
||||||
const sop = JSON.parse(localStorage.getItem('wp_suite_sop') || 'null');
|
complete = localStorage.getItem(ProjectData.key('wp_suite_sop_complete')) === '1';
|
||||||
|
const sop = JSON.parse(localStorage.getItem(ProjectData.key('wp_suite_sop')) || 'null');
|
||||||
projName = sop && sop.project && sop.project.name || '';
|
projName = sop && sop.project && sop.project.name || '';
|
||||||
// Only treat the SOP as belonging to this project when names match.
|
|
||||||
if(active && projName && active.name && projName !== active.name) complete = false;
|
|
||||||
} catch(e){}
|
} catch(e){}
|
||||||
|
|
||||||
const sopCard = document.getElementById('card-sop');
|
const sopCard = document.getElementById('card-sop');
|
||||||
|
|||||||
@@ -73,8 +73,24 @@
|
|||||||
if (p) { localStorage.setItem(LS_ACTIVE, p.id); localStorage.setItem(LS_ACTIVE_OBJ, JSON.stringify(p)); }
|
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); }
|
else { localStorage.removeItem(LS_ACTIVE); localStorage.removeItem(LS_ACTIVE_OBJ); }
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
// Per-project namespacing for the SOP/WP localStorage keys, e.g.
|
||||||
|
// key('wp_iwp_v1') → 'wp_iwp_v1__proj_ab12'
|
||||||
|
// Falls back to the bare key when no project is active.
|
||||||
|
key: function (base) { var id = this.getActiveId(); return id ? base + '__' + id : base; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// One-time discard of pre-multi-project (un-namespaced) SOP/WP data so stale
|
||||||
|
// global state can't leak across projects. (User chose: discard, don't migrate.)
|
||||||
|
try {
|
||||||
|
if (!localStorage.getItem('wp_ns_migrated_v1')) {
|
||||||
|
['wp_suite_sop', 'wp_suite_state', 'wp_suite_sop_complete', 'wp_iwp_v1'].forEach(function (k) {
|
||||||
|
try { localStorage.removeItem(k); } catch (e) {}
|
||||||
|
});
|
||||||
|
localStorage.setItem('wp_ns_migrated_v1', '1');
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
global.ProjectData = ProjectData;
|
global.ProjectData = ProjectData;
|
||||||
})(window);
|
})(window);
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ let currentStep = 1;
|
|||||||
let sopComplete = false;
|
let sopComplete = false;
|
||||||
let allComments = [];
|
let allComments = [];
|
||||||
|
|
||||||
|
// Per-project storage key: 'wp_suite_sop' → 'wp_suite_sop__<projId>' when a
|
||||||
|
// project is active. Keeps each project's SOP separate in the browser.
|
||||||
|
function SK(base){ try { return (typeof ProjectData !== 'undefined' && ProjectData.key) ? ProjectData.key(base) : base; } catch(e){ return base; } }
|
||||||
|
|
||||||
let state = {
|
let state = {
|
||||||
project: {name:'', number:'', client:'', division:'', site:''},
|
project: {name:'', number:'', client:'', division:'', site:''},
|
||||||
team: {pm:'', apm:'', cm:'', qm:''},
|
team: {pm:'', apm:'', cm:'', qm:''},
|
||||||
@@ -126,13 +130,15 @@ window.addEventListener('DOMContentLoaded',()=>{
|
|||||||
renderStandardConstraints();
|
renderStandardConstraints();
|
||||||
renderSequenceSteps();
|
renderSequenceSteps();
|
||||||
renderSources();
|
renderSources();
|
||||||
|
// Resolve the active project FIRST so per-project storage keys are correct
|
||||||
|
// before we restore this project's SOP.
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
applyProjectContext(params.get('project'));
|
||||||
restoreSavedSOP();
|
restoreSavedSOP();
|
||||||
updateStepUI();
|
updateStepUI();
|
||||||
updateProjectDisplay();
|
updateProjectDisplay();
|
||||||
|
|
||||||
// Deep-link: ?tab=sop | ?tab=wp | ?view=dashboard | ?project=<id> from the home page.
|
// Deep-link: ?tab=sop | ?tab=wp | ?view=dashboard from the home page.
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
applyProjectContext(params.get('project'));
|
|
||||||
const tab = params.get('tab');
|
const tab = params.get('tab');
|
||||||
if(params.get('view') === 'dashboard') switchTool('wp');
|
if(params.get('view') === 'dashboard') switchTool('wp');
|
||||||
else if(tab === 'wp' || tab === 'sop') switchTool(tab);
|
else if(tab === 'wp' || tab === 'sop') switchTool(tab);
|
||||||
@@ -203,9 +209,9 @@ function loadSampleData(){
|
|||||||
function restoreSavedSOP(){
|
function restoreSavedSOP(){
|
||||||
let savedState = null, savedSop = null, complete = false;
|
let savedState = null, savedSop = null, complete = false;
|
||||||
try {
|
try {
|
||||||
complete = localStorage.getItem('wp_suite_sop_complete') === '1';
|
complete = localStorage.getItem(SK('wp_suite_sop_complete')) === '1';
|
||||||
savedState = JSON.parse(localStorage.getItem('wp_suite_state') || 'null');
|
savedState = JSON.parse(localStorage.getItem(SK('wp_suite_state')) || 'null');
|
||||||
savedSop = JSON.parse(localStorage.getItem('wp_suite_sop') || 'null');
|
savedSop = JSON.parse(localStorage.getItem(SK('wp_suite_sop')) || 'null');
|
||||||
} catch(e){}
|
} catch(e){}
|
||||||
if(!complete || !savedState) return;
|
if(!complete || !savedState) return;
|
||||||
|
|
||||||
@@ -306,15 +312,18 @@ function onSOPReady(){
|
|||||||
// record so the SOP is authored against the chosen project.
|
// record so the SOP is authored against the chosen project.
|
||||||
let activeProject = null;
|
let activeProject = null;
|
||||||
function applyProjectContext(projectId){
|
function applyProjectContext(projectId){
|
||||||
try { if(typeof ProjectData !== 'undefined') activeProject = ProjectData.getActive(); } catch(e){}
|
try {
|
||||||
if(projectId && (!activeProject || activeProject.id !== projectId)){
|
if(typeof ProjectData !== 'undefined'){
|
||||||
// Deep-linked to a project we don't have cached yet — resolve it, then prefill.
|
if(projectId && ProjectData.getActiveId() !== projectId){
|
||||||
try {
|
// Deep-linked to a project that isn't the cached active one. Seed the id
|
||||||
if(typeof ProjectData !== 'undefined'){
|
// immediately so namespaced storage keys resolve, then fetch the full record.
|
||||||
|
const cached = ProjectData.getActive();
|
||||||
|
ProjectData.setActive(cached && cached.id === projectId ? cached : { id: projectId });
|
||||||
ProjectData.get(projectId).then(p => { if(p){ activeProject = p; ProjectData.setActive(p); prefillProjectFields(); updateProjectDisplay(); } });
|
ProjectData.get(projectId).then(p => { if(p){ activeProject = p; ProjectData.setActive(p); prefillProjectFields(); updateProjectDisplay(); } });
|
||||||
}
|
}
|
||||||
} catch(e){}
|
activeProject = ProjectData.getActive();
|
||||||
}
|
}
|
||||||
|
} catch(e){}
|
||||||
prefillProjectFields();
|
prefillProjectFields();
|
||||||
}
|
}
|
||||||
function prefillProjectFields(){
|
function prefillProjectFields(){
|
||||||
@@ -713,11 +722,12 @@ function completeSOP(){
|
|||||||
sopComplete = true;
|
sopComplete = true;
|
||||||
updateProjectDisplay();
|
updateProjectDisplay();
|
||||||
|
|
||||||
// Persist for the home page (green / "Review") and for the WP Creator tab.
|
// Persist for the home page (green / "Review") and for the WP Creator tab,
|
||||||
|
// namespaced to the active project so each project keeps its own SOP.
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('wp_suite_sop', JSON.stringify(sop));
|
localStorage.setItem(SK('wp_suite_sop'), JSON.stringify(sop));
|
||||||
localStorage.setItem('wp_suite_state', JSON.stringify(state));
|
localStorage.setItem(SK('wp_suite_state'), JSON.stringify(state));
|
||||||
localStorage.setItem('wp_suite_sop_complete', '1');
|
localStorage.setItem(SK('wp_suite_sop_complete'), '1');
|
||||||
} catch(e){}
|
} catch(e){}
|
||||||
|
|
||||||
track('sop_generated', {woTypes: sop.woTypes.length, constraints: sop.constraints.length});
|
track('sop_generated', {woTypes: sop.woTypes.length, constraints: sop.constraints.length});
|
||||||
|
|||||||
@@ -758,8 +758,10 @@ function showForm(){ hideDashboard(); document.querySelectorAll('.main > .card')
|
|||||||
|
|
||||||
// ── SAVED PACKAGES ───────────────────────────────────────────────────────────
|
// ── SAVED PACKAGES ───────────────────────────────────────────────────────────
|
||||||
const STORE_KEY='wp_iwp_v1';
|
const STORE_KEY='wp_iwp_v1';
|
||||||
function saveStore(){ try{ localStorage.setItem(STORE_KEY, JSON.stringify(savedPackages)); }catch(e){} }
|
// Per-project namespaced key so each project keeps its own packages in the browser.
|
||||||
function loadStore(){ try{ const d=JSON.parse(localStorage.getItem(STORE_KEY)); if(Array.isArray(d)) savedPackages=d; }catch(e){} }
|
function wpKey(base){ try{ return (typeof ProjectData!=='undefined'&&ProjectData.key)?ProjectData.key(base):base; }catch(e){ return base; } }
|
||||||
|
function saveStore(){ try{ localStorage.setItem(wpKey(STORE_KEY), JSON.stringify(savedPackages)); }catch(e){} }
|
||||||
|
function loadStore(){ try{ const d=JSON.parse(localStorage.getItem(wpKey(STORE_KEY))); savedPackages=Array.isArray(d)?d:[]; }catch(e){ savedPackages=[]; } }
|
||||||
function renderSavedList(){
|
function renderSavedList(){
|
||||||
const card=document.getElementById('saved-card'), body=document.getElementById('saved-body');
|
const card=document.getElementById('saved-card'), body=document.getElementById('saved-body');
|
||||||
document.getElementById('saved-count').textContent=savedPackages.length?`(${savedPackages.length})`:'';
|
document.getElementById('saved-count').textContent=savedPackages.length?`(${savedPackages.length})`:'';
|
||||||
@@ -1012,15 +1014,23 @@ document.querySelectorAll('#status-group .radio-pill').forEach(p=>p.addEventList
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// ── BOOT ─────────────────────────────────────────────────────────────────────
|
// ── BOOT ─────────────────────────────────────────────────────────────────────
|
||||||
|
// Resolve the active project BEFORE loading the store so namespaced keys resolve.
|
||||||
|
(function seedProject(){
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
activeProjectId = params.get('project') || (typeof ProjectData!=='undefined' && ProjectData.getActiveId && ProjectData.getActiveId()) || '';
|
||||||
|
if(activeProjectId && typeof ProjectData!=='undefined' && ProjectData.getActiveId && ProjectData.getActiveId()!==activeProjectId){
|
||||||
|
const cached = ProjectData.getActive && ProjectData.getActive();
|
||||||
|
ProjectData.setActive(cached && cached.id===activeProjectId ? cached : { id: activeProjectId });
|
||||||
|
}
|
||||||
|
})();
|
||||||
loadStore();
|
loadStore();
|
||||||
(function bootSOP(){
|
(function bootSOP(){
|
||||||
// When embedded in the Suite, hide the SOP import/sample controls (SOP is injected)
|
// When embedded in the Suite, hide the SOP import/sample controls (SOP is injected)
|
||||||
// and prefer the SOP the Suite just completed (persisted to localStorage).
|
// and prefer the SOP the Suite just completed (persisted to localStorage).
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
if(params.get('embedded')) document.body.classList.add('embedded');
|
if(params.get('embedded')) document.body.classList.add('embedded');
|
||||||
activeProjectId = params.get('project') || (typeof ProjectData!=='undefined' && ProjectData.getActiveId && ProjectData.getActiveId()) || '';
|
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem('wp_suite_sop');
|
const raw = localStorage.getItem(wpKey('wp_suite_sop'));
|
||||||
if(raw){
|
if(raw){
|
||||||
const d = JSON.parse(raw);
|
const d = JSON.parse(raw);
|
||||||
if(d && d.woTypes){ SOP = d; applySOP(); newPackage(); return; }
|
if(d && d.woTypes){ SOP = d; applySOP(); newPackage(); return; }
|
||||||
|
|||||||
Reference in New Issue
Block a user