From 34d67823cba89a533868183c153e0a3cf2170b8a Mon Sep 17 00:00:00 2001 From: surfingbytes Date: Thu, 25 Jun 2026 09:47:45 +0000 Subject: [PATCH] fix: honor default project landing page Share default landing-page resolution across startup and login redirects so default projects are loaded before navigation, while preserving explicit Overview navigation. --- .../composables/useRedirectToLastVisited.ts | 38 +++-------------- frontend/src/helpers/getDefaultPageRoute.ts | 38 +++++++++++++++++ frontend/src/router/index.ts | 41 ++----------------- .../e2e/misc/default-landing-page.spec.ts | 41 ++++++++++++++++--- .../tests/e2e/project/project-history.spec.ts | 28 +++++++++---- 5 files changed, 103 insertions(+), 83 deletions(-) create mode 100644 frontend/src/helpers/getDefaultPageRoute.ts diff --git a/frontend/src/composables/useRedirectToLastVisited.ts b/frontend/src/composables/useRedirectToLastVisited.ts index 56067c8f5..0811b9d24 100644 --- a/frontend/src/composables/useRedirectToLastVisited.ts +++ b/frontend/src/composables/useRedirectToLastVisited.ts @@ -1,8 +1,7 @@ import {useRouter} from 'vue-router' -import {getLastVisited, clearLastVisited, getLastVisitedPage} from '@/helpers/saveLastVisited' -import {useAuthStore} from '@/stores/auth' -import {useProjectStore} from '@/stores/projects' -import {DEFAULT_PAGE} from '@/constants/defaultPage' + +import {getDefaultPageRoute} from '@/helpers/getDefaultPageRoute' +import {getLastVisited, clearLastVisited} from '@/helpers/saveLastVisited' export function useRedirectToLastVisited() { @@ -22,37 +21,10 @@ export function useRedirectToLastVisited() { } } - function getDefaultPageRoute() { - const authStore = useAuthStore() - const projectStore = useProjectStore() - const defaultPage = authStore.settings?.frontendSettings?.defaultPage - - switch (defaultPage) { - case DEFAULT_PAGE.UPCOMING: - return {name: 'tasks.range'} - case DEFAULT_PAGE.DEFAULT_PROJECT: { - const projectId = authStore.settings?.defaultProjectId - if (projectId && projectStore.projects[projectId]) { - return {name: 'project.index', params: {projectId}} - } - return {name: 'home'} - } - case DEFAULT_PAGE.LAST_VISITED: { - const last = getLastVisitedPage() - if (last) { - return {name: last.name, params: last.params, query: last.query} - } - return {name: 'home'} - } - default: - return {name: 'home'} - } - } - - function redirectIfSaved() { + async function redirectIfSaved() { const lastRoute = getLastVisitedRoute() if (!lastRoute) { - return router.push(getDefaultPageRoute()) + return router.push(await getDefaultPageRoute() ?? {name: 'home'}) } return router.push(lastRoute) diff --git a/frontend/src/helpers/getDefaultPageRoute.ts b/frontend/src/helpers/getDefaultPageRoute.ts new file mode 100644 index 000000000..e3d5b35a9 --- /dev/null +++ b/frontend/src/helpers/getDefaultPageRoute.ts @@ -0,0 +1,38 @@ +import type {RouteLocationRaw} from 'vue-router' + +import {DEFAULT_PAGE} from '@/constants/defaultPage' +import {getLastVisitedPage} from '@/helpers/saveLastVisited' +import {useAuthStore} from '@/stores/auth' +import {useProjectStore} from '@/stores/projects' + +export async function getDefaultPageRoute(): Promise { + const authStore = useAuthStore() + const projectStore = useProjectStore() + const defaultPage = authStore.settings?.frontendSettings?.defaultPage + + switch (defaultPage) { + case DEFAULT_PAGE.UPCOMING: + return {name: 'tasks.range'} + case DEFAULT_PAGE.DEFAULT_PROJECT: { + const projectId = authStore.settings?.defaultProjectId + if (projectId) { + try { + await projectStore.loadProject(projectId) + return {name: 'project.index', params: {projectId}} + } catch { + return undefined + } + } + return undefined + } + case DEFAULT_PAGE.LAST_VISITED: { + const last = getLastVisitedPage() + if (last) { + return {name: last.name, params: last.params, query: last.query} + } + return undefined + } + default: + return undefined + } +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 1bdb279f0..445dbc931 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,6 +1,7 @@ import { createRouter, createWebHistory } from 'vue-router' import type { RouteLocation } from 'vue-router' -import {saveLastVisited, saveLastVisitedPage, getLastVisitedPage} from '@/helpers/saveLastVisited' +import {saveLastVisited, saveLastVisitedPage} from '@/helpers/saveLastVisited' +import {getDefaultPageRoute} from '@/helpers/getDefaultPageRoute' import {getProjectViewId} from '@/helpers/projectView' import {parseDateOrString} from '@/helpers/time/parseDateOrString' @@ -8,11 +9,9 @@ import {getNextWeekDate} from '@/helpers/time/getNextWeekDate' import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash' import {REDIRECT_HASH_PREFIX} from '@/constants/redirectHash' import {AUTH_ROUTE_NAMES} from '@/constants/authRouteNames' -import {DEFAULT_PAGE} from '@/constants/defaultPage' import {PRO_FEATURE} from '@/constants/proFeatures' import {useAuthStore} from '@/stores/auth' -import {useProjectStore} from '@/stores/projects' import {useBaseStore} from '@/stores/base' import {useConfigStore} from '@/stores/config' @@ -53,39 +52,7 @@ const router = createRouter({ return } - const redirectKey = 'defaultPageRedirectDone' - if (sessionStorage.getItem(redirectKey)) { - return - } - sessionStorage.setItem(redirectKey, 'true') - - const authStore = useAuthStore() - const projectStore = useProjectStore() - const defaultPage = authStore.settings?.frontendSettings?.defaultPage - - switch (defaultPage) { - case DEFAULT_PAGE.UPCOMING: - return {name: 'tasks.range'} - case DEFAULT_PAGE.DEFAULT_PROJECT: { - const projectId = authStore.settings?.defaultProjectId - if (projectId) { - try { - await projectStore.loadProject(projectId) - return {name: 'project.index', params: {projectId}} - } catch { - break - } - } - break - } - case DEFAULT_PAGE.LAST_VISITED: { - const last = getLastVisitedPage() - if (last) { - return {name: last.name, params: last.params, query: last.query} - } - break - } - } + return getDefaultPageRoute() }, }, { @@ -578,7 +545,7 @@ export async function getAuthForRoute(to: RouteLocation, authStore) { const isValidUserAppRoute = !AUTH_ROUTE_NAMES.has(to.name as string) && localStorage.getItem('emailConfirmToken') === null - if (isValidUserAppRoute) { + if (isValidUserAppRoute && to.name !== 'home') { saveLastVisited(to.name as string, to.params, to.query) } diff --git a/frontend/tests/e2e/misc/default-landing-page.spec.ts b/frontend/tests/e2e/misc/default-landing-page.spec.ts index cac1ed1cf..1f7490fb5 100644 --- a/frontend/tests/e2e/misc/default-landing-page.spec.ts +++ b/frontend/tests/e2e/misc/default-landing-page.spec.ts @@ -2,9 +2,10 @@ import {test, expect} from '../../support/fixtures' import {ProjectFactory} from '../../factories/project' import {UserFactory} from '../../factories/user' import {TaskFactory} from '../../factories/task' -import {login} from '../../support/authenticateUser' +import {login as apiLogin, setupApiUrl} from '../../support/authenticateUser' import {updateUserSettings} from '../../support/updateUserSettings' import {createDefaultViews} from '../project/prepareProjects' +import {TEST_PASSWORD} from '../../support/constants' test.describe('Default Landing Page', () => { test('shows overview page with default settings when no last visited page exists', async ({authenticatedPage: page}) => { @@ -19,7 +20,7 @@ test.describe('Default Landing Page', () => { frontend_settings: JSON.stringify({defaultPage: 'upcoming'}), }))[0] await ProjectFactory.create(1, {owner_id: user.id}) - await login(page, apiContext, user) + await apiLogin(page, apiContext, user) await page.goto('/') await page.waitForURL('**/tasks/by/upcoming**') @@ -32,7 +33,7 @@ test.describe('Default Landing Page', () => { const project = (await ProjectFactory.create(1, {owner_id: user.id}))[0] await createDefaultViews(project.id) - const {token} = await login(page, apiContext, user) + const {token} = await apiLogin(page, apiContext, user) await updateUserSettings(apiContext, token, { default_project_id: project.id, @@ -43,13 +44,41 @@ test.describe('Default Landing Page', () => { await page.waitForURL(`**/projects/${project.id}/**`) }) + test('redirects to default project after logging in from root', async ({page, apiContext}) => { + const user = (await UserFactory.create(1, { + frontend_settings: JSON.stringify({defaultPage: 'defaultProject'}), + }))[0] + const project = (await ProjectFactory.create(1, {owner_id: user.id}))[0] + await createDefaultViews(project.id) + + const {token} = await apiLogin(null, apiContext, user) + await updateUserSettings(apiContext, token, { + default_project_id: project.id, + overdue_tasks_reminders_time: '9:00', + }) + + await setupApiUrl(page) + await page.goto('/') + await expect(page).toHaveURL(/\/login/) + + await page.locator('input[id=username]').fill(user.username) + await page.locator('input[id=password]').fill(TEST_PASSWORD) + await page.locator('.button').filter({hasText: 'Login'}).click() + + await page.waitForURL(`**/projects/${project.id}/**`) + + await page.locator('nav.menu.top-menu a').filter({hasText: 'Overview'}).click() + await page.waitForLoadState('networkidle') + await expect(page).toHaveURL('/') + }) + test('falls back to overview when default project does not exist', async ({page, apiContext}) => { const user = (await UserFactory.create(1, { frontend_settings: JSON.stringify({defaultPage: 'defaultProject'}), }))[0] await ProjectFactory.create(1, {owner_id: user.id}) - const {token} = await login(page, apiContext, user) + const {token} = await apiLogin(page, apiContext, user) await updateUserSettings(apiContext, token, { default_project_id: 999999, @@ -69,7 +98,7 @@ test.describe('Default Landing Page', () => { const views = await createDefaultViews(project.id) await TaskFactory.create(1, {project_id: project.id}) - await login(page, apiContext, user) + await apiLogin(page, apiContext, user) await page.goto(`/projects/${project.id}/${views[0].id}`) await page.waitForLoadState('networkidle') @@ -86,7 +115,7 @@ test.describe('Default Landing Page', () => { await createDefaultViews(project.id) await TaskFactory.create(1, {project_id: project.id}) - await login(page, apiContext, user) + await apiLogin(page, apiContext, user) await page.goto(`/projects/${project.id}/1`) await page.waitForLoadState('networkidle') diff --git a/frontend/tests/e2e/project/project-history.spec.ts b/frontend/tests/e2e/project/project-history.spec.ts index 51a4caa5c..0e9e9a8cf 100644 --- a/frontend/tests/e2e/project/project-history.spec.ts +++ b/frontend/tests/e2e/project/project-history.spec.ts @@ -4,7 +4,16 @@ import {ProjectViewFactory} from '../../factories/project_view' import {updateUserSettings} from '../../support/updateUserSettings' import type {Page} from '@playwright/test' -async function visitProjectsToBuildHistory(page: Page, projects: any[]) { +interface ProjectHistoryEntry { + id: number +} + +interface ProjectSeed { + id: number + title: string +} + +async function visitProjectsToBuildHistory(page: Page, projects: ProjectSeed[]) { for (const project of projects) { const loadProjectPromise = page.waitForResponse(response => response.url().includes(`/projects/${project.id}`) && response.request().method() === 'GET', @@ -14,7 +23,7 @@ async function visitProjectsToBuildHistory(page: Page, projects: any[]) { await page.waitForFunction( (projectId) => { const history = JSON.parse(localStorage.getItem('projectHistory') || '[]') - return history.some((h: any) => h.id === projectId) + return history.some((h: ProjectHistoryEntry) => h.id === projectId) }, project.id, ) @@ -47,7 +56,7 @@ test.describe('Project History', () => { await page.waitForFunction( (projectId) => { const history = JSON.parse(localStorage.getItem('projectHistory') || '[]') - return history.some((h: any) => h.id === projectId) + return history.some((h: ProjectHistoryEntry) => h.id === projectId) }, projects[i].id, ) @@ -76,11 +85,16 @@ test.describe('Project History', () => { }, false) } - // Navigate to home first so the default-page redirect guard is primed - // (sets sessionStorage flag), preventing later page.goto('/') from - // redirecting to the last visited project. + // Keep reloads on the overview page while this test focuses on the + // last-viewed section visibility. await page.goto('/') await page.waitForLoadState('networkidle') + const token = await page.evaluate(() => localStorage.getItem('token')) + await updateUserSettings(apiContext, token!, { + frontendSettings: { + defaultPage: 'overview', + }, + }) // Visit projects to build up history await visitProjectsToBuildHistory(page, projects) @@ -90,7 +104,6 @@ test.describe('Project History', () => { await expect(page.locator('body')).toContainText('Last viewed') // Disable the setting via API - const token = await page.evaluate(() => localStorage.getItem('token')) await updateUserSettings(apiContext, token!, { frontendSettings: { showLastViewed: false, @@ -122,6 +135,7 @@ test.describe('Project History', () => { const token = await page.evaluate(() => localStorage.getItem('token')) await updateUserSettings(apiContext, token!, { frontendSettings: { + defaultPage: 'overview', showLastViewed: false, }, })