WIP: feat: route modals everywhere #2735

Closed
dpschen wants to merge 15 commits from dpschen/frontend:feature/route-modals-everywhere into main
16 changed files with 57 additions and 57 deletions
Showing only changes of commit 308cfe4748 - Show all commits

View File

@ -16,7 +16,7 @@
{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }} {{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}
</h1> </h1>
<BaseButton :to="{name: 'list.info', params: {listId: currentList.id}, state: {backdropView: $route.fullPath}}" class="info-button"> <BaseButton :to="{name: 'list.info', params: {listId: currentList.id}, state: {backdropRoutePath: $route.fullPath}}" class="info-button">
<icon icon="circle-info"/> <icon icon="circle-info"/>
</BaseButton> </BaseButton>
@ -75,7 +75,7 @@
{{ $t('keyboardShortcuts.title') }} {{ $t('keyboardShortcuts.title') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{name: 'about', state: {backdropView: $route.fullPath}}" :to="{name: 'about', state: {backdropRoutePath: $route.fullPath}}"
> >
{{ $t('about.title') }} {{ $t('about.title') }}
</dropdown-item> </dropdown-item>

View File

@ -32,14 +32,13 @@
<quick-actions/> <quick-actions/>
<router-view v-if="routeWithModal" :route="routeWithModal" v-slot="{ Component }"> <router-view :route="baseRoute" 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"/>
<!-- test --> </keep-alive>
<!-- </keep-alive> -->
</router-view> </router-view>
<component :is="currentModal" /> <component :is="modalRoute" />

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.
<BaseButton <BaseButton
class="keyboard-shortcuts-button d-print-none" class="keyboard-shortcuts-button d-print-none"
@ -67,7 +66,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} = useRouteWithModal() const {baseRoute, modalRoute} = useRouteWithModal()
const baseStore = useBaseStore() const baseStore = useBaseStore()
const background = computed(() => baseStore.background) const background = computed(() => baseStore.background)

View File

@ -10,13 +10,13 @@
<template v-if="isSavedFilter(list)"> <template v-if="isSavedFilter(list)">
<dropdown-item <dropdown-item
:to="{ name: 'filter.settings.edit', params: { listId: list.id }, state: {backdropView: $route.fullPath} }" :to="{ name: 'filter.settings.edit', params: { listId: list.id }, state: {backdropRoutePath: $route.fullPath} }"
icon="pen" icon="pen"
> >
{{ $t('menu.edit') }} {{ $t('menu.edit') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'filter.settings.delete', params: { listId: list.id }, state: {backdropView: $route.fullPath} }" :to="{ name: 'filter.settings.delete', params: { listId: list.id }, state: {backdropRoutePath: $route.fullPath} }"
icon="trash-alt" icon="trash-alt"
> >
{{ $t('misc.delete') }} {{ $t('misc.delete') }}
@ -25,7 +25,7 @@
<template v-else-if="list.isArchived"> <template v-else-if="list.isArchived">
<dropdown-item <dropdown-item
:to="{ name: 'list.settings.archive', params: { listId: list.id }, state: {backdropView: $route.fullPath} }" :to="{ name: 'list.settings.archive', params: { listId: list.id }, state: {backdropRoutePath: $route.fullPath} }"
icon="archive" icon="archive"
> >
{{ $t('menu.unarchive') }} {{ $t('menu.unarchive') }}
@ -33,32 +33,32 @@
</template> </template>
<template v-else> <template v-else>
<dropdown-item <dropdown-item
:to="{ name: 'list.settings.edit', params: { listId: list.id }, state: {backdropView: $route.fullPath} }" :to="{ name: 'list.settings.edit', params: { listId: list.id }, state: {backdropRoutePath: $route.fullPath} }"
icon="pen" icon="pen"
> >
{{ $t('menu.edit') }} {{ $t('menu.edit') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
v-if="backgroundsEnabled" v-if="backgroundsEnabled"
:to="{ name: 'list.settings.background', params: { listId: list.id }, state: {backdropView: $route.fullPath} }" :to="{ name: 'list.settings.background', params: { listId: list.id }, state: {backdropRoutePath: $route.fullPath} }"
icon="image" icon="image"
> >
{{ $t('menu.setBackground') }} {{ $t('menu.setBackground') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'list.settings.share', params: { listId: list.id }, state: {backdropView: $route.fullPath} }" :to="{ name: 'list.settings.share', params: { listId: list.id }, state: {backdropRoutePath: $route.fullPath} }"
icon="share-alt" icon="share-alt"
> >
{{ $t('menu.share') }} {{ $t('menu.share') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'list.settings.duplicate', params: { listId: list.id }, state: {backdropView: $route.fullPath} }" :to="{ name: 'list.settings.duplicate', params: { listId: list.id }, state: {backdropRoutePath: $route.fullPath} }"
icon="paste" icon="paste"
> >
{{ $t('menu.duplicate') }} {{ $t('menu.duplicate') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'list.settings.archive', params: { listId: list.id }, state: {backdropView: $route.fullPath} }" :to="{ name: 'list.settings.archive', params: { listId: list.id }, state: {backdropRoutePath: $route.fullPath} }"
icon="archive" icon="archive"
> >
{{ $t('menu.archive') }} {{ $t('menu.archive') }}
@ -73,7 +73,7 @@
type="dropdown" type="dropdown"
/> />
<dropdown-item <dropdown-item
:to="{ name: 'list.settings.delete', params: { listId: list.id }, state: {backdropView: $route.fullPath} }" :to="{ name: 'list.settings.delete', params: { listId: list.id }, state: {backdropRoutePath: $route.fullPath} }"
icon="trash-alt" icon="trash-alt"
class="has-text-danger" class="has-text-danger"
> >

View File

@ -10,7 +10,7 @@
<template v-if="namespace.isArchived"> <template v-if="namespace.isArchived">
<dropdown-item <dropdown-item
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id }, state: { backdropView: $route.fullPath } }" :to="{ name: 'namespace.settings.archive', params: { id: namespace.id }, state: { backdropRoutePath: $route.fullPath } }"
icon="archive" icon="archive"
> >
{{ $t('menu.unarchive') }} {{ $t('menu.unarchive') }}
@ -18,25 +18,25 @@
</template> </template>
<template v-else> <template v-else>
<dropdown-item <dropdown-item
:to="{ name: 'namespace.settings.edit', params: { id: namespace.id }, state: { backdropView: $route.fullPath } }" :to="{ name: 'namespace.settings.edit', params: { id: namespace.id }, state: { backdropRoutePath: $route.fullPath } }"
icon="pen" icon="pen"
> >
{{ $t('menu.edit') }} {{ $t('menu.edit') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'namespace.settings.share', params: { namespaceId: namespace.id }, state: { backdropView: $route.fullPath } }" :to="{ name: 'namespace.settings.share', params: { namespaceId: namespace.id }, state: { backdropRoutePath: $route.fullPath } }"
icon="share-alt" icon="share-alt"
> >
{{ $t('menu.share') }} {{ $t('menu.share') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'list.create', params: { namespaceId: namespace.id }, state: { backdropView: $route.fullPath } }" :to="{ name: 'list.create', params: { namespaceId: namespace.id }, state: { backdropRoutePath: $route.fullPath } }"
icon="plus" icon="plus"
> >
{{ $t('menu.newList') }} {{ $t('menu.newList') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id }, state: { backdropView: $route.fullPath } }" :to="{ name: 'namespace.settings.archive', params: { id: namespace.id }, state: { backdropRoutePath: $route.fullPath } }"
icon="archive" icon="archive"
> >
{{ $t('menu.archive') }} {{ $t('menu.archive') }}
@ -51,7 +51,7 @@
type="dropdown" type="dropdown"
/> />
<dropdown-item <dropdown-item
:to="{ name: 'namespace.settings.delete', params: { id: namespace.id }, state: { backdropView: $route.fullPath } }" :to="{ name: 'namespace.settings.delete', params: { id: namespace.id }, state: { backdropRoutePath: $route.fullPath } }"
icon="trash-alt" icon="trash-alt"
class="has-text-danger" class="has-text-danger"
> >

View File

@ -153,7 +153,7 @@ function openTask(e: {
router.push({ router.push({
name: 'task.detail', name: 'task.detail',
params: {id: e.bar.ganttBarConfig.id}, params: {id: e.bar.ganttBarConfig.id},
state: {backdropView: router.currentRoute.value.fullPath}, state: {backdropRoutePath: router.currentRoute.value.fullPath},
}) })
} }

View File

@ -121,7 +121,7 @@ function openTaskDetail() {
router.push({ router.push({
name: 'task.detail', name: 'task.detail',
params: {id: props.task.id}, params: {id: props.task.id},
state: {backdropView: router.currentRoute.value.fullPath}, state: {backdropRoutePath: router.currentRoute.value.fullPath},
}) })
} }

View File

@ -225,7 +225,7 @@ const taskDetailRoute = computed(() => ({
name: 'task.detail', name: 'task.detail',
params: {id: task.value.id}, params: {id: task.value.id},
// TODO: re-enable opening task detail in modal // TODO: re-enable opening task detail in modal
// state: { backdropView: router.currentRoute.value.fullPath }, // state: { backdropRoutePath: router.currentRoute.value.fullPath },
})) }))

View File

@ -2,8 +2,8 @@ import { computed, shallowRef, watch, h, type VNode, ref } from 'vue'
import { useRoute, useRouter, loadRouteLocation, type RouteLocationNormalizedLoaded, type RouteLocationRaw } from 'vue-router' import { useRoute, useRouter, loadRouteLocation, type RouteLocationNormalizedLoaded, type RouteLocationRaw } from 'vue-router'
import router from '@/router' import router from '@/router'
// 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) { function getRouteProps(route: RouteLocationNormalizedLoaded) {
const routePropsOption = route.matched[0]?.props.default const routePropsOption = route.matched[0]?.props.default
return routePropsOption return routePropsOption
@ -23,11 +23,11 @@ export function useRouteWithModal() {
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const backdropView = computed<RouteLocationRaw | undefined>(() => { const historyStateBackdropRoutePath = computed<RouteLocationRaw | undefined>(() => {
// every time the fullPath changes we check the history state // every time the fullPath changes we check the history state
// this happens also initially // this happens also initially
return route.fullPath return route.fullPath
? window.history.state?.backdropView ? window.history.state?.backdropRoutePath
: undefined : undefined
}) })
@ -38,13 +38,14 @@ export function useRouteWithModal() {
const baseRoute = shallowRef<RouteLocationNormalizedLoaded>() const baseRoute = shallowRef<RouteLocationNormalizedLoaded>()
watch( watch(
[backdropView, routerIsReady], [routerIsReady, historyStateBackdropRoutePath],
async () => { async () => {
if (routerIsReady.value === false || !route.fullPath) { if (routerIsReady.value === false || !route.fullPath) {
// wait until we can work with routes
return return
} }

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 (backdropView.value === undefined) { if (historyStateBackdropRoutePath.value === undefined) {
if (route.meta?.showAsModal !== true) { if (route.meta?.showAsModal !== true) {
baseRoute.value = route baseRoute.value = route
return return
@ -58,34 +59,34 @@ export function useRouteWithModal() {
// 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
baseRoute.value = await resolveAndLoadRoute(backdropView.value) baseRoute.value = await resolveAndLoadRoute(historyStateBackdropRoutePath.value)
}, },
{immediate: true}, {immediate: true},
) )
const backdropRoute = computed(() => route.fullPath !== baseRoute.value?.fullPath ? baseRoute.value : undefined) const backdropRoute = computed(() => route.fullPath !== baseRoute.value?.fullPath ? baseRoute.value : undefined)
const currentModal = shallowRef<VNode>() const modalRoute = shallowRef<VNode>()
watch( watch(
[backdropRoute, baseRoute, route], [backdropRoute, route],
() => { () => {
if (routerIsReady.value === false || !route.fullPath || !backdropRoute.value) { if (routerIsReady.value === false || !route.fullPath || !backdropRoute.value) {
currentModal.value = undefined modalRoute.value = undefined
return return
} }
const props = getRouteProps(route) const props = getRouteProps(route)
props.backdropView = backdropRoute.value 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
if (!component) { if (!component) {
currentModal.value = undefined modalRoute.value = undefined
return return
} }
currentModal.value = h(component, props) modalRoute.value = h(component, props)
}, },
{immediate: true}, {immediate: true},
) )
@ -105,8 +106,8 @@ export function useRouteWithModal() {
} }
return { return {
routeWithModal: baseRoute, baseRoute,
currentModal, modalRoute,
closeModal, closeModal,
} }
} }

View File

@ -21,7 +21,7 @@
<template v-if="defaultNamespaceId > 0"> <template v-if="defaultNamespaceId > 0">
<p class="mt-4">{{ $t('home.list.newText') }}</p> <p class="mt-4">{{ $t('home.list.newText') }}</p>
<x-button <x-button
:to="{ name: 'list.create', params: { namespaceId: defaultNamespaceId }, state: { backdropView: $route.fullPath } }" :to="{ name: 'list.create', params: { namespaceId: defaultNamespaceId }, state: { backdropRoutePath: $route.fullPath } }"
:shadow="false" :shadow="false"
class="ml-2" class="ml-2"
> >

View File

@ -3,7 +3,7 @@
<x-button <x-button
:to="{ :to="{
name:'labels.create', name:'labels.create',
state: { backdropView: $route.fullPath } state: { backdropRoutePath: $route.fullPath }
}" }"
class="is-pulled-right" class="is-pulled-right"
icon="plus" icon="plus"
@ -20,7 +20,7 @@
{{ $t('label.newCTA') }} {{ $t('label.newCTA') }}
<router-link :to="{ <router-link :to="{
name:'labels.create', name:'labels.create',
state: { backdropView: $route.fullPath } state: { backdropRoutePath: $route.fullPath }
}">{{ $t('label.create.title') }}.</router-link> }">{{ $t('label.create.title') }}.</router-link>
</p> </p>
</div> </div>

View File

@ -282,7 +282,7 @@ const taskDetailRoutes = computed(() => Object.fromEntries(
{ {
name: 'task.detail', name: 'task.detail',
params: {id}, params: {id},
// state: { backdropView: router.currentRoute.value.fullPath }, // state: { backdropRoutePath: router.currentRoute.value.fullPath },
}, },
])), ])),
)) ))

