feat: implement modals with vue router 4 #816

Merged
konrad merged 62 commits from dpschen/frontend:feature/vue3-modals-with-router-4 into main 2022-02-05 16:49:04 +00:00
10 changed files with 263 additions and 233 deletions
Showing only changes of commit 5916a44724 - Show all commits

View File

@ -44,7 +44,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {watch, computed, shallowRef, watchEffect} from 'vue' import {watch, computed, shallowRef, watchEffect, h} 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'
@ -66,19 +66,36 @@ function useRouteWithModal() {
} }
}) })
const currentModal = shallowRef(null) const currentModal = shallowRef(null)
watchEffect(() => { watchEffect(() => {
currentModal.value = historyState.value.backdropView const hasBackdropView = historyState.value.backdropView
? route.matched[0]?.components.default
if (!hasBackdropView) {
currentModal.value = null
return
}
// logic from vue-router
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
const routePropsOption = route.matched[0]?.props.default
const routeProps = routePropsOption
? routePropsOption === true
? route.params
: typeof routePropsOption === 'function'
? routePropsOption(route)
: routePropsOption
: null : null
currentModal.value = h(
route.matched[0]?.components.default,
routeProps,
)
}) })
return { routeWithModal, currentModal } return { routeWithModal, currentModal }
} }
useRouteWithModal() const { routeWithModal, currentModal } = useRouteWithModal()
const store = useStore() const store = useStore()

View File

@ -1,5 +1,5 @@
import { ref, watch, computed } from 'vue' import { ref, shallowReactive, watch, computed } from 'vue'
import { useRoute } from 'vue-router' import {useRoute} from 'vue-router'
import TaskCollectionService from '@/services/taskCollection' import TaskCollectionService from '@/services/taskCollection'
@ -13,102 +13,104 @@ export const getDefaultParams = () => ({
filter_concat: 'and', filter_concat: 'and',
}) })
const filters = {
done: {
value: false,
comparator: 'equals',
dpschen marked this conversation as resolved Outdated

Maybe shallowReactive fits here better?

Maybe shallowReactive fits here better?
concat: 'and',
},
}
dpschen marked this conversation as resolved Outdated

Please indent.

Please indent.

Done

