feat: make taskList a composable

This commit is contained in:
Dominik Pschenitschni 2021-10-25 22:17:23 +02:00
parent 5a0c0eff9f
commit 281c922de1
No known key found for this signature in database
GPG key ID: B257AC0149F43A77
6 changed files with 254 additions and 237 deletions

View file

@ -191,7 +191,7 @@ import NamespaceService from '@/services/namespace'
import EditLabels from '@/components/tasks/partials/editLabels.vue' import EditLabels from '@/components/tasks/partials/editLabels.vue'
import {objectToSnakeCase} from '@/helpers/case' import {objectToSnakeCase} from '@/helpers/case'
import {getDefaultParams} from '@/components/tasks/mixins/taskList' import {getDefaultParams} from '@/composables/taskList'
// FIXME: merge with DEFAULT_PARAMS in taskList.js // FIXME: merge with DEFAULT_PARAMS in taskList.js
const DEFAULT_PARAMS = { const DEFAULT_PARAMS = {

View file

@ -1,101 +0,0 @@
import TaskCollectionService from '@/services/taskCollection'
// FIXME: merge with DEFAULT_PARAMS in filters.vue
export const getDefaultParams = () => ({
sort_by: ['position', 'id'],
order_by: ['asc', 'desc'],
filter_by: ['done'],
filter_value: ['false'],
filter_comparator: ['equals'],
filter_concat: 'and',
})
/**
* This mixin provides a base set of methods and properties to get tasks on a list.
*/
export default {
data() {
return {
taskCollectionService: new TaskCollectionService(),
tasks: [],
currentPage: 0,
loadedList: null,
searchTerm: '',
showTaskFilter: false,
params: {...getDefaultParams()},
}
},
watch: {
// Only listen for query path changes
'$route.query': {
handler: 'loadTasksForPage',
immediate: true,
},
'$route.path': 'loadTasksOnSavedFilter',
},
methods: {
async loadTasks(
page,
search = '',
params = null,
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 (
this.$route.name !== 'list.list' &&
this.$route.name !== 'list.table' &&
!forceLoading
) {
return
}
if (params === null) {
params = this.params
}
if (search !== '') {
params.s = search
}
const list = {listId: parseInt(this.$route.params.listId)}
const currentList = {
id: list.listId,
params,
search,
page,
}
if (JSON.stringify(currentList) === JSON.stringify(this.loadedList) && !forceLoading) {
return
}
this.tasks = []
this.tasks = await this.taskCollectionService.getAll(list, params, page)
this.currentPage = page
this.loadedList = JSON.parse(JSON.stringify(currentList))
},
loadTasksForPage(e) {
// The page parameter can be undefined, in the case where the user loads a new list from the side bar menu
let page = Number(e.page)
if (typeof e.page === 'undefined') {
page = 1
}
let search = e.search
if (typeof e.search === 'undefined') {
search = ''
}
this.initTasks(page, search)
},
loadTasksOnSavedFilter() {
if (typeof this.$route.params.listId !== 'undefined' && parseInt(this.$route.params.listId) < 0) {
this.loadTasks(1, '', null, true)
}
},
},
}

112
src/composables/taskList.js Normal file
View file

@ -0,0 +1,112 @@
import { ref, watch, computed } from 'vue'
import { useRoute } from 'vue-router'
import TaskCollectionService from '@/services/taskCollection'
// FIXME: merge with DEFAULT_PARAMS in filters.vue
export const getDefaultParams = () => ({
sort_by: ['position', 'id'],
order_by: ['asc', 'desc'],
filter_by: ['done'],
filter_value: ['false'],
filter_comparator: ['equals'],
filter_concat: 'and',
})
/**
* This mixin provides a base set of methods and properties to get tasks on a list.
*/
export function createTaskList(initTasks) {
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 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))
}
async function loadTasksForPage(query) {
const { page, search } = query
initTasks(params)
await loadTasks(
// 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),
search,
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
watch(() => route.query, loadTasksForPage, { immediate: true })
watch(() => route.path, loadTasksOnSavedFilter)
}
return {
tasks,
initTaskList,
loading,
totalPages,
currentPage,
showTaskFilter,
loadTasks,
searchTerm,
params,
}
}

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': currentListType === 'list'}" :class="{'is-active': $route.name === 'list.list'}"
:to="{ name: 'list.list', params: { listId: listId } }"> :to="{ name: 'list.list', params: { listId: 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': currentListType === 'gantt'}" :class="{'is-active': $route.name === 'list.gantt'}"
:to="{ name: 'list.gantt', params: { listId: listId } }"> :to="{ name: 'list.gantt', params: { listId: 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': currentListType === 'table'}" :class="{'is-active': $route.name === 'list.table'}"
:to="{ name: 'list.table', params: { listId: listId } }"> :to="{ name: 'list.table', params: { listId: 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': currentListType === 'kanban'}" :class="{'is-active': $route.name === 'list.kanban'}"
:to="{ name: 'list.kanban', params: { listId: listId } }"> :to="{ name: 'list.kanban', params: { listId: listId } }">
{{ $t('list.kanban.title') }} {{ $t('list.kanban.title') }}
</router-link> </router-link>
@ -69,11 +69,6 @@ export default {
}, },
}, },
computed: { computed: {
currentListType() {
// default: 'list',
return ''
},
// Computed property to let "listId" always have a value // Computed property to let "listId" always have a value
listId() { listId() {
return typeof this.$route.params.listId === 'undefined' ? 0 : this.$route.params.listId return typeof this.$route.params.listId === 'undefined' ? 0 : this.$route.params.listId

View file

@ -1,6 +1,6 @@
<template> <template>
<div <div
:class="{ 'is-loading': taskCollectionService.loading }" :class="{ 'is-loading': loading }"
class="loader-container is-max-width-desktop list-view" class="loader-container is-max-width-desktop list-view"
> >
<div <div
@ -26,7 +26,7 @@
</div> </div>
<div class="control"> <div class="control">
<x-button <x-button
:loading="taskCollectionService.loading" :loading="loading"
@click="searchTasks" @click="searchTasks"
:shadow="false" :shadow="false"
> >
@ -59,7 +59,7 @@
/> />
</template> </template>
<nothing v-if="ctaVisible && tasks.length === 0 && !taskCollectionService.loading"> <nothing v-if="ctaVisible && tasks.length === 0 && !loading">
{{ $t('list.list.empty') }} {{ $t('list.list.empty') }}
<a @click="focusNewTaskInput()"> <a @click="focusNewTaskInput()">
{{ $t('list.list.newTaskCta') }} {{ $t('list.list.newTaskCta') }}
@ -118,7 +118,7 @@
</div> </div>
<Pagination <Pagination
:total-pages="taskCollectionService.totalPages" :total-pages="totalPages"
:current-page="currentPage" :current-page="currentPage"
/> />
</card> </card>
@ -130,13 +130,16 @@
</template> </template>
<script> <script>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import TaskService from '../../../services/task' import TaskService from '../../../services/task'
import TaskModel from '../../../models/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 taskList from '../../../components/tasks/mixins/taskList' import { createTaskList } 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'
@ -172,8 +175,6 @@ export default {
data() { data() {
return { return {
taskService: new TaskService(), taskService: new TaskService(),
isTaskEdit: false,
taskEditTask: TaskModel,
ctaVisible: false, ctaVisible: false,
showTaskSearch: false, showTaskSearch: false,
@ -184,9 +185,6 @@ export default {
}, },
} }
}, },
mixins: [
taskList,
],
components: { components: {
Nothing, Nothing,
FilterPopup, FilterPopup,
@ -199,15 +197,28 @@ export default {
}, },
setup() { setup() {
return { const taskEditTask = ref(TaskModel)
showTaskDetail: useShowModal(), const isTaskEdit = ref(false)
}
}, // This function initializes the tasks page and loads the first page of tasks
function beforeLoad() {
taskEditTask.value = null
isTaskEdit.value = false
}
const taskList = createTaskList(beforeLoad)
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.
saveListView(this.$route.params.listId, this.$route.name) const route = useRoute()
saveListView(route.params.listId, route.name)
return {
taskEditTask,
isTaskEdit,
showTaskDetail: useShowModal(),
...taskList,
}
}, },
computed: { computed: {
isAlphabeticalSorting() { isAlphabeticalSorting() {
@ -247,17 +258,11 @@ export default {
// When clicking on the search button, @blur from the input is fired. If we // When clicking on the search button, @blur from the input is fired. If we
// would then directly hide the whole search bar directly, no click event // would then directly hide the whole search bar directly, no click event
// from the button gets fired. To prevent this, we wait 200ms until we hide // from the button gets fired. To prevent this, we wait 200ms until we hide
// everything so the button has a chance of firering the search event. // everything so the button has a chance of firing the search event.
setTimeout(() => { setTimeout(() => {
this.showTaskSearch = false this.showTaskSearch = false
}, 200) }, 200)
}, },
// This function initializes the tasks page and loads the first page of tasks
initTasks(page, search = '') {
this.taskEditTask = null
this.isTaskEdit = false
this.loadTasks(page, search)
},
focusNewTaskInput() { focusNewTaskInput() {
this.$refs.newTaskInput.$refs.newTaskInput.focus() this.$refs.newTaskInput.$refs.newTaskInput.focus()
}, },

View file

@ -1,5 +1,5 @@
<template> <template>
<div :class="{'is-loading': taskCollectionService.loading}" class="table-view loader-container"> <div :class="{'is-loading': loading}" class="table-view loader-container">
<div class="filter-container"> <div class="filter-container">
<div class="items"> <div class="items">
<popup> <popup>
@ -169,7 +169,7 @@
</div> </div>
<Pagination <Pagination
:total-pages="taskCollectionService.totalPages" :total-pages="totalPages"
:current-page="currentPage" :current-page="currentPage"
/> />
</card> </card>
@ -185,9 +185,10 @@
</template> </template>
<script> <script>
import {useRoute} from 'vue-router' import { defineComponent, ref, reactive, computed, toRaw } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import taskList from '@/components/tasks/mixins/taskList' 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'
@ -200,26 +201,7 @@ 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'
export default { const ACTIVE_COLUMNS_DEFAULT = {
name: 'Table',
components: {
Popup,
Done,
FilterPopup,
Sort,
Fancycheckbox,
DateTableCell,
Labels,
PriorityLabel,
User,
Pagination,
},
mixins: [
taskList,
],
data() {
return {
activeColumns: {
id: true, id: true,
done: true, done: true,
title: true, title: true,
@ -233,56 +215,50 @@ export default {
created: false, created: false,
updated: false, updated: false,
createdBy: false, createdBy: false,
}, }
sortBy: {
const SORT_BY_DEFAULT = {
id: 'desc', id: 'desc',
}, }
}
}, function useSavedView(activeColumns, sortBy) {
computed: {
taskDetailRoutes() {
const taskDetailRoutes = {}
this.tasks.forEach(({id}) => {
taskDetailRoutes[id] = {
name: 'task.detail',
params: { id },
state: { backgroundView: this.$router.currentRoute.value.fullPath },
}
})
return taskDetailRoutes
},
},
created() {
const savedShowColumns = localStorage.getItem('tableViewColumns') const savedShowColumns = localStorage.getItem('tableViewColumns')
if (savedShowColumns !== null) { if (savedShowColumns !== null) {
this.activeColumns = JSON.parse(savedShowColumns) Object.assign(activeColumns, JSON.parse(savedShowColumns))
} }
const savedSortBy = localStorage.getItem('tableViewSortBy') const savedSortBy = localStorage.getItem('tableViewSortBy')
if (savedSortBy !== null) { if (savedSortBy !== null) {
this.sortBy = JSON.parse(savedSortBy) sortBy.value = JSON.parse(savedSortBy)
} }
}
this.params.filter_by = [] export default defineComponent({
this.params.filter_value = [] name: 'Table',
this.params.filter_comparator = [] components: {
Popup,
this.initTasks(1) Done,
FilterPopup,
Sort,
Fancycheckbox,
DateTableCell,
Labels,
PriorityLabel,
User,
Pagination,
}, },
setup() { setup() {
// Save the current list view to local storage const activeColumns = reactive({ ...ACTIVE_COLUMNS_DEFAULT })
// We use local storage and not vuex here to make it persistent across reloads. const sortBy = ref({ ...SORT_BY_DEFAULT })
const route = useRoute()
console.log(route.value) useSavedView(activeColumns, sortBy)
saveListView(route.value.params.listId, route.value.name)
}, function beforeLoad(params) {
methods: {
initTasks(page, search = '') {
// 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.
const sortKeys = Object.keys(this.sortBy)
let hasIdFilter = false let hasIdFilter = false
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)
@ -293,39 +269,70 @@ export default {
if (hasIdFilter) { if (hasIdFilter) {
sortKeys.push('id') sortKeys.push('id')
} }
params.value.sort_by = sortKeys
const params = this.params params.value.order_by = sortKeys.map(s => sortBy.value[s])
params.sort_by = []
params.order_by = []
sortKeys.map(s => {
params.sort_by.push(s)
params.order_by.push(this.sortBy[s])
})
this.loadTasks(page, search, params)
},
sort(property) {
const order = this.sortBy[property]
if (typeof order === 'undefined' || order === 'none') {
this.sortBy[property] = 'desc'
} else if (order === 'desc') {
this.sortBy[property] = 'asc'
} else {
delete this.sortBy[property]
} }
this.initTasks(this.currentPage, this.searchTerm)
const taskList = createTaskList(beforeLoad)
Object.assign(taskList.params.value, {
filter_by: [],
filter_value: [],
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 // Save the order to be able to retrieve them later
localStorage.setItem('tableViewSortBy', JSON.stringify(this.sortBy)) 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,
}
}, },
saveTaskColumns() { })
localStorage.setItem('tableViewColumns', JSON.stringify(this.activeColumns))
},
},
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.table-view { .table {
.table {
background: transparent; background: transparent;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
@ -337,7 +344,6 @@ export default {
.user { .user {
margin: 0; margin: 0;
} }
}
} }
.columns-filter { .columns-filter {