WIP: feat: route modals everywhere #2735
|
@ -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()
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
"@types/lodash.clonedeep": "4.5.7",
|
||||
"@types/sortablejs": "1.15.0",
|
||||
"@vueuse/core": "9.6.0",
|
||||
"@vueuse/integrations": "9.6.0",
|
||||
"@vueuse/router": "9.6.0",
|
||||
"axios": "0.27.2",
|
||||
"blurhash": "2.0.4",
|
||||
|
@ -45,6 +46,7 @@
|
|||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.21",
|
||||
"floating-vue": "2.0.0-beta.20",
|
||||
"focus-trap": "^7.1.0",
|
||||
"highlight.js": "11.6.0",
|
||||
"is-touch-device": "1.0.1",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
|
|
110
pnpm-lock.yaml
110
pnpm-lock.yaml
|
@ -33,6 +33,7 @@ specifiers:
|
|||
'@vue/test-utils': 2.2.4
|
||||
'@vue/tsconfig': 0.1.3
|
||||
'@vueuse/core': 9.6.0
|
||||
'@vueuse/integrations': 9.6.0
|
||||
'@vueuse/router': 9.6.0
|
||||
autoprefixer: 10.4.13
|
||||
axios: 0.27.2
|
||||
|
@ -56,6 +57,7 @@ specifiers:
|
|||
flatpickr: 4.6.13
|
||||
flexsearch: 0.7.21
|
||||
floating-vue: 2.0.0-beta.20
|
||||
focus-trap: ^7.1.0
|
||||
happy-dom: 7.7.0
|
||||
highlight.js: 11.6.0
|
||||
is-touch-device: 1.0.1
|
||||
|
@ -104,6 +106,7 @@ dependencies:
|
|||
'@types/lodash.clonedeep': 4.5.7
|
||||
'@types/sortablejs': 1.15.0
|
||||
'@vueuse/core': 9.6.0_vue@3.2.45
|
||||
'@vueuse/integrations': 9.6.0_v6so3rexzmt44vm4rsvyznwkka
|
||||
'@vueuse/router': 9.6.0_xsxatmlnmmg5bcuv3xdnj6fj7y
|
||||
axios: 0.27.2
|
||||
blurhash: 2.0.4
|
||||
|
@ -118,6 +121,7 @@ dependencies:
|
|||
flatpickr: 4.6.13
|
||||
flexsearch: 0.7.21
|
||||
floating-vue: 2.0.0-beta.20_vue@3.2.45
|
||||
focus-trap: 7.1.0
|
||||
highlight.js: 11.6.0
|
||||
is-touch-device: 1.0.1
|
||||
lodash.clonedeep: 4.5.0
|
||||
|
@ -3236,13 +3240,18 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/types/5.43.0:
|
||||
resolution: {integrity: sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/types/5.44.0:
|
||||
resolution: {integrity: sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/typescript-estree/5.44.0_mmt6grxdx77rvjuvebzbfquz6y:
|
||||
resolution: {integrity: sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw==}
|
||||
/@typescript-eslint/typescript-estree/5.43.0_mmt6grxdx77rvjuvebzbfquz6y:
|
||||
resolution: {integrity: sha512-BZ1WVe+QQ+igWal2tDbNg1j2HWUkAa+CVqdU79L4HP9izQY6CNhXfkNwd1SS4+sSZAP/EthI1uiCSY/+H0pROg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
|
@ -3250,8 +3259,8 @@ packages:
|
|||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.44.0
|
||||
'@typescript-eslint/visitor-keys': 5.44.0
|
||||
'@typescript-eslint/types': 5.43.0
|
||||
'@typescript-eslint/visitor-keys': 5.43.0
|
||||
debug: 4.3.4_supports-color@9.2.1
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
|
@ -3262,6 +3271,27 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/typescript-estree/5.43.0_typescript@4.9.3:
|
||||
resolution: {integrity: sha512-BZ1WVe+QQ+igWal2tDbNg1j2HWUkAa+CVqdU79L4HP9izQY6CNhXfkNwd1SS4+sSZAP/EthI1uiCSY/+H0pROg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.43.0
|
||||
'@typescript-eslint/visitor-keys': 5.43.0
|
||||
debug: 4.3.4
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.3.7
|
||||
tsutils: 3.21.0_typescript@4.9.3
|
||||
typescript: 4.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/typescript-estree/5.44.0_typescript@4.9.3:
|
||||
resolution: {integrity: sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
@ -3303,6 +3333,14 @@ packages:
|
|||
- typescript
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/visitor-keys/5.43.0:
|
||||
resolution: {integrity: sha512-icl1jNH/d18OVHLfcwdL3bWUKsBeIiKYTGxMJCoGe7xFht+E4QgzOqoWYrU8XSLJWhVw8nTacbm03v23J/hFTg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.43.0
|
||||
eslint-visitor-keys: 3.3.0
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/visitor-keys/5.44.0:
|
||||
resolution: {integrity: sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
@ -3661,6 +3699,54 @@ packages:
|
|||
- vue
|
||||
dev: false
|
||||
|
||||
/@vueuse/integrations/9.6.0_v6so3rexzmt44vm4rsvyznwkka:
|
||||
resolution: {integrity: sha512-+rs2OWY/3spxoAGQMnlHQpxf8ErAYf4D1bT0aXaPnxphmtYgexm6KIjTFpBbcQnHwVi1g2ET1SJoQL16yDrgWA==}
|
||||
peerDependencies:
|
||||
async-validator: '*'
|
||||
axios: '*'
|
||||
change-case: '*'
|
||||
drauu: '*'
|
||||
focus-trap: '*'
|
||||
fuse.js: '*'
|
||||
idb-keyval: '*'
|
||||
jwt-decode: '*'
|
||||
nprogress: '*'
|
||||
qrcode: '*'
|
||||
universal-cookie: '*'
|
||||
peerDependenciesMeta:
|
||||
async-validator:
|
||||
optional: true
|
||||
axios:
|
||||
optional: true
|
||||
change-case:
|
||||
optional: true
|
||||
drauu:
|
||||
optional: true
|
||||
focus-trap:
|
||||
optional: true
|
||||
fuse.js:
|
||||
optional: true
|
||||
idb-keyval:
|
||||
optional: true
|
||||
jwt-decode:
|
||||
optional: true
|
||||
nprogress:
|
||||
optional: true
|
||||
qrcode:
|
||||
optional: true
|
||||
universal-cookie:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@vueuse/core': 9.6.0_vue@3.2.45
|
||||
'@vueuse/shared': 9.6.0_vue@3.2.45
|
||||
axios: 0.27.2
|
||||
focus-trap: 7.1.0
|
||||
vue-demi: 0.12.1_vue@3.2.45
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
dev: false
|
||||
|
||||
/@vueuse/metadata/9.6.0:
|
||||
resolution: {integrity: sha512-sIC8R+kWkIdpi5X2z2Gk8TRYzmczDwHRhEFfCu2P+XW2JdPoXrziqsGpDDsN7ykBx4ilwieS7JUIweVGhvZ93w==}
|
||||
dev: false
|
||||
|
@ -5775,7 +5861,7 @@ packages:
|
|||
resolution: {integrity: sha512-lR78AugfUSBojwlSRZBeEqQ1l8LI7rbxOl1qTUnGLcjZQDjZmrZCb7R46rK8U8B5WzFvJrxa7fEBA8FoD/n5fA==}
|
||||
engines: {node: ^12.20.0 || ^14.14.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.3
|
||||
'@typescript-eslint/typescript-estree': 5.43.0_typescript@4.9.3
|
||||
ast-module-types: 3.0.0
|
||||
node-source-walk: 5.0.0
|
||||
typescript: 4.9.3
|
||||
|
@ -5787,7 +5873,7 @@ packages:
|
|||
resolution: {integrity: sha512-lR78AugfUSBojwlSRZBeEqQ1l8LI7rbxOl1qTUnGLcjZQDjZmrZCb7R46rK8U8B5WzFvJrxa7fEBA8FoD/n5fA==}
|
||||
engines: {node: ^12.20.0 || ^14.14.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 5.44.0_mmt6grxdx77rvjuvebzbfquz6y
|
||||
'@typescript-eslint/typescript-estree': 5.43.0_mmt6grxdx77rvjuvebzbfquz6y
|
||||
ast-module-types: 3.0.0
|
||||
node-source-walk: 5.0.0
|
||||
typescript: 4.9.3
|
||||
|
@ -7008,6 +7094,12 @@ packages:
|
|||
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
|
||||
dev: true
|
||||
|
||||
/focus-trap/7.1.0:
|
||||
resolution: {integrity: sha512-CuJvwUBfJCWcU6fc4xr3UwMF5vWnox4isXAixCwrPzCsPKOQjP9T+nTlYT2t+vOmQL8MOQ16eim99XhjQHAuiQ==}
|
||||
dependencies:
|
||||
tabbable: 6.0.1
|
||||
dev: false
|
||||
|
||||
/folder-walker/3.2.0:
|
||||
resolution: {integrity: sha512-VjAQdSLsl6AkpZNyrQJfO7BXLo4chnStqb055bumZMbRUPpVuPN3a4ktsnRCmrFZjtMlYLkyXiR5rAs4WOpC4Q==}
|
||||
dependencies:
|
||||
|
@ -12255,6 +12347,10 @@ packages:
|
|||
resolution: {integrity: sha512-P3cgh2bpaPvAO2NE3uRp/n6hmk4xPX4DQf+UzTlCAycssKdqhp6hjw+ENWe+aUS7TogKRFtptMosTSFeC6R55g==}
|
||||
dev: true
|
||||
|
||||
/tabbable/6.0.1:
|
||||
resolution: {integrity: sha512-SYJSIgeyXW7EuX1ytdneO5e8jip42oHWg9xl/o3oTYhmXusZVgiA+VlPvjIN+kHii9v90AmzTZEBcsEvuAY+TA==}
|
||||
dev: false
|
||||
|
||||
/tabtab/3.0.2:
|
||||
resolution: {integrity: sha512-jANKmUe0sIQc/zTALTBy186PoM/k6aPrh3A7p6AaAfF6WPSbTx1JYeGIGH162btpH+mmVEXln+UxwViZHO2Jhg==}
|
||||
dependencies:
|
||||
|
@ -12968,7 +13064,7 @@ packages:
|
|||
dev: true
|
||||
|
||||
/verror/1.10.0:
|
||||
resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
|
||||
resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=}
|
||||
engines: {'0': node >=0.6.0}
|
||||
dependencies:
|
||||
assert-plus: 1.0.0
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
export default { inheritAttrs: false }
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* This component makes it possible to optionally wrap around another component.
|
||||
* It's based on these ideas:
|
||||
*
|
||||
* - https://github.com/vuejs/rfcs/pull/449
|
||||
* - https://github.com/vuejs/rfcs/discussions/448#discussioncomment-2769396=
|
||||
*/
|
||||
defineProps<{
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
is: any,
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component v-if="is" :is="is" v-bind="$attrs">
|
||||
<slot />
|
||||
</component>
|
||||
<slot v-else />
|
||||
</template>
|
|
@ -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},}" 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',}"
|
||||
>
|
||||
{{ $t('about.title') }}
|
||||
</dropdown-item>
|
||||
|
|
|
@ -32,20 +32,13 @@
|
|||
|
||||
<quick-actions/>
|
||||
|
||||
<router-view :route="routeWithModal" v-slot="{ Component }">
|
||||
<router-view :route="baseRoute" v-slot="{ Component }">
|
||||
<keep-alive :include="['list.list', 'list.gantt', 'list.table', 'list.kanban']">
|
||||
<component :is="Component"/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
|
||||
<modal
|
||||
:enabled="Boolean(currentModal)"
|
||||
@close="closeModal()"
|
||||
variant="scrolling"
|
||||
class="task-detail-view-modal"
|
||||
>
|
||||
<component :is="currentModal"/>
|
||||
</modal>
|
||||
<component :is="modalRoute" />
|
||||
|
||||
<BaseButton
|
||||
class="keyboard-shortcuts-button d-print-none"
|
||||
|
@ -73,7 +66,7 @@ import {useLabelStore} from '@/stores/labels'
|
|||
import {useRouteWithModal} from '@/composables/useRouteWithModal'
|
||||
import {useRenewTokenOnFocus} from '@/composables/useRenewTokenOnFocus'
|
||||
|
||||
const {routeWithModal, currentModal, closeModal} = useRouteWithModal()
|
||||
const {baseRoute, modalRoute} = useRouteWithModal()
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const background = computed(() => baseStore.background)
|
||||
|
|
|
@ -367,6 +367,7 @@ watch(
|
|||
left: 0;
|
||||
z-index: 7;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
@ -379,6 +380,7 @@ watch(
|
|||
right: 0;
|
||||
z-index: 9;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ddd;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,38 +1,38 @@
|
|||
<template>
|
||||
<modal @close="$router.back()" :overflow="true" :wide="wide">
|
||||
<modal :overflow="true" :wide="wide" #default="{ onClose }">
|
||||
<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) {
|
||||
emit('create', onClose)
|
||||
emit('primary', onClose)
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -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">
|
||||
<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,13 @@ export default {
|
|||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {nextTick, ref, watchEffect} from 'vue'
|
||||
import {useScrollLock} from '@vueuse/core'
|
||||
import {useFocusTrap} from '@vueuse/integrations/useFocusTrap'
|
||||
import type {RouteLocationRaw} from 'vue-router'
|
||||
|
||||
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,
|
||||
|
@ -81,22 +84,34 @@ const props = withDefaults(defineProps<{
|
|||
wide?: boolean,
|
||||
transitionName?: 'modal' | 'fade',
|
||||
variant?: 'default' | 'hint-modal' | 'scrolling',
|
||||
closeRoute?: RouteLocationRaw,
|
||||
}>(), {
|
||||
enabled: true,
|
||||
transitionName: 'modal',
|
||||
variant: 'default',
|
||||
})
|
||||
|
||||
defineEmits(['close', 'submit'])
|
||||
|
||||
const attrs = useAttrs()
|
||||
|
||||
const modal = ref<HTMLElement | null>(null)
|
||||
const scrollLock = useScrollLock(modal)
|
||||
const emit = defineEmits(['close', 'submit'])
|
||||
|
||||
const scrollLock = useScrollLock(document.body)
|
||||
watchEffect(() => {
|
||||
scrollLock.value = props.enabled
|
||||
})
|
||||
|
||||
const modal = ref(null)
|
||||
const {activate, deactivate} = useFocusTrap(modal)
|
||||
watchEffect(() => {
|
||||
if (props.enabled) {
|
||||
// wait for content to be loaded
|
||||
nextTick(() => activate())
|
||||
} else {
|
||||
deactivate()
|
||||
}
|
||||
})
|
||||
|
||||
function onClose() {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -122,6 +137,8 @@ $modal-width: 1024px;
|
|||
height: 100%;
|
||||
max-height: 100vh;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.default .modal-content,
|
||||
|
@ -131,6 +148,7 @@ $modal-width: 1024px;
|
|||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
overflow: visible; // reset bulma
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
margin: 0;
|
||||
|
|
|
@ -153,7 +153,6 @@ function openTask(e: {
|
|||
router.push({
|
||||
name: 'task.detail',
|
||||
params: {id: e.bar.ganttBarConfig.id},
|
||||
state: {backdropView: router.currentRoute.value.fullPath},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -121,7 +121,6 @@ function openTaskDetail() {
|
|||
router.push({
|
||||
name: 'task.detail',
|
||||
params: {id: props.task.id},
|
||||
state: {backdropView: router.currentRoute.value.fullPath},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
/>
|
||||
|
||||
<router-link
|
||||
:to="taskDetailRoute"
|
||||
:to="{name: 'task.detail', params: {id: task.id}}"
|
||||
:class="{ 'done': task.done}"
|
||||
class="tasktext"
|
||||
>
|
||||
|
@ -221,14 +221,6 @@ const currentList = computed(() => {
|
|||
} : baseStore.currentList
|
||||
})
|
||||
|
||||
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 },
|
||||
}))
|
||||
|
||||
|
||||
async function markAsDone(checked: boolean) {
|
||||
const updateFunc = async () => {
|
||||
const newTask = await taskStore.update(task.value)
|
||||
|
|
|
@ -1,56 +1,200 @@
|
|||
import { computed, shallowRef, watchEffect, h, type VNode } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {computed, shallowRef, watch, h, type VNode, ref} from 'vue'
|
||||
import {useRoute, useRouter, loadRouteLocation, type RouteLocationNormalizedLoaded, type RouteLocationRaw, START_LOCATION} from 'vue-router'
|
||||
import router, { handleRedirectRecord } from '@/router'
|
||||
|
||||
// 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
|
||||
? routePropsOption === true
|
||||
? route.params
|
||||
: typeof routePropsOption === 'function'
|
||||
? routePropsOption(route)
|
||||
: routePropsOption
|
||||
: {}
|
||||
}
|
||||
|
||||
function resolveAndLoadRoute(route: RouteLocationRaw, currentLocation?: RouteLocationNormalizedLoaded) {
|
||||
const resolvedRoute = router.resolve(route, currentLocation)
|
||||
|
||||
// resolvedRoute.matched.forEach((record) => console.log(record.redirect))
|
||||
|
||||
// e.g. 'list.index' will always redirect, so we need to resolve the redirected route record
|
||||
const redirectedRoute = handleRedirectRecord(resolvedRoute) || resolvedRoute
|
||||
|
||||
return loadRouteLocation(router.resolve(redirectedRoute, currentLocation))
|
||||
}
|
||||
|
||||
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 historyStateBackdropRoutePath = computed<RouteLocationRaw | undefined>(() => {
|
||||
// every time the fullPath changes we check the history state
|
||||
// this happens also initially
|
||||
return route.fullPath
|
||||
? history.state?.backdropRoutePath
|
||||
: undefined
|
||||
})
|
||||
|
||||
const currentModal = shallowRef<VNode>()
|
||||
watchEffect(() => {
|
||||
if (!backdropView.value) {
|
||||
currentModal.value = undefined
|
||||
const isInitialNavigation = ref(true)
|
||||
|
||||
router.beforeEach((to, from) => {
|
||||
isInitialNavigation.value = from === START_LOCATION
|
||||
})
|
||||
|
||||
const lastBackdropRoutePath = ref<string>()
|
||||
router.afterEach((to, from) => {
|
||||
const resolvedRoute = router.resolve(to)
|
||||
if (!resolvedRoute.meta.modal) {
|
||||
// this route doesn't define that it can be a modal
|
||||
return
|
||||
}
|
||||
|
||||
// this is adapted from vue-router
|
||||
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
|
||||
const routePropsOption = route.matched[0]?.props.default
|
||||
const routeProps = routePropsOption
|
||||
? routePropsOption === true
|
||||
? route.params
|
||||
: typeof routePropsOption === 'function'
|
||||
? routePropsOption(route)
|
||||
: routePropsOption
|
||||
: {}
|
||||
let backdropRoutePath
|
||||
if (resolvedRoute.meta?.modal === true || resolvedRoute.meta.modal?.force !== true) {
|
||||
// this route can be shown as modal or as normal view
|
||||
|
||||
routeProps.backdropView = backdropView.value
|
||||
// TODO: add new state or prop instead of checking backdropRoutePath here
|
||||
// e.g. `modal` with `'normal' | 'sidebar' | 'modal' | 'sheet'`
|
||||
if (!history.state.backdropRoutePath) {
|
||||
// since this modal is optional and we don't have a backdropRoutePath we don't do anything
|
||||
return
|
||||
}
|
||||
} else if (lastBackdropRoutePath.value) {
|
||||
// there was already a modal in the last route
|
||||
// we have to save this in the lastBackdropRoutePath ref because
|
||||
// the state gets overwritten
|
||||
|
||||
const component = route.matched[0]?.components?.default
|
||||
|
||||
if (!component) {
|
||||
currentModal.value = undefined
|
||||
return
|
||||
// let's show that backdropRoute again
|
||||
// TODO: add support for multiple modal layers here
|
||||
// FIXME: use `history.state.backdropRoutePath` from last route
|
||||
// FIXME: this might be wrong, because we redirect to our current path forever??
|
||||
backdropRoutePath = lastBackdropRoutePath.value
|
||||
} else if (isInitialNavigation.value === false) {
|
||||
backdropRoutePath = from.fullPath
|
||||
} else if (resolvedRoute.meta.modal?.defaultBackdropRoute) {
|
||||
const {defaultBackdropRoute} = resolvedRoute.meta.modal
|
||||
const routeRaw = typeof defaultBackdropRoute === 'function'
|
||||
? defaultBackdropRoute(resolvedRoute)
|
||||
: defaultBackdropRoute
|
||||
backdropRoutePath = router.resolve(routeRaw).fullPath
|
||||
}
|
||||
currentModal.value = h(component, routeProps)
|
||||
|
||||
if (backdropRoutePath === undefined) {
|
||||
console.log('No defaultBackdropRoute defined for this route')
|
||||
|
||||
// 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
|
||||
backdropRoutePath = router.resolve({ name: 'home' }).fullPath
|
||||
}
|
||||
|
||||
lastBackdropRoutePath.value = backdropRoutePath
|
||||
|
||||
history.replaceState({
|
||||
...history.state,
|
||||
backdropRoutePath,
|
||||
}, '')
|
||||
})
|
||||
|
||||
const routerIsReady = ref(false)
|
||||
router.isReady().then(() => {
|
||||
routerIsReady.value = true
|
||||
})
|
||||
|
||||
function closeModal() {
|
||||
const historyState = computed(() => route.fullPath && window.history.state)
|
||||
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 (historyState.value) {
|
||||
router.back()
|
||||
if (historyStateBackdropRoutePath.value === undefined) {
|
||||
if (typeof route.meta?.modal === 'boolean' || route.meta.modal?.force !== true) {
|
||||
hasModal.value = false
|
||||
baseRoute.value = route
|
||||
return
|
||||
}
|
||||
// the route forces to be shown as a modal
|
||||
hasModal.value = true
|
||||
let routeRaw: RouteLocationRaw
|
||||
if (route.meta.modal?.defaultBackdropRoute) {
|
||||
const {defaultBackdropRoute} = route.meta.modal
|
||||
routeRaw = typeof defaultBackdropRoute === 'function'
|
||||
? defaultBackdropRoute(route)
|
||||
: defaultBackdropRoute
|
||||
} else {
|
||||
// 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
|
||||
routeRaw = { name: 'home' }
|
||||
}
|
||||
|
||||
baseRoute.value = await resolveAndLoadRoute(routeRaw, baseRoute.value)
|
||||
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, baseRoute.value)
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const backdropRoute = computed(() => route.fullPath !== baseRoute.value?.fullPath ? baseRoute.value : undefined)
|
||||
|
||||
const modalRoute = shallowRef<VNode>()
|
||||
watch(
|
||||
() => [hasModal.value, backdropRoute.value, route],
|
||||
() => {
|
||||
if (hasModal.value === false) {
|
||||
modalRoute.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
const props = getRouteProps(route)
|
||||
|
||||
props.isModal = true
|
||||
props.backdropRoutePath = backdropRoute.value?.fullPath
|
||||
props.onClose = closeModal
|
||||
|
||||
const component = route.matched[0]?.components?.default
|
||||
|
||||
if (!component) {
|
||||
modalRoute.value = undefined
|
||||
return
|
||||
}
|
||||
modalRoute.value = h(component, props)
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
async function closeModal() {
|
||||
await router.isReady()
|
||||
if (isInitialNavigation.value === false) {
|
||||
return router.back()
|
||||
} else if (backdropRoute.value !== undefined) {
|
||||
// TODO: Dialog modals might want to replace the route here via router.replace()
|
||||
return router.push(backdropRoute.value)
|
||||
}
|
||||
if (history.state === undefined) {
|
||||
return router.push({ name: 'home' })
|
||||
} else {
|
||||
const backdropRoute = historyState.value?.backdropView && router.resolve(historyState.value.backdropView)
|
||||
router.push(backdropRoute)
|
||||
router.back()
|
||||
// this should never happen
|
||||
throw new Error('')
|
||||
}
|
||||
}
|
||||
|
||||
return {routeWithModal, currentModal, closeModal}
|
||||
return {
|
||||
baseRoute,
|
||||
modalRoute,
|
||||
}
|
||||
}
|
|
@ -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,19 @@ 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
|
||||
modal?: boolean | {
|
||||
/** Forces the view to be always shown as modal */
|
||||
force?: boolean,
|
||||
/** Show this route as backdrop if there is no route history */
|
||||
defaultBackdropRoute?: RouteLocationRaw | ((to: RouteLocationNormalized) => RouteLocationRaw),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
|
@ -213,7 +225,10 @@ const router = createRouter({
|
|||
name: 'namespace.create',
|
||||
component: NewNamespaceComponent,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute: {name: 'namespaces.index'},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -221,7 +236,10 @@ const router = createRouter({
|
|||
name: 'namespace.settings.edit',
|
||||
component: NamespaceSettingEdit,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute: {name: 'namespaces.index'},
|
||||
},
|
||||
},
|
||||
props: route => ({ namespaceId: Number(route.params.id as string) }),
|
||||
},
|
||||
|
@ -230,7 +248,10 @@ const router = createRouter({
|
|||
name: 'namespace.settings.share',
|
||||
component: NamespaceSettingShare,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute: {name: 'namespaces.index'},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -238,16 +259,22 @@ const router = createRouter({
|
|||
name: 'namespace.settings.archive',
|
||||
component: NamespaceSettingArchive,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute: {name: 'namespaces.index'},
|
||||
},
|
||||
},
|
||||
props: route => ({ namespaceId: parseInt(route.params.id as string) }),
|
||||
props: route => ({ namespaceId: Number(route.params.id as string) }),
|
||||
},
|
||||
{
|
||||
path: '/namespaces/:id/settings/delete',
|
||||
name: 'namespace.settings.delete',
|
||||
component: NamespaceSettingDelete,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute: {name: 'namespaces.index'},
|
||||
},
|
||||
},
|
||||
props: route => ({ namespaceId: Number(route.params.id as string) }),
|
||||
},
|
||||
|
@ -256,6 +283,14 @@ const router = createRouter({
|
|||
name: 'task.detail',
|
||||
component: TaskDetailView,
|
||||
props: route => ({ taskId: Number(route.params.id as string) }),
|
||||
meta: {
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute(route) {
|
||||
return { name: 'list.index', props: { listId: route.params.listId}}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/tasks/by/upcoming',
|
||||
|
@ -273,7 +308,10 @@ const router = createRouter({
|
|||
name: 'list.create',
|
||||
component: NewListComponent,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute: {name: 'namespaces.index'},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -282,7 +320,12 @@ const router = createRouter({
|
|||
component: ListSettingEdit,
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute(route) {
|
||||
return { name: 'list.index', props: { listId: route.params.listId}}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -290,7 +333,12 @@ const router = createRouter({
|
|||
name: 'list.settings.background',
|
||||
component: ListSettingBackground,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute(route) {
|
||||
return { name: 'list.index', props: { listId: route.params.listId}}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -298,7 +346,12 @@ const router = createRouter({
|
|||
name: 'list.settings.duplicate',
|
||||
component: ListSettingDuplicate,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute(route) {
|
||||
return { name: 'list.index', props: { listId: route.params.listId}}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -306,7 +359,12 @@ const router = createRouter({
|
|||
name: 'list.settings.share',
|
||||
component: ListSettingShare,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute(route) {
|
||||
return { name: 'list.index', props: { listId: route.params.listId}}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -314,7 +372,12 @@ const router = createRouter({
|
|||
name: 'list.settings.delete',
|
||||
component: ListSettingDelete,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute(route) {
|
||||
return { name: 'list.index', props: { listId: route.params.listId}}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -322,7 +385,12 @@ const router = createRouter({
|
|||
name: 'list.settings.archive',
|
||||
component: ListSettingArchive,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute(route) {
|
||||
return { name: 'list.index', props: { listId: route.params.listId}}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -330,7 +398,12 @@ const router = createRouter({
|
|||
name: 'filter.settings.edit',
|
||||
component: FilterEdit,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute(route) {
|
||||
return { name: 'list.index', props: { listId: route.params.listId}}
|
||||
},
|
||||
},
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
|
@ -339,7 +412,12 @@ const router = createRouter({
|
|||
name: 'filter.settings.delete',
|
||||
component: FilterDelete,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute(route) {
|
||||
return { name: 'list.index', props: { listId: route.params.listId}}
|
||||
},
|
||||
},
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
|
@ -348,7 +426,12 @@ const router = createRouter({
|
|||
name: 'list.info',
|
||||
component: ListInfo,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute(route) {
|
||||
return { name: 'list.index', props: { listId: route.params.listId}}
|
||||
},
|
||||
},
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
|
@ -358,8 +441,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 +455,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 +470,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 +478,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))
|
||||
|
@ -416,9 +498,13 @@ const router = createRouter({
|
|||
name: 'teams.create',
|
||||
component: NewTeamComponent,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute: {name: 'teams.index'},
|
||||
},
|
||||
},
|
||||
},
|
||||
// TODO: make modal
|
||||
{
|
||||
path: '/teams/:id/edit',
|
||||
name: 'teams.edit',
|
||||
|
@ -434,7 +520,10 @@ const router = createRouter({
|
|||
name: 'labels.create',
|
||||
component: NewLabelComponent,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute: {name: 'labels.index'},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -456,7 +545,10 @@ const router = createRouter({
|
|||
name: 'filters.create',
|
||||
component: FilterNew,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
modal: {
|
||||
force: true,
|
||||
defaultBackdropRoute: {name: 'namespaces.index'},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -468,6 +560,11 @@ const router = createRouter({
|
|||
path: '/about',
|
||||
name: 'about',
|
||||
component: About,
|
||||
meta: {
|
||||
modal: {
|
||||
force: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
@ -516,4 +613,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
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<template>
|
||||
<modal
|
||||
@close="$router.back()"
|
||||
@submit="deleteFilter()"
|
||||
>
|
||||
<modal @submit="deleteFilter()">
|
||||
<template #header>
|
||||
<span>{{ $t('filters.delete.header') }}</span>
|
||||
</template>
|
||||
|
|
|
@ -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')"
|
||||
|
|
|
@ -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') }}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div :class="{ 'is-loading': loading}" class="loader-container">
|
||||
<x-button
|
||||
:to="{name:'labels.create'}"
|
||||
:to="{ name:'labels.create' }"
|
||||
class="is-pulled-right"
|
||||
icon="plus"
|
||||
>
|
||||
|
|
|
@ -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') }}
|
||||
|
|
|
@ -181,7 +181,6 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import {toRef, computed, type Ref} from 'vue'
|
||||
|
||||
import {useStorage} from '@vueuse/core'
|
||||
|
||||
import ListWrapper from '@/components/list/ListWrapper.vue'
|
||||
|
@ -274,15 +273,12 @@ function sort(property: keyof SortBy) {
|
|||
sortByParam.value = sortBy.value
|
||||
}
|
||||
|
||||
// TODO: re-enable opening task detail in modal
|
||||
// const router = useRouter()
|
||||
const taskDetailRoutes = computed(() => Object.fromEntries(
|
||||
tasks.value.map(({id}) => ([
|
||||
id,
|
||||
{
|
||||
name: 'task.detail',
|
||||
params: {id},
|
||||
// state: { backdropView: router.currentRoute.value.fullPath },
|
||||
},
|
||||
])),
|
||||
))
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,15 @@ 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}))
|
||||
useTitle(() => list.value
|
||||
? t('list.archive.title', {list: list.value.title})
|
||||
: undefined,
|
||||
)
|
||||
|
||||
async function archiveList() {
|
||||
async function archiveList(onClose: () => void) {
|
||||
try {
|
||||
const newList = await listStore.updateList({
|
||||
...list.value,
|
||||
|
@ -43,7 +42,7 @@ async function archiveList() {
|
|||
useBaseStore().setCurrentList(newList)
|
||||
success({message: t('list.archive.success')})
|
||||
} finally {
|
||||
router.back()
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -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)"
|
||||
>
|
||||
{{ $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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
@primary="save"
|
||||
:tertiary="$t('misc.delete')"
|
||||
@tertiary="$router.push({ name: 'list.settings.delete', params: { id: listId } })"
|
||||
#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>
|
||||
|
|
|
@ -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} }"
|
||||
class="is-pulled-right mr-4"
|
||||
variant="secondary"
|
||||
icon="archive"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<template>
|
||||
<modal
|
||||
@close="$router.back()"
|
||||
@submit="deleteNamespace()"
|
||||
>
|
||||
<modal @submit="deleteNamespace()">
|
||||
<template #header><span>{{ title }}</span></template>
|
||||
|
||||
<template #text>
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
@primary="save"
|
||||
:tertiary="$t('misc.delete')"
|
||||
@tertiary="$router.push({ name: 'namespace.settings.delete', params: { id: $route.params.id } })"
|
||||
#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>
|
|
@ -1,4 +1,5 @@
|
|||
<template>
|
||||
<OptionalWrapper :is="isModal && Modal" variant="scrolling">
|
||||
<div
|
||||
class="loader-container task-view-container"
|
||||
:class="{
|
||||
|
@ -283,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}"
|
||||
|
@ -442,15 +459,18 @@
|
|||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</OptionalWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, reactive, toRef, shallowReactive, computed, watch, nextTick, type PropType} from 'vue'
|
||||
import {useRouter, type RouteLocation} from 'vue-router'
|
||||
import {useRouter} from 'vue-router'
|
||||
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'
|
||||
|
||||
|
@ -480,7 +500,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'
|
||||
|
@ -502,13 +525,12 @@ const props = defineProps({
|
|||
type: Number as PropType<ITask['id']>,
|
||||
required: true,
|
||||
},
|
||||
backdropView: {
|
||||
type: String as PropType<RouteLocation['fullPath']>,
|
||||
isModal: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits(['close'])
|
||||
|
||||
const router = useRouter()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
|
@ -521,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,
|
||||
|
@ -577,8 +616,6 @@ const color = computed(() => {
|
|||
|
||||
const hasAttachments = computed(() => attachmentStore.attachments.length > 0)
|
||||
|
||||
const isModal = computed(() => Boolean(props.backdropView))
|
||||
|
||||
function attachmentUpload(file: File, onSuccess?: (url: string) => void) {
|
||||
return uploadFile(taskId.value, file, onSuccess)
|
||||
}
|
||||
|
@ -803,9 +840,11 @@ async function setPercentDone(percentDone: number) {
|
|||
--primary-light: hsla(var(--primary-h), var(--primary-s), 73%, var(--primary-a));
|
||||
padding-bottom: 0;
|
||||
|
||||
@media screen and (min-width: $desktop) {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
&:not(.is-modal) {
|
||||
@media screen and (min-width: $desktop) {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-view {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<create-edit
|
||||
class="new-team"
|
||||
:title="title"
|
||||
@create="newTeam()"
|
||||
:primary-disabled="team.name === ''"
|
||||
|
|
Reference in New Issue