WIP: feat: route modals everywhere #2735
|
@ -19,7 +19,7 @@ describe('Team', () => {
|
||||||
.contains('Create a new team')
|
.contains('Create a new team')
|
||||||
cy.get('input.input')
|
cy.get('input.input')
|
||||||
.type(newTeamName)
|
.type(newTeamName)
|
||||||
cy.get('.button')
|
cy.get('.new-team button')
|
||||||
.contains('Create')
|
.contains('Create')
|
||||||
.click()
|
.click()
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"@types/lodash.clonedeep": "4.5.7",
|
"@types/lodash.clonedeep": "4.5.7",
|
||||||
"@types/sortablejs": "1.15.0",
|
"@types/sortablejs": "1.15.0",
|
||||||
"@vueuse/core": "9.6.0",
|
"@vueuse/core": "9.6.0",
|
||||||
|
"@vueuse/integrations": "9.6.0",
|
||||||
"@vueuse/router": "9.6.0",
|
"@vueuse/router": "9.6.0",
|
||||||
"axios": "0.27.2",
|
"axios": "0.27.2",
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.4",
|
||||||
|
@ -45,6 +46,7 @@
|
||||||
"flatpickr": "4.6.13",
|
"flatpickr": "4.6.13",
|
||||||
"flexsearch": "0.7.21",
|
"flexsearch": "0.7.21",
|
||||||
"floating-vue": "2.0.0-beta.20",
|
"floating-vue": "2.0.0-beta.20",
|
||||||
|
"focus-trap": "^7.1.0",
|
||||||
"highlight.js": "11.6.0",
|
"highlight.js": "11.6.0",
|
||||||
"is-touch-device": "1.0.1",
|
"is-touch-device": "1.0.1",
|
||||||
"lodash.clonedeep": "4.5.0",
|
"lodash.clonedeep": "4.5.0",
|
||||||
|
|
110
pnpm-lock.yaml
|
@ -33,6 +33,7 @@ specifiers:
|
||||||
'@vue/test-utils': 2.2.4
|
'@vue/test-utils': 2.2.4
|
||||||
'@vue/tsconfig': 0.1.3
|
'@vue/tsconfig': 0.1.3
|
||||||
'@vueuse/core': 9.6.0
|
'@vueuse/core': 9.6.0
|
||||||
|
'@vueuse/integrations': 9.6.0
|
||||||
'@vueuse/router': 9.6.0
|
'@vueuse/router': 9.6.0
|
||||||
autoprefixer: 10.4.13
|
autoprefixer: 10.4.13
|
||||||
axios: 0.27.2
|
axios: 0.27.2
|
||||||
|
@ -56,6 +57,7 @@ specifiers:
|
||||||
flatpickr: 4.6.13
|
flatpickr: 4.6.13
|
||||||
flexsearch: 0.7.21
|
flexsearch: 0.7.21
|
||||||
floating-vue: 2.0.0-beta.20
|
floating-vue: 2.0.0-beta.20
|
||||||
|
focus-trap: ^7.1.0
|
||||||
happy-dom: 7.7.0
|
happy-dom: 7.7.0
|
||||||
highlight.js: 11.6.0
|
highlight.js: 11.6.0
|
||||||
is-touch-device: 1.0.1
|
is-touch-device: 1.0.1
|
||||||
|
@ -104,6 +106,7 @@ dependencies:
|
||||||
'@types/lodash.clonedeep': 4.5.7
|
'@types/lodash.clonedeep': 4.5.7
|
||||||
'@types/sortablejs': 1.15.0
|
'@types/sortablejs': 1.15.0
|
||||||
'@vueuse/core': 9.6.0_vue@3.2.45
|
'@vueuse/core': 9.6.0_vue@3.2.45
|
||||||
|
'@vueuse/integrations': 9.6.0_v6so3rexzmt44vm4rsvyznwkka
|
||||||
'@vueuse/router': 9.6.0_xsxatmlnmmg5bcuv3xdnj6fj7y
|
'@vueuse/router': 9.6.0_xsxatmlnmmg5bcuv3xdnj6fj7y
|
||||||
axios: 0.27.2
|
axios: 0.27.2
|
||||||
blurhash: 2.0.4
|
blurhash: 2.0.4
|
||||||
|
@ -118,6 +121,7 @@ dependencies:
|
||||||
flatpickr: 4.6.13
|
flatpickr: 4.6.13
|
||||||
flexsearch: 0.7.21
|
flexsearch: 0.7.21
|
||||||
floating-vue: 2.0.0-beta.20_vue@3.2.45
|
floating-vue: 2.0.0-beta.20_vue@3.2.45
|
||||||
|
focus-trap: 7.1.0
|
||||||
highlight.js: 11.6.0
|
highlight.js: 11.6.0
|
||||||
is-touch-device: 1.0.1
|
is-touch-device: 1.0.1
|
||||||
lodash.clonedeep: 4.5.0
|
lodash.clonedeep: 4.5.0
|
||||||
|
@ -3236,13 +3240,18 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
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:
|
/@typescript-eslint/types/5.44.0:
|
||||||
resolution: {integrity: sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ==}
|
resolution: {integrity: sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@typescript-eslint/typescript-estree/5.44.0_mmt6grxdx77rvjuvebzbfquz6y:
|
/@typescript-eslint/typescript-estree/5.43.0_mmt6grxdx77rvjuvebzbfquz6y:
|
||||||
resolution: {integrity: sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw==}
|
resolution: {integrity: sha512-BZ1WVe+QQ+igWal2tDbNg1j2HWUkAa+CVqdU79L4HP9izQY6CNhXfkNwd1SS4+sSZAP/EthI1uiCSY/+H0pROg==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '*'
|
typescript: '*'
|
||||||
|
@ -3250,8 +3259,8 @@ packages:
|
||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 5.44.0
|
'@typescript-eslint/types': 5.43.0
|
||||||
'@typescript-eslint/visitor-keys': 5.44.0
|
'@typescript-eslint/visitor-keys': 5.43.0
|
||||||
debug: 4.3.4_supports-color@9.2.1
|
debug: 4.3.4_supports-color@9.2.1
|
||||||
globby: 11.1.0
|
globby: 11.1.0
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
@ -3262,6 +3271,27 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
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:
|
/@typescript-eslint/typescript-estree/5.44.0_typescript@4.9.3:
|
||||||
resolution: {integrity: sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw==}
|
resolution: {integrity: sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
@ -3303,6 +3333,14 @@ packages:
|
||||||
- typescript
|
- typescript
|
||||||
dev: true
|
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:
|
/@typescript-eslint/visitor-keys/5.44.0:
|
||||||
resolution: {integrity: sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ==}
|
resolution: {integrity: sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
@ -3661,6 +3699,54 @@ packages:
|
||||||
- vue
|
- vue
|
||||||
dev: false
|
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:
|
/@vueuse/metadata/9.6.0:
|
||||||
resolution: {integrity: sha512-sIC8R+kWkIdpi5X2z2Gk8TRYzmczDwHRhEFfCu2P+XW2JdPoXrziqsGpDDsN7ykBx4ilwieS7JUIweVGhvZ93w==}
|
resolution: {integrity: sha512-sIC8R+kWkIdpi5X2z2Gk8TRYzmczDwHRhEFfCu2P+XW2JdPoXrziqsGpDDsN7ykBx4ilwieS7JUIweVGhvZ93w==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -5775,7 +5861,7 @@ packages:
|
||||||
resolution: {integrity: sha512-lR78AugfUSBojwlSRZBeEqQ1l8LI7rbxOl1qTUnGLcjZQDjZmrZCb7R46rK8U8B5WzFvJrxa7fEBA8FoD/n5fA==}
|
resolution: {integrity: sha512-lR78AugfUSBojwlSRZBeEqQ1l8LI7rbxOl1qTUnGLcjZQDjZmrZCb7R46rK8U8B5WzFvJrxa7fEBA8FoD/n5fA==}
|
||||||
engines: {node: ^12.20.0 || ^14.14.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.14.0 || >=16.0.0}
|
||||||
dependencies:
|
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
|
ast-module-types: 3.0.0
|
||||||
node-source-walk: 5.0.0
|
node-source-walk: 5.0.0
|
||||||
typescript: 4.9.3
|
typescript: 4.9.3
|
||||||
|
@ -5787,7 +5873,7 @@ packages:
|
||||||
resolution: {integrity: sha512-lR78AugfUSBojwlSRZBeEqQ1l8LI7rbxOl1qTUnGLcjZQDjZmrZCb7R46rK8U8B5WzFvJrxa7fEBA8FoD/n5fA==}
|
resolution: {integrity: sha512-lR78AugfUSBojwlSRZBeEqQ1l8LI7rbxOl1qTUnGLcjZQDjZmrZCb7R46rK8U8B5WzFvJrxa7fEBA8FoD/n5fA==}
|
||||||
engines: {node: ^12.20.0 || ^14.14.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.14.0 || >=16.0.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/typescript-estree': 5.44.0_mmt6grxdx77rvjuvebzbfquz6y
|
'@typescript-eslint/typescript-estree': 5.43.0_mmt6grxdx77rvjuvebzbfquz6y
|
||||||
ast-module-types: 3.0.0
|
ast-module-types: 3.0.0
|
||||||
node-source-walk: 5.0.0
|
node-source-walk: 5.0.0
|
||||||
typescript: 4.9.3
|
typescript: 4.9.3
|
||||||
|
@ -7008,6 +7094,12 @@ packages:
|
||||||
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
|
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
|
||||||
dev: true
|
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:
|
/folder-walker/3.2.0:
|
||||||
resolution: {integrity: sha512-VjAQdSLsl6AkpZNyrQJfO7BXLo4chnStqb055bumZMbRUPpVuPN3a4ktsnRCmrFZjtMlYLkyXiR5rAs4WOpC4Q==}
|
resolution: {integrity: sha512-VjAQdSLsl6AkpZNyrQJfO7BXLo4chnStqb055bumZMbRUPpVuPN3a4ktsnRCmrFZjtMlYLkyXiR5rAs4WOpC4Q==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -12255,6 +12347,10 @@ packages:
|
||||||
resolution: {integrity: sha512-P3cgh2bpaPvAO2NE3uRp/n6hmk4xPX4DQf+UzTlCAycssKdqhp6hjw+ENWe+aUS7TogKRFtptMosTSFeC6R55g==}
|
resolution: {integrity: sha512-P3cgh2bpaPvAO2NE3uRp/n6hmk4xPX4DQf+UzTlCAycssKdqhp6hjw+ENWe+aUS7TogKRFtptMosTSFeC6R55g==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tabbable/6.0.1:
|
||||||
|
resolution: {integrity: sha512-SYJSIgeyXW7EuX1ytdneO5e8jip42oHWg9xl/o3oTYhmXusZVgiA+VlPvjIN+kHii9v90AmzTZEBcsEvuAY+TA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/tabtab/3.0.2:
|
/tabtab/3.0.2:
|
||||||
resolution: {integrity: sha512-jANKmUe0sIQc/zTALTBy186PoM/k6aPrh3A7p6AaAfF6WPSbTx1JYeGIGH162btpH+mmVEXln+UxwViZHO2Jhg==}
|
resolution: {integrity: sha512-jANKmUe0sIQc/zTALTBy186PoM/k6aPrh3A7p6AaAfF6WPSbTx1JYeGIGH162btpH+mmVEXln+UxwViZHO2Jhg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -12968,7 +13064,7 @@ packages:
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/verror/1.10.0:
|
/verror/1.10.0:
|
||||||
resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
|
resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=}
|
||||||
engines: {'0': node >=0.6.0}
|
engines: {'0': node >=0.6.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
assert-plus: 1.0.0
|
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) }}
|
{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}
|
||||||
</h1>
|
</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"/>
|
<icon icon="circle-info"/>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
{{ $t('keyboardShortcuts.title') }}
|
{{ $t('keyboardShortcuts.title') }}
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
:to="{name: 'about'}"
|
:to="{name: 'about',}"
|
||||||
>
|
>
|
||||||
{{ $t('about.title') }}
|
{{ $t('about.title') }}
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
|
|
|
@ -32,20 +32,13 @@
|
||||||
|
|
||||||
<quick-actions/>
|
<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']">
|
<keep-alive :include="['list.list', 'list.gantt', 'list.table', 'list.kanban']">
|
||||||
<component :is="Component"/>
|
<component :is="Component"/>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
|
|
||||||
<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.
|
|||||||
:enabled="Boolean(currentModal)"
|
|
||||||
@close="closeModal()"
|
|
||||||
variant="scrolling"
|
|
||||||
class="task-detail-view-modal"
|
|
||||||
>
|
|
||||||
<component :is="currentModal"/>
|
|
||||||
</modal>
|
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
class="keyboard-shortcuts-button d-print-none"
|
class="keyboard-shortcuts-button d-print-none"
|
||||||
|
@ -73,7 +66,7 @@ import {useLabelStore} from '@/stores/labels'
|
||||||
import {useRouteWithModal} from '@/composables/useRouteWithModal'
|
import {useRouteWithModal} from '@/composables/useRouteWithModal'
|
||||||
import {useRenewTokenOnFocus} from '@/composables/useRenewTokenOnFocus'
|
import {useRenewTokenOnFocus} from '@/composables/useRenewTokenOnFocus'
|
||||||
|
|
||||||
const {routeWithModal, currentModal, closeModal} = useRouteWithModal()
|
const {baseRoute, modalRoute} = useRouteWithModal()
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const background = computed(() => baseStore.background)
|
const background = computed(() => baseStore.background)
|
||||||
|
|
|
@ -367,6 +367,7 @@ watch(
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 7;
|
z-index: 7;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
display: none;
|
display: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
@ -379,6 +380,7 @@ watch(
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
display: none;
|
display: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
faEllipsisH,
|
faEllipsisH,
|
||||||
faEllipsisV,
|
faEllipsisV,
|
||||||
faExclamation,
|
faExclamation,
|
||||||
|
faExpand,
|
||||||
faEye,
|
faEye,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faFillDrip,
|
faFillDrip,
|
||||||
|
@ -95,6 +96,7 @@ library.add(faCog)
|
||||||
library.add(faComments)
|
library.add(faComments)
|
||||||
library.add(faEllipsisH)
|
library.add(faEllipsisH)
|
||||||
library.add(faEllipsisV)
|
library.add(faEllipsisV)
|
||||||
|
library.add(faExpand)
|
||||||
library.add(faExclamation)
|
library.add(faExclamation)
|
||||||
library.add(faEye)
|
library.add(faEye)
|
||||||
library.add(faEyeSlash)
|
library.add(faEyeSlash)
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
<template>
|
<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
|
<card
|
||||||
:title="title"
|
:title="title"
|
||||||
:shadow="false"
|
:shadow="false"
|
||||||
:padding="false"
|
:padding="false"
|
||||||
class="has-text-left"
|
class="has-text-left"
|
||||||
:has-close="true"
|
:has-close="true"
|
||||||
@close="$router.back()"
|
@close="onClose"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
>
|
>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<slot/>
|
<slot :onClose="onClose" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<slot name="footer">
|
<slot name="footer" :onClose="onClose">
|
||||||
<x-button
|
<x-button
|
||||||
v-if="tertiary !== ''"
|
v-if="tertiary !== ''"
|
||||||
:shadow="false"
|
:shadow="false"
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
@click.prevent.stop="$emit('tertiary')"
|
@click="$emit('tertiary')"
|
||||||
>
|
>
|
||||||
{{ tertiary }}
|
{{ tertiary }}
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button
|
<x-button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@click.prevent.stop="$router.back()"
|
@click="onClose"
|
||||||
>
|
>
|
||||||
{{ $t('misc.cancel') }}
|
{{ $t('misc.cancel') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button
|
<x-button
|
||||||
v-if="hasPrimaryAction"
|
v-if="hasPrimaryAction"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@click.prevent.stop="primary()"
|
@click="primary(onClose)"
|
||||||
:icon="primaryIcon"
|
:icon="primaryIcon"
|
||||||
:disabled="primaryDisabled || loading"
|
:disabled="primaryDisabled || loading"
|
||||||
class="ml-2"
|
class="ml-2"
|
||||||
|
@ -83,10 +83,11 @@ defineProps({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 'close'
|
||||||
const emit = defineEmits(['create', 'primary', 'tertiary'])
|
const emit = defineEmits(['create', 'primary', 'tertiary'])
|
||||||
|
|
||||||
function 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')
|
emit('create', onClose)
|
||||||
emit('primary')
|
emit('primary', onClose)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<!-- FIXME: transition should not be included in the modal -->
|
<!-- 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
|
<section
|
||||||
v-if="enabled"
|
v-if="enabled"
|
||||||
class="modal-mask"
|
class="modal-mask"
|
||||||
|
@ -10,11 +10,11 @@
|
||||||
variant,
|
variant,
|
||||||
]"
|
]"
|
||||||
ref="modal"
|
ref="modal"
|
||||||
v-bind="attrs"
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="modal-container"
|
class="modal-container"
|
||||||
@click.self.prevent.stop="$emit('close')"
|
@click.self.prevent.stop="onClose"
|
||||||
v-shortcut="'Escape'"
|
v-shortcut="'Escape'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -25,13 +25,13 @@
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
@click="$emit('close')"
|
@click="onClose"
|
||||||
class="close"
|
class="close"
|
||||||
>
|
>
|
||||||
<icon icon="times"/>
|
<icon icon="times"/>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
|
||||||
<slot>
|
<slot name="default" :onClose="onClose">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,14 +40,14 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<x-button
|
<x-button
|
||||||
@click="$emit('close')"
|
@click="onClose"
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
class="has-text-danger"
|
class="has-text-danger"
|
||||||
>
|
>
|
||||||
{{ $t('misc.cancel') }}
|
{{ $t('misc.cancel') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button
|
<x-button
|
||||||
@click="$emit('submit')"
|
@click="$emit('submit', onClose)"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
v-cy="'modalPrimary'"
|
v-cy="'modalPrimary'"
|
||||||
:shadow="false"
|
:shadow="false"
|
||||||
|
@ -70,10 +70,13 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import {ref, useAttrs, watchEffect} from 'vue'
|
|
||||||
import {useScrollLock} from '@vueuse/core'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
enabled?: boolean,
|
enabled?: boolean,
|
||||||
|
@ -81,22 +84,34 @@ const props = withDefaults(defineProps<{
|
||||||
wide?: boolean,
|
wide?: boolean,
|
||||||
transitionName?: 'modal' | 'fade',
|
transitionName?: 'modal' | 'fade',
|
||||||
variant?: 'default' | 'hint-modal' | 'scrolling',
|
variant?: 'default' | 'hint-modal' | 'scrolling',
|
||||||
|
closeRoute?: RouteLocationRaw,
|
||||||
}>(), {
|
}>(), {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
transitionName: 'modal',
|
transitionName: 'modal',
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
})
|
})
|
||||||
|
|
||||||
defineEmits(['close', 'submit'])
|
const emit = defineEmits(['close', 'submit'])
|
||||||
|
|
||||||
const attrs = useAttrs()
|
|
||||||
|
|
||||||
const modal = ref<HTMLElement | null>(null)
|
|
||||||
const scrollLock = useScrollLock(modal)
|
|
||||||
|
|
||||||
|
const scrollLock = useScrollLock(document.body)
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
scrollLock.value = props.enabled
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -122,6 +137,8 @@ $modal-width: 1024px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.default .modal-content,
|
.default .modal-content,
|
||||||
|
@ -131,6 +148,7 @@ $modal-width: 1024px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
|
overflow: visible; // reset bulma
|
||||||
|
|
||||||
@media screen and (max-width: $tablet) {
|
@media screen and (max-width: $tablet) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -153,7 +153,6 @@ function openTask(e: {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'task.detail',
|
name: 'task.detail',
|
||||||
params: {id: e.bar.ganttBarConfig.id},
|
params: {id: e.bar.ganttBarConfig.id},
|
||||||
state: {backdropView: router.currentRoute.value.fullPath},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,6 @@ function openTaskDetail() {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'task.detail',
|
name: 'task.detail',
|
||||||
params: {id: props.task.id},
|
params: {id: props.task.id},
|
||||||
state: {backdropView: router.currentRoute.value.fullPath},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
:to="taskDetailRoute"
|
:to="{name: 'task.detail', params: {id: task.id}}"
|
||||||
:class="{ 'done': task.done}"
|
:class="{ 'done': task.done}"
|
||||||
class="tasktext"
|
class="tasktext"
|
||||||
>
|
>
|
||||||
|
@ -221,14 +221,6 @@ const currentList = computed(() => {
|
||||||
} : baseStore.currentList
|
} : 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) {
|
async function markAsDone(checked: boolean) {
|
||||||
const updateFunc = async () => {
|
const updateFunc = async () => {
|
||||||
const newTask = await taskStore.update(task.value)
|
const newTask = await taskStore.update(task.value)
|
||||||
|
|
|
@ -1,56 +1,200 @@
|
||||||
import { computed, shallowRef, watchEffect, h, type VNode } from 'vue'
|
import {computed, shallowRef, watch, h, type VNode, ref} from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
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() {
|
export function useRouteWithModal() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const backdropView = computed(() => route.fullPath && window.history.state.backdropView)
|
|
||||||
|
const historyStateBackdropRoutePath = computed<RouteLocationRaw | undefined>(() => {
|
||||||
const routeWithModal = computed(() => {
|
// every time the fullPath changes we check the history state
|
||||||
return backdropView.value
|
// this happens also initially
|
||||||
? router.resolve(backdropView.value)
|
return route.fullPath
|
||||||
: route
|
? history.state?.backdropRoutePath
|
||||||
|
: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentModal = shallowRef<VNode>()
|
const isInitialNavigation = ref(true)
|
||||||
watchEffect(() => {
|
|
||||||
if (!backdropView.value) {
|
router.beforeEach((to, from) => {
|
||||||
currentModal.value = undefined
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is adapted from vue-router
|
let backdropRoutePath
|
||||||
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
|
if (resolvedRoute.meta?.modal === true || resolvedRoute.meta.modal?.force !== true) {
|
||||||
const routePropsOption = route.matched[0]?.props.default
|
// this route can be shown as modal or as normal view
|
||||||
const routeProps = routePropsOption
|
|
||||||
? routePropsOption === true
|
|
||||||
? route.params
|
|
||||||
: typeof routePropsOption === 'function'
|
|
||||||
? routePropsOption(route)
|
|
||||||
: routePropsOption
|
|
||||||
: {}
|
|
||||||
|
|
||||||
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
|
// let's show that backdropRoute again
|
||||||
|
// TODO: add support for multiple modal layers here
|
||||||
if (!component) {
|
// FIXME: use `history.state.backdropRoutePath` from last route
|
||||||
currentModal.value = undefined
|
// FIXME: this might be wrong, because we redirect to our current path forever??
|
||||||
return
|
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 hasModal = ref(false)
|
||||||
const historyState = computed(() => route.fullPath && window.history.state)
|
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) {
|
if (historyStateBackdropRoutePath.value === undefined) {
|
||||||
router.back()
|
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 {
|
} else {
|
||||||
const backdropRoute = historyState.value?.backdropView && router.resolve(historyState.value.backdropView)
|
router.back()
|
||||||
router.push(backdropRoute)
|
// 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 {createRouter, createWebHistory, type RouteLocation, type RouteLocationRaw} from 'vue-router'
|
||||||
import type { RouteLocation } from 'vue-router'
|
|
||||||
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
||||||
|
|
||||||
import {saveListView, getListView} from '@/helpers/saveListView'
|
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 EditTeamComponent = () => import('@/views/teams/EditTeam.vue')
|
||||||
const NewTeamComponent = () => import('@/views/teams/NewTeam.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({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
scrollBehavior(to, from, savedPosition) {
|
scrollBehavior(to, from, savedPosition) {
|
||||||
|
@ -213,7 +225,10 @@ const router = createRouter({
|
||||||
name: 'namespace.create',
|
name: 'namespace.create',
|
||||||
component: NewNamespaceComponent,
|
component: NewNamespaceComponent,
|
||||||
meta: {
|
meta: {
|
||||||
showAsModal: true,
|
modal: {
|
||||||
|
force: true,
|
||||||
|
defaultBackdropRoute: {name: 'namespaces.index'},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -221,7 +236,10 @@ const router = createRouter({
|
||||||
name: 'namespace.settings.edit',
|
name: 'namespace.settings.edit',
|
||||||
component: NamespaceSettingEdit,
|
component: NamespaceSettingEdit,
|
||||||
meta: {
|
meta: {
|
||||||
showAsModal: true,
|
modal: {
|
||||||
|
force: true,
|
||||||
|
defaultBackdropRoute: {name: 'namespaces.index'},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
props: route => ({ namespaceId: Number(route.params.id as string) }),
|
props: route => ({ namespaceId: Number(route.params.id as string) }),
|
||||||
},
|
},
|
||||||
|
@ -230,7 +248,10 @@ const router = createRouter({
|
||||||
name: 'namespace.settings.share',
|
name: 'namespace.settings.share',
|
||||||
component: NamespaceSettingShare,
|
component: NamespaceSettingShare,
|
||||||
meta: {
|
meta: {
|
||||||
showAsModal: true,
|
modal: {
|
||||||
|
force: true,
|
||||||
|
defaultBackdropRoute: {name: 'namespaces.index'},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -238,16 +259,22 @@ const router = createRouter({
|
||||||
name: 'namespace.settings.archive',
|
name: 'namespace.settings.archive',
|
||||||
component: NamespaceSettingArchive,
|
component: NamespaceSettingArchive,
|
||||||
meta: {
|
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',
|
path: '/namespaces/:id/settings/delete',
|
||||||
name: 'namespace.settings.delete',
|
name: 'namespace.settings.delete',
|
||||||
component: NamespaceSettingDelete,
|
component: NamespaceSettingDelete,
|
||||||
meta: {
|
meta: {
|
||||||
showAsModal: true,
|
modal: {
|
||||||
|
force: true,
|
||||||
|
defaultBackdropRoute: {name: 'namespaces.index'},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
props: route => ({ namespaceId: Number(route.params.id as string) }),
|
props: route => ({ namespaceId: Number(route.params.id as string) }),
|
||||||
},
|
},
|
||||||
|
@ -256,6 +283,14 @@ const router = createRouter({
|
||||||
name: 'task.detail',
|
name: 'task.detail',
|
||||||
component: TaskDetailView,
|
component: TaskDetailView,
|
||||||
props: route => ({ taskId: Number(route.params.id as string) }),
|
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',
|
path: '/tasks/by/upcoming',
|
||||||
|
@ -273,7 +308,10 @@ const router = createRouter({
|
||||||
name: 'list.create',
|
name: 'list.create',
|
||||||
component: NewListComponent,
|
component: NewListComponent,
|
||||||
meta: {
|
meta: {
|
||||||
showAsModal: true,
|
modal: {
|
||||||
|
force: true,
|
||||||
|
defaultBackdropRoute: {name: 'namespaces.index'},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -282,7 +320,12 @@ const router = createRouter({
|
||||||
component: ListSettingEdit,
|
component: ListSettingEdit,
|
||||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||||
meta: {
|
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',
|
name: 'list.settings.background',
|
||||||
component: ListSettingBackground,
|
component: ListSettingBackground,
|
||||||
meta: {
|
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',
|
name: 'list.settings.duplicate',
|
||||||
component: ListSettingDuplicate,
|
component: ListSettingDuplicate,
|
||||||
meta: {
|
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',
|
name: 'list.settings.share',
|
||||||
component: ListSettingShare,
|
component: ListSettingShare,
|
||||||
meta: {
|
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',
|
name: 'list.settings.delete',
|
||||||
component: ListSettingDelete,
|
component: ListSettingDelete,
|
||||||
meta: {
|
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',
|
name: 'list.settings.archive',
|
||||||
component: ListSettingArchive,
|
component: ListSettingArchive,
|
||||||
meta: {
|
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',
|
name: 'filter.settings.edit',
|
||||||
component: FilterEdit,
|
component: FilterEdit,
|
||||||
meta: {
|
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) }),
|
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||||
},
|
},
|
||||||
|
@ -339,7 +412,12 @@ const router = createRouter({
|
||||||
name: 'filter.settings.delete',
|
name: 'filter.settings.delete',
|
||||||
component: FilterDelete,
|
component: FilterDelete,
|
||||||
meta: {
|
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) }),
|
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||||
},
|
},
|
||||||
|
@ -348,7 +426,12 @@ const router = createRouter({
|
||||||
name: 'list.info',
|
name: 'list.info',
|
||||||
component: ListInfo,
|
component: ListInfo,
|
||||||
meta: {
|
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) }),
|
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||||
},
|
},
|
||||||
|
@ -358,8 +441,7 @@ const router = createRouter({
|
||||||
redirect(to) {
|
redirect(to) {
|
||||||
// Redirect the user to list view by default
|
// Redirect the user to list view by default
|
||||||
|
|
||||||
const savedListView = getListView(to.params.listId)
|
const savedListView = getListView(Number(to.params.listId))
|
||||||
console.debug('Replaced list view with', savedListView)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: router.hasRoute(savedListView)
|
name: router.hasRoute(savedListView)
|
||||||
|
@ -373,14 +455,14 @@ const router = createRouter({
|
||||||
path: '/lists/:listId/list',
|
path: '/lists/:listId/list',
|
||||||
name: 'list.list',
|
name: 'list.list',
|
||||||
component: ListList,
|
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) }),
|
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/gantt',
|
path: '/lists/:listId/gantt',
|
||||||
name: 'list.gantt',
|
name: 'list.gantt',
|
||||||
component: ListGantt,
|
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.
|
// FIXME: test if `useRoute` would be the same. If it would use it instead.
|
||||||
props: route => ({route}),
|
props: route => ({route}),
|
||||||
},
|
},
|
||||||
|
@ -388,7 +470,7 @@ const router = createRouter({
|
||||||
path: '/lists/:listId/table',
|
path: '/lists/:listId/table',
|
||||||
name: 'list.table',
|
name: 'list.table',
|
||||||
component: ListTable,
|
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) }),
|
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -396,7 +478,7 @@ const router = createRouter({
|
||||||
name: 'list.kanban',
|
name: 'list.kanban',
|
||||||
component: ListKanban,
|
component: ListKanban,
|
||||||
beforeEnter: (to) => {
|
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
|
// Properly set the page title when a task popup is closed
|
||||||
const listStore = useListStore()
|
const listStore = useListStore()
|
||||||
const listFromStore = listStore.getListById(Number(to.params.listId))
|
const listFromStore = listStore.getListById(Number(to.params.listId))
|
||||||
|
@ -416,9 +498,13 @@ const router = createRouter({
|
||||||
name: 'teams.create',
|
name: 'teams.create',
|
||||||
component: NewTeamComponent,
|
component: NewTeamComponent,
|
||||||
meta: {
|
meta: {
|
||||||
showAsModal: true,
|
modal: {
|
||||||
|
force: true,
|
||||||
|
defaultBackdropRoute: {name: 'teams.index'},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: make modal
|
||||||
{
|
{
|
||||||
path: '/teams/:id/edit',
|
path: '/teams/:id/edit',
|
||||||
name: 'teams.edit',
|
name: 'teams.edit',
|
||||||
|
@ -434,7 +520,10 @@ const router = createRouter({
|
||||||
name: 'labels.create',
|
name: 'labels.create',
|
||||||
component: NewLabelComponent,
|
component: NewLabelComponent,
|
||||||
meta: {
|
meta: {
|
||||||
showAsModal: true,
|
modal: {
|
||||||
|
force: true,
|
||||||
|
defaultBackdropRoute: {name: 'labels.index'},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -456,7 +545,10 @@ const router = createRouter({
|
||||||
name: 'filters.create',
|
name: 'filters.create',
|
||||||
component: FilterNew,
|
component: FilterNew,
|
||||||
meta: {
|
meta: {
|
||||||
showAsModal: true,
|
modal: {
|
||||||
|
force: true,
|
||||||
|
defaultBackdropRoute: {name: 'namespaces.index'},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -468,6 +560,11 @@ const router = createRouter({
|
||||||
path: '/about',
|
path: '/about',
|
||||||
name: 'about',
|
name: 'about',
|
||||||
component: About,
|
component: About,
|
||||||
|
meta: {
|
||||||
|
modal: {
|
||||||
|
force: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -516,4 +613,59 @@ router.beforeEach(async (to) => {
|
||||||
return getAuthForRoute(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
|
export default router
|
|
@ -113,7 +113,7 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
|
||||||
router.push({name: 'list.index', params: {listId: getListId(filter.value)}})
|
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)
|
const response = await filterService.update(filter.value)
|
||||||
await namespaceStore.loadNamespaces()
|
await namespaceStore.loadNamespaces()
|
||||||
success({message: t('filters.edit.success')})
|
success({message: t('filters.edit.success')})
|
||||||
|
@ -123,7 +123,7 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
|
||||||
id: getListId(filter.value),
|
id: getListId(filter.value),
|
||||||
title: filter.value.title,
|
title: filter.value.title,
|
||||||
}))
|
}))
|
||||||
router.back()
|
callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteFilter() {
|
async function deleteFilter() {
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<modal
|
||||||
@close="$router.back()"
|
|
||||||
transition-name="fade"
|
transition-name="fade"
|
||||||
variant="hint-modal"
|
variant="hint-modal"
|
||||||
>
|
>
|
||||||
|
<template #default="{onClose}">
|
||||||
<card
|
<card
|
||||||
class="has-no-shadow"
|
class="has-no-shadow"
|
||||||
:title="$t('about.title')"
|
:title="$t('about.title')"
|
||||||
:has-close="true"
|
:has-close="true"
|
||||||
@close="$router.back()"
|
@close="onClose"
|
||||||
:padding="false"
|
:padding="false"
|
||||||
>
|
>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
|
@ -22,12 +22,13 @@
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<x-button
|
<x-button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@click.prevent.stop="$router.back()"
|
@click="onClose"
|
||||||
>
|
>
|
||||||
{{ $t('misc.close') }}
|
{{ $t('misc.close') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
</template>
|
</template>
|
||||||
</card>
|
</card>
|
||||||
|
</template>
|
||||||
</modal>
|
</modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<modal @submit="deleteFilter()">
|
||||||
@close="$router.back()"
|
|
||||||
@submit="deleteFilter()"
|
|
||||||
>
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<span>{{ $t('filters.delete.header') }}</span>
|
<span>{{ $t('filters.delete.header') }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -6,15 +6,16 @@
|
||||||
@primary="saveFilter"
|
@primary="saveFilter"
|
||||||
:tertiary="$t('misc.delete')"
|
:tertiary="$t('misc.delete')"
|
||||||
@tertiary="$router.push({ name: 'filter.settings.delete', params: { id: listId } })"
|
@tertiary="$router.push({ name: 'filter.settings.delete', params: { id: listId } })"
|
||||||
|
#default="{onClose}"
|
||||||
>
|
>
|
||||||
<form @submit.prevent="saveFilter()">
|
<form @submit.prevent="saveFilter(onClose)">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="title">{{ $t('filters.attributes.title') }}</label>
|
<label class="label" for="title">{{ $t('filters.attributes.title') }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input
|
<input
|
||||||
:class="{ 'disabled': filterService.loading}"
|
:class="{ 'disabled': filterService.loading}"
|
||||||
:disabled="filterService.loading || undefined"
|
:disabled="filterService.loading || undefined"
|
||||||
@keyup.enter="saveFilter"
|
@keyup.enter="saveFilter(onClose)"
|
||||||
class="input"
|
class="input"
|
||||||
id="title"
|
id="title"
|
||||||
:placeholder="$t('filters.attributes.titlePlaceholder')"
|
:placeholder="$t('filters.attributes.titlePlaceholder')"
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<modal variant="hint-modal">
|
||||||
@close="$router.back()"
|
|
||||||
variant="hint-modal"
|
|
||||||
>
|
|
||||||
<card class="has-no-shadow" :title="$t('filters.create.title')">
|
<card class="has-no-shadow" :title="$t('filters.create.title')">
|
||||||
<p>
|
<p>
|
||||||
{{ $t('filters.create.description') }}
|
{{ $t('filters.create.description') }}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="{ 'is-loading': loading}" class="loader-container">
|
<div :class="{ 'is-loading': loading}" class="loader-container">
|
||||||
<x-button
|
<x-button
|
||||||
:to="{name:'labels.create'}"
|
:to="{ name:'labels.create' }"
|
||||||
class="is-pulled-right"
|
class="is-pulled-right"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<modal>
|
||||||
@close="$router.back()"
|
<card :title="list.title">
|
||||||
>
|
|
||||||
<card
|
|
||||||
:title="list.title"
|
|
||||||
>
|
|
||||||
<div class="has-text-left" v-html="htmlDescription" v-if="htmlDescription !== ''"></div>
|
<div class="has-text-left" v-html="htmlDescription" v-if="htmlDescription !== ''"></div>
|
||||||
<p v-else class="is-italic">
|
<p v-else class="is-italic">
|
||||||
{{ $t('list.noDescriptionAvailable') }}
|
{{ $t('list.noDescriptionAvailable') }}
|
||||||
|
|
|
@ -181,7 +181,6 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {toRef, computed, type Ref} from 'vue'
|
import {toRef, computed, type Ref} from 'vue'
|
||||||
|
|
||||||
import {useStorage} from '@vueuse/core'
|
import {useStorage} from '@vueuse/core'
|
||||||
|
|
||||||
import ListWrapper from '@/components/list/ListWrapper.vue'
|
import ListWrapper from '@/components/list/ListWrapper.vue'
|
||||||
|
@ -274,15 +273,12 @@ function sort(property: keyof SortBy) {
|
||||||
sortByParam.value = sortBy.value
|
sortByParam.value = sortBy.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: re-enable opening task detail in modal
|
|
||||||
// const router = useRouter()
|
|
||||||
const taskDetailRoutes = computed(() => Object.fromEntries(
|
const taskDetailRoutes = computed(() => Object.fromEntries(
|
||||||
tasks.value.map(({id}) => ([
|
tasks.value.map(({id}) => ([
|
||||||
id,
|
id,
|
||||||
{
|
{
|
||||||
name: 'task.detail',
|
name: 'task.detail',
|
||||||
params: {id},
|
params: {id},
|
||||||
// state: { backdropView: router.currentRoute.value.fullPath },
|
|
||||||
},
|
},
|
||||||
])),
|
])),
|
||||||
))
|
))
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
<template>
|
<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">
|
<div class="field">
|
||||||
<label class="label" for="listTitle">{{ $t('list.title') }}</label>
|
<label class="label" for="listTitle">{{ $t('list.title') }}</label>
|
||||||
<div
|
<div
|
||||||
|
@ -9,7 +14,7 @@
|
||||||
<input
|
<input
|
||||||
:class="{ disabled: listService.loading }"
|
:class="{ disabled: listService.loading }"
|
||||||
@keyup.enter="createNewList()"
|
@keyup.enter="createNewList()"
|
||||||
@keyup.esc="$router.back()"
|
@keyup.esc="onClose"
|
||||||
class="input"
|
class="input"
|
||||||
:placeholder="$t('list.create.titlePlaceholder')"
|
:placeholder="$t('list.create.titlePlaceholder')"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<modal @submit="archiveList">
|
||||||
@close="$router.back()"
|
|
||||||
@submit="archiveList()"
|
|
||||||
>
|
|
||||||
<template #header><span>{{ list.isArchived ? $t('list.archive.unarchive') : $t('list.archive.archive') }}</span></template>
|
<template #header><span>{{ list.isArchived ? $t('list.archive.unarchive') : $t('list.archive.archive') }}</span></template>
|
||||||
|
|
||||||
<template #text>
|
<template #text>
|
||||||
|
@ -17,7 +14,7 @@ export default {name: 'list-setting-archive'}
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed} from 'vue'
|
import {computed} from 'vue'
|
||||||
import {useRouter, useRoute} from 'vue-router'
|
import {useRoute} from 'vue-router'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
|
@ -28,13 +25,15 @@ import {useListStore} from '@/stores/lists'
|
||||||
|
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
const listStore = useListStore()
|
const listStore = useListStore()
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const list = computed(() => listStore.getListById(route.params.listId))
|
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 {
|
try {
|
||||||
const newList = await listStore.updateList({
|
const newList = await listStore.updateList({
|
||||||
...list.value,
|
...list.value,
|
||||||
|
@ -43,7 +42,7 @@ async function archiveList() {
|
||||||
useBaseStore().setCurrentList(newList)
|
useBaseStore().setCurrentList(newList)
|
||||||
success({message: t('list.archive.success')})
|
success({message: t('list.archive.success')})
|
||||||
} finally {
|
} finally {
|
||||||
router.back()
|
onClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -73,19 +73,19 @@
|
||||||
</x-button>
|
</x-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer="{onClose}">
|
||||||
<x-button
|
<x-button
|
||||||
v-if="hasBackground"
|
v-if="hasBackground"
|
||||||
:shadow="false"
|
:shadow="false"
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
class="is-danger"
|
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') }}
|
{{ $t('list.background.remove') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button
|
<x-button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@click.prevent.stop="$router.back()"
|
@click="onClose"
|
||||||
>
|
>
|
||||||
{{ $t('misc.close') }}
|
{{ $t('misc.close') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
|
@ -100,7 +100,7 @@ export default { name: 'list-setting-background' }
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, computed, shallowReactive} from 'vue'
|
import {ref, computed, shallowReactive} from 'vue'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import {useRoute, useRouter} from 'vue-router'
|
import {useRoute} from 'vue-router'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
@ -127,7 +127,6 @@ const SEARCH_DEBOUNCE = 300
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
useTitle(() => t('list.background.title'))
|
useTitle(() => t('list.background.title'))
|
||||||
|
|
||||||
|
@ -216,13 +215,13 @@ async function uploadBackground() {
|
||||||
success({message: t('list.background.success')})
|
success({message: t('list.background.success')})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeBackground() {
|
async function removeBackground(onClose: () => void) {
|
||||||
const list = await listService.value.removeBackground(currentList.value)
|
const list = await listService.value.removeBackground(currentList.value)
|
||||||
await baseStore.handleSetCurrentList({list, forceUpdate: true})
|
await baseStore.handleSetCurrentList({list, forceUpdate: true})
|
||||||
namespaceStore.setListInNamespaceById(list)
|
namespaceStore.setListInNamespaceById(list)
|
||||||
listStore.setList(list)
|
listStore.setList(list)
|
||||||
success({message: t('list.background.removeSuccess')})
|
success({message: t('list.background.removeSuccess')})
|
||||||
router.back()
|
onClose()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<modal @submit="deleteList()">
|
||||||
@close="$router.back()"
|
|
||||||
@submit="deleteList()"
|
|
||||||
>
|
|
||||||
<template #header><span>{{ $t('list.delete.header') }}</span></template>
|
<template #header><span>{{ $t('list.delete.header') }}</span></template>
|
||||||
|
|
||||||
<template #text>
|
<template #text>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
@primary="save"
|
@primary="save"
|
||||||
:tertiary="$t('misc.delete')"
|
:tertiary="$t('misc.delete')"
|
||||||
@tertiary="$router.push({ name: 'list.settings.delete', params: { id: listId } })"
|
@tertiary="$router.push({ name: 'list.settings.delete', params: { id: listId } })"
|
||||||
|
#default="{onClose}"
|
||||||
>
|
>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="title">{{ $t('list.title') }}</label>
|
<label class="label" for="title">{{ $t('list.title') }}</label>
|
||||||
|
@ -13,7 +14,7 @@
|
||||||
<input
|
<input
|
||||||
:class="{ 'disabled': isLoading}"
|
:class="{ 'disabled': isLoading}"
|
||||||
:disabled="isLoading || undefined"
|
:disabled="isLoading || undefined"
|
||||||
@keyup.enter="save"
|
@keyup.enter="save(onClose)"
|
||||||
class="input"
|
class="input"
|
||||||
id="title"
|
id="title"
|
||||||
:placeholder="$t('list.edit.titlePlaceholder')"
|
:placeholder="$t('list.edit.titlePlaceholder')"
|
||||||
|
@ -33,7 +34,7 @@
|
||||||
<input
|
<input
|
||||||
:class="{ 'disabled': isLoading}"
|
:class="{ 'disabled': isLoading}"
|
||||||
:disabled="isLoading || undefined"
|
:disabled="isLoading || undefined"
|
||||||
@keyup.enter="save"
|
@keyup.enter="save(onClose)"
|
||||||
class="input"
|
class="input"
|
||||||
id="identifier"
|
id="identifier"
|
||||||
:placeholder="$t('list.edit.identifierPlaceholder')"
|
:placeholder="$t('list.edit.identifierPlaceholder')"
|
||||||
|
@ -71,7 +72,6 @@ export default { name: 'list-setting-edit' }
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {PropType} from 'vue'
|
import type {PropType} from 'vue'
|
||||||
import {useRouter} from 'vue-router'
|
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
import Editor from '@/components/input/AsyncEditor'
|
import Editor from '@/components/input/AsyncEditor'
|
||||||
|
@ -92,17 +92,15 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
|
|
||||||
const {list, save: saveList, isLoading} = useList(props.listId)
|
const {list, save: saveList, isLoading} = useList(props.listId)
|
||||||
|
|
||||||
useTitle(() => list?.title ? t('list.edit.title', {list: list.title}) : '')
|
useTitle(() => list?.title ? t('list.edit.title', {list: list.title}) : '')
|
||||||
|
|
||||||
async function save() {
|
async function save(onClose: () => void) {
|
||||||
await saveList()
|
await saveList()
|
||||||
await useBaseStore().handleSetCurrentList({list})
|
await useBaseStore().handleSetCurrentList({list})
|
||||||
router.back()
|
onClose()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button
|
<x-button
|
||||||
v-if="n.isArchived"
|
v-if="n.isArchived"
|
||||||
:to="{name: 'namespace.settings.archive', params: {id: n.id}}"
|
:to="{name: 'namespace.settings.archive', params: {id: n.id} }"
|
||||||
class="is-pulled-right mr-4"
|
class="is-pulled-right mr-4"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon="archive"
|
icon="archive"
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<create-edit
|
<create-edit
|
||||||
:title="$t('namespace.create.title')"
|
:title="$t('namespace.create.title')"
|
||||||
@create="newNamespace()"
|
@create="newNamespace"
|
||||||
:primary-disabled="namespace.title === ''"
|
:primary-disabled="namespace.title === ''"
|
||||||
|
#default="{onClose}"
|
||||||
>
|
>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="namespaceTitle">{{ $t('namespace.attributes.title') }}</label>
|
<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.
|
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. -->
|
Hence we place the listener on the input field directly. -->
|
||||||
<input
|
<input
|
||||||
@keyup.enter="newNamespace()"
|
@keyup.enter="newNamespace(onClose)"
|
||||||
@keyup.esc="$router.back()"
|
@keyup.esc="onClose"
|
||||||
class="input"
|
class="input"
|
||||||
:placeholder="$t('namespace.attributes.titlePlaceholder')"
|
:placeholder="$t('namespace.attributes.titlePlaceholder')"
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -46,7 +47,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, shallowReactive} from 'vue'
|
import {ref, shallowReactive} from 'vue'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import {useRouter} from 'vue-router'
|
|
||||||
|
|
||||||
import Message from '@/components/misc/message.vue'
|
import Message from '@/components/misc/message.vue'
|
||||||
import CreateEdit from '@/components/misc/create-edit.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 namespaceService = shallowReactive(new NamespaceService())
|
||||||
|
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
useTitle(() => t('namespace.create.title'))
|
useTitle(() => t('namespace.create.title'))
|
||||||
|
|
||||||
async function newNamespace() {
|
async function newNamespace(onClose: () => void) {
|
||||||
if (namespace.value.title === '') {
|
if (namespace.value.title === '') {
|
||||||
showError.value = true
|
showError.value = true
|
||||||
return
|
return
|
||||||
|
@ -79,6 +78,6 @@ async function newNamespace() {
|
||||||
const newNamespace = await namespaceService.create(namespace.value)
|
const newNamespace = await namespaceService.create(namespace.value)
|
||||||
useNamespaceStore().addNamespace(newNamespace)
|
useNamespaceStore().addNamespace(newNamespace)
|
||||||
success({message: t('namespace.create.success')})
|
success({message: t('namespace.create.success')})
|
||||||
router.back()
|
onClose()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<modal @submit="archiveNamespace">
|
||||||
@close="$router.back()"
|
|
||||||
@submit="archiveNamespace()"
|
|
||||||
>
|
|
||||||
<template #header><span>{{ title }}</span></template>
|
<template #header><span>{{ title }}</span></template>
|
||||||
|
|
||||||
<template #text>
|
<template #text>
|
||||||
|
@ -23,7 +20,6 @@ export default { name: 'namespace-setting-archive' }
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {watch, ref, computed, shallowReactive, type PropType} from 'vue'
|
import {watch, ref, computed, shallowReactive, type PropType} from 'vue'
|
||||||
import {useRouter} from 'vue-router'
|
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
|
@ -41,7 +37,6 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
|
|
||||||
const namespaceStore = useNamespaceStore()
|
const namespaceStore = useNamespaceStore()
|
||||||
|
@ -69,7 +64,7 @@ const title = computed(() => {
|
||||||
})
|
})
|
||||||
useTitle(title)
|
useTitle(title)
|
||||||
|
|
||||||
async function archiveNamespace() {
|
async function archiveNamespace(onClose: () => void) {
|
||||||
try {
|
try {
|
||||||
const isArchived = !namespace.value.isArchived
|
const isArchived = !namespace.value.isArchived
|
||||||
const archivedNamespace = await namespaceService.update({
|
const archivedNamespace = await namespaceService.update({
|
||||||
|
@ -83,7 +78,7 @@ async function archiveNamespace() {
|
||||||
: t('namespace.archive.unarchiveSuccess'),
|
: t('namespace.archive.unarchiveSuccess'),
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
router.back()
|
onClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<modal @submit="deleteNamespace()">
|
||||||
@close="$router.back()"
|
|
||||||
@submit="deleteNamespace()"
|
|
||||||
>
|
|
||||||
<template #header><span>{{ title }}</span></template>
|
<template #header><span>{{ title }}</span></template>
|
||||||
|
|
||||||
<template #text>
|
<template #text>
|
||||||
|
|
|
@ -6,8 +6,9 @@
|
||||||
@primary="save"
|
@primary="save"
|
||||||
:tertiary="$t('misc.delete')"
|
:tertiary="$t('misc.delete')"
|
||||||
@tertiary="$router.push({ name: 'namespace.settings.delete', params: { id: $route.params.id } })"
|
@tertiary="$router.push({ name: 'namespace.settings.delete', params: { id: $route.params.id } })"
|
||||||
|
#default="{onClose}"
|
||||||
>
|
>
|
||||||
<form @submit.prevent="save()">
|
<form @submit.prevent="save(onClose)">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="namespacetext">{{ $t('namespace.attributes.title') }}</label>
|
<label class="label" for="namespacetext">{{ $t('namespace.attributes.title') }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
@ -58,7 +59,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {nextTick, ref, watch} from 'vue'
|
import {nextTick, ref, watch} from 'vue'
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
import router from '@/router'
|
|
||||||
|
|
||||||
import AsyncEditor from '@/components/input/AsyncEditor'
|
import AsyncEditor from '@/components/input/AsyncEditor'
|
||||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||||
|
@ -110,11 +110,11 @@ async function loadNamespace() {
|
||||||
title.value = t('namespace.edit.title', {namespace: namespace.value.title})
|
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)
|
const updatedNamespace = await namespaceService.value.update(namespace.value)
|
||||||
// Update the namespace in the parent
|
// Update the namespace in the parent
|
||||||
namespaceStore.setNamespaceById(updatedNamespace)
|
namespaceStore.setNamespaceById(updatedNamespace)
|
||||||
success({message: t('namespace.edit.success')})
|
success({message: t('namespace.edit.success')})
|
||||||
router.back()
|
onClose()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -1,4 +1,5 @@
|
||||||
<template>
|
<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
|
<div
|
||||||
class="loader-container task-view-container"
|
class="loader-container task-view-container"
|
||||||
:class="{
|
:class="{
|
||||||
|
@ -283,6 +284,22 @@
|
||||||
<comments :can-write="canWrite" :task-id="taskId"/>
|
<comments :can-write="canWrite" :task-id="taskId"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-one-third action-buttons d-print-none" v-if="canWrite || isModal">
|
<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">
|
<template v-if="canWrite">
|
||||||
<x-button
|
<x-button
|
||||||
:class="{'is-success': !task.done}"
|
:class="{'is-success': !task.done}"
|
||||||
|
@ -442,15 +459,18 @@
|
||||||
</template>
|
</template>
|
||||||
</modal>
|
</modal>
|
||||||
</div>
|
</div>
|
||||||
|
</OptionalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {ref, reactive, toRef, shallowReactive, computed, watch, nextTick, type PropType} from 'vue'
|
import {ref, reactive, toRef, shallowReactive, computed, watch, nextTick, type PropType} from 'vue'
|
||||||
import {useRouter, type RouteLocation} from 'vue-router'
|
import {useRouter} from 'vue-router'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import {unrefElement} from '@vueuse/core'
|
import {unrefElement} from '@vueuse/core'
|
||||||
import cloneDeep from 'lodash.clonedeep'
|
import cloneDeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
|
import {handleRedirectRecord} from '@/router'
|
||||||
|
|
||||||
import TaskService from '@/services/task'
|
import TaskService from '@/services/task'
|
||||||
import TaskModel, {TASK_DEFAULT_COLOR} from '@/models/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 Reminders from '@/components/tasks/partials/reminders.vue'
|
||||||
import RepeatAfter from '@/components/tasks/partials/repeatAfter.vue'
|
import RepeatAfter from '@/components/tasks/partials/repeatAfter.vue'
|
||||||
import TaskSubscription from '@/components/misc/subscription.vue'
|
import TaskSubscription from '@/components/misc/subscription.vue'
|
||||||
|
|
||||||
|
import OptionalWrapper from '@/components/base/OptionalWrapper.vue'
|
||||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||||
|
import Modal from '@/components/misc/modal.vue'
|
||||||
|
|
||||||
import {uploadFile} from '@/helpers/attachments'
|
import {uploadFile} from '@/helpers/attachments'
|
||||||
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
||||||
|
@ -502,13 +525,12 @@ const props = defineProps({
|
||||||
type: Number as PropType<ITask['id']>,
|
type: Number as PropType<ITask['id']>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
backdropView: {
|
isModal: {
|
||||||
type: String as PropType<RouteLocation['fullPath']>,
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineEmits(['close'])
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
|
|
||||||
|
@ -521,6 +543,23 @@ const kanbanStore = useKanbanStore()
|
||||||
const task = reactive<ITask>(new TaskModel())
|
const task = reactive<ITask>(new TaskModel())
|
||||||
useTitle(toRef(task, 'title'))
|
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
|
// 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,
|
// 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,
|
// 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 hasAttachments = computed(() => attachmentStore.attachments.length > 0)
|
||||||
|
|
||||||
const isModal = computed(() => Boolean(props.backdropView))
|
|
||||||
|
|
||||||
function attachmentUpload(file: File, onSuccess?: (url: string) => void) {
|
function attachmentUpload(file: File, onSuccess?: (url: string) => void) {
|
||||||
return uploadFile(taskId.value, file, onSuccess)
|
return uploadFile(taskId.value, file, onSuccess)
|
||||||
}
|
}
|
||||||
|
@ -803,9 +840,11 @@ async function setPercentDone(percentDone: number) {
|
||||||
--primary-light: hsla(var(--primary-h), var(--primary-s), 73%, var(--primary-a));
|
--primary-light: hsla(var(--primary-h), var(--primary-s), 73%, var(--primary-a));
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
|
||||||
@media screen and (min-width: $desktop) {
|
&:not(.is-modal) {
|
||||||
padding-bottom: 1rem;
|
@media screen and (min-width: $desktop) {
|
||||||
}
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-view {
|
.task-view {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<create-edit
|
<create-edit
|
||||||
|
class="new-team"
|
||||||
:title="title"
|
:title="title"
|
||||||
@create="newTeam()"
|
@create="newTeam()"
|
||||||
:primary-disabled="team.name === ''"
|
: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.