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 {objectToSnakeCase} from '@/helpers/case'
import {getDefaultParams} from '@/components/tasks/mixins/taskList'
import {getDefaultParams} from '@/composables/taskList'
// FIXME: merge with DEFAULT_PARAMS in taskList.js
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
v-shortcut="'g l'"
:title="$t('keyboardShortcuts.list.switchToListView')"
:class="{'is-active': currentListType === 'list'}"
:class="{'is-active': $route.name === 'list.list'}"
:to="{ name: 'list.list', params: { listId: listId } }">
{{ $t('list.list.title') }}
</router-link>
<router-link
v-shortcut="'g g'"
:title="$t('keyboardShortcuts.list.switchToGanttView')"
:class="{'is-active': currentListType === 'gantt'}"
:class="{'is-active': $route.name === 'list.gantt'}"
:to="{ name: 'list.gantt', params: { listId: listId } }">
{{ $t('list.gantt.title') }}
</router-link>
<router-link
v-shortcut="'g t'"
:title="$t('keyboardShortcuts.list.switchToTableView')"
:class="{'is-active': currentListType === 'table'}"
:class="{'is-active': $route.name === 'list.table'}"
:to="{ name: 'list.table', params: { listId: listId } }">
{{ $t('list.table.title') }}
</router-link>
<router-link
v-shortcut="'g k'"
:title="$t('keyboardShortcuts.list.switchToKanbanView')"
:class="{'is-active': currentListType === 'kanban'}"
:class="{'is-active': $route.name === 'list.kanban'}"
:to="{ name: 'list.kanban', params: { listId: listId } }">
{{ $t('list.kanban.title') }}
</router-link>
@ -69,11 +69,6 @@ export default {
},
},
computed: {
currentListType() {
// default: 'list',
return ''
},
// Computed property to let "listId" always have a value
listId() {
return typeof this.$route.params.listId === 'undefined' ? 0 : this.$route.params.listId

View file

@ -1,6 +1,6 @@
<template>
<div
:class="{ 'is-loading': taskCollectionService.loading }"
:class="{ 'is-loading': loading }"
class="loader-container is-max-width-desktop list-view"
>
<div
@ -26,7 +26,7 @@
</div>
<div class="control">
<x-button
:loading="taskCollectionService.loading"
:loading="loading"
@click="searchTasks"
:shadow="false"
>
@ -59,7 +59,7 @@
/>
</template>
<nothing v-if="ctaVisible && tasks.length === 0 && !taskCollectionService.loading">
<nothing v-if="ctaVisible && tasks.length === 0 && !loading">
{{ $t('list.list.empty') }}
<a @click="focusNewTaskInput()">
{{ $t('list.list.newTaskCta') }}
@ -117,8 +117,8 @@
/>
</div>
<Pagination
:total-pages="taskCollectionService.totalPages"
<Pagination
:total-pages="totalPages"
:current-page="currentPage"
/>
</card>
@ -130,13 +130,16 @@
</template>
<script>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import TaskService from '../../../services/task'
import TaskModel from '../../../models/task'
import EditTask from '../../../components/tasks/edit-task'
import AddTask from '../../../components/tasks/add-task'
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 Rights from '../../../models/constants/rights.json'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
@ -172,8 +175,6 @@ export default {
data() {
return {
taskService: new TaskService(),
isTaskEdit: false,
taskEditTask: TaskModel,
ctaVisible: false,
showTaskSearch: false,
@ -184,9 +185,6 @@ export default {
},
}
},
mixins: [
taskList,
],
components: {
Nothing,
FilterPopup,
@ -199,15 +197,28 @@ export default {
},
setup() {
return {
showTaskDetail: useShowModal(),
}
},
const taskEditTask = ref(TaskModel)
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
// 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: {
isAlphabeticalSorting() {
@ -247,17 +258,11 @@ export default {
// 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
// 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(() => {
this.showTaskSearch = false
}, 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() {
this.$refs.newTaskInput.$refs.newTaskInput.focus()
},

View file

@ -1,5 +1,5 @@
<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="items">
<popup>
@ -169,7 +169,7 @@
</div>
<Pagination
:total-pages="taskCollectionService.totalPages"
:total-pages="totalPages"
:current-page="currentPage"
/>
</card>
@ -185,9 +185,10 @@
</template>
<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 User from '@/components/misc/user'
import PriorityLabel from '@/components/tasks/partials/priorityLabel'
@ -200,7 +201,39 @@ import FilterPopup from '@/components/list/partials/filter-popup.vue'
import Pagination from '@/components/misc/pagination.vue'
import Popup from '@/components/misc/popup'
export default {
const ACTIVE_COLUMNS_DEFAULT = {
id: true,
done: true,
title: true,
priority: false,
labels: true,
assignees: true,
dueDate: true,
startDate: false,
endDate: false,
percentDone: false,
created: false,
updated: false,
createdBy: false,
}
const SORT_BY_DEFAULT = {
id: 'desc',
}
function useSavedView(activeColumns, sortBy) {
const savedShowColumns = localStorage.getItem('tableViewColumns')
if (savedShowColumns !== null) {
Object.assign(activeColumns, JSON.parse(savedShowColumns))
}
const savedSortBy = localStorage.getItem('tableViewSortBy')
if (savedSortBy !== null) {
sortBy.value = JSON.parse(savedSortBy)
}
}
export default defineComponent({
name: 'Table',
components: {
Popup,
@ -214,75 +247,18 @@ export default {
User,
Pagination,
},
mixins: [
taskList,
],
data() {
return {
activeColumns: {
id: true,
done: true,
title: true,
priority: false,
labels: true,
assignees: true,
dueDate: true,
startDate: false,
endDate: false,
percentDone: false,
created: false,
updated: false,
createdBy: false,
},
sortBy: {
id: 'desc',
},
}
},
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')
if (savedShowColumns !== null) {
this.activeColumns = JSON.parse(savedShowColumns)
}
const savedSortBy = localStorage.getItem('tableViewSortBy')
if (savedSortBy !== null) {
this.sortBy = JSON.parse(savedSortBy)
}
this.params.filter_by = []
this.params.filter_value = []
this.params.filter_comparator = []
this.initTasks(1)
},
setup() {
// 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()
console.log(route.value)
saveListView(route.value.params.listId, route.value.name)
},
methods: {
initTasks(page, search = '') {
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.
const sortKeys = Object.keys(this.sortBy)
let hasIdFilter = false
const sortKeys = Object.keys(sortBy.value)
for (const s of sortKeys) {
if (s === 'id') {
sortKeys.splice(s, 1)
@ -293,50 +269,80 @@ export default {
if (hasIdFilter) {
sortKeys.push('id')
}
const params = this.params
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]
params.value.sort_by = sortKeys
params.value.order_by = sortKeys.map(s => sortBy.value[s])
}
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') {
this.sortBy[property] = 'desc'
sortBy.value[property] = 'desc'
} else if (order === 'desc') {
this.sortBy[property] = 'asc'
sortBy.value[property] = 'asc'
} else {
delete this.sortBy[property]
delete sortBy.value[property]
}
this.initTasks(this.currentPage, this.searchTerm)
beforeLoad(taskList.currentPage.value, taskList.searchTerm.value)
// Save the order to be able to retrieve them later
localStorage.setItem('tableViewSortBy', JSON.stringify(this.sortBy))
},
saveTaskColumns() {
localStorage.setItem('tableViewColumns', JSON.stringify(this.activeColumns))
},
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,
}
},
}
})
</script>
<style lang="scss" scoped>
.table-view {
.table {
background: transparent;
overflow-x: auto;
overflow-y: hidden;
.table {
background: transparent;
overflow-x: auto;
overflow-y: hidden;
th {
white-space: nowrap;
}
th {
white-space: nowrap;
}
.user {
margin: 0;
}
.user {
margin: 0;
}
}