feat: fix expand and collapse route

This commit is contained in:
Dominik Pschenitschni 2022-11-23 13:59:18 +01:00
parent 7b59f14465
commit 16c2b63c3d
Signed by: dpschen
GPG Key ID: B257AC0149F43A77
5 changed files with 118 additions and 13 deletions

View File

@ -20,6 +20,7 @@ import {
faEllipsisH,
faEllipsisV,
faExclamation,
faExpand,
faEye,
faEyeSlash,
faFillDrip,
@ -95,6 +96,7 @@ library.add(faCog)
library.add(faComments)
library.add(faEllipsisH)
library.add(faEllipsisV)
library.add(faExpand)
library.add(faExclamation)
library.add(faEye)
library.add(faEyeSlash)

View File

@ -70,7 +70,7 @@ export default {
</script>
<script lang="ts" setup>
import {ref, watchEffect} from 'vue'
import {nextTick, ref, watchEffect} from 'vue'
import {useScrollLock} from '@vueuse/core'
import {useFocusTrap} from '@vueuse/integrations/useFocusTrap'
import type {RouteLocationRaw} from 'vue-router'
@ -102,7 +102,8 @@ const modal = ref(null)
const {activate, deactivate} = useFocusTrap(modal)
watchEffect(() => {
if (props.enabled) {
activate()
// wait for content to be loaded
nextTick(() => activate())
} else {
deactivate()
}

View File

@ -36,29 +36,34 @@ export function useRouteWithModal() {
routerIsReady.value = true
})
const hasModal = ref(false)
const baseRoute = shallowRef<RouteLocationNormalizedLoaded>()
watch(
[routerIsReady, historyStateBackdropRoutePath],
async () => {
if (routerIsReady.value === false || !route.fullPath) {
// wait until we can work with routes
hasModal.value = false
return
}
if (historyStateBackdropRoutePath.value === undefined) {
if (route.meta?.showAsModal !== true) {
hasModal.value = false
baseRoute.value = route
return
}
// TODO: maybe load parent route here in the future,
// via: route.matched[route.matched.length - 2]
// see: https://router.vuejs.org/guide/migration/#removal-of-parent-from-route-locations
hasModal.value = true
baseRoute.value = await resolveAndLoadRoute({ name: 'home' })
return
}
// we get the resolved route from the fullpath
// and wait for the route component to be loaded before we assign it
hasModal.value = true
baseRoute.value = await resolveAndLoadRoute(historyStateBackdropRoutePath.value)
},
{immediate: true},
@ -68,9 +73,9 @@ export function useRouteWithModal() {
const modalRoute = shallowRef<VNode>()
watch(
[backdropRoute, route],
() => [hasModal.value, backdropRoute.value, route],
() => {
if (routerIsReady.value === false || !route.fullPath || !backdropRoute.value) {
if (hasModal.value === false) {
modalRoute.value = undefined
return
}
@ -78,7 +83,7 @@ export function useRouteWithModal() {
const props = getRouteProps(route)
props.isModal = true
props.backdropRoutePath = backdropRoute.value.fullPath
props.backdropRoutePath = backdropRoute.value?.fullPath
props.onClose = closeModal
const component = route.matched[0]?.components?.default
@ -102,6 +107,7 @@ export function useRouteWithModal() {
return router.push({ name: 'home' })
} else {
router.back()
// this should never happen
throw new Error('')
}
}

View File

@ -1,5 +1,4 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteLocation } from 'vue-router'
import {createRouter, createWebHistory, type RouteLocation, type RouteLocationRaw} from 'vue-router'
import {saveLastVisited} from '@/helpers/saveLastVisited'
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 NewTeamComponent = () => import('@/views/teams/NewTeam.vue')
declare module 'vue-router' {
interface RouteMeta {
title?: string
showAsModal?: boolean
}
}
const router = createRouter({
history: createWebHistory(),
scrollBehavior(to, from, savedPosition) {
@ -358,8 +365,7 @@ const router = createRouter({
redirect(to) {
// Redirect the user to list view by default
const savedListView = getListView(to.params.listId)
console.debug('Replaced list view with', savedListView)
const savedListView = getListView(Number(to.params.listId))
return {
name: router.hasRoute(savedListView)
@ -373,14 +379,14 @@ const router = createRouter({
path: '/lists/:listId/list',
name: 'list.list',
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) }),
},
{
path: '/lists/:listId/gantt',
name: 'list.gantt',
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.
props: route => ({route}),
},
@ -388,7 +394,7 @@ const router = createRouter({
path: '/lists/:listId/table',
name: 'list.table',
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) }),
},
{
@ -396,7 +402,7 @@ const router = createRouter({
name: 'list.kanban',
component: ListKanban,
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
const listStore = useListStore()
const listFromStore = listStore.getListById(Number(to.params.listId))
@ -519,4 +525,59 @@ router.beforeEach(async (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

View File

@ -284,6 +284,22 @@
<comments :can-write="canWrite" :task-id="taskId"/>
</div>
<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">
<x-button
:class="{'is-success': !task.done}"
@ -453,6 +469,8 @@ import {useI18n} from 'vue-i18n'
import {unrefElement} from '@vueuse/core'
import cloneDeep from 'lodash.clonedeep'
import {handleRedirectRecord} from '@/router'
import TaskService from '@/services/task'
import TaskModel, {TASK_DEFAULT_COLOR} from '@/models/task'
@ -525,6 +543,23 @@ const kanbanStore = useKanbanStore()
const task = reactive<ITask>(new TaskModel())
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
// 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,