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.
This commit is contained in:
surfingbytes 2026-06-25 09:47:45 +00:00
parent 0aa3952e34
commit 34d67823cb
5 changed files with 103 additions and 83 deletions

View File

@ -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)

View File

@ -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<RouteLocationRaw | undefined> {
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
}
}

View File

@ -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)
}

View File

@ -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')

View File

@ -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,
},
})