View File

@ -5,7 +5,7 @@
:primary-label="$t('misc.save')" :primary-label="$t('misc.save')"
@primary="save" @primary="save"
:tertiary="$t('misc.delete')" :tertiary="$t('misc.delete')"
@tertiary="$router.push({ name: 'list.settings.delete', params: { id: listId }, state: {backdropView: $route.fullPath} })" @tertiary="$router.push({ name: 'list.settings.delete', params: { id: listId }, state: {backdropRoutePath: $route.fullPath} })"
#default="{onClose}" #default="{onClose}"
> >
<div class="field"> <div class="field">

View File

@ -6,10 +6,10 @@
</fancycheckbox> </fancycheckbox>
<div class="action-buttons"> <div class="action-buttons">
<x-button :to="{name: 'filters.create', state: {backdropView: $route.fullPath}}" icon="filter"> <x-button :to="{name: 'filters.create', state: {backdropRoutePath: $route.fullPath}}" icon="filter">
{{ $t('filters.create.title') }} {{ $t('filters.create.title') }}
</x-button> </x-button>
<x-button :to="{name: 'namespace.create', state: {backdropView: $route.fullPath}}" icon="plus" v-cy="'new-namespace'"> <x-button :to="{name: 'namespace.create', state: {backdropRoutePath: $route.fullPath}}" icon="plus" v-cy="'new-namespace'">
{{ $t('namespace.create.title') }} {{ $t('namespace.create.title') }}
</x-button> </x-button>
</div> </div>
@ -17,7 +17,7 @@
<p v-if="namespaces.length === 0" class="has-text-centered has-text-grey mt-4 is-italic"> <p v-if="namespaces.length === 0" class="has-text-centered has-text-grey mt-4 is-italic">
{{ $t('namespace.noneAvailable') }} {{ $t('namespace.noneAvailable') }}
<BaseButton :to="{name: 'namespace.create', state: {backdropView: $route.fullPath}}"> <BaseButton :to="{name: 'namespace.create', state: {backdropRoutePath: $route.fullPath}}">
{{ $t('namespace.create.title') }}. {{ $t('namespace.create.title') }}.
</BaseButton> </BaseButton>
</p> </p>
@ -25,7 +25,7 @@
<section :key="`n${n.id}`" class="namespace" v-for="n in namespaces"> <section :key="`n${n.id}`" class="namespace" v-for="n in namespaces">
<x-button <x-button
v-if="n.id > 0 && n.lists.length > 0" v-if="n.id > 0 && n.lists.length > 0"
:to="{name: 'list.create', params: {namespaceId: n.id}, state: { backdropView: $route.fullPath }}" :to="{name: 'list.create', params: {namespaceId: n.id}, state: { backdropRoutePath: $route.fullPath }}"
class="is-pulled-right" class="is-pulled-right"
variant="secondary" variant="secondary"
icon="plus" icon="plus"
@ -34,7 +34,7 @@
</x-button> </x-button>
<x-button <x-button
v-if="n.isArchived" v-if="n.isArchived"
:to="{name: 'namespace.settings.archive', params: {id: n.id}, state: { backdropView: $route.fullPath } }" :to="{name: 'namespace.settings.archive', params: {id: n.id}, state: { backdropRoutePath: $route.fullPath } }"
class="is-pulled-right mr-4" class="is-pulled-right mr-4"
variant="secondary" variant="secondary"
icon="archive" icon="archive"
@ -51,7 +51,7 @@
<p v-if="n.lists.length === 0" class="has-text-centered has-text-grey mt-4 is-italic"> <p v-if="n.lists.length === 0" class="has-text-centered has-text-grey mt-4 is-italic">
{{ $t('namespace.noLists') }} {{ $t('namespace.noLists') }}
<BaseButton :to="{name: 'list.create', params: {namespaceId: n.id}, state: { backdropView: $route.fullPath }}"> <BaseButton :to="{name: 'list.create', params: {namespaceId: n.id}, state: { backdropRoutePath: $route.fullPath }}">
{{ $t('namespace.createList') }} {{ $t('namespace.createList') }}
</BaseButton> </BaseButton>
</p> </p>

