WIP: feat: route modals everywhere #2735

Closed
dpschen wants to merge 15 commits from dpschen/frontend:feature/route-modals-everywhere into main
5 changed files with 109 additions and 60 deletions
Showing only changes of commit 13e68ac94d - Show all commits

View File

@ -5,12 +5,7 @@ export default { inheritAttrs: false }
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import { useAttrs } from 'vue'
defineProps<{ is: any }>() defineProps<{ is: any }>()
const attrs = useAttrs()
console.log(JSON.parse(JSON.stringify(attrs)))
</script> </script>
<template> <template>

View File

@ -32,16 +32,14 @@
<quick-actions/> <quick-actions/>
<router-view :route="routeWithModal" v-slot="{ Component }"> <router-view v-if="routeWithModal" :route="routeWithModal" v-slot="{ Component }">
<keep-alive :include="['list.list', 'list.gantt', 'list.table', 'list.kanban']"> <!-- <keep-alive :include="['list.list', 'list.gantt', 'list.table', 'list.kanban']"> -->
<component :is="Component"/> <component :is="Component"/>
</keep-alive> <!-- test -->
<!-- </keep-alive> -->
</router-view> </router-view>

Every modal component now has to provide it's own modal.

This makes it possible for us to have different modal implementations. Currently we do this by changing the type prop of the modal. It might be easier for us if we create something like a BaseModal to abstract the general modal functionality and then use that to create styled Modals with specific functionality. For example we coudl create a dedicated Dialog modal (this might actually be the same as the current create-edit, I'm not sure here if that was the intended use @konrad).

Every modal component now has to provide it's own modal. This makes it possible for us to have different modal implementations. Currently we do this by changing the `type` prop of the modal. It might be easier for us if we create something like a `BaseModal` to abstract the general modal functionality and then use that to create styled Modals with specific functionality. For example we coudl create a dedicated `Dialog` modal (this might actually be the same as the current `create-edit`, I'm not sure here if that was the intended use @konrad).

That sounds like it could be a good idea.

IIRC my main goal with the create-edit component was to be able to easily re-use a shell for creating or editing.

That sounds like it could be a good idea. IIRC my main goal with the `create-edit` component was to be able to easily re-use a shell for creating or editing.
<component <component :is="currentModal" />
:is="currentModal"
@close="closeModal()"
/>
<BaseButton <BaseButton
class="keyboard-shortcuts-button d-print-none" class="keyboard-shortcuts-button d-print-none"
@ -69,7 +67,7 @@ import {useLabelStore} from '@/stores/labels'
import {useRouteWithModal} from '@/composables/useRouteWithModal' import {useRouteWithModal} from '@/composables/useRouteWithModal'
import {useRenewTokenOnFocus} from '@/composables/useRenewTokenOnFocus' import {useRenewTokenOnFocus} from '@/composables/useRenewTokenOnFocus'
const {routeWithModal, currentModal, closeModal} = useRouteWithModal() const {routeWithModal, currentModal} = useRouteWithModal()
const baseStore = useBaseStore() const baseStore = useBaseStore()
const background = computed(() => baseStore.background) const background = computed(() => baseStore.background)

View File

