WIP: feat: route modals everywhere #2735
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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('')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
Reference in New Issue
Something was broken here: since we checked for
historyState.value
in theif
condition it could never have a value in theelse
cause.