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
13 changed files with 314 additions and 411 deletions
Showing only changes of commit c70211ad32 - Show all commits

View File

@ -22,12 +22,9 @@
<router-view :route="routeWithModal"/>
<!-- TODO: is this still used? -->
<router-view name="popup" v-slot="{ Component }">
<transition name="modal">
<component :is="Component" />
</transition>
</router-view>
<transition name="modal">
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).
<component v-if="currentModal" :is="currentModal" />
</transition>
<a
class="keyboard-shortcuts-button"
@ -42,7 +39,7 @@
</template>
dpschen marked this conversation as resolved Outdated

I suppose this is used because we don't have a modal wrapper? Using "TaskDetailView" in the general content with component feels a little out of place IMHO 😅

I suppose this is used because we don't have a modal wrapper? Using "TaskDetailView" in the general content with component feels a little out of place IMHO 😅

See it like the router-view conponent of vue router: it's also not a view itself. regardless I just wanted to make the most simple change. I guess it should be renamed to something like modal-view and maybe even merged with modal 🤔 - but that can also happen later.

See it like the router-view conponent of vue router: it's also not a view itself. regardless I just wanted to make the most simple change. I guess it should be renamed to something like modal-view and maybe even merged with modal 🤔 - but that can also happen later.

I removed the component task-detail-view-modal in 6827390b77 and was able to merge it with the modal itself.

As a result this is obsolete now =)

I removed the component `task-detail-view-modal` in https://kolaente.dev/vikunja/frontend/commit/6827390b77ae6e186e7b0163651c19ca9a247d2f and was able to merge it with the modal itself. As a result this is obsolete now =)
<script lang="ts" setup>
import {watch, computed} from 'vue'
import {watch, computed, shallowRef, watchEffect} from 'vue'
import {useStore} from 'vuex'
import {useRoute, useRouter} from 'vue-router'
import {useEventListener} from '@vueuse/core'
@ -64,7 +61,16 @@ function useRouteWithModal() {
}
})
return { routeWithModal }
const currentModal = shallowRef(null)
watchEffect(() => {
currentModal.value = historyState.value.backgroundView
? route.matched[0]?.components.default
: null
})
return { routeWithModal, currentModal }
}
useRouteWithModal()

View File

