feat: provide listId prop via router
This commit is contained in:
parent
6d62ca1ada
commit
5916a44724
10 changed files with 263 additions and 233 deletions
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
concat: 'and',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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 loading = computed(() => taskCollectionService.value.loading)
|
|
||||||
const totalPages = computed(() => taskCollectionService.value.totalPages)
|
|
||||||
|
|
||||||
const tasks = ref([])
|
|
||||||
const currentPage = ref(0)
|
|
||||||
const loadedList = ref(null)
|
|
||||||
const searchTerm = ref('')
|
|
||||||
const showTaskFilter = ref(false)
|
|
||||||
const params = ref({...getDefaultParams()})
|
const params = ref({...getDefaultParams()})
|
||||||
|
|
||||||
const route = useRoute()
|
const search = ref('')
|
||||||
|
const page = ref(1)
|
||||||
|
|
||||||
async function loadTasks(
|
const sortBy = ref({ ...SORT_BY_DEFAULT })
|
||||||
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.
|
// This makes sure an id sort order is always sorted last.
|
||||||
if (
|
// When tasks would be sorted first by id and then by whatever else was specified, the id sort takes
|
||||||
route.name !== 'list.list' &&
|
// precedence over everything else, making any other sort columns pretty useless.
|
||||||
route.name !== 'list.table' &&
|
function formatSortOrder(params) {
|
||||||
!forceLoading
|
let hasIdFilter = false
|
||||||
) {
|
const sortKeys = Object.keys(sortBy.value)
|
||||||
return
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search !== '') {
|
const getAllTasksParams = computed(() => {
|
||||||
loadParams.s = search
|
let loadParams = {...params.value}
|
||||||
|
|
||||||
|
if (search.value !== '') {
|
||||||
|
loadParams.s = search.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = {listId: parseInt(route.params.listId)}
|
loadParams = formatSortOrder(loadParams)
|
||||||
|
|
||||||
const currentList = {
|
return [
|
||||||
id: list.listId,
|
{listId: listId.value},
|
||||||
params: loadParams,
|
loadParams,
|
||||||
search,
|
page.value || 1,
|
||||||
page,
|
]
|
||||||
}
|
})
|
||||||
if (
|
|
||||||
JSON.stringify(currentList) === JSON.stringify(loadedList.value) &&
|
|
||||||
!forceLoading
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.value = []
|
const taskCollectionService = shallowReactive(new TaskCollectionService())
|
||||||
tasks.value = await taskCollectionService.value.getAll(list, loadParams, page)
|
const loading = computed(() => taskCollectionService.loading)
|
||||||
currentPage.value = page
|
const totalPages = computed(() => taskCollectionService.totalPages)
|
||||||
loadedList.value = JSON.parse(JSON.stringify(currentList))
|
|
||||||
|
|
||||||
|
const tasks = ref([])
|
||||||
|
async function loadTasks() {
|
||||||
|
tasks.value = await taskCollectionService.getAll(...getAllTasksParams.value)
|
||||||
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,
|
}, { immediate: true })
|
||||||
params.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTasksOnSavedFilter() {
|
|
||||||
if (
|
|
||||||
typeof route.params.listId !== 'undefined' &&
|
|
||||||
parseInt(route.params.listId) < 0
|
|
||||||
) {
|
|
||||||
await loadTasks(1, '', null, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTaskList() {
|
|
||||||
// Only listen for query path changes
|
// Only listen for query path changes
|
||||||
watch(() => route.query, loadTasksForPage, { immediate: true })
|
watch(() => JSON.stringify(getAllTasksParams.value), (newParams, oldParams) => {
|
||||||
watch(() => route.path, loadTasksOnSavedFilter)
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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',
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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]
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue