WIP: feat: route modals everywhere #2735

Closed
dpschen wants to merge 15 commits from dpschen/frontend:feature/route-modals-everywhere into main
30 changed files with 155 additions and 137 deletions
Showing only changes of commit 6fc47ee8bd - Show all commits

View File

@ -19,7 +19,7 @@ describe('Team', () => {
.contains('Create a new team')
cy.get('input.input')
.type(newTeamName)
cy.get('.button')
cy.get('.new-team button')
.contains('Create')
.click()

View File

@ -0,0 +1,21 @@
<!-- https://github.com/vuejs/rfcs/pull/449 -->
dpschen marked this conversation as resolved Outdated

This component makes it possible to optionally wrap around another component. If the is prop defines a component it will be rendered as a wrapper around the slot content. If the is prop is undefined there won't be any wrapper.

This component makes it possible to optionally wrap around another component. If the `is` prop defines a component it will be rendered as a wrapper around the slot content. If the `is` prop is undefined there won't be any wrapper.
<!-- https://github.com/vuejs/rfcs/discussions/448#discussioncomment-2769396= -->
<script lang="ts">
export default { inheritAttrs: false }
</script>
<script setup lang="ts">
import { useAttrs } from 'vue'
defineProps<{ is: any }>()
const attrs = useAttrs()
console.log(JSON.parse(JSON.stringify(attrs)))
dpschen marked this conversation as resolved Outdated

Remove this

Remove this
</script>
<template>
<component v-if="is" :is="is" v-bind="$attrs">
<slot />
</component>
<slot v-else />
</template>

View File

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

View File

@ -38,14 +38,10 @@
</keep-alive>
</router-view>
<modal
:enabled="Boolean(currentModal)"
<component

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.
:is="currentModal"
@close="closeModal()"
variant="scrolling"
class="task-detail-view-modal"
>
<component :is="currentModal"/>
</modal>
/>
<BaseButton
class="keyboard-shortcuts-button d-print-none"

View File

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

View File

@ -1,38 +1,38 @@
<template>
<modal @close="$router.back()" :overflow="true" :wide="wide">
<modal :overflow="true" :wide="wide" #default="{ onClose }">

I removed the @close event definitions on all internal modals because what happens when that event get's fired should instead be defined by the parent. By removing this the parents onClose / @close should automatically be passed to the <modal>.
By passing the onClose then to the slot content, the slot is still able to use the method.

I removed the `@close` event definitions on all internal modals because what happens when that event get's fired should instead be defined by the parent. By removing this the parents `onClose` / `@close` should automatically be passed to the `<modal>`. By passing the `onClose` then to the slot content, the slot is still able to use the method.
<card
:title="title"
:shadow="false"
:padding="false"
class="has-text-left"
:has-close="true"
@close="$router.back()"
@close="onClose"
:loading="loading"
>
<div class="p-4">
<slot/>
<slot :onClose="onClose" />
</div>
<template #footer>
<slot name="footer">
<slot name="footer" :onClose="onClose">
<x-button
v-if="tertiary !== ''"
:shadow="false"
variant="tertiary"
@click.prevent.stop="$emit('tertiary')"
@click="$emit('tertiary')"
>
{{ tertiary }}
</x-button>
<x-button
variant="secondary"
@click.prevent.stop="$router.back()"
@click="onClose"
>
{{ $t('misc.cancel') }}
</x-button>
<x-button
v-if="hasPrimaryAction"
variant="primary"
@click.prevent.stop="primary()"
@click="primary(onClose)"
:icon="primaryIcon"
:disabled="primaryDisabled || loading"
class="ml-2"
@ -83,10 +83,11 @@ defineProps({
},
})
// 'close'
const emit = defineEmits(['create', 'primary', 'tertiary'])
function primary() {
emit('create')
emit('primary')
function primary(onClose: () => void) {

We pass onClose as a callback parameter to the primary emit so that it can reuse the onClose after the primary action finished (unsure if that makes sense).

We pass `onClose` as a callback parameter to the `primary` emit so that it can reuse the `onClose` after the primary action finished (unsure if that makes sense).
emit('create', onClose)
emit('primary', onClose)
}
</script>

View File

@ -1,7 +1,7 @@
<template>
<Teleport to="body">
<!-- FIXME: transition should not be included in the modal -->
<CustomTransition :name="transitionName" appear>
<CustomTransition :name="transitionName" appear :appear-class="transitionName">

The appear transition is currently broken. Fix this.

The appear transition is currently broken. Fix this.
<section
v-if="enabled"
class="modal-mask"
@ -10,11 +10,11 @@
variant,
]"
ref="modal"
v-bind="attrs"
v-bind="$attrs"
>
<div
class="modal-container"
@click.self.prevent.stop="$emit('close')"
@click.self.prevent.stop="onClose"
v-shortcut="'Escape'"
>
<div
@ -25,13 +25,13 @@
}"
>
<BaseButton
@click="$emit('close')"
@click="onClose"
class="close"
>
<icon icon="times"/>
</BaseButton>
<slot>
<slot name="default" :onClose="onClose">
<div class="header">
<slot name="header"></slot>
</div>
@ -40,14 +40,14 @@
</div>
<div class="actions">
<x-button
@click="$emit('close')"
@click="onClose"
variant="tertiary"
class="has-text-danger"
>
{{ $t('misc.cancel') }}
</x-button>
<x-button
@click="$emit('submit')"
@click="$emit('submit', onClose)"
variant="primary"
v-cy="'modalPrimary'"
:shadow="false"
@ -70,10 +70,11 @@ export default {
</script>
<script lang="ts" setup>
import {ref, watchEffect} from 'vue'
import {useScrollLock} from '@vueuse/core'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import {ref, useAttrs, watchEffect} from 'vue'
import {useScrollLock} from '@vueuse/core'
const props = withDefaults(defineProps<{
enabled?: boolean,
@ -87,9 +88,7 @@ const props = withDefaults(defineProps<{
variant: 'default',
})
defineEmits(['close', 'submit'])
const attrs = useAttrs()
const emit = defineEmits(['close', 'submit'])
const modal = ref<HTMLElement | null>(null)
const scrollLock = useScrollLock(modal)
@ -97,6 +96,10 @@ const scrollLock = useScrollLock(modal)
watchEffect(() => {
scrollLock.value = props.enabled
})
function onClose() {

FIXME: either we use an implicit attrs.onClose or we explicit define a close emit. Both should not be mixed.

FIXME: either we use an implicit `attrs.onClose` or we explicit define a `close` emit. Both should not be mixed.
emit('close')
}
</script>
<style lang="scss" scoped>

View File

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

View File

@ -44,10 +44,10 @@ export function useRouteWithModal() {
function closeModal() {
const historyState = computed(() => route.fullPath && window.history.state)
if (historyState.value) {
if (historyState.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.
router.back()
} else {
const backdropRoute = historyState.value?.backdropView && router.resolve(historyState.value.backdropView)
const backdropRoute = historyState.value.backdropView && router.resolve(historyState.value.backdropView)
router.push(backdropRoute)
}
}

View File

@ -113,7 +113,7 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
router.push({name: 'list.index', params: {listId: getListId(filter.value)}})
}
async function saveFilter() {
async function saveFilter(callback: () => void) {
const response = await filterService.update(filter.value)
await namespaceStore.loadNamespaces()
success({message: t('filters.edit.success')})
@ -123,7 +123,7 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
id: getListId(filter.value),
title: filter.value.title,
}))
router.back()
callback()
}
async function deleteFilter() {

View File

@ -1,14 +1,14 @@
<template>
<modal
@close="$router.back()"
transition-name="fade"
variant="hint-modal"
>
>
<template #default="{onClose}">
<card
class="has-no-shadow"
:title="$t('about.title')"
:has-close="true"
@close="$router.back()"
@close="onClose"
:padding="false"
>
<div class="p-4">
@ -22,12 +22,13 @@
<template #footer>
<x-button
variant="secondary"
@click.prevent.stop="$router.back()"
@click="onClose"
>
{{ $t('misc.close') }}
</x-button>
</template>
</card>
</template>
</modal>
</template>

View File

@ -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 } }"
:to="{ name: 'list.create', params: { namespaceId: defaultNamespaceId }, state: { backdropView: $route.fullPath } }"
:shadow="false"
class="ml-2"
>

View File

@ -1,8 +1,5 @@
<template>
<modal
@close="$router.back()"
@submit="deleteFilter()"
>
<modal @submit="deleteFilter()">
<template #header>
<span>{{ $t('filters.delete.header') }}</span>
</template>

View File

@ -6,15 +6,16 @@
@primary="saveFilter"
:tertiary="$t('misc.delete')"
@tertiary="$router.push({ name: 'filter.settings.delete', params: { id: listId } })"
#default="{onClose}"
>
<form @submit.prevent="saveFilter()">
<form @submit.prevent="saveFilter(onClose)">
<div class="field">
<label class="label" for="title">{{ $t('filters.attributes.title') }}</label>
<div class="control">
<input
:class="{ 'disabled': filterService.loading}"
:disabled="filterService.loading || undefined"
@keyup.enter="saveFilter"
@keyup.enter="saveFilter(onClose)"
class="input"
id="title"
:placeholder="$t('filters.attributes.titlePlaceholder')"

View File

@ -1,8 +1,5 @@
<template>
<modal
@close="$router.back()"
variant="hint-modal"
>
<modal variant="hint-modal">
<card class="has-no-shadow" :title="$t('filters.create.title')">
<p>
{{ $t('filters.create.description') }}

View File

@ -1,7 +1,10 @@
<template>
<div :class="{ 'is-loading': loading}" class="loader-container">
<x-button
:to="{name:'labels.create'}"
:to="{
name:'labels.create',
state: { backdropView: $route.fullPath }
}"
class="is-pulled-right"
icon="plus"
>
@ -15,7 +18,10 @@
</p>
<p v-else class="has-text-centered has-text-grey is-italic">
{{ $t('label.newCTA') }}
<router-link :to="{name:'labels.create'}">{{ $t('label.create.title') }}.</router-link>
<router-link :to="{
name:'labels.create',
state: { backdropView: $route.fullPath }
}">{{ $t('label.create.title') }}.</router-link>
</p>
</div>

