feat: unify modal view
fix: List.vue
This commit is contained in:
parent
281c922de1
commit
c70211ad32
13 changed files with 314 additions and 411 deletions
|
@ -22,12 +22,9 @@
|
||||||
|
|
||||||
<router-view :route="routeWithModal"/>
|
<router-view :route="routeWithModal"/>
|
||||||
|
|
||||||
<!-- TODO: is this still used? -->
|
<transition name="modal">
|
||||||
<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>
|
||||||
|
|
||||||
<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())
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue