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

View File

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

View File

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

View File

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

View File

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