Done
const SORT_BY_DEFAULT = {
id: 'desc',
}
/** /**
* 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 useTaskList(initTasks) { export function useTaskList(listId) {
const taskCollectionService = ref(new TaskCollectionService()) const params = ref({...getDefaultParams()})
const loading = computed(() => taskCollectionService.value.loading)
const totalPages = computed(() => taskCollectionService.value.totalPages) const search = ref('')
const page = ref(1)
const sortBy = ref({ ...SORT_BY_DEFAULT })
// This makes sure an id sort order is always sorted last.

TODO: check if this works

TODO: check if this works
// 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.
function formatSortOrder(params) {
let hasIdFilter = false
const sortKeys = Object.keys(sortBy.value)
for (const s of sortKeys) {
if (s === 'id') {
sortKeys.splice(s, 1)
hasIdFilter = true
break
}
}
if (hasIdFilter) {
sortKeys.push('id')
}
params.sort_by = sortKeys
params.order_by = sortKeys.map(s => sortBy.value[s])
return params
}
const getAllTasksParams = computed(() => {
let loadParams = {...params.value}
if (search.value !== '') {
loadParams.s = search.value
}
loadParams = formatSortOrder(loadParams)
return [
{listId: listId.value},
loadParams,
page.value || 1,
]
})
const taskCollectionService = shallowReactive(new TaskCollectionService())
const loading = computed(() => taskCollectionService.loading)
const totalPages = computed(() => taskCollectionService.totalPages)
const tasks = ref([]) const tasks = ref([])
const currentPage = ref(0) async function loadTasks() {
const loadedList = ref(null) tasks.value = await taskCollectionService.getAll(...getAllTasksParams.value)
const searchTerm = ref('')
const showTaskFilter = ref(false)
const params = ref({...getDefaultParams()})
const route = useRoute()
async function loadTasks(
page = 1,
search = '',
loadParams = { ...params.value },
forceLoading = false,
) {
// Because this function is triggered every time on topNavigation, we're putting a condition here to only load it when we actually want to show tasks
// FIXME: This is a bit hacky -> Cleanup.
if (
route.name !== 'list.list' &&
route.name !== 'list.table' &&
!forceLoading
) {
return
}
if (search !== '') {
loadParams.s = search
}
const list = {listId: parseInt(route.params.listId)}
const currentList = {
id: list.listId,
params: loadParams,
search,
page,
}
if (
JSON.stringify(currentList) === JSON.stringify(loadedList.value) &&
!forceLoading
) {
return
}
tasks.value = []
tasks.value = await taskCollectionService.value.getAll(list, loadParams, page)
currentPage.value = page
loadedList.value = JSON.parse(JSON.stringify(currentList))
return tasks.value return tasks.value
} }
async function loadTasksForPage(query) { const route = useRoute()
const { page, search } = query watch(() => route.query, (query) => {
initTasks(params) const { page: pageQuery, search: searchQuery } = query
return await loadTasks( search.value = searchQuery
// The page parameter can be undefined, in the case where the user loads a new list from the side bar menu page.value = pageQuery
typeof page === 'undefined' ? 1 : Number(page),
search,
params.value,
)
}
async function loadTasksOnSavedFilter() {
if (
typeof route.params.listId !== 'undefined' &&
parseInt(route.params.listId) < 0
) {
await loadTasks(1, '', null, true)
}
}
function initTaskList() { }, { immediate: true })
// Only listen for query path changes
watch(() => route.query, loadTasksForPage, { immediate: true })
watch(() => route.path, loadTasksOnSavedFilter) // Only listen for query path changes
} watch(() => JSON.stringify(getAllTasksParams.value), (newParams, oldParams) => {
if (oldParams === newParams) {
return
}
loadTasks()
}, { immediate: true })
return { return {
tasks, tasks,
initTaskList,
loading, loading,
totalPages, totalPages,
currentPage, currentPage: page,
showTaskFilter,
loadTasks, loadTasks,
searchTerm, searchTerm: search,
params, params,
} }
} }

View File

@ -244,6 +244,9 @@ const router = createRouter({
path: '/tasks/:id', path: '/tasks/:id',
name: 'task.detail', name: 'task.detail',
component: TaskDetailViewModal, component: TaskDetailViewModal,
props: route => ({
taskId: parseInt(route.params.id),
}),
}, },
{ {
path: '/tasks/by/upcoming', path: '/tasks/by/upcoming',
@ -341,21 +344,33 @@ const router = createRouter({
path: '/lists/:listId/list', path: '/lists/:listId/list',
name: 'list.list', name: 'list.list',
component: ListList, component: ListList,
props: route => ({
listId: parseInt(route.params.listId),
}),
}, },
{ {
path: '/lists/:listId/gantt', path: '/lists/:listId/gantt',
name: 'list.gantt', name: 'list.gantt',
component: ListGantt, component: ListGantt,
props: route => ({
listId: parseInt(route.params.listId),
}),
}, },
{ {
path: '/lists/:listId/table', path: '/lists/:listId/table',
name: 'list.table', name: 'list.table',
component: ListTable, component: ListTable,
props: route => ({
listId: parseInt(route.params.listId),
}),
}, },
{ {
path: '/lists/:listId/kanban', path: '/lists/:listId/kanban',
name: 'list.kanban', name: 'list.kanban',
component: ListKanban, component: ListKanban,
props: route => ({
listId: parseInt(route.params.listId),
}),
}, },
{ {
path: '/teams', path: '/teams',

View File

@ -1,5 +1,5 @@
<template> <template>
<ListWrapper class="list-gantt"> <ListWrapper class="list-gantt" :list-id="props.listId" viewName="gantt">
<template #header> <template #header>
<div class="gantt-options p-4"> <div class="gantt-options p-4">
<fancycheckbox class="is-block" v-model="showTaskswithoutDates"> <fancycheckbox class="is-block" v-model="showTaskswithoutDates">
@ -54,7 +54,7 @@
:date-from="dateFrom" :date-from="dateFrom"
:date-to="dateTo" :date-to="dateTo"
:day-width="dayWidth" :day-width="dayWidth"
:list-id="Number($route.params.listId)" :list-id="props.listId"
:show-taskswithout-dates="showTaskswithoutDates" :show-taskswithout-dates="showTaskswithoutDates"
/> />
@ -64,16 +64,23 @@
</ListWrapper> </ListWrapper>
</template> </template>
<script setup> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import flatPickr from 'vue-flatpickr-component' import flatPickr from 'vue-flatpickr-component'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import ListWrapper from './ListWrapper' import ListWrapper from './ListWrapper.vue'
import GanttChart from '@/components/tasks/gantt-component' import GanttChart from '@/components/tasks/gantt-component.vue'
import Fancycheckbox from '@/components/input/fancycheckbox' import Fancycheckbox from '@/components/input/fancycheckbox.vue'
const props = defineProps({
listId: {
type: Number,
required: true,
},
})
const DEFAULT_DAY_COUNT = 35 const DEFAULT_DAY_COUNT = 35
@ -85,7 +92,7 @@ const dateFrom = ref(new Date((new Date()).setDate(now.value.getDate() - 15)))
const dateTo = ref(new Date((new Date()).setDate(now.value.getDate() + 30))) const dateTo = ref(new Date((new Date()).setDate(now.value.getDate() + 30)))
const {t} = useI18n() const {t} = useI18n()
const {store} = useStore() const store = useStore()
const flatPickerConfig = computed(() => ({ const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatShort'), altFormat: t('date.altFormatShort'),
altInput: true, altInput: true,

View File

@ -1,5 +1,5 @@
<template> <template>
<ListWrapper class="list-kanban"> <ListWrapper class="list-kanban" :list-id="listId" viewName="kanban">
<template #header> <template #header>
<div class="filter-container" v-if="isSavedFilter"> <div class="filter-container" v-if="isSavedFilter">
<div class="items"> <div class="items">
@ -263,6 +263,14 @@ export default {
FilterPopup, FilterPopup,
draggable, draggable,
}, },
props: {
listId: {
type: Number,
required: true,
},
},
data() { data() {
return { return {
taskContainerRefs: {}, taskContainerRefs: {},
@ -310,7 +318,7 @@ export default {
}, },
loadBucketParameter() { loadBucketParameter() {
return { return {
listId: this.$route.params.listId, listId: this.listId,
params: this.params, params: this.params,
} }
}, },
@ -350,16 +358,11 @@ export default {
methods: { methods: {
loadBuckets() { loadBuckets() {
// Prevent trying to load buckets if the task popup view is active
if (this.$route.name !== 'list.kanban') {
return
}
const {listId, params} = this.loadBucketParameter const {listId, params} = this.loadBucketParameter
this.collapsedBuckets = getCollapsedBucketState(listId) this.collapsedBuckets = getCollapsedBucketState(listId)
console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $route.params =`, this.$route.params) console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $attrs = ${this.$attrs} $route.params =`, this.$route.params)
this.$store.dispatch('kanban/loadBucketsForList', {listId, params}) this.$store.dispatch('kanban/loadBucketsForList', {listId, params})
}, },
@ -434,7 +437,7 @@ export default {
const task = await this.$store.dispatch('tasks/createNewTask', { const task = await this.$store.dispatch('tasks/createNewTask', {
title: this.newTaskText, title: this.newTaskText,
bucketId, bucketId,
listId: this.$route.params.listId, listId: this.listId,
}) })
this.newTaskText = '' this.newTaskText = ''
this.$store.commit('kanban/addTaskToBucket', task) this.$store.commit('kanban/addTaskToBucket', task)
@ -456,7 +459,7 @@ export default {
const newBucket = new BucketModel({ const newBucket = new BucketModel({
title: this.newBucketTitle, title: this.newBucketTitle,
listId: parseInt(this.$route.params.listId), listId: this.listId,
}) })
await this.$store.dispatch('kanban/createBucket', newBucket) await this.$store.dispatch('kanban/createBucket', newBucket)
@ -476,7 +479,7 @@ export default {
async deleteBucket() { async deleteBucket() {
const bucket = new BucketModel({ const bucket = new BucketModel({
id: this.bucketToDelete, id: this.bucketToDelete,
listId: parseInt(this.$route.params.listId), listId: this.listId,
}) })
try { try {
@ -564,7 +567,7 @@ export default {
collapseBucket(bucket) { collapseBucket(bucket) {
this.collapsedBuckets[bucket.id] = true this.collapsedBuckets[bucket.id] = true
saveCollapsedBucketState(this.$route.params.listId, this.collapsedBuckets) saveCollapsedBucketState(this.listId, this.collapsedBuckets)
}, },
unCollapseBucket(bucket) { unCollapseBucket(bucket) {
if (!this.collapsedBuckets[bucket.id]) { if (!this.collapsedBuckets[bucket.id]) {
@ -572,7 +575,7 @@ export default {
} }
this.collapsedBuckets[bucket.id] = false this.collapsedBuckets[bucket.id] = false
saveCollapsedBucketState(this.$route.params.listId, this.collapsedBuckets) saveCollapsedBucketState(this.listId, this.collapsedBuckets)
}, },
}, },
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<ListWrapper class="list-list"> <ListWrapper class="list-list" :list-id="listId" viewName="list">
<template #header> <template #header>
<div <div
class="filter-container" class="filter-container"
@ -132,9 +132,9 @@
</template> </template>
<script> <script>
import { ref } from 'vue' import { ref, toRef, defineComponent } from 'vue'
import ListWrapper from './ListWrapper' import ListWrapper from './ListWrapper.vue'
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'
@ -167,8 +167,16 @@ function sortTasks(tasks) {
}) })
} }
export default { export default defineComponent({
name: 'List', name: 'List',
props: {
listId: {
type: Number,
required: true,
},
},
data() { data() {
return { return {
ctaVisible: false, ctaVisible: false,
@ -192,19 +200,17 @@ export default {
Pagination, Pagination,
}, },
setup() { setup(props) {
const taskEditTask = ref(null) 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
function beforeLoad() { // function beforeLoad() {
taskEditTask.value = null // taskEditTask.value = null
isTaskEdit.value = false // isTaskEdit.value = false
} // }
const taskList = useTaskList(beforeLoad) const taskList = useTaskList(toRef(props, 'listId'))
taskList.initTaskList()
return { return {
taskEditTask, taskEditTask,
@ -312,7 +318,7 @@ export default {
this.tasks[e.newIndex] = updatedTask this.tasks[e.newIndex] = updatedTask
}, },
}, },
} })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,5 +1,5 @@
<template> <template>
<ListWrapper class="list-table"> <ListWrapper class="list-table" :list-id="listId" viewName="table">
<template #header> <template #header>
<div class="filter-container"> <div class="filter-container">
<div class="items"> <div class="items">
@ -15,50 +15,47 @@
</template> </template>
<template #content="{isOpen}"> <template #content="{isOpen}">
<card class="columns-filter" :class="{'is-open': isOpen}"> <card class="columns-filter" :class="{'is-open': isOpen}">
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.id">#</fancycheckbox> <fancycheckbox v-model="activeColumns.id">#</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.done"> <fancycheckbox v-model="activeColumns.done">
{{ $t('task.attributes.done') }} {{ $t('task.attributes.done') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.title"> <fancycheckbox v-model="activeColumns.title">
{{ $t('task.attributes.title') }} {{ $t('task.attributes.title') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.priority"> <fancycheckbox v-model="activeColumns.priority">
{{ $t('task.attributes.priority') }} {{ $t('task.attributes.priority') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.labels"> <fancycheckbox v-model="activeColumns.labels">
{{ $t('task.attributes.labels') }} {{ $t('task.attributes.labels') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.assignees"> <fancycheckbox v-model="activeColumns.assignees">
{{ $t('task.attributes.assignees') }} {{ $t('task.attributes.assignees') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.dueDate"> <fancycheckbox v-model="activeColumns.dueDate">
{{ $t('task.attributes.dueDate') }} {{ $t('task.attributes.dueDate') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.startDate"> <fancycheckbox v-model="activeColumns.startDate">
{{ $t('task.attributes.startDate') }} {{ $t('task.attributes.startDate') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.endDate"> <fancycheckbox v-model="activeColumns.endDate">
{{ $t('task.attributes.endDate') }} {{ $t('task.attributes.endDate') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.percentDone"> <fancycheckbox v-model="activeColumns.percentDone">
{{ $t('task.attributes.percentDone') }} {{ $t('task.attributes.percentDone') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.created"> <fancycheckbox v-model="activeColumns.created">
{{ $t('task.attributes.created') }} {{ $t('task.attributes.created') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.updated"> <fancycheckbox v-model="activeColumns.updated">
{{ $t('task.attributes.updated') }} {{ $t('task.attributes.updated') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.createdBy"> <fancycheckbox v-model="activeColumns.createdBy">
{{ $t('task.attributes.createdBy') }} {{ $t('task.attributes.createdBy') }}
</fancycheckbox> </fancycheckbox>
</card> </card>
</template> </template>
</popup> </popup>
<filter-popup <filter-popup v-model="params" />
v-model="params"
@update:modelValue="loadTasks()"
/>
</div> </div>
</div> </div>
</template> </template>
@ -182,21 +179,23 @@
</ListWrapper> </ListWrapper>
</template> </template>
<script setup> <script setup lang="ts">
import { ref, reactive, computed, toRaw } from 'vue' import { toRef, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import ListWrapper from './ListWrapper' import { useStorage } from '@vueuse/core'
import ListWrapper from './ListWrapper.vue'
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.vue'
import PriorityLabel from '@/components/tasks/partials/priorityLabel' import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
import Labels from '@/components/tasks/partials/labels' import Labels from '@/components/tasks/partials/labels.vue'
import DateTableCell from '@/components/tasks/partials/date-table-cell' import DateTableCell from '@/components/tasks/partials/date-table-cell.vue'
import Fancycheckbox from '@/components/input/fancycheckbox' import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import Sort from '@/components/tasks/partials/sort' import Sort from '@/components/tasks/partials/sort.vue'
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.vue'
import { useTaskList } from '@/composables/taskList' import { useTaskList } from '@/composables/taskList'
@ -216,57 +215,27 @@ const ACTIVE_COLUMNS_DEFAULT = {
createdBy: false, createdBy: false,
} }
const props = defineProps({
listId: {
type: Number,
required: true,
},
})
const SORT_BY_DEFAULT = { const SORT_BY_DEFAULT = {
id: 'desc', id: 'desc',
} }
function useSavedView(activeColumns, sortBy) { const activeColumns = useStorage('tableViewColumns', { ...ACTIVE_COLUMNS_DEFAULT })
const savedShowColumns = localStorage.getItem('tableViewColumns') const sortBy = useStorage('tableViewSortBy', { ...SORT_BY_DEFAULT })
if (savedShowColumns !== null) {
Object.assign(activeColumns, JSON.parse(savedShowColumns))
}
const savedSortBy = localStorage.getItem('tableViewSortBy')
if (savedSortBy !== null) {
sortBy.value = JSON.parse(savedSortBy)
}
}
const activeColumns = reactive({ ...ACTIVE_COLUMNS_DEFAULT })
const sortBy = ref({ ...SORT_BY_DEFAULT })
useSavedView(activeColumns, sortBy)
function beforeLoad(params) {
// This makes sure an id sort order is always sorted last.
// When tasks would be sorted first by id and then by whatever else was specified, the id sort takes
// precedence over everything else, making any other sort columns pretty useless.
let hasIdFilter = false
const sortKeys = Object.keys(sortBy.value)
for (const s of sortKeys) {
if (s === 'id') {
sortKeys.splice(s, 1)
hasIdFilter = true
break
}
}
if (hasIdFilter) {
sortKeys.push('id')
}
params.value.sort_by = sortKeys
params.value.order_by = sortKeys.map(s => sortBy.value[s])
}
const { const {
tasks, tasks,
loading, loading,
params, params,
loadTasks,
totalPages, totalPages,
currentPage, currentPage,
searchTerm, } = useTaskList(toRef(props, 'listId'))
initTaskList,
} = useTaskList(beforeLoad)
Object.assign(params.value, { Object.assign(params.value, {
filter_by: [], filter_by: [],
@ -274,8 +243,19 @@ Object.assign(params.value, {
filter_comparator: [], filter_comparator: [],
}) })
const router = useRouter() // FIXME: by doing this we can have multiple sort orders
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]
}
}
const router = useRouter()
const taskDetailRoutes = computed(() => Object.fromEntries( const taskDetailRoutes = computed(() => Object.fromEntries(
tasks.value.map(({id}) => ([ tasks.value.map(({id}) => ([
id, id,
@ -286,26 +266,6 @@ const taskDetailRoutes = computed(() => Object.fromEntries(
}, },
])), ])),
)) ))
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

@ -8,28 +8,28 @@
<router-link <router-link
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': viewName === 'list'}"
:to="{ name: 'list.list', params: { 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': viewName === 'gantt'}"
:to="{ name: 'list.gantt', params: { 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': viewName === 'table'}"
:to="{ name: 'list.table', params: { 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': viewName === 'kanban'}"
:to="{ name: 'list.kanban', params: { listId } }"> :to="{ name: 'list.kanban', params: { listId } }">
{{ $t('list.kanban.title') }} {{ $t('list.kanban.title') }}
</router-link> </router-link>
@ -46,11 +46,11 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import {ref, shallowRef, computed, watchEffect} from 'vue' import {ref, shallowRef, computed, watchEffect} from 'vue'
import {useRoute} from 'vue-router' import {useRoute} from 'vue-router'
import Message from '@/components/misc/message' import Message from '@/components/misc/message.vue'
import ListModel from '@/models/list' import ListModel from '@/models/list'
import ListService from '@/services/list' import ListService from '@/services/list'
@ -63,11 +63,22 @@ import {saveListView} from '@/helpers/saveListView'
import {saveListToHistory} from '@/modules/listHistory' import {saveListToHistory} from '@/modules/listHistory'
import { useTitle } from '@/composables/useTitle' import { useTitle } from '@/composables/useTitle'
const props = defineProps({
listId: {
type: Number,
required: true,
},
viewName: {
type: String,
required: true,
},
})
const route = useRoute() const route = useRoute()
// 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.
saveListView(route.params.listId, route.name) saveListView(props.listId, props.viewName)
const listService = shallowRef(new ListService()) const listService = shallowRef(new ListService())
const loadedListId = ref(0) const loadedListId = ref(0)
@ -80,14 +91,12 @@ const currentList = computed(() => {
} : store.state.currentList } : store.state.currentList
}) })
// Computed property to let "listId" always have a value
const listId = computed(() => typeof route.params.listId === 'undefined' ? 0 : parseInt(route.params.listId))
// call again the method if the listId changes // call again the method if the listId changes
watchEffect(() => loadList(listId.value)) watchEffect(() => loadList(props.listId))
useTitle(() => currentList.value.id ? getListTitle(currentList.value) : '') useTitle(() => currentList.value.id ? getListTitle(currentList.value) : '')
async function loadList(listIdToLoad) { async function loadList(listIdToLoad: number) {
const listData = {id: listIdToLoad} const listData = {id: listIdToLoad}
saveListToHistory(listData) saveListToHistory(listData)
@ -97,8 +106,8 @@ async function loadList(listIdToLoad) {
// We don't do this for the table view because that does not change tasks. // We don't do this for the table view because that does not change tasks.
// FIXME: remove this // FIXME: remove this
if ( if (
route.name === 'list.list' || props.viewName === 'list.list' ||
route.name === 'list.gantt' props.viewName === 'list.gantt'
) { ) {
store.commit('kanban/setListId', 0) store.commit('kanban/setListId', 0)
} }
@ -116,7 +125,7 @@ async function loadList(listIdToLoad) {
return return
} }
console.debug(`Loading list, $route.name = ${route.name}, $route.params =`, route.params, `, loadedListId = ${loadedListId.value}, currentList = `, currentList.value) console.debug(`Loading list, props.viewName = ${props.viewName}, $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. // 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) const list = new ListModel(listData)
@ -124,7 +133,7 @@ async function loadList(listIdToLoad) {
const loadedList = await listService.value.get(list) const loadedList = await listService.value.get(list)
await store.dispatch(CURRENT_LIST, loadedList) await store.dispatch(CURRENT_LIST, loadedList)
} finally { } finally {
loadedListId.value = listId.value loadedListId.value = props.listId
} }
} }
</script> </script>

View File

@ -475,6 +475,14 @@ export default {
description, description,
heading, heading,
}, },
props: {
taskId: {
type: Number,
required: true,
},
},
data() { data() {
return { return {
taskService: new TaskService(), taskService: new TaskService(),
@ -525,10 +533,6 @@ export default {
}, },
}, },
computed: { computed: {
taskId() {
const {id} = this.$route.params
return id === undefined ? id : Number(id)
},
currentList() { currentList() {
return this.$store.state[CURRENT_LIST] return this.$store.state[CURRENT_LIST]
}, },

View File

@ -7,15 +7,22 @@
<a @click="close()" class="close"> <a @click="close()" class="close">
<icon icon="times"/> <icon icon="times"/>
</a> </a>
<task-detail-view/> <task-detail-view :task-id="props.taskId"/>
</modal> </modal>
</template> </template>
<script setup> <script setup lang="ts">
import {computed} from 'vue' import {computed} from 'vue'
import {useRouter, useRoute} from 'vue-router' import {useRouter, useRoute} from 'vue-router'
import TaskDetailView from './TaskDetailView' import TaskDetailView from './TaskDetailView.vue'
const props = defineProps({
taskId: {
type: Number,
required: true,
},
})
const route = useRoute() const route = useRoute()
const historyState = computed(() => route.fullPath && window.history.state) const historyState = computed(() => route.fullPath && window.history.state)