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
|
@ -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">
|
||||
dpschen marked this conversation as resolved
Outdated
|
||||
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<{
|
||||
dpschen marked this conversation as resolved
Outdated
|
||||
/**
|
||||
* 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" />
|
||||
dpschen
commented
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 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).
konrad
commented
That sounds like it could be a good idea. IIRC my main goal with the That sounds like it could be a good idea.
IIRC my main goal with the `create-edit` component was to be able to easily re-use a shell for creating or editing.
|
||||
|
||||
<BaseButton
|
||||
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 }">
|
||||
dpschen
commented
I removed the 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) {
|
||||
dpschen
commented
We pass 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>
|
||||
|
|
|
@ -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">
|
||||
dpschen
commented
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,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
|
||||
})
|
||||
|
||||
dpschen
commented
FIXME: either we use an implicit FIXME: either we use an implicit `attrs.onClose` or we explicit define a `close` emit. Both should not be mixed.
|
||||
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>()
|
||||
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.
|
||||
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)"
|
||||
dpschen
commented
We should not need to use 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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
dpschen
commented
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="{
|
||||
|
@ -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 === ''"
|
||||
|
|
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 theis
prop is undefined there won't be any wrapper.