feat: implement modals with vue router 4 #816
|
@ -22,12 +22,9 @@
|
||||||
|
|
||||||
<router-view :route="routeWithModal"/>
|
<router-view :route="routeWithModal"/>
|
||||||
|
|
||||||
<!-- TODO: is this still used? -->
|
<transition name="modal">
|
||||||
dpschen marked this conversation as resolved
Outdated
|
|||||||
<router-view name="popup" v-slot="{ Component }">
|
<component v-if="currentModal" :is="currentModal" />
|
||||||
<transition name="modal">
|
</transition>
|
||||||
<component :is="Component" />
|
|
||||||
</transition>
|
|
||||||
</router-view>
|
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class="keyboard-shortcuts-button"
|
class="keyboard-shortcuts-button"
|
||||||
|
@ -42,7 +39,7 @@
|
||||||
</template>
|
</template>
|
||||||
dpschen marked this conversation as resolved
Outdated
konrad
commented
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 😅
dpschen
commented
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.
dpschen
commented
I removed the component 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>
|
<script lang="ts" setup>
|
||||||
import {watch, computed} from 'vue'
|
import {watch, computed, shallowRef, watchEffect} from 'vue'
|
||||||
import {useStore} from 'vuex'
|
import {useStore} from 'vuex'
|
||||||
import {useRoute, useRouter} from 'vue-router'
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
import {useEventListener} from '@vueuse/core'
|
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()
|
useRouteWithModal()
|
||||||
|
|
|
@ -2,21 +2,22 @@
|
||||||
<dropdown>
|
<dropdown>
|
||||||
<template v-if="isSavedFilter">
|
<template v-if="isSavedFilter">
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
:to="{ name: `${listRoutePrefix}.edit`, params: { listId: list.id } }"
|
:to="{ name: 'filter.settings.edit', params: { listId: list.id } }"
|
||||||
icon="pen"
|
icon="pen"
|
||||||
>
|
>
|
||||||
{{ $t('menu.edit') }}
|
{{ $t('menu.edit') }}
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
:to="{ name: `${listRoutePrefix}.delete`, params: { listId: list.id } }"
|
:to="{ name: 'filter.settings.delete', params: { listId: list.id } }"
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
>
|
>
|
||||||
{{ $t('misc.delete') }}
|
{{ $t('misc.delete') }}
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="list.isArchived">
|
<template v-else-if="list.isArchived">
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
:to="{ name: `${listRoutePrefix}.archive`, params: { listId: list.id } }"
|
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
|
||||||
icon="archive"
|
icon="archive"
|
||||||
>
|
>
|
||||||
{{ $t('menu.unarchive') }}
|
{{ $t('menu.unarchive') }}
|
||||||
|
@ -24,32 +25,32 @@
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
:to="{ name: `${listRoutePrefix}.edit`, params: { listId: list.id } }"
|
:to="{ name: 'list.settings.edit', params: { listId: list.id } }"
|
||||||
icon="pen"
|
icon="pen"
|
||||||
>
|
>
|
||||||
{{ $t('menu.edit') }}
|
{{ $t('menu.edit') }}
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
:to="{ name: `${listRoutePrefix}.background`, params: { listId: list.id } }"
|
|
||||||
v-if="backgroundsEnabled"
|
v-if="backgroundsEnabled"
|
||||||
|
:to="{ name: 'list.settings.background', params: { listId: list.id } }"
|
||||||
icon="image"
|
icon="image"
|
||||||
>
|
>
|
||||||
{{ $t('menu.setBackground') }}
|
{{ $t('menu.setBackground') }}
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
:to="{ name: `${listRoutePrefix}.share`, params: { listId: list.id } }"
|
:to="{ name: 'list.settings.share', params: { listId: list.id } }"
|
||||||
icon="share-alt"
|
icon="share-alt"
|
||||||
>
|
>
|
||||||
{{ $t('menu.share') }}
|
{{ $t('menu.share') }}
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
:to="{ name: `${listRoutePrefix}.duplicate`, params: { listId: list.id } }"
|
:to="{ name: 'list.settings.duplicate', params: { listId: list.id } }"
|
||||||
icon="paste"
|
icon="paste"
|
||||||
>
|
>
|
||||||
{{ $t('menu.duplicate') }}
|
{{ $t('menu.duplicate') }}
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
:to="{ name: `${listRoutePrefix}.archive`, params: { listId: list.id } }"
|
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
|
||||||
icon="archive"
|
icon="archive"
|
||||||
>
|
>
|
||||||
{{ $t('menu.archive') }}
|
{{ $t('menu.archive') }}
|
||||||
|
@ -63,7 +64,7 @@
|
||||||
@change="sub => subscription = sub"
|
@change="sub => subscription = sub"
|
||||||
/>
|
/>
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
:to="{ name: `${listRoutePrefix}.delete`, params: { listId: list.id } }"
|
:to="{ name: 'list.settings.delete', params: { listId: list.id } }"
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
class="has-text-danger"
|
class="has-text-danger"
|
||||||
>
|
>
|
||||||
|
@ -101,24 +102,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
backgroundsEnabled() {
|
backgroundsEnabled() {
|
||||||
return this.$store.state.config.enabledBackgroundProviders !== null && this.$store.state.config.enabledBackgroundProviders.length > 0
|
return 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`
|
|
||||||
},
|
},
|
||||||
isSavedFilter() {
|
isSavedFilter() {
|
||||||
return getSavedFilterIdFromListId(this.list.id) > 0
|
return getSavedFilterIdFromListId(this.list.id) > 0
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const getDefaultParams = () => ({
|
||||||
/**
|
/**
|
||||||
* This mixin provides a base set of methods and properties to get tasks on a list.
|
* 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())
|
const taskCollectionService = ref(new TaskCollectionService())
|
||||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
Maybe shallowReactive fits here better? Maybe shallowReactive fits here better?
|
|||||||
const loading = computed(() => taskCollectionService.value.loading)
|
const loading = computed(() => taskCollectionService.value.loading)
|
||||||
const totalPages = computed(() => taskCollectionService.value.totalPages)
|
const totalPages = computed(() => taskCollectionService.value.totalPages)
|
||||||
|
@ -70,12 +70,14 @@ export function createTaskList(initTasks) {
|
||||||
tasks.value = await taskCollectionService.value.getAll(list, loadParams, page)
|
tasks.value = await taskCollectionService.value.getAll(list, loadParams, page)
|
||||||
currentPage.value = page
|
currentPage.value = page
|
||||||
loadedList.value = JSON.parse(JSON.stringify(currentList))
|
loadedList.value = JSON.parse(JSON.stringify(currentList))
|
||||||
|
|
||||||
|
return tasks.value
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTasksForPage(query) {
|
async function loadTasksForPage(query) {
|
||||||
const { page, search } = query
|
const { page, search } = query
|
||||||
initTasks(params)
|
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
|
// 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),
|
typeof page === 'undefined' ? 1 : Number(page),
|
||||||
search,
|
search,
|
||||||
|
|
|
@ -13,8 +13,8 @@ import DataExportDownload from '../views/user/DataExportDownload'
|
||||||
// Tasks
|
// Tasks
|
||||||
import ShowTasksInRangeComponent from '../views/tasks/ShowTasksInRange'
|
import ShowTasksInRangeComponent from '../views/tasks/ShowTasksInRange'
|
||||||
import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth'
|
import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth'
|
||||||
import TaskDetailView from '../views/tasks/TaskDetailView'
|
|
||||||
import ListNamespaces from '../views/namespaces/ListNamespaces'
|
import ListNamespaces from '../views/namespaces/ListNamespaces'
|
||||||
|
import TaskDetailViewModal from '../views/tasks/TaskDetailViewModal'
|
||||||
// Team Handling
|
// Team Handling
|
||||||
import ListTeamsComponent from '../views/teams/ListTeams'
|
import ListTeamsComponent from '../views/teams/ListTeams'
|
||||||
// Label Handling
|
// Label Handling
|
||||||
|
@ -29,6 +29,7 @@ import Kanban from '../views/list/views/Kanban'
|
||||||
import List from '../views/list/views/List'
|
import List from '../views/list/views/List'
|
||||||
import Gantt from '../views/list/views/Gantt'
|
import Gantt from '../views/list/views/Gantt'
|
||||||
import Table from '../views/list/views/Table'
|
import Table from '../views/list/views/Table'
|
||||||
|
|
||||||
// List Settings
|
// List Settings
|
||||||
import ListSettingEdit from '../views/list/settings/edit'
|
import ListSettingEdit from '../views/list/settings/edit'
|
||||||
import ListSettingBackground from '../views/list/settings/background'
|
import ListSettingBackground from '../views/list/settings/background'
|
||||||
|
@ -200,109 +201,123 @@ const router = createRouter({
|
||||||
{
|
{
|
||||||
path: '/namespaces/new',
|
path: '/namespaces/new',
|
||||||
name: 'namespace.create',
|
name: 'namespace.create',
|
||||||
components: {
|
component: NewNamespaceComponent,
|
||||||
popup: NewNamespaceComponent,
|
meta: {
|
||||||
},
|
showAsModal: true,
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/namespaces/:id/list',
|
|
||||||
name: 'list.create',
|
|
||||||
components: {
|
|
||||||
popup: NewListComponent,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/namespaces/:id/settings/edit',
|
path: '/namespaces/:id/settings/edit',
|
||||||
name: 'namespace.settings.edit',
|
name: 'namespace.settings.edit',
|
||||||
components: {
|
component: NamespaceSettingEdit,
|
||||||
popup: NamespaceSettingEdit,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/namespaces/:id/settings/share',
|
path: '/namespaces/:id/settings/share',
|
||||||
name: 'namespace.settings.share',
|
name: 'namespace.settings.share',
|
||||||
components: {
|
component: NamespaceSettingShare,
|
||||||
popup: NamespaceSettingShare,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/namespaces/:id/settings/archive',
|
path: '/namespaces/:id/settings/archive',
|
||||||
name: 'namespace.settings.archive',
|
name: 'namespace.settings.archive',
|
||||||
components: {
|
component: NamespaceSettingArchive,
|
||||||
popup: NamespaceSettingArchive,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/namespaces/:id/settings/delete',
|
path: '/namespaces/:id/settings/delete',
|
||||||
name: 'namespace.settings.delete',
|
name: 'namespace.settings.delete',
|
||||||
components: {
|
component: NamespaceSettingDelete,
|
||||||
popup: NamespaceSettingDelete,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tasks/:id',
|
path: '/tasks/:id',
|
||||||
name: 'task.detail',
|
name: 'task.detail',
|
||||||
component: TaskDetailView,
|
component: TaskDetailViewModal,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tasks/by/upcoming',
|
path: '/tasks/by/upcoming',
|
||||||
name: 'tasks.range',
|
name: 'tasks.range',
|
||||||
component: ShowTasksInRangeComponent,
|
component: ShowTasksInRangeComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/lists/:id/new',
|
||||||
|
name: 'list.create',
|
||||||
|
component: NewListComponent,
|
||||||
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/edit',
|
path: '/lists/:listId/settings/edit',
|
||||||
name: 'list.settings.edit',
|
name: 'list.settings.edit',
|
||||||
components: {
|
component: ListSettingEdit,
|
||||||
popup: ListSettingEdit,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/background',
|
path: '/lists/:listId/settings/background',
|
||||||
name: 'list.settings.background',
|
name: 'list.settings.background',
|
||||||
components: {
|
component: ListSettingBackground,
|
||||||
popup: ListSettingBackground,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/duplicate',
|
path: '/lists/:listId/settings/duplicate',
|
||||||
name: 'list.settings.duplicate',
|
name: 'list.settings.duplicate',
|
||||||
components: {
|
component: ListSettingDuplicate,
|
||||||
popup: ListSettingDuplicate,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/share',
|
path: '/lists/:listId/settings/share',
|
||||||
name: 'list.settings.share',
|
name: 'list.settings.share',
|
||||||
components: {
|
component: ListSettingShare,
|
||||||
popup: ListSettingShare,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/delete',
|
path: '/lists/:listId/settings/delete',
|
||||||
name: 'list.settings.delete',
|
name: 'list.settings.delete',
|
||||||
components: {
|
component: ListSettingDelete,
|
||||||
popup: ListSettingDelete,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/archive',
|
path: '/lists/:listId/settings/archive',
|
||||||
name: 'list.settings.archive',
|
name: 'list.settings.archive',
|
||||||
components: {
|
component: ListSettingArchive,
|
||||||
popup: ListSettingArchive,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/edit',
|
path: '/lists/:listId/settings/edit',
|
||||||
name: 'filter.settings.edit',
|
name: 'filter.settings.edit',
|
||||||
components: {
|
component: FilterEdit,
|
||||||
popup: FilterEdit,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/delete',
|
path: '/lists/:listId/settings/delete',
|
||||||
name: 'filter.settings.delete',
|
name: 'filter.settings.delete',
|
||||||
components: {
|
component: FilterDelete,
|
||||||
popup: FilterDelete,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -340,8 +355,9 @@ const router = createRouter({
|
||||||
{
|
{
|
||||||
path: '/teams/new',
|
path: '/teams/new',
|
||||||
name: 'teams.create',
|
name: 'teams.create',
|
||||||
components: {
|
component: NewTeamComponent,
|
||||||
popup: NewTeamComponent,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -357,8 +373,9 @@ const router = createRouter({
|
||||||
{
|
{
|
||||||
path: '/labels/new',
|
path: '/labels/new',
|
||||||
name: 'labels.create',
|
name: 'labels.create',
|
||||||
components: {
|
component: NewLabelComponent,
|
||||||
popup: NewLabelComponent,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -374,8 +391,9 @@ const router = createRouter({
|
||||||
{
|
{
|
||||||
path: '/filters/new',
|
path: '/filters/new',
|
||||||
name: 'filters.create',
|
name: 'filters.create',
|
||||||
components: {
|
component: FilterNew,
|
||||||
popup: FilterNew,
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import {HTTPFactory} from '@/http-common'
|
import {HTTPFactory} from '@/http-common'
|
||||||
import {getCurrentLanguage, saveLanguage} from '@/i18n'
|
import {i18n, getCurrentLanguage, saveLanguage} from '@/i18n'
|
||||||
import {LOADING} from '../mutation-types'
|
import {LOADING} from '../mutation-types'
|
||||||
import UserModel from '@/models/user'
|
import UserModel from '@/models/user'
|
||||||
import UserSettingsService from '@/services/userSettings'
|
import UserSettingsService from '@/services/userSettings'
|
||||||
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
|
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
|
||||||
import {setLoading} from '@/store/helper'
|
import {setLoading} from '@/store/helper'
|
||||||
import {i18n} from '@/i18n'
|
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
|
import {redirectToProvider} from '@/helpers/redirectToProvider'
|
||||||
|
|
||||||
const AUTH_TYPES = {
|
const AUTH_TYPES = {
|
||||||
'UNKNOWN': 0,
|
'UNKNOWN': 0,
|
||||||
|
@ -201,7 +201,19 @@ export default {
|
||||||
ctx.commit('authenticated', authenticated)
|
ctx.commit('authenticated', authenticated)
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
ctx.commit('info', null)
|
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)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {CONFIG} from '../mutation-types'
|
import {CONFIG} from '../mutation-types'
|
||||||
import {HTTPFactory} from '@/http-common'
|
import {HTTPFactory} from '@/http-common'
|
||||||
import {objectToCamelCase} from '@/helpers/case'
|
import {objectToCamelCase} from '@/helpers/case'
|
||||||
import {redirectToProvider} from '../../helpers/redirectToProvider'
|
|
||||||
import {parseURL} from 'ufo'
|
import {parseURL} from 'ufo'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -75,16 +74,5 @@ export default {
|
||||||
ctx.commit(CONFIG, info)
|
ctx.commit(CONFIG, info)
|
||||||
return 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])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
|
@ -51,10 +51,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ShowTasks class="mt-4" :show-all="true" v-if="hasLists" :key="showTasksKey"/>
|
<ShowTasks class="mt-4" :show-all="true" v-if="hasLists" :key="showTasksKey"/>
|
||||||
|
|
||||||
<transition name="modal">
|
|
||||||
<task-detail-view-modal v-if="showTaskDetail" />
|
|
||||||
</transition>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -71,9 +67,6 @@ import {getHistory} from '@/modules/listHistory'
|
||||||
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
||||||
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
|
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
|
||||||
import {useDateTimeSalutation} from '@/composables/useDateTimeSalutation'
|
import {useDateTimeSalutation} from '@/composables/useDateTimeSalutation'
|
||||||
import TaskDetailViewModal, { useShowModal } from '@/views/tasks/TaskDetailViewModal.vue'
|
|
||||||
|
|
||||||
const showTaskDetail = useShowModal()
|
|
||||||
|
|
||||||
const welcome = useDateTimeSalutation()
|
const welcome = useDateTimeSalutation()
|
||||||
|
|
||||||
|
|
|
@ -9,153 +9,128 @@
|
||||||
v-shortcut="'g l'"
|
v-shortcut="'g l'"
|
||||||
:title="$t('keyboardShortcuts.list.switchToListView')"
|
:title="$t('keyboardShortcuts.list.switchToListView')"
|
||||||
:class="{'is-active': $route.name === 'list.list'}"
|
:class="{'is-active': $route.name === 'list.list'}"
|
||||||
:to="{ name: 'list.list', params: { listId: listId } }">
|
:to="{ name: 'list.list', params: { listId } }">
|
||||||
{{ $t('list.list.title') }}
|
{{ $t('list.list.title') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-shortcut="'g g'"
|
v-shortcut="'g g'"
|
||||||
:title="$t('keyboardShortcuts.list.switchToGanttView')"
|
:title="$t('keyboardShortcuts.list.switchToGanttView')"
|
||||||
:class="{'is-active': $route.name === 'list.gantt'}"
|
:class="{'is-active': $route.name === 'list.gantt'}"
|
||||||
:to="{ name: 'list.gantt', params: { listId: listId } }">
|
:to="{ name: 'list.gantt', params: { listId } }">
|
||||||
{{ $t('list.gantt.title') }}
|
{{ $t('list.gantt.title') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-shortcut="'g t'"
|
v-shortcut="'g t'"
|
||||||
:title="$t('keyboardShortcuts.list.switchToTableView')"
|
:title="$t('keyboardShortcuts.list.switchToTableView')"
|
||||||
:class="{'is-active': $route.name === 'list.table'}"
|
:class="{'is-active': $route.name === 'list.table'}"
|
||||||
:to="{ name: 'list.table', params: { listId: listId } }">
|
:to="{ name: 'list.table', params: { listId } }">
|
||||||
{{ $t('list.table.title') }}
|
{{ $t('list.table.title') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-shortcut="'g k'"
|
v-shortcut="'g k'"
|
||||||
:title="$t('keyboardShortcuts.list.switchToKanbanView')"
|
:title="$t('keyboardShortcuts.list.switchToKanbanView')"
|
||||||
:class="{'is-active': $route.name === 'list.kanban'}"
|
:class="{'is-active': $route.name === 'list.kanban'}"
|
||||||
:to="{ name: 'list.kanban', params: { listId: listId } }">
|
:to="{ name: 'list.kanban', params: { listId } }">
|
||||||
{{ $t('list.kanban.title') }}
|
{{ $t('list.kanban.title') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<transition name="fade">
|
<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') }}
|
{{ $t('list.archived') }}
|
||||||
</message>
|
</Message>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import {ref, shallowRef, computed, watchEffect} from 'vue'
|
||||||
|
import {useRouter, useRoute} from 'vue-router'
|
||||||
|
|
||||||
import Message from '@/components/misc/message'
|
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 {
|
import ListModel from '@/models/list'
|
||||||
components: {Message},
|
import ListService from '@/services/list'
|
||||||
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)
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadList() {
|
import {store} from '@/store'
|
||||||
if (this.$route.name.includes('.settings.')) {
|
import {CURRENT_LIST} from '@/store/mutation-types'
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
// beforeRouteEnter(to) {
|
||||||
// switched to it. This ensures updates done to tasks in the gantt or list views are consistently
|
// Redirect the user to list view by default
|
||||||
// shown in all views while preventing reloads when closing a task popup.
|
if (route.name !== 'list.index') {
|
||||||
// We don't do this for the table view because that does not change tasks.
|
const savedListView = getListView(route.params.listId)
|
||||||
if (
|
console.debug('Replaced list view with', savedListView)
|
||||||
this.$route.name === 'list.list' ||
|
router.replace({name: 'list.list', params: {id: route.params.listId}})
|
||||||
this.$route.name === 'list.gantt'
|
}
|
||||||
) {
|
// },
|
||||||
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
|
const currentList = computed(() => {
|
||||||
// // at all. Users will then have to click on the list view menu again which is quite confusing.
|
return typeof store.state.currentList === 'undefined' ? {
|
||||||
// if (this.$route.name === 'list.index') {
|
id: 0,
|
||||||
// return this.replaceListView()
|
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
|
// Computed property to let "listId" always have a value
|
||||||
// the currently loaded list has the right set.
|
const listId = computed(() => typeof route.params.listId === 'undefined' ? 0 : parseInt(route.params.listId))
|
||||||
if (
|
// call again the method if the listId changes
|
||||||
(
|
watchEffect(() => loadList(listId.value))
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
dpschen
commented
Remove this Remove this
|
|||||||
// Redirect the user to list view by default
|
useTitle(() => currentList.value.id ? getListTitle(currentList.value) : '')
|
||||||
if (
|
|
||||||
this.$route.name !== 'list.list' &&
|
|
||||||
this.$route.name !== 'list.gantt' &&
|
|
||||||
this.$route.name !== 'list.table' &&
|
|
||||||
this.$route.name !== 'list.kanban'
|
|
||||||
) {
|
|
||||||
return this.replaceListView()
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
// This invalidates the loaded list at the kanban board which lets it reload its content when
|
||||||
const list = new ListModel(listData)
|
// switched to it. This ensures updates done to tasks in the gantt or list views are consistently
|
||||||
try {
|
// shown in all views while preventing reloads when closing a task popup.
|
||||||
const loadedList = await this.listService.get(list)
|
// We don't do this for the table view because that does not change tasks.
|
||||||
await this.$store.dispatch(CURRENT_LIST, loadedList)
|
// FIXME: remove this
|
||||||
this.setTitle(this.getListTitle(loadedList))
|
if (
|
||||||
} finally {
|
route.name === 'list.list' ||
|
||||||
this.listLoaded = this.$route.params.listId
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -52,61 +52,43 @@
|
||||||
:show-taskswithout-dates="showTaskswithoutDates"
|
:show-taskswithout-dates="showTaskswithoutDates"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<transition name="modal">
|
|
||||||
<task-detail-view-modal v-if="showTaskDetail" />
|
|
||||||
</transition>
|
|
||||||
</card>
|
</card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import GanttChart from '../../../components/tasks/gantt-component'
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import flatPickr from 'vue-flatpickr-component'
|
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 {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 {
|
const showTaskswithoutDates = ref(false)
|
||||||
name: 'Gantt',
|
const dayWidth = ref(35)
|
||||||
components: {
|
const dateFrom = ref(new Date((new Date()).setDate((new Date()).getDate() - 15)))
|
||||||
Fancycheckbox,
|
const dateTo = ref(new Date((new Date()).setDate((new Date()).getDate() + 30)))
|
||||||
flatPickr,
|
|
||||||
GanttChart,
|
|
||||||
TaskDetailViewModal,
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -205,9 +205,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition name="modal">
|
<transition name="modal">
|
||||||
<task-detail-view-modal v-if="showTaskDetail" />
|
|
||||||
<modal
|
<modal
|
||||||
v-else-if="showBucketDeleteModal"
|
v-if="showBucketDeleteModal"
|
||||||
@close="showBucketDeleteModal = false"
|
@close="showBucketDeleteModal = false"
|
||||||
@submit="deleteBucket()"
|
@submit="deleteBucket()"
|
||||||
>
|
>
|
||||||
|
@ -236,7 +235,6 @@ import Dropdown from '@/components/misc/dropdown.vue'
|
||||||
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'
|
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'
|
||||||
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
|
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
|
||||||
import KanbanCard from '@/components/tasks/partials/kanban-card'
|
import KanbanCard from '@/components/tasks/partials/kanban-card'
|
||||||
import TaskDetailViewModal, { useShowModal } from '@/views/tasks/TaskDetailViewModal.vue'
|
|
||||||
|
|
||||||
const DRAG_OPTIONS = {
|
const DRAG_OPTIONS = {
|
||||||
// sortable options
|
// sortable options
|
||||||
|
@ -256,7 +254,6 @@ export default {
|
||||||
Dropdown,
|
Dropdown,
|
||||||
FilterPopup,
|
FilterPopup,
|
||||||
draggable,
|
draggable,
|
||||||
TaskDetailViewModal,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -293,12 +290,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
|
||||||
return {
|
|
||||||
showTaskDetail: useShowModal(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
// Save the current list view to local storage
|
// Save the current list view to local storage
|
||||||
// We use local storage and not vuex here to make it persistent across reloads.
|
// We use local storage and not vuex here to make it persistent across reloads.
|
||||||
|
|
|
@ -122,10 +122,6 @@
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
/>
|
/>
|
||||||
</card>
|
</card>
|
||||||
|
|
||||||
<transition name="modal">
|
|
||||||
<task-detail-view-modal v-if="showTaskDetail" />
|
|
||||||
</transition>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -133,13 +129,10 @@
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import TaskService from '../../../services/task'
|
|
||||||
import TaskModel from '../../../models/task'
|
|
||||||
|
|
||||||
import EditTask from '../../../components/tasks/edit-task'
|
import EditTask from '../../../components/tasks/edit-task'
|
||||||
import AddTask from '../../../components/tasks/add-task'
|
import AddTask from '../../../components/tasks/add-task'
|
||||||
import SingleTaskInList from '../../../components/tasks/partials/singleTaskInList'
|
import SingleTaskInList from '../../../components/tasks/partials/singleTaskInList'
|
||||||
import { createTaskList } from '@/composables/taskList'
|
import { useTaskList } from '@/composables/taskList'
|
||||||
import {saveListView} from '@/helpers/saveListView'
|
import {saveListView} from '@/helpers/saveListView'
|
||||||
import Rights from '../../../models/constants/rights.json'
|
import Rights from '../../../models/constants/rights.json'
|
||||||
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
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 Nothing from '@/components/misc/nothing.vue'
|
||||||
import Pagination from '@/components/misc/pagination.vue'
|
import Pagination from '@/components/misc/pagination.vue'
|
||||||
import {ALPHABETICAL_SORT} from '@/components/list/partials/filters.vue'
|
import {ALPHABETICAL_SORT} from '@/components/list/partials/filters.vue'
|
||||||
import TaskDetailViewModal, { useShowModal } from '@/views/tasks/TaskDetailViewModal.vue'
|
|
||||||
|
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
|
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
|
||||||
|
@ -174,7 +166,6 @@ export default {
|
||||||
name: 'List',
|
name: 'List',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
taskService: new TaskService(),
|
|
||||||
ctaVisible: false,
|
ctaVisible: false,
|
||||||
showTaskSearch: false,
|
showTaskSearch: false,
|
||||||
|
|
||||||
|
@ -193,11 +184,10 @@ export default {
|
||||||
AddTask,
|
AddTask,
|
||||||
draggable,
|
draggable,
|
||||||
Pagination,
|
Pagination,
|
||||||
TaskDetailViewModal,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const taskEditTask = ref(TaskModel)
|
const taskEditTask = ref(null)
|
||||||
const isTaskEdit = ref(false)
|
const isTaskEdit = ref(false)
|
||||||
|
|
||||||
// This function initializes the tasks page and loads the first page of tasks
|
// This function initializes the tasks page and loads the first page of tasks
|
||||||
|
@ -206,17 +196,18 @@ export default {
|
||||||
isTaskEdit.value = false
|
isTaskEdit.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskList = createTaskList(beforeLoad)
|
const taskList = useTaskList(beforeLoad)
|
||||||
|
|
||||||
// Save the current list view to local storage
|
// Save the current list view to local storage
|
||||||
// We use local storage and not vuex here to make it persistent across reloads.
|
// We use local storage and not vuex here to make it persistent across reloads.
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
saveListView(route.params.listId, route.name)
|
saveListView(route.params.listId, route.name)
|
||||||
|
|
||||||
|
taskList.initTaskList()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
taskEditTask,
|
taskEditTask,
|
||||||
isTaskEdit,
|
isTaskEdit,
|
||||||
showTaskDetail: useShowModal(),
|
|
||||||
...taskList,
|
...taskList,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -68,19 +68,19 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th v-if="activeColumns.id">
|
<th v-if="activeColumns.id">
|
||||||
#
|
#
|
||||||
<sort :order="sortBy.id" @click="sort('id')"/>
|
<Sort :order="sortBy.id" @click="sort('id')"/>
|
||||||
</th>
|
</th>
|
||||||
<th v-if="activeColumns.done">
|
<th v-if="activeColumns.done">
|
||||||
{{ $t('task.attributes.done') }}
|
{{ $t('task.attributes.done') }}
|
||||||
<sort :order="sortBy.done" @click="sort('done')"/>
|
<Sort :order="sortBy.done" @click="sort('done')"/>
|
||||||
</th>
|
</th>
|
||||||
<th v-if="activeColumns.title">
|
<th v-if="activeColumns.title">
|
||||||
{{ $t('task.attributes.title') }}
|
{{ $t('task.attributes.title') }}
|
||||||
<sort :order="sortBy.title" @click="sort('title')"/>
|
<Sort :order="sortBy.title" @click="sort('title')"/>
|
||||||
</th>
|
</th>
|
||||||
<th v-if="activeColumns.priority">
|
<th v-if="activeColumns.priority">
|
||||||
{{ $t('task.attributes.priority') }}
|
{{ $t('task.attributes.priority') }}
|
||||||
<sort :order="sortBy.priority" @click="sort('priority')"/>
|
<Sort :order="sortBy.priority" @click="sort('priority')"/>
|
||||||
</th>
|
</th>
|
||||||
<th v-if="activeColumns.labels">
|
<th v-if="activeColumns.labels">
|
||||||
{{ $t('task.attributes.labels') }}
|
{{ $t('task.attributes.labels') }}
|
||||||
|
@ -90,27 +90,27 @@
|
||||||
</th>
|
</th>
|
||||||
<th v-if="activeColumns.dueDate">
|
<th v-if="activeColumns.dueDate">
|
||||||
{{ $t('task.attributes.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>
|
||||||
<th v-if="activeColumns.startDate">
|
<th v-if="activeColumns.startDate">
|
||||||
{{ $t('task.attributes.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>
|
||||||
<th v-if="activeColumns.endDate">
|
<th v-if="activeColumns.endDate">
|
||||||
{{ $t('task.attributes.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>
|
||||||
<th v-if="activeColumns.percentDone">
|
<th v-if="activeColumns.percentDone">
|
||||||
{{ $t('task.attributes.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>
|
||||||
<th v-if="activeColumns.created">
|
<th v-if="activeColumns.created">
|
||||||
{{ $t('task.attributes.created') }}
|
{{ $t('task.attributes.created') }}
|
||||||
<sort :order="sortBy.created" @click="sort('created')"/>
|
<Sort :order="sortBy.created" @click="sort('created')"/>
|
||||||
</th>
|
</th>
|
||||||
<th v-if="activeColumns.updated">
|
<th v-if="activeColumns.updated">
|
||||||
{{ $t('task.attributes.updated') }}
|
{{ $t('task.attributes.updated') }}
|
||||||
<sort :order="sortBy.updated" @click="sort('updated')"/>
|
<Sort :order="sortBy.updated" @click="sort('updated')"/>
|
||||||
</th>
|
</th>
|
||||||
<th v-if="activeColumns.createdBy">
|
<th v-if="activeColumns.createdBy">
|
||||||
{{ $t('task.attributes.createdBy') }}
|
{{ $t('task.attributes.createdBy') }}
|
||||||
|
@ -173,22 +173,13 @@
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
/>
|
/>
|
||||||
</card>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { defineComponent, ref, reactive, computed, toRaw } from 'vue'
|
import { ref, reactive, computed, toRaw } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
|
||||||
import { createTaskList } from '@/composables/taskList'
|
|
||||||
import Done from '@/components/misc/Done.vue'
|
import Done from '@/components/misc/Done.vue'
|
||||||
import User from '@/components/misc/user'
|
import User from '@/components/misc/user'
|
||||||
import PriorityLabel from '@/components/tasks/partials/priorityLabel'
|
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 DateTableCell from '@/components/tasks/partials/date-table-cell'
|
||||||
import Fancycheckbox from '@/components/input/fancycheckbox'
|
import Fancycheckbox from '@/components/input/fancycheckbox'
|
||||||
import Sort from '@/components/tasks/partials/sort'
|
import Sort from '@/components/tasks/partials/sort'
|
||||||
import {saveListView} from '@/helpers/saveListView'
|
|
||||||
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
||||||
import Pagination from '@/components/misc/pagination.vue'
|
import Pagination from '@/components/misc/pagination.vue'
|
||||||
import Popup from '@/components/misc/popup'
|
import Popup from '@/components/misc/popup'
|
||||||
|
|
||||||
|
import { useTaskList } from '@/composables/taskList'
|
||||||
|
import {saveListView} from '@/helpers/saveListView'
|
||||||
|
|
||||||
const ACTIVE_COLUMNS_DEFAULT = {
|
const ACTIVE_COLUMNS_DEFAULT = {
|
||||||
id: true,
|
id: true,
|
||||||
done: true,
|
done: true,
|
||||||
|
@ -233,102 +226,86 @@ function useSavedView(activeColumns, sortBy) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
const activeColumns = reactive({ ...ACTIVE_COLUMNS_DEFAULT })
|
||||||
name: 'Table',
|
const sortBy = ref({ ...SORT_BY_DEFAULT })
|
||||||
components: {
|
|
||||||
Popup,
|
|
||||||
Done,
|
|
||||||
FilterPopup,
|
|
||||||
Sort,
|
|
||||||
Fancycheckbox,
|
|
||||||
DateTableCell,
|
|
||||||
Labels,
|
|
||||||
PriorityLabel,
|
|
||||||
User,
|
|
||||||
Pagination,
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const activeColumns = reactive({ ...ACTIVE_COLUMNS_DEFAULT })
|
|
||||||
const sortBy = ref({ ...SORT_BY_DEFAULT })
|
|
||||||
|
|
||||||
useSavedView(activeColumns, sortBy)
|
useSavedView(activeColumns, sortBy)
|
||||||
|
|
||||||
function beforeLoad(params) {
|
function beforeLoad(params) {
|
||||||
// This makes sure an id sort order is always sorted last.
|
// 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
|
// 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.
|
// precedence over everything else, making any other sort columns pretty useless.
|
||||||
let hasIdFilter = false
|
let hasIdFilter = false
|
||||||
const sortKeys = Object.keys(sortBy.value)
|
const sortKeys = Object.keys(sortBy.value)
|
||||||
for (const s of sortKeys) {
|
for (const s of sortKeys) {
|
||||||
if (s === 'id') {
|
if (s === 'id') {
|
||||||
sortKeys.splice(s, 1)
|
sortKeys.splice(s, 1)
|
||||||
hasIdFilter = true
|
hasIdFilter = true
|
||||||
break
|
break
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasIdFilter) {
|
|
||||||
sortKeys.push('id')
|
|
||||||
}
|
|
||||||
params.value.sort_by = sortKeys
|
|
||||||
params.value.order_by = sortKeys.map(s => sortBy.value[s])
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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, {
|
Object.assign(params.value, {
|
||||||
filter_by: [],
|
filter_by: [],
|
||||||
filter_value: [],
|
filter_value: [],
|
||||||
filter_comparator: [],
|
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,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -4,35 +4,19 @@
|
||||||
variant="scrolling"
|
variant="scrolling"
|
||||||
class="task-detail-view-modal"
|
class="task-detail-view-modal"
|
||||||
>
|
>
|
||||||
<a @click="close()" class="close">
|
<a @click="close()" class="close">
|
||||||
<icon icon="times"/>
|
<icon icon="times"/>
|
||||||
</a>
|
</a>
|
||||||
<task-detail-view/>
|
<task-detail-view/>
|
||||||
</modal>
|
</modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import TaskDetailView from './TaskDetailView'
|
import TaskDetailView from './TaskDetailView'
|
||||||
import {computed} from 'vue'
|
import router from '@/router'
|
||||||
import {useRoute} from 'vue-router'
|
|
||||||
|
|
||||||
export function useShowModal() {
|
function close() {
|
||||||
const route = useRoute()
|
router.back()
|
||||||
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()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
Reference in New Issue
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.
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).