WIP: feat: route modals everywhere #2735
|
@ -16,7 +16,7 @@
|
|||
{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}
|
||||
</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"/>
|
||||
</BaseButton>
|
||||
|
||||
|
@ -75,7 +75,7 @@
|
|||
{{ $t('keyboardShortcuts.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{name: 'about', state: {backdropView: $route.fullPath}}"
|
||||
:to="{name: 'about', state: {backdropRoutePath: $route.fullPath}}"
|
||||
>
|
||||
{{ $t('about.title') }}
|
||||
</dropdown-item>
|
||||
|
|
|
@ -32,14 +32,13 @@
|
|||
|
||||
<quick-actions/>
|
||||
|
||||
<router-view v-if="routeWithModal" :route="routeWithModal" v-slot="{ Component }">
|
||||
<!-- <keep-alive :include="['list.list', 'list.gantt', 'list.table', 'list.kanban']"> -->
|
||||
<router-view :route="baseRoute" v-slot="{ Component }">
|
||||
<keep-alive :include="['list.list', 'list.gantt', 'list.table', 'list.kanban']">
|
||||
<component :is="Component"/>
|
||||
<!-- test -->
|
||||
<!-- </keep-alive> -->
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
|
||||
<component :is="currentModal" />
|
||||
<component :is="modalRoute" />
|
||||
|
||||
|
||||
<BaseButton
|
||||
class="keyboard-shortcuts-button d-print-none"
|
||||
|
@ -67,7 +66,7 @@ import {useLabelStore} from '@/stores/labels'
|
|||
import {useRouteWithModal} from '@/composables/useRouteWithModal'
|
||||
import {useRenewTokenOnFocus} from '@/composables/useRenewTokenOnFocus'
|
||||
|
||||
const {routeWithModal, currentModal} = useRouteWithModal()
|
||||
const {baseRoute, modalRoute} = useRouteWithModal()
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const background = computed(() => baseStore.background)
|
||||
|
|
|
@ -10,13 +10,13 @@
|
|||
|
||||
<template v-if="isSavedFilter(list)">
|
||||
<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"
|
||||
>
|
||||
{{ $t('menu.edit') }}
|
||||
</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"
|
||||
>
|
||||
{{ $t('misc.delete') }}
|
||||
|
@ -25,7 +25,7 @@
|
|||
|
||||
<template v-else-if="list.isArchived">
|
||||
<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"
|
||||
>
|
||||
{{ $t('menu.unarchive') }}
|
||||
|
@ -33,32 +33,32 @@
|
|||
</template>
|
||||
<template v-else>
|
||||
<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"
|
||||
>
|
||||
{{ $t('menu.edit') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
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"
|
||||
>
|
||||
{{ $t('menu.setBackground') }}
|
||||
</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"
|
||||
>
|
||||
{{ $t('menu.share') }}
|
||||
</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"
|
||||
>
|
||||
{{ $t('menu.duplicate') }}
|
||||
</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"
|
||||
>
|
||||
{{ $t('menu.archive') }}
|
||||
|
@ -73,7 +73,7 @@
|
|||
type="dropdown"
|
||||
/>
|
||||
<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"
|
||||
class="has-text-danger"
|
||||
>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<template v-if="namespace.isArchived">
|
||||
<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"
|
||||
>
|
||||
{{ $t('menu.unarchive') }}
|
||||
|
@ -18,25 +18,25 @@
|
|||
</template>
|
||||
<template v-else>
|
||||
<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"
|
||||
>
|
||||
{{ $t('menu.edit') }}
|
||||
</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"
|
||||
>
|
||||
{{ $t('menu.share') }}
|
||||
</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"
|
||||
>
|
||||
{{ $t('menu.newList') }}
|
||||
</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"
|
||||
>
|
||||
{{ $t('menu.archive') }}
|
||||
|
@ -51,7 +51,7 @@
|
|||
type="dropdown"
|
||||
/>
|
||||
<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"
|
||||
class="has-text-danger"
|
||||
>
|
||||
|
|
|
@ -153,7 +153,7 @@ function openTask(e: {
|
|||
router.push({
|
||||
name: 'task.detail',
|
||||
params: {id: e.bar.ganttBarConfig.id},
|
||||
state: {backdropView: router.currentRoute.value.fullPath},
|
||||
state: {backdropRoutePath: router.currentRoute.value.fullPath},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -121,7 +121,7 @@ function openTaskDetail() {
|
|||
router.push({
|
||||
name: 'task.detail',
|
||||
params: {id: props.task.id},
|
||||
state: {backdropView: router.currentRoute.value.fullPath},
|
||||
state: {backdropRoutePath: router.currentRoute.value.fullPath},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -225,7 +225,7 @@ const taskDetailRoute = computed(() => ({
|
|||
name: 'task.detail',
|
||||
params: {id: task.value.id},
|
||||
// TODO: re-enable opening task detail in modal
|
||||
// state: { backdropView: router.currentRoute.value.fullPath },
|
||||
// state: { backdropRoutePath: router.currentRoute.value.fullPath },
|
||||
}))
|
||||
|
||||
|
||||
|
|
|
@ -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 router from '@/router'
|
||||
|
||||
// this is adapted from vue-router
|
||||
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
|
||||
// this is adapted from vue-router
|
||||
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
|
||||
function getRouteProps(route: RouteLocationNormalizedLoaded) {
|
||||
const routePropsOption = route.matched[0]?.props.default
|
||||
return routePropsOption
|
||||
|
@ -23,11 +23,11 @@ export function useRouteWithModal() {
|
|||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const backdropView = computed<RouteLocationRaw | undefined>(() => {
|
||||
const historyStateBackdropRoutePath = computed<RouteLocationRaw | undefined>(() => {
|
||||
// every time the fullPath changes we check the history state
|
||||
// this happens also initially
|
||||
return route.fullPath
|
||||
? window.history.state?.backdropView
|
||||
? window.history.state?.backdropRoutePath
|
||||
: undefined
|
||||
})
|
||||
|
||||
|
@ -38,13 +38,14 @@ export function useRouteWithModal() {
|
|||
|
||||
const baseRoute = shallowRef<RouteLocationNormalizedLoaded>()
|
||||
watch(
|
||||
[backdropView, routerIsReady],
|
||||
[routerIsReady, historyStateBackdropRoutePath],
|
||||
async () => {
|
||||
if (routerIsReady.value === false || !route.fullPath) {
|
||||
// wait until we can work with routes
|
||||
return
|
||||
}
|
||||
|
||||
dpschen
commented
Something was broken here: since we checked for 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) {
|
||||
baseRoute.value = route
|
||||
return
|
||||
|
@ -58,34 +59,34 @@ export function useRouteWithModal() {
|
|||
|
||||
// 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)
|
||||
baseRoute.value = await resolveAndLoadRoute(historyStateBackdropRoutePath.value)
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const backdropRoute = computed(() => route.fullPath !== baseRoute.value?.fullPath ? baseRoute.value : undefined)
|
||||
|
||||
const currentModal = shallowRef<VNode>()
|
||||
const modalRoute = shallowRef<VNode>()
|
||||
watch(
|
||||
[backdropRoute, baseRoute, route],
|
||||
[backdropRoute, route],
|
||||
() => {
|
||||
if (routerIsReady.value === false || !route.fullPath || !backdropRoute.value) {
|
||||
currentModal.value = undefined
|
||||
modalRoute.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
const props = getRouteProps(route)
|
||||
|
||||
props.backdropView = backdropRoute.value
|
||||
props.backdropRoutePath = backdropRoute.value.fullPath
|
||||
props.onClose = closeModal
|
||||
|
||||
const component = route.matched[0]?.components?.default
|
||||
|
||||
if (!component) {
|
||||
currentModal.value = undefined
|
||||
modalRoute.value = undefined
|
||||
return
|
||||
}
|
||||
currentModal.value = h(component, props)
|
||||
modalRoute.value = h(component, props)
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
@ -105,8 +106,8 @@ export function useRouteWithModal() {
|
|||
}
|
||||
|
||||
return {
|
||||
routeWithModal: baseRoute,
|
||||
currentModal,
|
||||
baseRoute,
|
||||
modalRoute,
|
||||
closeModal,
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@
|
|||
<template v-if="defaultNamespaceId > 0">
|
||||
<p class="mt-4">{{ $t('home.list.newText') }}</p>
|
||||
<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"
|
||||
class="ml-2"
|
||||
>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<x-button
|
||||
:to="{
|
||||
name:'labels.create',
|
||||
state: { backdropView: $route.fullPath }
|
||||
state: { backdropRoutePath: $route.fullPath }
|
||||
}"
|
||||
class="is-pulled-right"
|
||||
icon="plus"
|
||||
|
@ -20,7 +20,7 @@
|
|||
{{ $t('label.newCTA') }}
|
||||
<router-link :to="{
|
||||
name:'labels.create',
|
||||
state: { backdropView: $route.fullPath }
|
||||
state: { backdropRoutePath: $route.fullPath }
|
||||
}">{{ $t('label.create.title') }}.</router-link>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -282,7 +282,7 @@ const taskDetailRoutes = computed(() => Object.fromEntries(
|
|||
{
|
||||
name: 'task.detail',
|
||||
params: {id},
|
||||
// state: { backdropView: router.currentRoute.value.fullPath },
|
||||
// state: { backdropRoutePath: router.currentRoute.value.fullPath },
|
||||
},
|
||||
])),
|
||||
))
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
:primary-label="$t('misc.save')"
|
||||
@primary="save"
|
||||
: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}"
|
||||
>
|
||||
<div class="field">
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
</fancycheckbox>
|
||||
|
||||
<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') }}
|
||||
</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') }}
|
||||
</x-button>
|
||||
</div>
|
||||
|
@ -17,7 +17,7 @@
|
|||
|
||||
<p v-if="namespaces.length === 0" class="has-text-centered has-text-grey mt-4 is-italic">
|
||||
{{ $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') }}.
|
||||
</BaseButton>
|
||||
</p>
|
||||
|
@ -25,7 +25,7 @@
|
|||
<section :key="`n${n.id}`" class="namespace" v-for="n in namespaces">
|
||||
<x-button
|
||||
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"
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
|
@ -34,7 +34,7 @@
|
|||
</x-button>
|
||||
<x-button
|
||||
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"
|
||||
variant="secondary"
|
||||
icon="archive"
|
||||
|
@ -51,7 +51,7 @@
|
|||
|
||||
<p v-if="n.lists.length === 0" class="has-text-centered has-text-grey mt-4 is-italic">
|
||||
{{ $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') }}
|
||||
</BaseButton>
|
||||
</p>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
:primary-label="$t('misc.save')"
|
||||
@primary="save"
|
||||
: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}"
|
||||
>
|
||||
<form @submit.prevent="save(onClose)">
|
||||
|
|
|
@ -507,7 +507,7 @@ const props = defineProps({
|
|||
type: Number as PropType<ITask['id']>,
|
||||
required: true,
|
||||
},
|
||||
backdropView: {
|
||||
backdropRoutePath: {
|
||||
type: String as PropType<RouteLocation['fullPath']>,
|
||||
},
|
||||
})
|
||||
|
@ -580,7 +580,7 @@ const color = computed(() => {
|
|||
|
||||
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) {
|
||||
return uploadFile(taskId.value, file, onSuccess)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<x-button
|
||||
:to="{
|
||||
name:'teams.create',
|
||||
state: { backdropView: $route.fullPath },
|
||||
state: { backdropRoutePath: $route.fullPath },
|
||||
}"
|
||||
class="is-pulled-right"
|
||||
icon="plus"
|
||||
|
@ -23,7 +23,7 @@
|
|||
{{ $t('team.noTeams') }}
|
||||
<router-link :to="{
|
||||
name: 'teams.create',
|
||||
state: { backdropView: $route.fullPath },
|
||||
state: { backdropRoutePath: $route.fullPath },
|
||||
}">
|
||||
{{ $t('team.create.title') }}.
|
||||
</router-link>
|
||||
|
|
Reference in New Issue
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 aBaseModal
to abstract the general modal functionality and then use that to create styled Modals with specific functionality. For example we coudl create a dedicatedDialog
modal (this might actually be the same as the currentcreate-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.