feat: fix expand and collapse route
This commit is contained in:
parent
7b59f14465
commit
16c2b63c3d
|
@ -20,6 +20,7 @@ import {
|
||||||
faEllipsisH,
|
faEllipsisH,
|
||||||
faEllipsisV,
|
faEllipsisV,
|
||||||
faExclamation,
|
faExclamation,
|
||||||
|
faExpand,
|
||||||
faEye,
|
faEye,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faFillDrip,
|
faFillDrip,
|
||||||
|
@ -95,6 +96,7 @@ library.add(faCog)
|
||||||
library.add(faComments)
|
library.add(faComments)
|
||||||
library.add(faEllipsisH)
|
library.add(faEllipsisH)
|
||||||
library.add(faEllipsisV)
|
library.add(faEllipsisV)
|
||||||
|
library.add(faExpand)
|
||||||
library.add(faExclamation)
|
library.add(faExclamation)
|
||||||
library.add(faEye)
|
library.add(faEye)
|
||||||
library.add(faEyeSlash)
|
library.add(faEyeSlash)
|
||||||
|
|
|
@ -70,7 +70,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {ref, watchEffect} from 'vue'
|
import {nextTick, ref, watchEffect} from 'vue'
|
||||||
import {useScrollLock} from '@vueuse/core'
|
import {useScrollLock} from '@vueuse/core'
|
||||||
import {useFocusTrap} from '@vueuse/integrations/useFocusTrap'
|
import {useFocusTrap} from '@vueuse/integrations/useFocusTrap'
|
||||||
import type {RouteLocationRaw} from 'vue-router'
|
import type {RouteLocationRaw} from 'vue-router'
|
||||||
|
@ -102,7 +102,8 @@ const modal = ref(null)
|
||||||
const {activate, deactivate} = useFocusTrap(modal)
|
const {activate, deactivate} = useFocusTrap(modal)
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (props.enabled) {
|
if (props.enabled) {
|
||||||
activate()
|
// wait for content to be loaded
|
||||||
|
nextTick(() => activate())
|
||||||
} else {
|
} else {
|
||||||
deactivate()
|
deactivate()
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,29 +36,34 @@ export function useRouteWithModal() {
|
||||||
routerIsReady.value = true
|
routerIsReady.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const hasModal = ref(false)
|
||||||
const baseRoute = shallowRef<RouteLocationNormalizedLoaded>()
|
const baseRoute = shallowRef<RouteLocationNormalizedLoaded>()
|
||||||
watch(
|
watch(
|
||||||
[routerIsReady, historyStateBackdropRoutePath],
|
[routerIsReady, historyStateBackdropRoutePath],
|
||||||
async () => {
|
async () => {
|
||||||
if (routerIsReady.value === false || !route.fullPath) {
|
if (routerIsReady.value === false || !route.fullPath) {
|
||||||
// wait until we can work with routes
|
// wait until we can work with routes
|
||||||
|
hasModal.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (historyStateBackdropRoutePath.value === undefined) {
|
if (historyStateBackdropRoutePath.value === undefined) {
|
||||||
if (route.meta?.showAsModal !== true) {
|
if (route.meta?.showAsModal !== true) {
|
||||||
|
hasModal.value = false
|
||||||
baseRoute.value = route
|
baseRoute.value = route
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO: maybe load parent route here in the future,
|
// TODO: maybe load parent route here in the future,
|
||||||
// via: route.matched[route.matched.length - 2]
|
// via: route.matched[route.matched.length - 2]
|
||||||
// see: https://router.vuejs.org/guide/migration/#removal-of-parent-from-route-locations
|
// see: https://router.vuejs.org/guide/migration/#removal-of-parent-from-route-locations
|
||||||
|
hasModal.value = true
|
||||||
baseRoute.value = await resolveAndLoadRoute({ name: 'home' })
|
baseRoute.value = await resolveAndLoadRoute({ name: 'home' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// we get the resolved route from the fullpath
|
// we get the resolved route from the fullpath
|
||||||
// and wait for the route component to be loaded before we assign it
|
// and wait for the route component to be loaded before we assign it
|
||||||
|
hasModal.value = true
|
||||||
baseRoute.value = await resolveAndLoadRoute(historyStateBackdropRoutePath.value)
|
baseRoute.value = await resolveAndLoadRoute(historyStateBackdropRoutePath.value)
|
||||||
},
|
},
|
||||||
{immediate: true},
|
{immediate: true},
|
||||||
|
@ -68,9 +73,9 @@ export function useRouteWithModal() {
|
||||||
|
|
||||||
const modalRoute = shallowRef<VNode>()
|
const modalRoute = shallowRef<VNode>()
|
||||||
watch(
|
watch(
|
||||||
[backdropRoute, route],
|
() => [hasModal.value, backdropRoute.value, route],
|
||||||
() => {
|
() => {
|
||||||
if (routerIsReady.value === false || !route.fullPath || !backdropRoute.value) {
|
if (hasModal.value === false) {
|
||||||
modalRoute.value = undefined
|
modalRoute.value = undefined
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -78,7 +83,7 @@ export function useRouteWithModal() {
|
||||||
const props = getRouteProps(route)
|
const props = getRouteProps(route)
|
||||||
|
|
||||||
props.isModal = true
|
props.isModal = true
|
||||||
props.backdropRoutePath = backdropRoute.value.fullPath
|
props.backdropRoutePath = backdropRoute.value?.fullPath
|
||||||
props.onClose = closeModal
|
props.onClose = closeModal
|
||||||
|
|
||||||
const component = route.matched[0]?.components?.default
|
const component = route.matched[0]?.components?.default
|
||||||
|
@ -102,6 +107,7 @@ export function useRouteWithModal() {
|
||||||
return router.push({ name: 'home' })
|
return router.push({ name: 'home' })
|
||||||
} else {
|
} else {
|
||||||
router.back()
|
router.back()
|
||||||
|
// this should never happen
|
||||||
throw new Error('')
|
throw new Error('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import {createRouter, createWebHistory, type RouteLocation, type RouteLocationRaw} from 'vue-router'
|
||||||
import type { RouteLocation } from 'vue-router'
|
|
||||||
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
||||||
|
|
||||||
import {saveListView, getListView} from '@/helpers/saveListView'
|
import {saveListView, getListView} from '@/helpers/saveListView'
|
||||||
|
@ -80,6 +79,14 @@ const NewNamespaceComponent = () => import('@/views/namespaces/NewNamespace.vue'
|
||||||
const EditTeamComponent = () => import('@/views/teams/EditTeam.vue')
|
const EditTeamComponent = () => import('@/views/teams/EditTeam.vue')
|
||||||
const NewTeamComponent = () => import('@/views/teams/NewTeam.vue')
|
const NewTeamComponent = () => import('@/views/teams/NewTeam.vue')
|
||||||
|
|
||||||
|
|
||||||
|
declare module 'vue-router' {
|
||||||
|
interface RouteMeta {
|
||||||
|
title?: string
|
||||||
|
showAsModal?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
scrollBehavior(to, from, savedPosition) {
|
scrollBehavior(to, from, savedPosition) {
|
||||||
|
@ -358,8 +365,7 @@ const router = createRouter({
|
||||||
redirect(to) {
|
redirect(to) {
|
||||||
// Redirect the user to list view by default
|
// Redirect the user to list view by default
|
||||||
|
|
||||||
const savedListView = getListView(to.params.listId)
|
const savedListView = getListView(Number(to.params.listId))
|
||||||
console.debug('Replaced list view with', savedListView)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: router.hasRoute(savedListView)
|
name: router.hasRoute(savedListView)
|
||||||
|
@ -373,14 +379,14 @@ const router = createRouter({
|
||||||
path: '/lists/:listId/list',
|
path: '/lists/:listId/list',
|
||||||
name: 'list.list',
|
name: 'list.list',
|
||||||
component: ListList,
|
component: ListList,
|
||||||
beforeEnter: (to) => saveListView(to.params.listId, to.name),
|
beforeEnter: (to) => saveListView(Number(to.params.listId), to.name as string),
|
||||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/gantt',
|
path: '/lists/:listId/gantt',
|
||||||
name: 'list.gantt',
|
name: 'list.gantt',
|
||||||
component: ListGantt,
|
component: ListGantt,
|
||||||
beforeEnter: (to) => saveListView(to.params.listId, to.name),
|
beforeEnter: (to) => saveListView(Number(to.params.listId), to.name as string),
|
||||||
// FIXME: test if `useRoute` would be the same. If it would use it instead.
|
// FIXME: test if `useRoute` would be the same. If it would use it instead.
|
||||||
props: route => ({route}),
|
props: route => ({route}),
|
||||||
},
|
},
|
||||||
|
@ -388,7 +394,7 @@ const router = createRouter({
|
||||||
path: '/lists/:listId/table',
|
path: '/lists/:listId/table',
|
||||||
name: 'list.table',
|
name: 'list.table',
|
||||||
component: ListTable,
|
component: ListTable,
|
||||||
beforeEnter: (to) => saveListView(to.params.listId, to.name),
|
beforeEnter: (to) => saveListView(Number(to.params.listId), to.name as string),
|
||||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -396,7 +402,7 @@ const router = createRouter({
|
||||||
name: 'list.kanban',
|
name: 'list.kanban',
|
||||||
component: ListKanban,
|
component: ListKanban,
|
||||||
beforeEnter: (to) => {
|
beforeEnter: (to) => {
|
||||||
saveListView(to.params.listId, to.name)
|
saveListView(Number(to.params.listId), to.name as string)
|
||||||
// Properly set the page title when a task popup is closed
|
// Properly set the page title when a task popup is closed
|
||||||
const listStore = useListStore()
|
const listStore = useListStore()
|
||||||
const listFromStore = listStore.getListById(Number(to.params.listId))
|
const listFromStore = listStore.getListById(Number(to.params.listId))
|
||||||
|
@ -519,4 +525,59 @@ router.beforeEach(async (to) => {
|
||||||
return getAuthForRoute(to)
|
return getAuthForRoute(to)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// https://github.com/vuejs/router/blob/4386ec992f2b96e7309c88f5174f667d9a8a26b2/packages/router/src/router.ts#L595-L628
|
||||||
|
export function handleRedirectRecord(to: RouteLocation): RouteLocationRaw | void {
|
||||||
|
const lastMatched = to.matched[to.matched.length - 1]
|
||||||
|
if (!lastMatched || !lastMatched.redirect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { redirect } = lastMatched
|
||||||
|
let newTargetLocation = redirect
|
||||||
|
|
||||||
|
if (typeof redirect === 'function') {
|
||||||
|
newTargetLocation = redirect(to)
|
||||||
|
}
|
||||||
|
if (typeof newTargetLocation === 'string') {
|
||||||
|
newTargetLocation =
|
||||||
|
newTargetLocation.includes('?') || newTargetLocation.includes('#')
|
||||||
|
// because I didn't want to copy `locationAsObject` as well
|
||||||
|
// I replaced it with `router.resolve`.
|
||||||
|
// This might cause problems, but I'm not sure of which kind
|
||||||
|
// ? (newTargetLocation = locationAsObject(newTargetLocation))
|
||||||
|
? (newTargetLocation = router.resolve(newTargetLocation))
|
||||||
|
: // force empty params
|
||||||
|
{ path: newTargetLocation }
|
||||||
|
// @ts-expect-error: force empty params when a string is passed to let
|
||||||
|
// the router parse them again
|
||||||
|
newTargetLocation.params = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
// __DEV__ &&
|
||||||
|
!('path' in newTargetLocation) &&
|
||||||
|
!('name' in newTargetLocation)
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`Invalid redirect found:\n${JSON.stringify(
|
||||||
|
newTargetLocation,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n when navigating to "${
|
||||||
|
to.fullPath
|
||||||
|
}". A redirect must contain a name or path. This will break in production.`,
|
||||||
|
)
|
||||||
|
throw new Error('Invalid redirect')
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign(
|
||||||
|
{
|
||||||
|
query: to.query,
|
||||||
|
hash: to.hash,
|
||||||
|
// avoid transferring params if the redirect has a path
|
||||||
|
params: 'path' in newTargetLocation ? {} : to.params,
|
||||||
|
},
|
||||||
|
newTargetLocation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default router
|
export default router
|
|
@ -284,6 +284,22 @@
|
||||||
<comments :can-write="canWrite" :task-id="taskId"/>
|
<comments :can-write="canWrite" :task-id="taskId"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-one-third action-buttons d-print-none" v-if="canWrite || isModal">
|
<div class="column is-one-third action-buttons d-print-none" v-if="canWrite || isModal">
|
||||||
|
<x-button
|
||||||
|
v-if="isModal"
|
||||||
|
:to="{ name: 'task.detail', params: {id: task.id}, state: { backdropRoutePath: undefined }, force: true}"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
<span class="icon is-small"><icon icon="expand"/></span>
|
||||||
|
Expand
|
||||||
|
</x-button>
|
||||||
|
<x-button
|
||||||
|
v-else
|
||||||
|
:to="overlayRoute"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
<span class="icon is-small"><icon icon="expand"/></span>
|
||||||
|
Show as Overlay
|
||||||
|
</x-button>
|
||||||
<template v-if="canWrite">
|
<template v-if="canWrite">
|
||||||
<x-button
|
<x-button
|
||||||
:class="{'is-success': !task.done}"
|
:class="{'is-success': !task.done}"
|
||||||
|
@ -453,6 +469,8 @@ import {useI18n} from 'vue-i18n'
|
||||||
import {unrefElement} from '@vueuse/core'
|
import {unrefElement} from '@vueuse/core'
|
||||||
import cloneDeep from 'lodash.clonedeep'
|
import cloneDeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
|
import {handleRedirectRecord} from '@/router'
|
||||||
|
|
||||||
import TaskService from '@/services/task'
|
import TaskService from '@/services/task'
|
||||||
import TaskModel, {TASK_DEFAULT_COLOR} from '@/models/task'
|
import TaskModel, {TASK_DEFAULT_COLOR} from '@/models/task'
|
||||||
|
|
||||||
|
@ -525,6 +543,23 @@ const kanbanStore = useKanbanStore()
|
||||||
const task = reactive<ITask>(new TaskModel())
|
const task = reactive<ITask>(new TaskModel())
|
||||||
useTitle(toRef(task, 'title'))
|
useTitle(toRef(task, 'title'))
|
||||||
|
|
||||||
|
const overlayRoute = computed(() => {
|
||||||
|
const resolvedbackdropRoute = router.resolve({ name: 'list.index', params: {listId: task.listId} })
|
||||||
|
|
||||||
|
// resolvedbackdropRoute.matched.forEach((record) => console.log(record.redirect))
|
||||||
|
|
||||||
|
// 'list.index' will always redirect, so we need to resolve the redirected route record
|
||||||
|
const backdropRoutePath = handleRedirectRecord(resolvedbackdropRoute)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'task.detail',
|
||||||
|
params: {id: task.id},
|
||||||
|
state: { backdropRoutePath },
|
||||||
|
// if force is not enabled it seems like vue router doesn't recognise the route change
|
||||||
|
force: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// We doubled the task color property here because verte does not have a real change property, leading
|
// We doubled the task color property here because verte does not have a real change property, leading
|
||||||
// to the color property change being triggered when the # is removed from it, leading to an update,
|
// to the color property change being triggered when the # is removed from it, leading to an update,
|
||||||
// which leads in turn to a change... This creates an infinite loop in which the task is updated, changed,
|
// which leads in turn to a change... This creates an infinite loop in which the task is updated, changed,
|
||||||
|
|
Reference in New Issue