View File

@ -1,10 +1,6 @@
<template>
<modal
@close="$router.back()"
>
<card
:title="list.title"
>
<modal>
<card :title="list.title">
<div class="has-text-left" v-html="htmlDescription" v-if="htmlDescription !== ''"></div>
<p v-else class="is-italic">
{{ $t('list.noDescriptionAvailable') }}

View File

@ -1,5 +1,10 @@
<template>
<create-edit :title="$t('list.create.header')" @create="createNewList()" :primary-disabled="list.title === ''">
<create-edit
:title="$t('list.create.header')"
@create="createNewList()"
:primary-disabled="list.title === ''"
#default="{onClose}"
>
<div class="field">
<label class="label" for="listTitle">{{ $t('list.title') }}</label>
<div
@ -9,7 +14,7 @@
<input
:class="{ disabled: listService.loading }"
@keyup.enter="createNewList()"
@keyup.esc="$router.back()"
@keyup.esc="onClose"
class="input"
:placeholder="$t('list.create.titlePlaceholder')"
type="text"

View File

@ -1,8 +1,5 @@
<template>
<modal
@close="$router.back()"
@submit="archiveList()"
>
<modal @submit="archiveList">
<template #header><span>{{ list.isArchived ? $t('list.archive.unarchive') : $t('list.archive.archive') }}</span></template>
<template #text>
@ -17,7 +14,7 @@ export default {name: 'list-setting-archive'}
<script setup lang="ts">
import {computed} from 'vue'
import {useRouter, useRoute} from 'vue-router'
import {useRoute} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {success} from '@/message'
@ -28,13 +25,12 @@ import {useListStore} from '@/stores/lists'
const {t} = useI18n({useScope: 'global'})
const listStore = useListStore()
const router = useRouter()
const route = useRoute()
const list = computed(() => listStore.getListById(route.params.listId))
useTitle(() => t('list.archive.title', {list: list.value.title}))
async function archiveList() {
async function archiveList(onClose: () => void) {
try {
const newList = await listStore.updateList({
...list.value,
@ -43,7 +39,7 @@ async function archiveList() {
useBaseStore().setCurrentList(newList)
success({message: t('list.archive.success')})
} finally {
router.back()
onClose()
}
}
</script>

View File

@ -73,19 +73,19 @@
</x-button>
</template>
<template #footer>
<template #footer="{onClose}">
<x-button
v-if="hasBackground"
:shadow="false"
variant="tertiary"
class="is-danger"
@click.prevent.stop="removeBackground"
@click="removeBackground(onClose)"

We should not need to use .prevent and .stop with the vueuse onClickOutside.

We should not need to use `.prevent` and `.stop` with the vueuse `onClickOutside`.
>
{{ $t('list.background.remove') }}
</x-button>
<x-button
variant="secondary"
@click.prevent.stop="$router.back()"
@click="onClose"
>
{{ $t('misc.close') }}
</x-button>
@ -100,7 +100,7 @@ export default { name: 'list-setting-background' }
<script setup lang="ts">
import {ref, computed, shallowReactive} from 'vue'
import {useI18n} from 'vue-i18n'
import {useRoute, useRouter} from 'vue-router'
import {useRoute} from 'vue-router'
import debounce from 'lodash.debounce'
import BaseButton from '@/components/base/BaseButton.vue'
@ -127,7 +127,6 @@ const SEARCH_DEBOUNCE = 300
const {t} = useI18n({useScope: 'global'})
const baseStore = useBaseStore()
const route = useRoute()
const router = useRouter()
useTitle(() => t('list.background.title'))
@ -216,13 +215,13 @@ async function uploadBackground() {
success({message: t('list.background.success')})
}
async function removeBackground() {
async function removeBackground(onClose: () => void) {
const list = await listService.value.removeBackground(currentList.value)
await baseStore.handleSetCurrentList({list, forceUpdate: true})
namespaceStore.setListInNamespaceById(list)
listStore.setList(list)
success({message: t('list.background.removeSuccess')})
router.back()
onClose()
}
</script>

View File

@ -1,8 +1,5 @@
<template>
<modal
@close="$router.back()"
@submit="deleteList()"
>
<modal @submit="deleteList()">
<template #header><span>{{ $t('list.delete.header') }}</span></template>
<template #text>

View File

@ -5,7 +5,8 @@
:primary-label="$t('misc.save')"
@primary="save"
:tertiary="$t('misc.delete')"
@tertiary="$router.push({ name: 'list.settings.delete', params: { id: listId } })"
@tertiary="$router.push({ name: 'list.settings.delete', params: { id: listId }, state: {backdropView: $route.fullPath} })"
#default="{onClose}"
>
<div class="field">
<label class="label" for="title">{{ $t('list.title') }}</label>
@ -13,7 +14,7 @@
<input
:class="{ 'disabled': isLoading}"
:disabled="isLoading || undefined"
@keyup.enter="save"
@keyup.enter="save(onClose)"
class="input"
id="title"
:placeholder="$t('list.edit.titlePlaceholder')"
@ -33,7 +34,7 @@
<input
:class="{ 'disabled': isLoading}"
:disabled="isLoading || undefined"
@keyup.enter="save"
@keyup.enter="save(onClose)"
class="input"
id="identifier"
:placeholder="$t('list.edit.identifierPlaceholder')"
@ -71,7 +72,6 @@ export default { name: 'list-setting-edit' }
<script setup lang="ts">
import type {PropType} from 'vue'
import {useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n'
import Editor from '@/components/input/AsyncEditor'
@ -92,17 +92,15 @@ const props = defineProps({
},
})
const router = useRouter()
const {t} = useI18n({useScope: 'global'})
const {list, save: saveList, isLoading} = useList(props.listId)
useTitle(() => list?.title ? t('list.edit.title', {list: list.title}) : '')
async function save() {
async function save(onClose: () => void) {
await saveList()
await useBaseStore().handleSetCurrentList({list})
router.back()
onClose()
}
</script>

View File

@ -6,10 +6,10 @@
</fancycheckbox>
<div class="action-buttons">
<x-button :to="{name: 'filters.create'}" icon="filter">
<x-button :to="{name: 'filters.create', state: {backdropView: $route.fullPath}}" icon="filter">
{{ $t('filters.create.title') }}
</x-button>
<x-button :to="{name: 'namespace.create'}" icon="plus" v-cy="'new-namespace'">
<x-button :to="{name: 'namespace.create', state: {backdropView: $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'}">
<BaseButton :to="{name: 'namespace.create', state: {backdropView: $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}}"
:to="{name: 'list.create', params: {namespaceId: n.id}, state: { backdropView: $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}}"
:to="{name: 'namespace.settings.archive', params: {id: n.id}, state: { backdropView: $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}}">
<BaseButton :to="{name: 'list.create', params: {namespaceId: n.id}, state: { backdropView: $route.fullPath }}">
{{ $t('namespace.createList') }}
</BaseButton>
</p>

View File

@ -1,8 +1,9 @@
<template>
<create-edit
:title="$t('namespace.create.title')"
@create="newNamespace()"
@create="newNamespace"
:primary-disabled="namespace.title === ''"
#default="{onClose}"
>
<div class="field">
<label class="label" for="namespaceTitle">{{ $t('namespace.attributes.title') }}</label>
@ -14,8 +15,8 @@
But with the input modal here since it autofocuses the input that input field catches the focus instead.
Hence we place the listener on the input field directly. -->
<input
@keyup.enter="newNamespace()"
@keyup.esc="$router.back()"
@keyup.enter="newNamespace(onClose)"
@keyup.esc="onClose"
class="input"
:placeholder="$t('namespace.attributes.titlePlaceholder')"
type="text"
@ -46,7 +47,6 @@
<script setup lang="ts">
import {ref, shallowReactive} from 'vue'
import {useI18n} from 'vue-i18n'
import {useRouter} from 'vue-router'
import Message from '@/components/misc/message.vue'
import CreateEdit from '@/components/misc/create-edit.vue'
@ -65,11 +65,10 @@ const namespace = ref<INamespace>(new NamespaceModel())
const namespaceService = shallowReactive(new NamespaceService())
const {t} = useI18n({useScope: 'global'})
const router = useRouter()
useTitle(() => t('namespace.create.title'))
async function newNamespace() {
async function newNamespace(onClose: () => void) {
if (namespace.value.title === '') {
showError.value = true
return
@ -79,6 +78,6 @@ async function newNamespace() {
const newNamespace = await namespaceService.create(namespace.value)
useNamespaceStore().addNamespace(newNamespace)
success({message: t('namespace.create.success')})
router.back()
onClose()
}
</script>

View File

@ -1,8 +1,5 @@
<template>
<modal
@close="$router.back()"
@submit="archiveNamespace()"
>
<modal @submit="archiveNamespace">
<template #header><span>{{ title }}</span></template>
<template #text>
@ -23,7 +20,6 @@ export default { name: 'namespace-setting-archive' }
<script setup lang="ts">
import {watch, ref, computed, shallowReactive, type PropType} from 'vue'
import {useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {success} from '@/message'
@ -41,7 +37,6 @@ const props = defineProps({
},
})
const router = useRouter()
const {t} = useI18n({useScope: 'global'})
const namespaceStore = useNamespaceStore()
@ -69,7 +64,7 @@ const title = computed(() => {
})
useTitle(title)
async function archiveNamespace() {
async function archiveNamespace(onClose: () => void) {
try {
const isArchived = !namespace.value.isArchived
const archivedNamespace = await namespaceService.update({
@ -83,7 +78,7 @@ async function archiveNamespace() {
: t('namespace.archive.unarchiveSuccess'),
})
} finally {
router.back()
onClose()
}
}
</script>

View File

@ -1,8 +1,5 @@
<template>
<modal
@close="$router.back()"
@submit="deleteNamespace()"
>
<modal @submit="deleteNamespace()">
<template #header><span>{{ title }}</span></template>
<template #text>

View File

@ -5,9 +5,10 @@
:primary-label="$t('misc.save')"
@primary="save"
:tertiary="$t('misc.delete')"
@tertiary="$router.push({ name: 'namespace.settings.delete', params: { id: $route.params.id } })"
@tertiary="$router.push({ name: 'namespace.settings.delete', params: { id: $route.params.id }, state: { backdropView: $route.fullPath } })"
#default="{onClose}"
>
<form @submit.prevent="save()">
<form @submit.prevent="save(onClose)">
<div class="field">
<label class="label" for="namespacetext">{{ $t('namespace.attributes.title') }}</label>
<div class="control">
@ -58,7 +59,6 @@
<script lang="ts" setup>
import {nextTick, ref, watch} from 'vue'
import {success} from '@/message'
import router from '@/router'
import AsyncEditor from '@/components/input/AsyncEditor'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
@ -110,11 +110,11 @@ async function loadNamespace() {
title.value = t('namespace.edit.title', {namespace: namespace.value.title})
}
async function save() {
async function save(onClose: () => void) {
const updatedNamespace = await namespaceService.value.update(namespace.value)
// Update the namespace in the parent
namespaceStore.setNamespaceById(updatedNamespace)
success({message: t('namespace.edit.success')})
router.back()
onClose()
}
</script>

View File

@ -1,4 +1,5 @@
<template>
<OptionalWrapper :is="isModal && Modal" variant="scrolling">

By using the OptionalWrapper we can use components that can be used with or without Modal

By using the OptionalWrapper we can use components that can be used with or without Modal
<div
class="loader-container task-view-container"
:class="{
@ -442,10 +443,11 @@
</template>
</modal>
</div>
</OptionalWrapper>
</template>
<script lang="ts" setup>
import {ref, reactive, toRef, shallowReactive, computed, watch, nextTick, type PropType} from 'vue'
import {ref, reactive, toRef, shallowReactive, computed, watch, nextTick, type PropType, useAttrs} from 'vue'
import {useRouter, type RouteLocation} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {unrefElement} from '@vueuse/core'
@ -480,7 +482,10 @@ import RelatedTasks from '@/components/tasks/partials/relatedTasks.vue'
import Reminders from '@/components/tasks/partials/reminders.vue'
import RepeatAfter from '@/components/tasks/partials/repeatAfter.vue'
import TaskSubscription from '@/components/misc/subscription.vue'
import OptionalWrapper from '@/components/base/OptionalWrapper.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import Modal from '@/components/misc/modal.vue'
import {uploadFile} from '@/helpers/attachments'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
@ -508,7 +513,8 @@ const props = defineProps({
})
defineEmits(['close'])
const attrs = useAttrs()
console.log(JSON.parse(JSON.stringify(attrs)))

Remove log

Remove log
const router = useRouter()
const {t} = useI18n({useScope: 'global'})

View File

@ -1,7 +1,10 @@
<template>
<div class="content loader-container is-max-width-desktop" :class="{ 'is-loading': teamService.loading}">
<x-button
:to="{name:'teams.create'}"
:to="{
name:'teams.create',
state: { backdropView: $route.fullPath },
}"
class="is-pulled-right"
icon="plus"
>
@ -18,7 +21,10 @@
</ul>
<p v-else-if="!teamService.loading" class="has-text-centered has-text-grey is-italic">
{{ $t('team.noTeams') }}
<router-link :to="{name: 'teams.create'}">
<router-link :to="{
name: 'teams.create',
state: { backdropView: $route.fullPath },
}">
{{ $t('team.create.title') }}.
</router-link>
</p>

View File

@ -1,5 +1,6 @@
<template>
<create-edit
class="new-team"
:title="title"
@create="newTeam()"
:primary-disabled="team.name === ''"