@ -2,21 +2,22 @@
<dropdown>
<template v-if="isSavedFilter">
<dropdown-item
:to="{ name: `${listRoutePrefix}.edit`, params: { listId: list.id } }"
:to="{ name: 'filter.settings.edit', params: { listId: list.id } }"
icon="pen"
>
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.delete`, params: { listId: list.id } }"
:to="{ name: 'filter.settings.delete', params: { listId: list.id } }"
icon="trash-alt"
>
{{ $t('misc.delete') }}
</dropdown-item>
</template>
<template v-else-if="list.isArchived">
<dropdown-item
:to="{ name: `${listRoutePrefix}.archive`, params: { listId: list.id } }"
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
icon="archive"
>
{{ $t('menu.unarchive') }}
@ -24,32 +25,32 @@
</template>
<template v-else>
<dropdown-item
:to="{ name: `${listRoutePrefix}.edit`, params: { listId: list.id } }"
:to="{ name: 'list.settings.edit', params: { listId: list.id } }"
icon="pen"
>
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.background`, params: { listId: list.id } }"
v-if="backgroundsEnabled"
:to="{ name: 'list.settings.background', params: { listId: list.id } }"
icon="image"
>
{{ $t('menu.setBackground') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.share`, params: { listId: list.id } }"
:to="{ name: 'list.settings.share', params: { listId: list.id } }"
icon="share-alt"
>
{{ $t('menu.share') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.duplicate`, params: { listId: list.id } }"
:to="{ name: 'list.settings.duplicate', params: { listId: list.id } }"
icon="paste"
>
{{ $t('menu.duplicate') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.archive`, params: { listId: list.id } }"
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
icon="archive"
>
{{ $t('menu.archive') }}
@ -63,7 +64,7 @@
@change="sub => subscription = sub"
/>
<dropdown-item
:to="{ name: `${listRoutePrefix}.delete`, params: { listId: list.id } }"
:to="{ name: 'list.settings.delete', params: { listId: list.id } }"
icon="trash-alt"
class="has-text-danger"
>
@ -101,24 +102,7 @@ export default {
},
computed: {
backgroundsEnabled() {
return this.$store.state.config.enabledBackgroundProviders !== null && this.$store.state.config.enabledBackgroundProviders.length > 0
},
listRoutePrefix() {
let name = 'list'
if (this.$route.name !== null && this.$route.name.startsWith('list.')) {
// HACK: we should implement a better routing for the modals
const settingsRoutes = ['edit', 'delete', 'archive', 'background', 'share', 'duplicate']
const suffix = settingsRoutes.find((route) => this.$route.name.endsWith(`.settings.${route}`))
name = this.$route.name.replace(`.settings.${suffix}`,'')
}
if (this.isSavedFilter) {
name = name.replace('list.', 'filter.')
}
return `${name}.settings`
return this.$store.state.config.enabledBackgroundProviders?.length > 0
},
isSavedFilter() {
return getSavedFilterIdFromListId(this.list.id) > 0

View File

@ -16,7 +16,7 @@ export const getDefaultParams = () => ({
/**
* This mixin provides a base set of methods and properties to get tasks on a list.
*/
export function createTaskList(initTasks) {
export function useTaskList(initTasks) {
const taskCollectionService = ref(new TaskCollectionService())
dpschen marked this conversation as resolved Outdated

Maybe shallowReactive fits here better?

Maybe shallowReactive fits here better?
const loading = computed(() => taskCollectionService.value.loading)
const totalPages = computed(() => taskCollectionService.value.totalPages)
@ -70,12 +70,14 @@ export function createTaskList(initTasks) {
tasks.value = await taskCollectionService.value.getAll(list, loadParams, page)
currentPage.value = page
loadedList.value = JSON.parse(JSON.stringify(currentList))
return tasks.value
}
async function loadTasksForPage(query) {
const { page, search } = query
initTasks(params)
await loadTasks(
return await loadTasks(
// The page parameter can be undefined, in the case where the user loads a new list from the side bar menu
typeof page === 'undefined' ? 1 : Number(page),
search,

View File

@ -13,8 +13,8 @@ import DataExportDownload from '../views/user/DataExportDownload'
// Tasks
import ShowTasksInRangeComponent from '../views/tasks/ShowTasksInRange'
import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth'
import TaskDetailView from '../views/tasks/TaskDetailView'
import ListNamespaces from '../views/namespaces/ListNamespaces'
import TaskDetailViewModal from '../views/tasks/TaskDetailViewModal'
// Team Handling
import ListTeamsComponent from '../views/teams/ListTeams'
// Label Handling
@ -29,6 +29,7 @@ import Kanban from '../views/list/views/Kanban'
import List from '../views/list/views/List'
import Gantt from '../views/list/views/Gantt'
import Table from '../views/list/views/Table'
// List Settings
import ListSettingEdit from '../views/list/settings/edit'
import ListSettingBackground from '../views/list/settings/background'
@ -200,109 +201,123 @@ const router = createRouter({
{
path: '/namespaces/new',
name: 'namespace.create',
components: {
popup: NewNamespaceComponent,
},
},
{
path: '/namespaces/:id/list',
name: 'list.create',
components: {
popup: NewListComponent,
component: NewNamespaceComponent,
meta: {
showAsModal: true,
},
},
{
path: '/namespaces/:id/settings/edit',
name: 'namespace.settings.edit',
components: {
popup: NamespaceSettingEdit,
component: NamespaceSettingEdit,
meta: {
showAsModal: true,
},
},
{
path: '/namespaces/:id/settings/share',
name: 'namespace.settings.share',
components: {
popup: NamespaceSettingShare,
component: NamespaceSettingShare,
meta: {
showAsModal: true,
},
},
{
path: '/namespaces/:id/settings/archive',
name: 'namespace.settings.archive',
components: {
popup: NamespaceSettingArchive,
component: NamespaceSettingArchive,
meta: {
showAsModal: true,
},
},
{
path: '/namespaces/:id/settings/delete',
name: 'namespace.settings.delete',
components: {
popup: NamespaceSettingDelete,
component: NamespaceSettingDelete,
meta: {
showAsModal: true,
},
},
{
path: '/tasks/:id',
name: 'task.detail',
component: TaskDetailView,
component: TaskDetailViewModal,
},
{
path: '/tasks/by/upcoming',
name: 'tasks.range',
component: ShowTasksInRangeComponent,
},
{
path: '/lists/:id/new',
name: 'list.create',
component: NewListComponent,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/edit',
name: 'list.settings.edit',
components: {
popup: ListSettingEdit,
component: ListSettingEdit,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/background',
name: 'list.settings.background',
components: {
popup: ListSettingBackground,
component: ListSettingBackground,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/duplicate',
name: 'list.settings.duplicate',
components: {
popup: ListSettingDuplicate,
component: ListSettingDuplicate,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/share',
name: 'list.settings.share',
components: {
popup: ListSettingShare,
component: ListSettingShare,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/delete',
name: 'list.settings.delete',
components: {
popup: ListSettingDelete,
component: ListSettingDelete,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/archive',
name: 'list.settings.archive',
components: {
popup: ListSettingArchive,
component: ListSettingArchive,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/edit',
name: 'filter.settings.edit',
components: {
popup: FilterEdit,
component: FilterEdit,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/delete',
name: 'filter.settings.delete',
components: {
popup: FilterDelete,
component: FilterDelete,
meta: {
showAsModal: true,
},
},
{
@ -340,8 +355,9 @@ const router = createRouter({
{
path: '/teams/new',
name: 'teams.create',
components: {
popup: NewTeamComponent,
component: NewTeamComponent,
meta: {
showAsModal: true,
},
},
{
@ -357,8 +373,9 @@ const router = createRouter({
{
path: '/labels/new',
name: 'labels.create',
components: {
popup: NewLabelComponent,
component: NewLabelComponent,
meta: {
showAsModal: true,
},
},
{
@ -374,8 +391,9 @@ const router = createRouter({
{
path: '/filters/new',
name: 'filters.create',
components: {
popup: FilterNew,
component: FilterNew,
meta: {
showAsModal: true,
},
},
{

View File

@ -1,12 +1,12 @@
import {HTTPFactory} from '@/http-common'
import {getCurrentLanguage, saveLanguage} from '@/i18n'
import {i18n, getCurrentLanguage, saveLanguage} from '@/i18n'
import {LOADING} from '../mutation-types'
import UserModel from '@/models/user'
import UserSettingsService from '@/services/userSettings'
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
import {setLoading} from '@/store/helper'
import {i18n} from '@/i18n'
import {success} from '@/message'
import {redirectToProvider} from '@/helpers/redirectToProvider'
const AUTH_TYPES = {
'UNKNOWN': 0,
@ -201,7 +201,19 @@ export default {
ctx.commit('authenticated', authenticated)
if (!authenticated) {
ctx.commit('info', null)
ctx.dispatch('config/redirectToProviderIfNothingElseIsEnabled', null, {root: true})
ctx.dispatch('redirectToProviderIfNothingElseIsEnabled')
}
},
redirectToProviderIfNothingElseIsEnabled({rootState}) {
const {auth} = rootState.config
if (
auth.local.enabled === false &&
auth.openidConnect.enabled &&
auth.openidConnect.providers?.length === 1 &&
window.location.pathname.startsWith('/login') // Kinda hacky, but prevents an endless loop.
) {
redirectToProvider(auth.openidConnect.providers[0], auth.openidConnect.redirectUrl)
}
},

View File

@ -1,7 +1,6 @@
import {CONFIG} from '../mutation-types'
import {HTTPFactory} from '@/http-common'
import {objectToCamelCase} from '@/helpers/case'
import {redirectToProvider} from '../../helpers/redirectToProvider'
import {parseURL} from 'ufo'
export default {
@ -75,16 +74,5 @@ export default {
ctx.commit(CONFIG, info)
return info
},
redirectToProviderIfNothingElseIsEnabled(ctx) {
if (ctx.state.auth.local.enabled === false &&
ctx.state.auth.openidConnect.enabled &&
ctx.state.auth.openidConnect.providers &&
ctx.state.auth.openidConnect.providers.length === 1 &&
window.location.pathname.startsWith('/login') // Kinda hacky, but prevents an endless loop.
) {
redirectToProvider(ctx.state.auth.openidConnect.providers[0])
}
},
},
}

View File

@ -51,10 +51,6 @@
</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>
@ -71,9 +67,6 @@ 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

@ -9,153 +9,128 @@
v-shortcut="'g l'"
:title="$t('keyboardShortcuts.list.switchToListView')"
:class="{'is-active': $route.name === 'list.list'}"
:to="{ name: 'list.list', params: { listId: listId } }">
:to="{ name: 'list.list', params: { listId } }">
{{ $t('list.list.title') }}
</router-link>
<router-link
v-shortcut="'g g'"
:title="$t('keyboardShortcuts.list.switchToGanttView')"
:class="{'is-active': $route.name === 'list.gantt'}"
:to="{ name: 'list.gantt', params: { listId: listId } }">
:to="{ name: 'list.gantt', params: { listId } }">
{{ $t('list.gantt.title') }}
</router-link>
<router-link
v-shortcut="'g t'"
:title="$t('keyboardShortcuts.list.switchToTableView')"
:class="{'is-active': $route.name === 'list.table'}"
:to="{ name: 'list.table', params: { listId: listId } }">
:to="{ name: 'list.table', params: { listId } }">
{{ $t('list.table.title') }}
</router-link>
<router-link
v-shortcut="'g k'"
:title="$t('keyboardShortcuts.list.switchToKanbanView')"
:class="{'is-active': $route.name === 'list.kanban'}"
:to="{ name: 'list.kanban', params: { listId: listId } }">
:to="{ name: 'list.kanban', params: { listId } }">
{{ $t('list.kanban.title') }}
</router-link>
</div>
</div>
<transition name="fade">
<message variant="warning" v-if="currentList.isArchived" class="mb-4">
<Message variant="warning" v-if="currentList.isArchived" class="mb-4">
{{ $t('list.archived') }}
</message>
</Message>
</transition>
<router-view/>
</div>
</template>
<script>
<script setup>
import {ref, shallowRef, computed, watchEffect} from 'vue'
import {useRouter, useRoute} from 'vue-router'
import Message from '@/components/misc/message'
import ListModel from '../../models/list'
import ListService from '../../services/list'
import {CURRENT_LIST} from '../../store/mutation-types'
import {getListView} from '../../helpers/saveListView'
import {saveListToHistory} from '../../modules/listHistory'
export default {
components: {Message},
data() {
return {
listService: new ListService(),
listLoaded: 0,
}
},
watch: {
// call again the method if the route changes
'$route.path': {
handler: 'loadList',
immediate: true,
},
},
computed: {
// Computed property to let "listId" always have a value
listId() {
return typeof this.$route.params.listId === 'undefined' ? 0 : this.$route.params.listId
},
background() {
return this.$store.state.background
},
currentList() {
return typeof this.$store.state.currentList === 'undefined' ? {
id: 0,
title: '',
isArchived: false,
} : this.$store.state.currentList
},
},
methods: {
replaceListView() {
const savedListView = getListView(this.$route.params.listId)
this.$router.replace({name: savedListView, params: {id: this.$route.params.listId}})
console.debug('Replaced list view with', savedListView)
},
import ListModel from '@/models/list'
import ListService from '@/services/list'
async loadList() {
if (this.$route.name.includes('.settings.')) {
return
}
import {store} from '@/store'
import {CURRENT_LIST} from '@/store/mutation-types'
const listData = {id: parseInt(this.$route.params.listId)}
import {getListView} from '@/helpers/saveListView'
import {getListTitle} from '@/helpers/getListTitle'
import {saveListToHistory} from '@/modules/listHistory'
import { useTitle } from '@/composables/useTitle'
saveListToHistory(listData)
const route = useRoute()
const router = useRouter()
this.setTitle(this.currentList.id ? this.getListTitle(this.currentList) : '')
const listService = shallowRef(new ListService())
const loadedListId = ref(0)
// This invalidates the loaded list at the kanban board which lets it reload its content when
// switched to it. This ensures updates done to tasks in the gantt or list views are consistently
// shown in all views while preventing reloads when closing a task popup.
// We don't do this for the table view because that does not change tasks.
if (
this.$route.name === 'list.list' ||
this.$route.name === 'list.gantt'
) {
this.$store.commit('kanban/setListId', 0)
}
// beforeRouteEnter(to) {
// Redirect the user to list view by default
if (route.name !== 'list.index') {
const savedListView = getListView(route.params.listId)
console.debug('Replaced list view with', savedListView)
router.replace({name: 'list.list', params: {id: route.params.listId}})
}
// },
// // 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()
// }
const currentList = computed(() => {
return typeof store.state.currentList === 'undefined' ? {
id: 0,
title: '',
isArchived: false,
} : store.state.currentList
})
// 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.
if (
(
this.$route.params.listId === this.listLoaded ||
typeof this.$route.params.listId === 'undefined' ||
this.$route.params.listId === this.currentList.id ||
parseInt(this.$route.params.listId) === this.currentList.id
)
&& typeof this.currentList !== 'undefined' && this.currentList.maxRight !== null
) {
return
}
// Computed property to let "listId" always have a value
const listId = computed(() => typeof route.params.listId === 'undefined' ? 0 : parseInt(route.params.listId))
// call again the method if the listId changes
watchEffect(() => loadList(listId.value))

Remove this

Remove this
// Redirect the user to list view by default
if (
this.$route.name !== 'list.list' &&
this.$route.name !== 'list.gantt' &&
this.$route.name !== 'list.table' &&
this.$route.name !== 'list.kanban'
) {
return this.replaceListView()
}
useTitle(() => currentList.value.id ? getListTitle(currentList.value) : '')
console.debug(`Loading list, $route.name = ${this.$route.name}, $route.params =`, this.$route.params, `, listLoaded = ${this.listLoaded}, currentList = `, this.currentList)
async function loadList(listId) {
const listData = {id: listId}
saveListToHistory(listData)
// We create an extra list object instead of creating it in this.list because that would trigger a ui update which would result in bad ux.
const list = new ListModel(listData)
try {
const loadedList = await this.listService.get(list)
await this.$store.dispatch(CURRENT_LIST, loadedList)
this.setTitle(this.getListTitle(loadedList))
} finally {
this.listLoaded = this.$route.params.listId
}
},
},
// This invalidates the loaded list at the kanban board which lets it reload its content when
// switched to it. This ensures updates done to tasks in the gantt or list views are consistently
// shown in all views while preventing reloads when closing a task popup.
// We don't do this for the table view because that does not change tasks.
// FIXME: remove this
if (
route.name === 'list.list' ||
route.name === 'list.gantt'
) {
store.commit('kanban/setListId', 0)
}
// 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.
if (
(
listId.value === loadedListId.value ||
typeof listId.value === 'undefined' ||
listId.value === currentList.value.id
)
&& typeof currentList.value !== 'undefined' && currentList.value.maxRight !== null
) {
return
}
console.debug(`Loading list, $route.name = ${route.name}, $route.params =`, route.params, `, loadedListId = ${loadedListId.value}, currentList = `, currentList.value)
// We create an extra list object instead of creating it in list.value because that would trigger a ui update which would result in bad ux.
const list = new ListModel(listData)
try {
const loadedList = await listService.value.get(list)
await store.dispatch(CURRENT_LIST, loadedList)
} finally {
loadedListId.value = listId
}
}
</script>

View File

@ -52,61 +52,43 @@
:show-taskswithout-dates="showTaskswithoutDates"
/>
<transition name="modal">
<task-detail-view-modal v-if="showTaskDetail" />
</transition>
</card>
</div>
</template>
<script>
import GanttChart from '../../../components/tasks/gantt-component'
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import flatPickr from 'vue-flatpickr-component'
import Fancycheckbox from '../../../components/input/fancycheckbox'
import { i18n } from '@/i18n'
import { store } from '@/store'
import GanttChart from '@/components/tasks/gantt-component'
import Fancycheckbox from '@/components/input/fancycheckbox'
import {saveListView} from '@/helpers/saveListView'
import TaskDetailViewModal, { useShowModal } from '@/views/tasks/TaskDetailViewModal.vue'
const route = useRoute()
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.
saveListView(route.params.listId, route.name)
export default {
name: 'Gantt',
components: {
Fancycheckbox,
flatPickr,
GanttChart,
TaskDetailViewModal,
const showTaskswithoutDates = ref(false)
const dayWidth = ref(35)
const dateFrom = ref(new Date((new Date()).setDate((new Date()).getDate() - 15)))
const dateTo = ref(new Date((new Date()).setDate((new Date()).getDate() + 30)))
const flatPickerConfig = computed(() => ({
altFormat: i18n.global.t('date.altFormatShort'),
altInput: true,
dateFormat: 'Y-m-d',
enableTime: false,
locale: {
firstDayOfWeek: store.state.auth.settings.weekStart,
},
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.
saveListView(this.$route.params.listId, this.$route.name)
},
data() {
return {
showTaskswithoutDates: false,
dayWidth: 35,
dateFrom: new Date((new Date()).setDate((new Date()).getDate() - 15)),
dateTo: new Date((new Date()).setDate((new Date()).getDate() + 30)),
}
},
computed: {
flatPickerConfig() {
return {
altFormat: this.$t('date.altFormatShort'),
altInput: true,
dateFormat: 'Y-m-d',
enableTime: false,
locale: {
firstDayOfWeek: this.$store.state.auth.settings.weekStart,
},
}
},
},
}
}))
</script>
<style lang="scss">

View File

@ -205,9 +205,8 @@
</div>
<transition name="modal">
<task-detail-view-modal v-if="showTaskDetail" />
<modal
v-else-if="showBucketDeleteModal"
v-if="showBucketDeleteModal"
@close="showBucketDeleteModal = false"
@submit="deleteBucket()"
>
@ -236,7 +235,6 @@ 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
@ -256,7 +254,6 @@ export default {
Dropdown,
FilterPopup,
draggable,
TaskDetailViewModal,
},
data() {
return {
@ -293,12 +290,6 @@ 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

@ -122,10 +122,6 @@
:current-page="currentPage"
/>
</card>
<transition name="modal">
<task-detail-view-modal v-if="showTaskDetail" />
</transition>
</div>
</template>
@ -133,13 +129,10 @@
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import TaskService from '../../../services/task'
import TaskModel from '../../../models/task'
import EditTask from '../../../components/tasks/edit-task'
import AddTask from '../../../components/tasks/add-task'
import SingleTaskInList from '../../../components/tasks/partials/singleTaskInList'
import { createTaskList } from '@/composables/taskList'
import { useTaskList } from '@/composables/taskList'
import {saveListView} from '@/helpers/saveListView'
import Rights from '../../../models/constants/rights.json'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
@ -147,7 +140,6 @@ import {HAS_TASKS} from '@/store/mutation-types'
import Nothing from '@/components/misc/nothing.vue'
import Pagination from '@/components/misc/pagination.vue'
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'
@ -174,7 +166,6 @@ export default {
name: 'List',
data() {
return {
taskService: new TaskService(),
ctaVisible: false,
showTaskSearch: false,
@ -193,11 +184,10 @@ export default {
AddTask,
draggable,
Pagination,
TaskDetailViewModal,
},
setup() {
const taskEditTask = ref(TaskModel)
const taskEditTask = ref(null)
const isTaskEdit = ref(false)
// This function initializes the tasks page and loads the first page of tasks
@ -206,17 +196,18 @@ export default {
isTaskEdit.value = false
}
const taskList = createTaskList(beforeLoad)
const taskList = useTaskList(beforeLoad)
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.
const route = useRoute()
saveListView(route.params.listId, route.name)
taskList.initTaskList()
return {
taskEditTask,
isTaskEdit,
showTaskDetail: useShowModal(),
...taskList,
}
},

View File

@ -68,19 +68,19 @@
<tr>
<th v-if="activeColumns.id">
#
<sort :order="sortBy.id" @click="sort('id')"/>
<Sort :order="sortBy.id" @click="sort('id')"/>
</th>
<th v-if="activeColumns.done">
{{ $t('task.attributes.done') }}
<sort :order="sortBy.done" @click="sort('done')"/>
<Sort :order="sortBy.done" @click="sort('done')"/>
</th>
<th v-if="activeColumns.title">
{{ $t('task.attributes.title') }}
<sort :order="sortBy.title" @click="sort('title')"/>
<Sort :order="sortBy.title" @click="sort('title')"/>
</th>
<th v-if="activeColumns.priority">
{{ $t('task.attributes.priority') }}
<sort :order="sortBy.priority" @click="sort('priority')"/>
<Sort :order="sortBy.priority" @click="sort('priority')"/>
</th>
<th v-if="activeColumns.labels">
{{ $t('task.attributes.labels') }}
@ -90,27 +90,27 @@
</th>
<th v-if="activeColumns.dueDate">
{{ $t('task.attributes.dueDate') }}
<sort :order="sortBy.due_date" @click="sort('due_date')"/>
<Sort :order="sortBy.due_date" @click="sort('due_date')"/>
</th>
<th v-if="activeColumns.startDate">
{{ $t('task.attributes.startDate') }}
<sort :order="sortBy.start_date" @click="sort('start_date')"/>
<Sort :order="sortBy.start_date" @click="sort('start_date')"/>
</th>
<th v-if="activeColumns.endDate">
{{ $t('task.attributes.endDate') }}
<sort :order="sortBy.end_date" @click="sort('end_date')"/>
<Sort :order="sortBy.end_date" @click="sort('end_date')"/>
</th>
<th v-if="activeColumns.percentDone">
{{ $t('task.attributes.percentDone') }}
<sort :order="sortBy.percent_done" @click="sort('percent_done')"/>
<Sort :order="sortBy.percent_done" @click="sort('percent_done')"/>
</th>
<th v-if="activeColumns.created">
{{ $t('task.attributes.created') }}
<sort :order="sortBy.created" @click="sort('created')"/>
<Sort :order="sortBy.created" @click="sort('created')"/>
</th>
<th v-if="activeColumns.updated">
{{ $t('task.attributes.updated') }}
<sort :order="sortBy.updated" @click="sort('updated')"/>
<Sort :order="sortBy.updated" @click="sort('updated')"/>
</th>
<th v-if="activeColumns.createdBy">
{{ $t('task.attributes.createdBy') }}
@ -173,22 +173,13 @@
:current-page="currentPage"
/>
</card>
<!-- This router view is used to show the task popup while keeping the table view itself -->
<router-view v-slot="{ Component }">
<transition name="modal">
<component :is="Component" />
</transition>
</router-view>
</div>
</template>
<script>
import { defineComponent, ref, reactive, computed, toRaw } from 'vue'
<script setup>
import { ref, reactive, computed, toRaw } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { createTaskList } from '@/composables/taskList'
import Done from '@/components/misc/Done.vue'
import User from '@/components/misc/user'
import PriorityLabel from '@/components/tasks/partials/priorityLabel'
@ -196,11 +187,13 @@ import Labels from '@/components/tasks/partials/labels'
import DateTableCell from '@/components/tasks/partials/date-table-cell'
import Fancycheckbox from '@/components/input/fancycheckbox'
import Sort from '@/components/tasks/partials/sort'
import {saveListView} from '@/helpers/saveListView'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import Pagination from '@/components/misc/pagination.vue'
import Popup from '@/components/misc/popup'
import { useTaskList } from '@/composables/taskList'
import {saveListView} from '@/helpers/saveListView'
const ACTIVE_COLUMNS_DEFAULT = {
id: true,
done: true,
@ -233,102 +226,86 @@ function useSavedView(activeColumns, sortBy) {
}
}
export default defineComponent({
name: 'Table',
components: {
Popup,
Done,
FilterPopup,
Sort,
Fancycheckbox,
DateTableCell,
Labels,
PriorityLabel,
User,
Pagination,
},
setup() {
const activeColumns = reactive({ ...ACTIVE_COLUMNS_DEFAULT })
const sortBy = ref({ ...SORT_BY_DEFAULT })
const activeColumns = reactive({ ...ACTIVE_COLUMNS_DEFAULT })
const sortBy = ref({ ...SORT_BY_DEFAULT })
useSavedView(activeColumns, sortBy)
useSavedView(activeColumns, sortBy)
function beforeLoad(params) {
// This makes sure an id sort order is always sorted last.
// When tasks would be sorted first by id and then by whatever else was specified, the id sort takes
// precedence over everything else, making any other sort columns pretty useless.
let hasIdFilter = false
const sortKeys = Object.keys(sortBy.value)
for (const s of sortKeys) {
if (s === 'id') {
sortKeys.splice(s, 1)
hasIdFilter = true
break
}
}
if (hasIdFilter) {
sortKeys.push('id')
}
params.value.sort_by = sortKeys
params.value.order_by = sortKeys.map(s => sortBy.value[s])
function beforeLoad(params) {
// This makes sure an id sort order is always sorted last.
// When tasks would be sorted first by id and then by whatever else was specified, the id sort takes
// precedence over everything else, making any other sort columns pretty useless.
let hasIdFilter = false
const sortKeys = Object.keys(sortBy.value)
for (const s of sortKeys) {
if (s === 'id') {
sortKeys.splice(s, 1)
hasIdFilter = true
break
}
}
if (hasIdFilter) {
sortKeys.push('id')
}
params.value.sort_by = sortKeys
params.value.order_by = sortKeys.map(s => sortBy.value[s])
}
const taskList = createTaskList(beforeLoad)
const {
tasks,
loading,
showTaskFilter,
params,
loadTasks,
totalPages,
currentPage,
searchTerm,
initTaskList,
} = useTaskList(beforeLoad)
Object.assign(taskList.params.value, {
filter_by: [],
filter_value: [],
filter_comparator: [],
})
const router = useRouter()
const taskDetailRoutes = computed(() => Object.fromEntries(
taskList.tasks.value.map(({id}) => ([
id,
{
name: 'task.detail',
params: { id },
state: { backgroundView: router.currentRoute.value.fullPath },
},
])),
))
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.
const route = useRoute()
saveListView(route.params.listId, route.name)
function sort(property) {
const order = sortBy.value[property]
if (typeof order === 'undefined' || order === 'none') {
sortBy.value[property] = 'desc'
} else if (order === 'desc') {
sortBy.value[property] = 'asc'
} else {
delete sortBy.value[property]
}
beforeLoad(taskList.currentPage.value, taskList.searchTerm.value)
// Save the order to be able to retrieve them later
localStorage.setItem('tableViewSortBy', JSON.stringify(sortBy.value))
}
function saveTaskColumns() {
localStorage.setItem('tableViewColumns', JSON.stringify(toRaw(activeColumns)))
}
taskList.initTaskList()
return {
...taskList,
sortBy,
activeColumns,
sort,
saveTaskColumns,
taskDetailRoutes,
}
},
Object.assign(params.value, {
filter_by: [],
filter_value: [],
filter_comparator: [],
})
const router = useRouter()
const taskDetailRoutes = computed(() => Object.fromEntries(
tasks.value.map(({id}) => ([
id,
{
name: 'task.detail',
params: { id },
state: { backgroundView: router.currentRoute.value.fullPath },
},
])),
))
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.
const route = useRoute()
saveListView(route.params.listId, route.name)
function sort(property) {
const order = sortBy.value[property]
if (typeof order === 'undefined' || order === 'none') {
sortBy.value[property] = 'desc'
} else if (order === 'desc') {
sortBy.value[property] = 'asc'
} else {
delete sortBy.value[property]
}
beforeLoad(currentPage.value, searchTerm.value)
// Save the order to be able to retrieve them later
localStorage.setItem('tableViewSortBy', JSON.stringify(sortBy.value))
}
function saveTaskColumns() {
localStorage.setItem('tableViewColumns', JSON.stringify(toRaw(activeColumns)))
}
initTaskList()
</script>
<style lang="scss" scoped>

View File

@ -4,35 +4,19 @@
variant="scrolling"
class="task-detail-view-modal"
>
<a @click="close()" class="close">
<icon icon="times"/>
</a>
<task-detail-view/>
<a @click="close()" class="close">
<icon icon="times"/>
</a>
<task-detail-view/>
</modal>
</template>
<script>
<script setup>
import TaskDetailView from './TaskDetailView'
import {computed} from 'vue'
import {useRoute} from 'vue-router'
import router from '@/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,
},
methods: {
close() {
this.$router.back()
},
},
function close() {
router.back()
}
</script>