You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When the user navigates from the viewer or editor to a sibling page (Project Settings, Pull Requests, Analytics, Suggestions, Dashboard) and then clicks Back to project, the state of the viewer/editor they came from is lost:
The viewer-vs-editor mode they were in resets to viewer (the bare URL).
The selected class/property/individual is dropped.
The active entity tab (Classes / Properties / Individuals) resets to Classes.
(Branch is preserved because it lives in sessionStorage; editor mode Standard/Developer is preserved because it lives in the editor-mode Zustand store.)
This makes round-trips through Settings or PRs feel like a hard reset of the user's place in the ontology.
Where the bug is
Every "Back to project" / "Back to projects" link in the side pages is a hard-coded Link to a bare URL:
app/projects/[id]/pull-requests/page.tsx:75 — same
app/projects/[id]/analytics/page.tsx:48 — same
app/projects/[id]/dashboard/page.tsx, app/projects/[id]/suggestions/page.tsx, app/projects/[id]/suggestions/review/page.tsx, app/projects/[id]/pull-requests/[prNumber]/page.tsx — comparable links to /
None of them carry the selection (?classIri= / ?propertyIri= / ?individualIri=) or the mode (/editor vs the bare path).
Proposed approach
Persist the user's most recent in-project URL (path + search params) per-project to sessionStorage, and have a small useProjectReturnHref(projectId) hook return that URL (with a sensible fallback to /projects/${projectId}).
1. Hook implementation
// lib/hooks/useProjectReturnHref.tsconstSTORAGE_KEY=(projectId: string)=>`ontokit:lastProjectUrl:${projectId}`;/** Track the current viewer/editor URL so cross-page nav can return to it. */exportfunctionuseTrackProjectReturnUrl(projectId: string){constpathname=usePathname();constsearchParams=useSearchParams();useEffect(()=>{if(!pathname)return;// Only track viewer or editor pages — not settings / PRs / etc.constisProjectPage=pathname===`/projects/${projectId}`||pathname===`/projects/${projectId}/editor`;if(!isProjectPage)return;constsearch=searchParams.toString();consturl=search ? `${pathname}?${search}` : pathname;try{sessionStorage.setItem(STORAGE_KEY(projectId),url);}catch{}},[projectId,pathname,searchParams]);}/** Return the last-known viewer/editor URL for this project, or fallback. */exportfunctionuseProjectReturnHref(projectId: string): string{constfallback=`/projects/${projectId}`;const[href,setHref]=useState(fallback);useEffect(()=>{try{conststored=sessionStorage.getItem(STORAGE_KEY(projectId));if(stored)setHref(stored);}catch{}},[projectId]);returnhref;}
2. Wiring
In app/projects/[id]/page.tsx and app/projects/[id]/editor/page.tsx, call useTrackProjectReturnUrl(projectId) so every URL change is mirrored to sessionStorage. The selection-store sync I already added in this PR fires on render anyway, so this is one more cheap effect.
In each side page (Settings, PRs, Analytics, Dashboard, Suggestions), replace the hard-coded href={\/projects/${projectId}`}on the **Back to project** link withhref={useProjectReturnHref(projectId)}`.
3. Fallback behavior
First visit (no stored URL): the hook returns /projects/${projectId}, which is today's behavior. Zero regression for users without prior session state.
Cross-tab: sessionStorage is per-tab, so each tab tracks its own most-recent state. Closing and reopening a tab loses the breadcrumb — acceptable.
Server render: hook returns the fallback synchronously (no sessionStorage on the server) and updates after mount. The link briefly points to the fallback for ~1 frame, then resolves to the stored URL. Good enough; we can polish later if it shows.
Done criteria
Selecting a class in the editor, navigating to Settings, and clicking Back to project lands back in the editor with the same class selected.
Same for property and individual selection.
Same when starting from the viewer instead of the editor.
First-visit / no-history fallback to /projects/${projectId} is unchanged.
No regression for users who deep-link directly to a side page (Back goes to the bare project URL because no breadcrumb was stored).
Out of scope
Restoring the active entity tab (Classes / Properties / Individuals) explicitly via URL. The selection store + the URL's ?<type>Iri= already pin the active tab indirectly via the in-page entityNavigationRef dispatch. If that turns out to be insufficient (e.g., user navigated to Properties tab without selecting a property), we can add an &tab=properties hint in a follow-up.
Restoring the editor's expanded class-tree state. Tree expansion is local component state; persisting it is a separate, larger feature.
Cross-tab restoration via localStorage. Out of scope; sessionStorage is the right scope for "where I was just now".
Related
PR feat: preserve entity selection across viewer/editor modes #104 (this branch) — established the URL contract for entity selection (?classIri= / ?propertyIri= / ?individualIri=) and the viewer/editor switcher. This bug is a sibling concern that became visible once selection-via-URL started actually working.
Could borrow the useSelectionStore pattern from this PR — but the return-URL is broader than just selection (also mode + tab), so a separate per-project breadcrumb is cleaner than overloading the selection store.
Problem
When the user navigates from the viewer or editor to a sibling page (Project Settings, Pull Requests, Analytics, Suggestions, Dashboard) and then clicks Back to project, the state of the viewer/editor they came from is lost:
This makes round-trips through Settings or PRs feel like a hard reset of the user's place in the ontology.
Where the bug is
Every "Back to project" / "Back to projects" link in the side pages is a hard-coded
Linkto a bare URL:app/projects/[id]/settings/page.tsx:910, 950—href={\/projects/${projectId}`}`app/projects/[id]/pull-requests/page.tsx:75— sameapp/projects/[id]/analytics/page.tsx:48— sameapp/projects/[id]/dashboard/page.tsx,app/projects/[id]/suggestions/page.tsx,app/projects/[id]/suggestions/review/page.tsx,app/projects/[id]/pull-requests/[prNumber]/page.tsx— comparable links to/None of them carry the selection (
?classIri=/?propertyIri=/?individualIri=) or the mode (/editorvs the bare path).Proposed approach
Persist the user's most recent in-project URL (path + search params) per-project to
sessionStorage, and have a smalluseProjectReturnHref(projectId)hook return that URL (with a sensible fallback to/projects/${projectId}).1. Hook implementation
2. Wiring
In
app/projects/[id]/page.tsxandapp/projects/[id]/editor/page.tsx, calluseTrackProjectReturnUrl(projectId)so every URL change is mirrored to sessionStorage. The selection-store sync I already added in this PR fires on render anyway, so this is one more cheap effect.In each side page (Settings, PRs, Analytics, Dashboard, Suggestions), replace the hard-coded
href={\/projects/${projectId}`}on the **Back to project** link withhref={useProjectReturnHref(projectId)}`.3. Fallback behavior
/projects/${projectId}, which is today's behavior. Zero regression for users without prior session state.Done criteria
/projects/${projectId}is unchanged.Out of scope
?<type>Iri=already pin the active tab indirectly via the in-page entityNavigationRef dispatch. If that turns out to be insufficient (e.g., user navigated to Properties tab without selecting a property), we can add an&tab=propertieshint in a follow-up.localStorage. Out of scope; sessionStorage is the right scope for "where I was just now".Related
?classIri=/?propertyIri=/?individualIri=) and the viewer/editor switcher. This bug is a sibling concern that became visible once selection-via-URL started actually working.useSelectionStorepattern from this PR — but the return-URL is broader than just selection (also mode + tab), so a separate per-project breadcrumb is cleaner than overloading the selection store.