This repository has been archived on 2024-02-08. You can view files and clone it, but cannot push or open issues or pull requests.
frontend/src/router/index.ts

538 lines
16 KiB
TypeScript
Raw Normal View History

2022-04-11 20:08:28 +00:00
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteLocation } from 'vue-router'
import {saveLastVisited} from '@/helpers/saveLastVisited'
import {saveProjectView, getProjectView} from '@/helpers/projectView'
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
import {setTitle} from '@/helpers/setTitle'
2023-04-01 13:17:09 +00:00
import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash'
import {useProjectStore} from '@/stores/projects'
2022-09-21 01:37:39 +00:00
import {useAuthStore} from '@/stores/auth'
import {useBaseStore} from '@/stores/base'
2021-11-14 20:33:53 +00:00
import HomeComponent from '@/views/Home.vue'
import NotFoundComponent from '@/views/404.vue'
const About = () => import('@/views/About.vue')
2018-09-08 20:27:13 +00:00
// User Handling
import LoginComponent from '@/views/user/Login.vue'
import RegisterComponent from '@/views/user/Register.vue'
import OpenIdAuth from '@/views/user/OpenIdAuth.vue'
const DataExportDownload = () => import('@/views/user/DataExportDownload.vue')
// Tasks
import UpcomingTasksComponent from '@/views/tasks/ShowTasks.vue'
import LinkShareAuthComponent from '@/views/sharing/LinkSharingAuth.vue'
const TaskDetailView = () => import('@/views/tasks/TaskDetailView.vue')
// Team Handling
const ListTeamsComponent = () => import('@/views/teams/ListTeams.vue')
// Label Handling
const ListLabelsComponent = () => import('@/views/labels/ListLabels.vue')
const NewLabelComponent = () => import('@/views/labels/NewLabel.vue')
// Migration
const MigrationComponent = () => import('@/views/migrate/Migration.vue')
const MigrationHandlerComponent = () => import('@/views/migrate/MigrationHandler.vue')
// Project Views
const ProjectList = () => import('@/views/project/ProjectList.vue')
const ProjectGantt = () => import('@/views/project/ProjectGantt.vue')
const ProjectTable = () => import('@/views/project/ProjectTable.vue')
// If we load the component async, using it as a backdrop view will not work. Instead, everything explodes
// with an error from the core saying "Cannot read properties of undefined (reading 'parentNode')"
// Of course, with no clear indicator of where the problem comes from.
// const ProjectKanban = () => import('@/views/project/ProjectKanban.vue')
import ProjectKanban from '@/views/project/ProjectKanban.vue'
const ProjectInfo = () => import('@/views/project/ProjectInfo.vue')
2021-11-01 17:19:59 +00:00
// Project Settings
const ListProjects = () => import('@/views/project/ListProjects.vue')
const ProjectSettingEdit = () => import('@/views/project/settings/edit.vue')
const ProjectSettingBackground = () => import('@/views/project/settings/background.vue')
const ProjectSettingDuplicate = () => import('@/views/project/settings/duplicate.vue')
const ProjectSettingShare = () => import('@/views/project/settings/share.vue')
const ProjectSettingWebhooks = () => import('@/views/project/settings/webhooks.vue')
const ProjectSettingDelete = () => import('@/views/project/settings/delete.vue')
const ProjectSettingArchive = () => import('@/views/project/settings/archive.vue')
// Saved Filters
const FilterNew = () => import('@/views/filters/FilterNew.vue')
const FilterEdit = () => import('@/views/filters/FilterEdit.vue')
const FilterDelete = () => import('@/views/filters/FilterDelete.vue')
2020-07-27 17:53:19 +00:00
const PasswordResetComponent = () => import('@/views/user/PasswordReset.vue')
const GetPasswordResetComponent = () => import('@/views/user/RequestPasswordReset.vue')
const UserSettingsComponent = () => import('@/views/user/Settings.vue')
const UserSettingsAvatarComponent = () => import('@/views/user/settings/Avatar.vue')
const UserSettingsCaldavComponent = () => import('@/views/user/settings/Caldav.vue')
const UserSettingsDataExportComponent = () => import('@/views/user/settings/DataExport.vue')
const UserSettingsDeletionComponent = () => import('@/views/user/settings/Deletion.vue')
const UserSettingsEmailUpdateComponent = () => import('@/views/user/settings/EmailUpdate.vue')
const UserSettingsGeneralComponent = () => import('@/views/user/settings/General.vue')
const UserSettingsPasswordUpdateComponent = () => import('@/views/user/settings/PasswordUpdate.vue')
const UserSettingsTOTPComponent = () => import('@/views/user/settings/TOTP.vue')
const UserSettingsApiTokensComponent = () => import('@/views/user/settings/ApiTokens.vue')
// Project Handling
const NewProjectComponent = () => import('@/views/project/NewProject.vue')
const EditTeamComponent = () => import('@/views/teams/EditTeam.vue')
const NewTeamComponent = () => import('@/views/teams/NewTeam.vue')
2021-08-20 13:17:19 +00:00
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
Kanban (#118) Add error message when trying to create an invalid new task in a bucket Prevent creation of new buckets if the bucket title is empty Disable deleting a bucket if it's the last one Disable dragging tasks when they are being updated Fix transition when opening tasks Send the user to list view by default Show loading spinner when updating multiple tasks Add loading spinner when moving tasks Add loading animation when bucket is loading / updating etc Add bucket title edit Fix creating new buckets Add loading animation Add removing buckets Fix creating a new bucket after tasks were moved Fix warning about labels on tasks Fix labels on tasks not updating after retrieval from api Fix property width Add closing and mobile design Make the task detail popup look good Move list views Move task detail view in a popup Add link to tasks Add saving the new task position after it was moved Fix creating new bucket Fix creating a new task Cleanup Disable user selection for task cards Fix drag placeholder Add dragging style to task Add placeholder + change animation duration More cleanup Cleanup / docs Working of dragging and dropping tasks Adjust markup and styling for new library Change kanban library to something that works Add basic calculation of new positions Don't try to create empty tasks Add indicator if a task is done Add moving tasks between buckets Make empty buckets a little smaller Add gimmick for button description Fix color Fix scrolling bucket layout Add creating a new bucket Add hiding the task input field Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/118
2020-04-25 23:11:34 +00:00
scrollBehavior(to, from, savedPosition) {
2019-10-20 19:40:44 +00:00
// If the user is using their forward/backward keys to navigate, we want to restore the scroll view
Kanban (#118) Add error message when trying to create an invalid new task in a bucket Prevent creation of new buckets if the bucket title is empty Disable deleting a bucket if it's the last one Disable dragging tasks when they are being updated Fix transition when opening tasks Send the user to list view by default Show loading spinner when updating multiple tasks Add loading spinner when moving tasks Add loading animation when bucket is loading / updating etc Add bucket title edit Fix creating new buckets Add loading animation Add removing buckets Fix creating a new bucket after tasks were moved Fix warning about labels on tasks Fix labels on tasks not updating after retrieval from api Fix property width Add closing and mobile design Make the task detail popup look good Move list views Move task detail view in a popup Add link to tasks Add saving the new task position after it was moved Fix creating new bucket Fix creating a new task Cleanup Disable user selection for task cards Fix drag placeholder Add dragging style to task Add placeholder + change animation duration More cleanup Cleanup / docs Working of dragging and dropping tasks Adjust markup and styling for new library Change kanban library to something that works Add basic calculation of new positions Don't try to create empty tasks Add indicator if a task is done Add moving tasks between buckets Make empty buckets a little smaller Add gimmick for button description Fix color Fix scrolling bucket layout Add creating a new bucket Add hiding the task input field Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/118
2020-04-25 23:11:34 +00:00
if (savedPosition) {
2019-10-20 19:40:44 +00:00
return savedPosition
}
// Scroll to anchor should still work
if (to.hash && !to.hash.startsWith(LINK_SHARE_HASH_PREFIX)) {
2022-01-30 15:48:02 +00:00
return {el: to.hash}
2019-10-20 19:40:44 +00:00
}
// Otherwise just scroll to the top
2021-08-20 13:17:19 +00:00
return {left: 0, top: 0}
2019-10-20 19:40:44 +00:00
},
2019-03-07 19:48:40 +00:00
routes: [
{
path: '/',
name: 'home',
component: HomeComponent,
2019-03-07 19:48:40 +00:00
},
2020-03-02 19:30:49 +00:00
{
2021-08-20 13:17:19 +00:00
path: '/:pathMatch(.*)*',
name: 'not-found',
component: NotFoundComponent,
},
// if you omit the last `*`, the `/` character in params will be encoded when resolving or pushing
{
path: '/:pathMatch(.*)',
name: 'bad-not-found',
2020-03-02 19:30:49 +00:00
component: NotFoundComponent,
},
2019-03-07 19:48:40 +00:00
{
path: '/login',
name: 'user.login',
component: LoginComponent,
meta: {
title: 'user.auth.login',
},
2019-03-07 19:48:40 +00:00
},
{
path: '/get-password-reset',
name: 'user.password-reset.request',
component: GetPasswordResetComponent,
meta: {
title: 'user.auth.resetPassword',
},
2019-03-07 19:48:40 +00:00
},
{
path: '/password-reset',
name: 'user.password-reset.reset',
component: PasswordResetComponent,
meta: {
title: 'user.auth.resetPassword',
},
2019-03-07 19:48:40 +00:00
},
{
path: '/register',
name: 'user.register',
component: RegisterComponent,
meta: {
2021-11-28 15:35:59 +00:00
title: 'user.auth.createAccount',
},
2019-03-07 19:48:40 +00:00
},
{
path: '/user/settings',
name: 'user.settings',
component: UserSettingsComponent,
redirect: {name: 'user.settings.general'},
children: [
{
path: '/user/settings/avatar',
name: 'user.settings.avatar',
component: UserSettingsAvatarComponent,
},
{
path: '/user/settings/caldav',
name: 'user.settings.caldav',
component: UserSettingsCaldavComponent,
},
{
path: '/user/settings/data-export',
name: 'user.settings.data-export',
component: UserSettingsDataExportComponent,
},
{
path: '/user/settings/deletion',
name: 'user.settings.deletion',
component: UserSettingsDeletionComponent,
},
{
path: '/user/settings/email-update',
name: 'user.settings.email-update',
component: UserSettingsEmailUpdateComponent,
},
{
path: '/user/settings/general',
name: 'user.settings.general',
component: UserSettingsGeneralComponent,
},
{
path: '/user/settings/password-update',
name: 'user.settings.password-update',
component: UserSettingsPasswordUpdateComponent,
},
{
path: '/user/settings/totp',
name: 'user.settings.totp',
component: UserSettingsTOTPComponent,
},
{
path: '/user/settings/api-tokens',
name: 'user.settings.apiTokens',
component: UserSettingsApiTokensComponent,
},
],
},
{
path: '/user/export/download',
name: 'user.export.download',
component: DataExportDownload,
},
{
path: '/share/:share/auth',
name: 'link-share.auth',
component: LinkShareAuthComponent,
},
{
path: '/tasks/:id',
name: 'task.detail',
component: TaskDetailView,
2022-09-15 10:52:38 +00:00
props: route => ({ taskId: Number(route.params.id as string) }),
},
{
path: '/tasks/by/upcoming',
name: 'tasks.range',
component: UpcomingTasksComponent,
props: route => ({
2022-02-09 16:52:27 +00:00
dateFrom: parseDateOrString(route.query.from as string, new Date()),
dateTo: parseDateOrString(route.query.to as string, getNextWeekDate()),
showNulls: route.query.showNulls === 'true',
showOverdue: route.query.showOverdue === 'true',
2022-02-06 19:32:21 +00:00
}),
},
2023-01-19 22:50:42 +00:00
{
// Redirect old list routes to the respective project routes
// see: https://router.vuejs.org/guide/essentials/dynamic-matching.html#catch-all-404-not-found-route
path: '/lists:pathMatch(.*)*',
name: 'lists',
redirect(to) {
return {
path: to.path.replace('/lists', '/projects'),
query: to.query,
hash: to.hash,
}
},
},
{
path: '/projects',
name: 'projects.index',
component: ListProjects,
},
2021-11-01 17:19:59 +00:00
{
2023-03-25 13:29:00 +00:00
path: '/projects/new',
name: 'project.create',
component: NewProjectComponent,
2021-11-01 17:19:59 +00:00
meta: {
showAsModal: true,
},
},
{
path: '/projects/:parentProjectId/new',
name: 'project.createFromParent',
component: NewProjectComponent,
props: route => ({ parentProjectId: Number(route.params.parentProjectId as string) }),
meta: {
showAsModal: true,
2021-11-01 17:19:59 +00:00
},
},
{
path: '/projects/:projectId/settings/edit',
name: 'project.settings.edit',
component: ProjectSettingEdit,
props: route => ({ projectId: Number(route.params.projectId as string) }),
2021-11-01 17:19:59 +00:00
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/background',
name: 'project.settings.background',
component: ProjectSettingBackground,
2021-11-01 17:19:59 +00:00
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/duplicate',
name: 'project.settings.duplicate',
component: ProjectSettingDuplicate,
2021-11-01 17:19:59 +00:00
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/share',
name: 'project.settings.share',
component: ProjectSettingShare,
2021-11-01 17:19:59 +00:00
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/webhooks',
name: 'project.settings.webhooks',
component: ProjectSettingWebhooks,
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/delete',
name: 'project.settings.delete',
component: ProjectSettingDelete,
2021-11-01 17:19:59 +00:00
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/archive',
name: 'project.settings.archive',
component: ProjectSettingArchive,
2021-11-01 17:19:59 +00:00
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/edit',
name: 'filter.settings.edit',
2021-11-01 17:19:59 +00:00
component: FilterEdit,
meta: {
showAsModal: true,
},
props: route => ({ projectId: Number(route.params.projectId as string) }),
},
{
path: '/projects/:projectId/settings/delete',
name: 'filter.settings.delete',
2021-11-01 17:19:59 +00:00
component: FilterDelete,
meta: {
showAsModal: true,
},
props: route => ({ projectId: Number(route.params.projectId as string) }),
},
{
path: '/projects/:projectId/info',
name: 'project.info',
component: ProjectInfo,
meta: {
showAsModal: true,
},
props: route => ({ projectId: Number(route.params.projectId as string) }),
},
2019-04-29 21:41:39 +00:00
{
path: '/projects/:projectId',
name: 'project.index',
redirect(to) {
2021-11-14 20:33:53 +00:00
// Redirect the user to list view by default
const savedProjectView = getProjectView(Number(to.params.projectId as string))
2021-11-14 20:33:53 +00:00
if (savedProjectView) {
console.log('Replaced list view with', savedProjectView)
}
2021-11-14 20:33:53 +00:00
return {
name: savedProjectView || 'project.list',
params: {projectId: to.params.projectId},
2021-11-14 20:33:53 +00:00
}
},
},
{
path: '/projects/:projectId/list',
name: 'project.list',
component: ProjectList,
beforeEnter: (to) => saveProjectView(to.params.projectId, to.name),
props: route => ({ projectId: Number(route.params.projectId as string) }),
2021-11-14 20:33:53 +00:00
},
{
path: '/projects/:projectId/gantt',
name: 'project.gantt',
component: ProjectGantt,
beforeEnter: (to) => saveProjectView(to.params.projectId, to.name),
// FIXME: test if `useRoute` would be the same. If it would use it instead.
props: route => ({route}),
2021-11-14 20:33:53 +00:00
},
{
path: '/projects/:projectId/table',
name: 'project.table',
component: ProjectTable,
beforeEnter: (to) => saveProjectView(to.params.projectId, to.name),
props: route => ({ projectId: Number(route.params.projectId as string) }),
2021-11-14 20:33:53 +00:00
},
{
path: '/projects/:projectId/kanban',
name: 'project.kanban',
component: ProjectKanban,
beforeEnter: (to) => {
saveProjectView(to.params.projectId, to.name)
// Properly set the page title when a task popup is closed
const projectStore = useProjectStore()
const projectFromStore = projectStore.projects[Number(to.params.projectId)]
if(projectFromStore) {
setTitle(projectFromStore.title)
}
},
props: route => ({ projectId: Number(route.params.projectId as string) }),
2019-04-29 21:41:39 +00:00
},
2019-03-07 19:48:40 +00:00
{
path: '/teams',
name: 'teams.index',
component: ListTeamsComponent,
2019-03-07 19:48:40 +00:00
},
{
path: '/teams/new',
name: 'teams.create',
2021-11-01 17:19:59 +00:00
component: NewTeamComponent,
meta: {
showAsModal: true,
},
2019-03-07 19:48:40 +00:00
},
{
path: '/teams/:id/edit',
name: 'teams.edit',
component: EditTeamComponent,
2019-03-07 19:48:40 +00:00
},
{
path: '/labels',
name: 'labels.index',
component: ListLabelsComponent,
2019-09-09 17:55:43 +00:00
},
{
path: '/labels/new',
name: 'labels.create',
2021-11-01 17:19:59 +00:00
component: NewLabelComponent,
meta: {
showAsModal: true,
},
},
{
path: '/migrate',
name: 'migrate.start',
component: MigrationComponent,
},
{
2020-05-24 13:31:27 +00:00
path: '/migrate/:service',
name: 'migrate.service',
component: MigrationHandlerComponent,
props: route => ({
service: route.params.service as string,
code: route.query.code as string,
}),
},
{
path: '/filters/new',
name: 'filters.create',
2021-11-01 17:19:59 +00:00
component: FilterNew,
meta: {
showAsModal: true,
},
},
{
path: '/auth/openid/:provider',
name: 'openid.auth',
component: OpenIdAuth,
},
{
path: '/about',
name: 'about',
component: About,
},
],
2021-08-20 13:17:19 +00:00
})
export async function getAuthForRoute(to: RouteLocation, authStore) {
2022-09-21 01:37:39 +00:00
if (authStore.authUser || authStore.authLinkShare) {
return
}
// Check if the route the user wants to go to is a route which needs authentication. We use this to
// redirect the user after successful login.
const isValidUserAppRoute = ![
'user.login',
'user.password-reset.request',
'user.password-reset.reset',
'user.register',
'link-share.auth',
'openid.auth',
].includes(to.name as string) &&
localStorage.getItem('passwordResetToken') === null &&
2022-10-20 14:15:58 +00:00
localStorage.getItem('emailConfirmToken') === null &&
!(to.name === 'home' && (typeof to.query.userPasswordReset !== 'undefined' || typeof to.query.userEmailConfirm !== 'undefined'))
if (isValidUserAppRoute) {
saveLastVisited(to.name as string, to.params, to.query)
}
const baseStore = useBaseStore()
// When trying this before the current user was fully loaded we might get a flash of the login screen
// in the user shell. To make sure this does not happen we check if everything is ready before trying.
if (!baseStore.ready) {
return
}
if (isValidUserAppRoute) {
return {name: 'user.login'}
}
2022-10-20 14:15:58 +00:00
if(localStorage.getItem('passwordResetToken') !== null && to.name !== 'user.password-reset.reset') {
2022-10-20 14:15:58 +00:00
return {name: 'user.password-reset.reset'}
}
if(localStorage.getItem('emailConfirmToken') !== null && to.name !== 'user.login') {
2022-10-20 14:15:58 +00:00
return {name: 'user.login'}
}
}
2021-08-20 13:17:19 +00:00
router.beforeEach(async (to, from) => {
const authStore = useAuthStore()
if(from.hash && from.hash.startsWith(LINK_SHARE_HASH_PREFIX)) {
to.hash = from.hash
}
2023-04-01 12:19:28 +00:00
if (to.hash.startsWith(LINK_SHARE_HASH_PREFIX) && !authStore.authLinkShare) {
2023-04-01 12:19:28 +00:00
saveLastVisited(to.name as string, to.params, to.query)
return {
name: 'link-share.auth',
params: {
share: to.hash.replace(LINK_SHARE_HASH_PREFIX, ''),
},
}
}
const newRoute = await getAuthForRoute(to, authStore)
if(newRoute) {
return {
...newRoute,
hash: to.hash,
}
}
if(!to.fullPath.endsWith(to.hash)) {
return to.fullPath + to.hash
}
})
2021-08-20 13:17:19 +00:00
export default router