View File

@ -5,7 +5,7 @@
:primary-label="$t('misc.save')" :primary-label="$t('misc.save')"
@primary="save" @primary="save"
:tertiary="$t('misc.delete')" :tertiary="$t('misc.delete')"
@tertiary="$router.push({ name: 'namespace.settings.delete', params: { id: $route.params.id }, state: { backdropView: $route.fullPath } })" @tertiary="$router.push({ name: 'namespace.settings.delete', params: { id: $route.params.id }, state: { backdropRoutePath: $route.fullPath } })"
#default="{onClose}" #default="{onClose}"
> >
<form @submit.prevent="save(onClose)"> <form @submit.prevent="save(onClose)">

View File

@ -507,7 +507,7 @@ const props = defineProps({
type: Number as PropType<ITask['id']>, type: Number as PropType<ITask['id']>,
required: true, required: true,
}, },
backdropView: { backdropRoutePath: {
type: String as PropType<RouteLocation['fullPath']>, type: String as PropType<RouteLocation['fullPath']>,
}, },
}) })
@ -580,7 +580,7 @@ const color = computed(() => {
const hasAttachments = computed(() => attachmentStore.attachments.length > 0) const hasAttachments = computed(() => attachmentStore.attachments.length > 0)
const isModal = computed(() => Boolean(props.backdropView)) const isModal = computed(() => Boolean(props.backdropRoutePath))
function attachmentUpload(file: File, onSuccess?: (url: string) => void) { function attachmentUpload(file: File, onSuccess?: (url: string) => void) {
return uploadFile(taskId.value, file, onSuccess) return uploadFile(taskId.value, file, onSuccess)

View File

@ -3,7 +3,7 @@
<x-button <x-button
:to="{ :to="{
name:'teams.create', name:'teams.create',
state: { backdropView: $route.fullPath }, state: { backdropRoutePath: $route.fullPath },
}" }"
class="is-pulled-right" class="is-pulled-right"
icon="plus" icon="plus"
@ -23,7 +23,7 @@
{{ $t('team.noTeams') }} {{ $t('team.noTeams') }}
<router-link :to="{ <router-link :to="{
name: 'teams.create', name: 'teams.create',
state: { backdropView: $route.fullPath }, state: { backdropRoutePath: $route.fullPath },
}"> }">
{{ $t('team.create.title') }}. {{ $t('team.create.title') }}.
</router-link> </router-link>