Keep Issues and artist detail history stable
- Route Issues to the React host even while the shell is still booting - Ignore stale bootstrap work when navigation changes mid-load - Clear artist-detail state when leaving the page so browser back can reach Library - Add smoke coverage for the artist-detail back-navigation path
This commit is contained in:
parent
8d6ab4eb74
commit
50c2d6882c
6 changed files with 92 additions and 4 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
// SoulSync WebUI JavaScript - Replicating PyQt6 GUI Functionality
|
// SoulSync WebUI JavaScript - Replicating PyQt6 GUI Functionality
|
||||||
|
|
||||||
|
const PAGE_WILL_CHANGE_EVENT = 'ss:webui-page-will-change';
|
||||||
|
|
||||||
// Global state management
|
// Global state management
|
||||||
let currentPage = 'dashboard';
|
let currentPage = 'dashboard';
|
||||||
let currentTrack = null;
|
let currentTrack = null;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,20 @@
|
||||||
// INITIALIZATION
|
// INITIALIZATION
|
||||||
// ===============================
|
// ===============================
|
||||||
|
let navigationEpoch = 0;
|
||||||
|
|
||||||
|
function notifyPageWillChange(nextPageId) {
|
||||||
|
const fromPageId = typeof currentPage === 'string' ? currentPage : null;
|
||||||
|
if (fromPageId === nextPageId) return;
|
||||||
|
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(PAGE_WILL_CHANGE_EVENT, {
|
||||||
|
detail: {
|
||||||
|
fromPageId,
|
||||||
|
toPageId: nextPageId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Accent Color System ----
|
// ---- Accent Color System ----
|
||||||
|
|
||||||
|
|
@ -2309,6 +2324,8 @@ function initializeWatchlist() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateToPage(pageId, options = {}) {
|
function navigateToPage(pageId, options = {}) {
|
||||||
|
navigationEpoch += 1;
|
||||||
|
|
||||||
if (!options.forceReload && pageId === currentPage) return;
|
if (!options.forceReload && pageId === currentPage) return;
|
||||||
|
|
||||||
// Permission guard — redirect to home page if not allowed
|
// Permission guard — redirect to home page if not allowed
|
||||||
|
|
@ -2322,11 +2339,25 @@ function navigateToPage(pageId, options = {}) {
|
||||||
|
|
||||||
const router = getWebRouter();
|
const router = getWebRouter();
|
||||||
if (router && !options.skipRouteChange) {
|
if (router && !options.skipRouteChange) {
|
||||||
|
notifyPageWillChange(pageId);
|
||||||
|
const route = router.routeManifest?.find((entry) => entry.pageId === pageId);
|
||||||
|
if (route?.kind === 'react') {
|
||||||
|
showReactHost(pageId);
|
||||||
|
setActivePageChrome(pageId);
|
||||||
|
}
|
||||||
return router.navigateToPage(pageId, { replace: options.replace === true });
|
return router.navigateToPage(pageId, { replace: options.replace === true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback path for initial bootstrap or environments without TanStack routing.
|
// Fallback path for initial bootstrap or environments without TanStack routing.
|
||||||
activatePage(pageId, { forceReload: options.forceReload === true });
|
const route = router?.routeManifest?.find((entry) => entry.pageId === pageId);
|
||||||
|
notifyPageWillChange(pageId);
|
||||||
|
const legacyPageElement = document.getElementById(`${pageId}-page`);
|
||||||
|
if (route?.kind === 'react' || !legacyPageElement) {
|
||||||
|
showReactHost(pageId);
|
||||||
|
setActivePageChrome(pageId);
|
||||||
|
} else {
|
||||||
|
activatePage(pageId, { forceReload: options.forceReload === true });
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.skipPushState) {
|
if (!options.skipPushState) {
|
||||||
const urlPath = pageId === 'dashboard' ? '/' : '/' + pageId;
|
const urlPath = pageId === 'dashboard' ? '/' : '/' + pageId;
|
||||||
|
|
|
||||||
|
|
@ -700,6 +700,27 @@ let artistDetailPageState = {
|
||||||
enhancedTrackSort: {}
|
enhancedTrackSort: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function clearArtistDetailPageState() {
|
||||||
|
if (artistDetailPageState.completionController) {
|
||||||
|
artistDetailPageState.completionController.abort();
|
||||||
|
artistDetailPageState.completionController = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
artistDetailPageState.currentArtistId = null;
|
||||||
|
artistDetailPageState.currentArtistName = null;
|
||||||
|
artistDetailPageState.currentArtistSource = null;
|
||||||
|
artistDetailPageState.originStack = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener(PAGE_WILL_CHANGE_EVENT, (event) => {
|
||||||
|
const detail = event.detail || {};
|
||||||
|
if (detail.fromPageId === 'artist-detail' && detail.toPageId !== 'artist-detail') {
|
||||||
|
clearArtistDetailPageState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Discography filter state
|
// Discography filter state
|
||||||
let discographyFilterState = {
|
let discographyFilterState = {
|
||||||
categories: { albums: true, eps: true, singles: true },
|
categories: { albums: true, eps: true, singles: true },
|
||||||
|
|
|
||||||
|
|
@ -1161,6 +1161,9 @@ async function startDownload(index) {
|
||||||
|
|
||||||
async function loadInitialData() {
|
async function loadInitialData() {
|
||||||
try {
|
try {
|
||||||
|
const initialPath = window.location.pathname;
|
||||||
|
const initialNavigationEpoch = navigationEpoch;
|
||||||
|
|
||||||
// Load artist bubble state first
|
// Load artist bubble state first
|
||||||
await hydrateArtistBubblesFromSnapshot();
|
await hydrateArtistBubblesFromSnapshot();
|
||||||
|
|
||||||
|
|
@ -1177,6 +1180,10 @@ async function loadInitialData() {
|
||||||
? urlPage
|
? urlPage
|
||||||
: homePage;
|
: homePage;
|
||||||
|
|
||||||
|
if (window.location.pathname !== initialPath || navigationEpoch !== initialNavigationEpoch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Always apply the target page to the legacy shell chrome.
|
// Always apply the target page to the legacy shell chrome.
|
||||||
const router = getWebRouter();
|
const router = getWebRouter();
|
||||||
const route = router?.routeManifest?.find((entry) => entry.pageId === targetPage);
|
const route = router?.routeManifest?.find((entry) => entry.pageId === targetPage);
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ function activateLegacyPath(pathname) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyPageWillChange(targetPage);
|
||||||
activatePage(targetPage, { forceReload: true });
|
activatePage(targetPage, { forceReload: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,6 +78,7 @@ function syncActivePageFromLocation() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyPageWillChange(targetPage);
|
||||||
const route = router?.routeManifest?.find((entry) => entry.pageId === targetPage);
|
const route = router?.routeManifest?.find((entry) => entry.pageId === targetPage);
|
||||||
if (route?.kind === 'react') {
|
if (route?.kind === 'react') {
|
||||||
showReactHost(targetPage);
|
showReactHost(targetPage);
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,11 @@ async function selectProfile(page: Page, baseURL: string, profileId = 1) {
|
||||||
async function waitForShellRoute(page: Page, pageId: string) {
|
async function waitForShellRoute(page: Page, pageId: string) {
|
||||||
if (pageId === 'issues') {
|
if (pageId === 'issues') {
|
||||||
await expect
|
await expect
|
||||||
.poll(async () =>
|
.poll(async () => page.evaluate(() => document.querySelector('.page.active')?.id ?? ''), {
|
||||||
page.evaluate(() => document.querySelector('.page.active')?.id ?? ''),
|
timeout: 15000,
|
||||||
)
|
})
|
||||||
.toBe('webui-react-root');
|
.toBe('webui-react-root');
|
||||||
|
await expect(page.getByTestId('issues-board')).toBeVisible({ timeout: 15000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,3 +107,27 @@ test('browser history restores top-level routes', async ({ page, baseURL }) => {
|
||||||
await waitForShellRoute(page, 'issues');
|
await waitForShellRoute(page, 'issues');
|
||||||
await expect(page).toHaveURL(/\/issues(?:\?status=open&category=all)?$/);
|
await expect(page).toHaveURL(/\/issues(?:\?status=open&category=all)?$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('browser history leaves artist detail when going back to library', async ({
|
||||||
|
page,
|
||||||
|
baseURL,
|
||||||
|
}) => {
|
||||||
|
if (!baseURL) {
|
||||||
|
test.skip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await selectProfile(page, baseURL);
|
||||||
|
|
||||||
|
await page.goto(new URL('/library', baseURL).toString(), { waitUntil: 'domcontentloaded' });
|
||||||
|
await waitForShellRoute(page, 'library');
|
||||||
|
await expect.poll(async () => page.locator('.library-artist-card').count()).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
await page.locator('.library-artist-card').first().click();
|
||||||
|
await waitForShellRoute(page, 'artist-detail');
|
||||||
|
await expect(page).toHaveURL(/\/artist-detail$/);
|
||||||
|
|
||||||
|
await page.goBack();
|
||||||
|
await waitForShellRoute(page, 'library');
|
||||||
|
await expect(page).toHaveURL(/\/library$/);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue