feat: implement modals with vue router 4 #816

Merged
konrad merged 62 commits from dpschen/frontend:feature/vue3-modals-with-router-4 into main 2022-02-05 16:49:04 +00:00
15 changed files with 152 additions and 284 deletions
Showing only changes of commit 5a0c0eff9f - Show all commits

View File

@ -20,8 +20,9 @@
<quick-actions/>
<router-view/>
<router-view :route="routeWithModal"/>
<!-- TODO: is this still used? -->
dpschen marked this conversation as resolved Outdated

I don't think it is, we should make sure the modal views keep their transition though. Might make sense to include that in the modal component itself?

I don't think it is, we should make sure the modal views keep their transition though. Might make sense to include that in the modal component itself?

Either that or put the transition inside something like a provider component. In that we could use the new teleport component. I was always using portal-vue in vue 2 for this kind of stuff.

Either that or put the transition inside something like a provider component. In that we could use the [new teleport component](https://v3.vuejs.org/api/built-in-components.html#teleport). I was always using [portal-vue](https://github.com/LinusBorg/portal-vue) in vue 2 for this kind of stuff.

I think using the teleport component allows for a cleaner solution since there are situations where you want a transition handled by the route and others where you want to have it handled by the outer component (like delete modals).

I think using the teleport component allows for a cleaner solution since there are situations where you want a transition handled by the route and others where you want to have it handled by the outer component (like delete modals).
<router-view name="popup" v-slot="{ Component }">
<transition name="modal">
<component :is="Component" />
@ -50,6 +51,24 @@ import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/muta
import Navigation from '@/components/home/navigation.vue'
import QuickActions from '@/components/quick-actions/quick-actions.vue'
function useRouteWithModal() {
const router = useRouter()
const route = useRoute()
const historyState = computed(() => route.fullPath && window.history.state)
const routeWithModal = computed(() => {
if (historyState.value.backgroundView) {
return router.resolve(historyState.value.backgroundView)
} else {
return route
}
})
return { routeWithModal }
}
useRouteWithModal()
const store = useStore()
const background = computed(() => store.state.background)

View File

@ -29,9 +29,10 @@
<script>
import Filters from '@/components/list/partials/filters'
import {getDefaultParams} from '@/components/tasks/mixins/taskList'
import Popup from '@/components/misc/popup'
import {getDefaultParams} from '@/composables/taskList'
export default {
name: 'filter-popup',
components: {

View File

@ -6,7 +6,9 @@
<message>
{{
s.available($route) ? $t('keyboardShortcuts.currentPageOnly') : $t('keyboardShortcuts.allPages')
s?.available($route)
? $t('keyboardShortcuts.currentPageOnly')
: $t('keyboardShortcuts.allPages')
}}
</message>
@ -17,7 +19,8 @@
class="shortcut-keys"
is="dd"
:keys="sc.keys"
:combination="typeof sc.combination !== 'undefined' ? $t(`keyboardShortcuts.${sc.combination}`) : null"/>
:combination="typeof sc.combination !== 'undefined' ? $t(`keyboardShortcuts.${sc.combination}`) : null"
/>
</template>
</dl>
</template>
@ -25,28 +28,17 @@
</modal>
</template>
<script>
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
import Shortcut from '@/components/misc/shortcut.vue'
import Message from '@/components/misc/message'
import {KEYBOARD_SHORTCUTS} from './shortcuts'
<script lang="ts" setup>
import {store} from '@/store'
export default {
name: 'keyboard-shortcuts',
components: {
Message,
Shortcut,
},
data() {
return {
shortcuts: KEYBOARD_SHORTCUTS,
}
},
methods: {
close() {
this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)
},
},
import Shortcut from '@/components/misc/shortcut.vue'
import Message from '@/components/misc/message.vue'
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
import {KEYBOARD_SHORTCUTS as shortcuts} from './shortcuts'
function close() {
store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)
}
</script>

View File

@ -5,7 +5,6 @@ const ctrl = isAppleDevice() ? '⌘' : 'ctrl'
export const KEYBOARD_SHORTCUTS = [
{
title: 'keyboardShortcuts.general',
available: () => null,
shortcuts: [
{
title: 'keyboardShortcuts.toggleMenu',
@ -55,13 +54,7 @@ export const KEYBOARD_SHORTCUTS = [
},
{
title: 'keyboardShortcuts.task.title',
available: (route) => [
'task.detail',
'task.list.detail',
'task.gantt.detail',
'task.kanban.detail',
'task.detail',
].includes(route.name),
available: (route) => route.name === 'task.detail',
shortcuts: [
{
title: 'keyboardShortcuts.task.assign',

View File

@ -67,7 +67,7 @@
<router-link
class="mt-2 has-text-centered is-block"
:to="{name: 'task.detail', params: {id: taskEditTask.id}}"
:to="taskDetailRoute"
>
{{ $t('task.openDetail') }}
</router-link>
@ -102,6 +102,15 @@ export default {
taskEditTask: TaskModel,
}
},
computed: {
taskDetailRoute() {
return {
name: 'task.detail',
params: { id: this.taskEditTask.id },
state: { backgroundView: this.$router.currentRoute.value.fullPath },
dpschen marked this conversation as resolved Outdated

I'm not sure about the naming of this because there's views with a background. Maybe that can get confusing?

I'm not sure about the naming of this because there's views with a background. Maybe that can get confusing?

That's true. Do you have a suggestion?

Maybe something like backdropView in the sense of https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter

That's true. Do you have a suggestion? Maybe something like backdropView in the sense of https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter

I like that idea.

I like that idea.

Done

Done
}
},
},
components: {
ColorPicker,
Reminders,

View File

@ -7,8 +7,8 @@
'has-light-text': !colorIsDark(task.hexColor) && task.hexColor !== `#${task.defaultColor}` && task.hexColor !== task.defaultColor,
}"
:style="{'background-color': task.hexColor !== '#' && task.hexColor !== `#${task.defaultColor}` ? task.hexColor : false}"
@click.ctrl="() => toggleTaskDone(task)"
@click.exact="() => $router.push({ name: 'task.kanban.detail', params: { id: task.id } })"
@click.ctrl="() => toggleTaskDone(task)"
@click.meta="() => toggleTaskDone(task)"
>
<span class="task-id">
@ -112,6 +112,13 @@ export default {
this.loadingInternal = false
}
},
openTaskDetail() {
this.$router.push({
name: 'task.detail',
params: { id: this.task.id },
state: { backgroundView: this.$router.currentRoute.value.fullPath },
})
},
},
}
</script>

View File

@ -8,7 +8,7 @@
>
</span>
<router-link
:to="{ name: taskDetailRoute, params: { id: task.id } }"
:to="taskDetailRoute"
:class="{ 'done': task.done}"
class="tasktext">
<span>
@ -126,10 +126,6 @@ export default {
type: Boolean,
default: false,
},
taskDetailRoute: {
type: String,
default: 'task.list.detail',
},
showList: {
type: Boolean,
default: false,
@ -167,6 +163,13 @@ export default {
title: '',
} : this.$store.state.currentList
},
taskDetailRoute() {
return {
name: 'task.detail',
params: { id: this.task.id },
state: { backgroundView: this.$router.currentRoute.value.fullPath },
}
},
},
methods: {
async markAsDone(checked) {

View File

@ -13,7 +13,6 @@ import DataExportDownload from '../views/user/DataExportDownload'
// Tasks
import ShowTasksInRangeComponent from '../views/tasks/ShowTasksInRange'
import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth'
import TaskDetailViewModal from '../views/tasks/TaskDetailViewModal'
import TaskDetailView from '../views/tasks/TaskDetailView'
import ListNamespaces from '../views/namespaces/ListNamespaces'
// Team Handling
@ -315,204 +314,21 @@ const router = createRouter({
path: '/lists/:listId/list',
name: 'list.list',
component: List,
children: [
{
path: '/tasks/:id',
name: 'task.list.detail',
component: TaskDetailViewModal,
},
{
path: '/lists/:listId/settings/edit',
name: 'list.list.settings.edit',
component: ListSettingEdit,
},
{
path: '/lists/:listId/settings/background',
name: 'list.list.settings.background',
component: ListSettingBackground,
},
{
path: '/lists/:listId/settings/duplicate',
name: 'list.list.settings.duplicate',
component: ListSettingDuplicate,
},
{
path: '/lists/:listId/settings/share',
name: 'list.list.settings.share',
component: ListSettingShare,
},
{
path: '/lists/:listId/settings/delete',
name: 'list.list.settings.delete',
component: ListSettingDelete,
},
{
path: '/lists/:listId/settings/archive',
name: 'list.list.settings.archive',
component: ListSettingArchive,
},
{
path: '/lists/:listId/settings/edit',
name: 'filter.list.settings.edit',
component: FilterEdit,
},
{
path: '/lists/:listId/settings/delete',
name: 'filter.list.settings.delete',
component: FilterDelete,
},
],
},
{
path: '/lists/:listId/gantt',
name: 'list.gantt',
component: Gantt,
children: [
{
path: '/tasks/:id',
name: 'task.gantt.detail',
component: TaskDetailViewModal,
},
{
path: '/lists/:listId/settings/edit',
name: 'list.gantt.settings.edit',
component: ListSettingEdit,
},
{
path: '/lists/:listId/settings/background',
name: 'list.gantt.settings.background',
component: ListSettingBackground,
},
{
path: '/lists/:listId/settings/duplicate',
name: 'list.gantt.settings.duplicate',
component: ListSettingDuplicate,
},
{
path: '/lists/:listId/settings/share',
name: 'list.gantt.settings.share',
component: ListSettingShare,
},
{
path: '/lists/:listId/settings/delete',
name: 'list.gantt.settings.delete',
component: ListSettingDelete,
},
{
path: '/lists/:listId/settings/archive',
name: 'list.gantt.settings.archive',
component: ListSettingArchive,
},
{
path: '/lists/:listId/settings/edit',
name: 'filter.gantt.settings.edit',
component: FilterEdit,
},
{
path: '/lists/:listId/settings/delete',
name: 'filter.gantt.settings.delete',
component: FilterDelete,
},
],
},
{
path: '/lists/:listId/table',
name: 'list.table',
component: Table,
children: [
{
path: '/lists/:listId/settings/edit',
name: 'list.table.settings.edit',
component: ListSettingEdit,
},
{
path: '/lists/:listId/settings/background',
name: 'list.table.settings.background',
component: ListSettingBackground,
},
{
path: '/lists/:listId/settings/duplicate',
name: 'list.table.settings.duplicate',
component: ListSettingDuplicate,
},
{
path: '/lists/:listId/settings/share',
name: 'list.table.settings.share',
component: ListSettingShare,
},
{
path: '/lists/:listId/settings/delete',
name: 'list.table.settings.delete',
component: ListSettingDelete,
},
{
path: '/lists/:listId/settings/archive',
name: 'list.table.settings.archive',
component: ListSettingArchive,
},
{
path: '/lists/:listId/settings/edit',
name: 'filter.table.settings.edit',
component: FilterEdit,
},
{
path: '/lists/:listId/settings/delete',
name: 'filter.table.settings.delete',
component: FilterDelete,
},
],
},
{
path: '/lists/:listId/kanban',
name: 'list.kanban',
component: Kanban,
children: [
{
path: '/tasks/:id',
name: 'task.kanban.detail',
component: TaskDetailViewModal,
},
{
path: '/lists/:listId/settings/edit',
name: 'list.kanban.settings.edit',
component: ListSettingEdit,
},
{
path: '/lists/:listId/settings/background',
name: 'list.kanban.settings.background',
component: ListSettingBackground,
},
{
path: '/lists/:listId/settings/duplicate',
name: 'list.kanban.settings.duplicate',
component: ListSettingDuplicate,
},
{
path: '/lists/:listId/settings/share',
name: 'list.kanban.settings.share',
component: ListSettingShare,
},
{
path: '/lists/:listId/settings/delete',
name: 'list.kanban.settings.delete',
component: ListSettingDelete,
},
{
path: '/lists/:listId/settings/archive',
name: 'list.kanban.settings.archive',
component: ListSettingArchive,
},
{
path: '/lists/:listId/settings/edit',
name: 'filter.kanban.settings.edit',
component: FilterEdit,
},
{
path: '/lists/:listId/settings/delete',
name: 'filter.kanban.settings.delete',
component: FilterDelete,
},
],
},
],
},

View File

@ -51,6 +51,10 @@
</div>
</div>
<ShowTasks class="mt-4" :show-all="true" v-if="hasLists" :key="showTasksKey"/>
<transition name="modal">
<task-detail-view-modal v-if="showTaskDetail" />
</transition>
</div>
</template>
@ -67,6 +71,9 @@ import {getHistory} from '@/modules/listHistory'
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
import {useDateTimeSalutation} from '@/composables/useDateTimeSalutation'
import TaskDetailViewModal, { useShowModal } from '@/views/tasks/TaskDetailViewModal.vue'
const showTaskDetail = useShowModal()
const welcome = useDateTimeSalutation()

View File

@ -8,28 +8,28 @@
<router-link
v-shortcut="'g l'"
:title="$t('keyboardShortcuts.list.switchToListView')"
:class="{'is-active': $route.name.includes('list.list')}"
:class="{'is-active': currentListType === 'list'}"
:to="{ name: 'list.list', params: { listId: listId } }">
{{ $t('list.list.title') }}
</router-link>
<router-link
v-shortcut="'g g'"
:title="$t('keyboardShortcuts.list.switchToGanttView')"
:class="{'is-active': $route.name.includes('list.gantt')}"
:class="{'is-active': currentListType === 'gantt'}"
:to="{ name: 'list.gantt', params: { listId: listId } }">
{{ $t('list.gantt.title') }}
</router-link>
<router-link
v-shortcut="'g t'"
:title="$t('keyboardShortcuts.list.switchToTableView')"
:class="{'is-active': $route.name.includes('list.table')}"
:class="{'is-active': currentListType === 'table'}"
:to="{ name: 'list.table', params: { listId: listId } }">
{{ $t('list.table.title') }}
</router-link>
<router-link
v-shortcut="'g k'"
:title="$t('keyboardShortcuts.list.switchToKanbanView')"
:class="{'is-active': $route.name.includes('list.kanban')}"
:class="{'is-active': currentListType === 'kanban'}"
:to="{ name: 'list.kanban', params: { listId: listId } }">
{{ $t('list.kanban.title') }}
</router-link>
@ -69,6 +69,11 @@ export default {
},
},
computed: {
currentListType() {
// default: 'list',
return ''
},
// Computed property to let "listId" always have a value
listId() {
return typeof this.$route.params.listId === 'undefined' ? 0 : this.$route.params.listId
@ -113,11 +118,11 @@ export default {
this.$store.commit('kanban/setListId', 0)
}
// When clicking again on a list in the menu, there would be no list view selected which means no list
// at all. Users will then have to click on the list view menu again which is quite confusing.
if (this.$route.name === 'list.index') {
return this.replaceListView()
}
// // When clicking again on a list in the menu, there would be no list view selected which means no list
// // at all. Users will then have to click on the list view menu again which is quite confusing.
// if (this.$route.name === 'list.index') {
// return this.replaceListView()
// }
// Don't load the list if we either already loaded it or aren't dealing with a list at all currently and
// the currently loaded list has the right set.

View File

@ -52,13 +52,9 @@
:show-taskswithout-dates="showTaskswithoutDates"
/>
<!-- This router view is used to show the task popup while keeping the gantt chart itself -->
<router-view v-slot="{ Component }">
<transition name="modal">
<component :is="Component" />
</transition>
</router-view>
<transition name="modal">
<task-detail-view-modal v-if="showTaskDetail" />
</transition>
</card>
</div>
</template>
@ -69,12 +65,20 @@ import flatPickr from 'vue-flatpickr-component'
import Fancycheckbox from '../../../components/input/fancycheckbox'
import {saveListView} from '@/helpers/saveListView'
import TaskDetailViewModal, { useShowModal } from '@/views/tasks/TaskDetailViewModal.vue'
export default {
name: 'Gantt',
components: {
Fancycheckbox,
flatPickr,
GanttChart,
TaskDetailViewModal,
},
setup() {
return {
showTaskDetail: useShowModal(),
}
},
created() {
// Save the current list view to local storage

View File

@ -204,18 +204,12 @@
</div>
</div>
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
<router-view v-slot="{ Component }">
<transition name="modal">
<component :is="Component"/>
</transition>
</router-view>
<transition name="modal">
<task-detail-view-modal v-if="showTaskDetail" />
<modal
v-else-if="showBucketDeleteModal"
@close="showBucketDeleteModal = false"
@submit="deleteBucket()"
v-if="showBucketDeleteModal"
>
<template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template>
@ -242,6 +236,7 @@ import Dropdown from '@/components/misc/dropdown.vue'
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
import KanbanCard from '@/components/tasks/partials/kanban-card'
import TaskDetailViewModal, { useShowModal } from '@/views/tasks/TaskDetailViewModal.vue'
const DRAG_OPTIONS = {
// sortable options
@ -261,6 +256,7 @@ export default {
Dropdown,
FilterPopup,
draggable,
TaskDetailViewModal,
},
data() {
return {
@ -296,6 +292,13 @@ export default {
},
}
},
setup() {
return {
showTaskDetail: useShowModal(),
}
},
created() {
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.

View File

@ -90,7 +90,6 @@
:disabled="!canWrite"
:the-task="t"
@taskUpdated="updateTasks"
task-detail-route="task.detail"
>
<template v-if="canWrite">
<span class="icon handle">
@ -124,12 +123,9 @@
/>
</card>
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
<router-view v-slot="{ Component }">
<transition name="modal">
<component :is="Component"/>
</transition>
</router-view>
<transition name="modal">
<task-detail-view-modal v-if="showTaskDetail" />
</transition>
</div>
</template>
@ -147,8 +143,8 @@ import FilterPopup from '@/components/list/partials/filter-popup.vue'
import {HAS_TASKS} from '@/store/mutation-types'
import Nothing from '@/components/misc/nothing.vue'
import Pagination from '@/components/misc/pagination.vue'
import Popup from '@/components/misc/popup'
import { ALPHABETICAL_SORT } from '@/components/list/partials/filters'
import {ALPHABETICAL_SORT} from '@/components/list/partials/filters.vue'
import TaskDetailViewModal, { useShowModal } from '@/views/tasks/TaskDetailViewModal.vue'
import draggable from 'vuedraggable'
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
@ -192,7 +188,6 @@ export default {
taskList,
],
components: {
Popup,
Nothing,
FilterPopup,
SingleTaskInList,
@ -200,7 +195,15 @@ export default {
AddTask,
draggable,
Pagination,
TaskDetailViewModal,
},
setup() {
return {
showTaskDetail: useShowModal(),
}
},
created() {
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.

View File

@ -120,7 +120,7 @@
<tbody>
<tr :key="t.id" v-for="t in tasks">
<td v-if="activeColumns.id">
<router-link :to="{name: 'task.detail', params: { id: t.id }}">
<router-link :to="taskDetailRoutes[t.id]">
<template v-if="t.identifier === ''">
#{{ t.index }}
</template>
@ -133,7 +133,7 @@
<Done :is-done="t.done" variant="small" />
</td>
<td v-if="activeColumns.title">
<router-link :to="{name: 'task.detail', params: { id: t.id }}">{{ t.title }}</router-link>
<router-link :to="taskDetailRoutes[t.id]">{{ t.title }}</router-link>
</td>
<td v-if="activeColumns.priority">
<priority-label :priority="t.priority" :done="t.done" :show-all="true"/>
@ -185,6 +185,8 @@
</template>
<script>
import {useRoute} from 'vue-router'
import taskList from '@/components/tasks/mixins/taskList'
import Done from '@/components/misc/Done.vue'
import User from '@/components/misc/user'
@ -237,6 +239,19 @@ export default {
},
}
},
computed: {
taskDetailRoutes() {
const taskDetailRoutes = {}
this.tasks.forEach(({id}) => {
taskDetailRoutes[id] = {
name: 'task.detail',
params: { id },
state: { backgroundView: this.$router.currentRoute.value.fullPath },
}
})
return taskDetailRoutes
},
},
created() {
const savedShowColumns = localStorage.getItem('tableViewColumns')
if (savedShowColumns !== null) {
@ -253,9 +268,13 @@ export default {
this.initTasks(1)
},
setup() {
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.
saveListView(this.$route.params.listId, this.$route.name)
const route = useRoute()
console.log(route.value)
saveListView(route.value.params.listId, route.value.name)
},
methods: {
initTasks(page, search = '') {

View File

@ -13,37 +13,24 @@
<script>
import TaskDetailView from './TaskDetailView'
import {computed} from 'vue'
import {useRoute} from 'vue-router'
export function useShowModal() {
const route = useRoute()
const historyState = computed(() => route.fullPath && window.history.state)
const show = computed(() => historyState.value.backgroundView)
return show
}
export default {
name: 'TaskDetailViewModal',
components: {
TaskDetailView,
},
data() {
return {
lastRoute: null,
}
},
beforeRouteEnter(to, from, next) {
next(vm => {
vm.lastRoute = from
})
},
beforeRouteLeave(to, from, next) {
if (from.name === 'task.kanban.detail' && to.name === 'task.detail') {
this.$router.replace({name: 'task.kanban.detail', params: to.params})
return
}
next()
},
methods: {
close() {
if (this.lastRoute === null) {
this.$router.back()
} else {
this.$router.push(this.lastRoute)
}
this.$router.back()
},
},
}