feat: unify modal view

fix: List.vue
This commit is contained in:
Dominik Pschenitschni 2021-11-01 18:19:59 +01:00
parent 281c922de1
commit c70211ad32
Signed by: dpschen
GPG Key ID: B257AC0149F43A77
13 changed files with 314 additions and 411 deletions

View File

@ -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()

View File

@ -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

View File

@ -16,7 +16,7 @@ export const getDefaultParams = () => ({
/** /**
* This mixin provides a base set of methods and properties to get tasks on a list. * 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,

View File

@ -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,
}, },
}, },
{ {

View File

@ -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)
} }
}, },

View File

@ -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])
}
},
}, },
} }

View File

@ -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()

View File

@ -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>

View File

@ -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">

View File

@ -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.

View File

@ -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,
} }
}, },

View File

@ -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>

View File

@ -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>