@ -1,36 +1,83 @@
import { computed, shallowRef, watchEffect, h, type VNode } from 'vue' import { computed, shallowRef, watch, h, type VNode, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter, loadRouteLocation, type RouteLocationNormalizedLoaded, type RouteLocationRaw } from 'vue-router'
import router from '@/router'
export function useRouteWithModal() {
const router = useRouter()
const route = useRoute()
const backdropView = computed(() => route.fullPath && window.history.state.backdropView)
const routeWithModal = computed(() => {
return backdropView.value
? router.resolve(backdropView.value)
: route
})
const currentModal = shallowRef<VNode>()
watchEffect(() => {
if (!backdropView.value) {
currentModal.value = undefined
return
}
// this is adapted from vue-router // this is adapted from vue-router
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125 // https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
function getRouteProps(route: RouteLocationNormalizedLoaded) {
const routePropsOption = route.matched[0]?.props.default const routePropsOption = route.matched[0]?.props.default
const routeProps = routePropsOption return routePropsOption
? routePropsOption === true ? routePropsOption === true
? route.params ? route.params
: typeof routePropsOption === 'function' : typeof routePropsOption === 'function'
? routePropsOption(route) ? routePropsOption(route)
: routePropsOption : routePropsOption
: {} : {}
}
routeProps.backdropView = backdropView.value function resolveAndLoadRoute(route: RouteLocationRaw) {
return loadRouteLocation(router.resolve(route))
}
export function useRouteWithModal() {
const router = useRouter()
const route = useRoute()
const backdropView = computed<RouteLocationRaw | undefined>(() => {
// every time the fullPath changes we check the history state
// this happens also initially
return route.fullPath
? window.history.state?.backdropView
: undefined
})
const routerIsReady = ref(false)
router.isReady().then(() => {
routerIsReady.value = true
})
const baseRoute = shallowRef<RouteLocationNormalizedLoaded>()
watch(
[backdropView, routerIsReady],
async () => {
if (routerIsReady.value === false || !route.fullPath) {
return
}
if (backdropView.value === undefined) {

Something was broken here: since we checked for historyState.value in the if condition it could never have a value in the else cause.

Something was broken here: since we checked for `historyState.value` in the `if` condition it could never have a value in the `else` cause.
if (route.meta?.showAsModal !== true) {
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
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
baseRoute.value = await resolveAndLoadRoute(backdropView.value)
},
{immediate: true},
)
const backdropRoute = computed(() => route.fullPath !== baseRoute.value?.fullPath ? baseRoute.value : undefined)
const currentModal = shallowRef<VNode>()
watch(
[backdropRoute, baseRoute, route],
() => {
if (routerIsReady.value === false || !route.fullPath || !backdropRoute.value) {
currentModal.value = undefined
return
}
const props = getRouteProps(route)
props.backdropView = backdropRoute.value
props.onClose = closeModal
const component = route.matched[0]?.components?.default const component = route.matched[0]?.components?.default
@ -38,19 +85,28 @@ export function useRouteWithModal() {
currentModal.value = undefined currentModal.value = undefined
return return
} }
currentModal.value = h(component, routeProps) currentModal.value = h(component, props)
}) },
{immediate: true},
)
function closeModal() { async function closeModal() {
const historyState = computed(() => route.fullPath && window.history.state) await router.isReady()
if (backdropRoute.value !== undefined) {
if (historyState.value === undefined) { // TODO: Dialog modals might want to replace the route here via router.replace()
router.back() return router.push(backdropRoute.value)
}
if (window.history.state === undefined) {
return router.push({ name: 'home' })
} else { } else {
const backdropRoute = historyState.value.backdropView && router.resolve(historyState.value.backdropView) router.back()
router.push(backdropRoute) throw new Error('')
} }
} }
return {routeWithModal, currentModal, closeModal} return {
routeWithModal: baseRoute,
currentModal,
closeModal,
}
} }

View File

@ -240,7 +240,7 @@ const router = createRouter({
meta: { meta: {
showAsModal: true, showAsModal: true,
}, },
props: route => ({ namespaceId: parseInt(route.params.id as string) }), props: route => ({ namespaceId: Number(route.params.id as string) }),
}, },
{ {
path: '/namespaces/:id/settings/delete', path: '/namespaces/:id/settings/delete',
@ -468,6 +468,9 @@ const router = createRouter({
path: '/about', path: '/about',
name: 'about', name: 'about',
component: About, component: About,
meta: {
showAsModal: true,
},
}, },
], ],
}) })

View File

@ -512,9 +512,6 @@ const props = defineProps({
}, },
}) })
defineEmits(['close'])
const attrs = useAttrs()
console.log(JSON.parse(JSON.stringify(attrs)))
const router = useRouter() const router = useRouter()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})

Remove log

Remove log