Task FIlters (#149)
Set done filter based on passed params Make due date filter actually work Move filters into seperate config Merge branch 'master' into feature/task-filters Change done task filter text Make sure done tasks are always shown in table view Table view filter improvements Add done filter to table view Fix indent Add filter icon Move search and filter container Add filter for done tasks Hide done tasks by default Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/149
This commit is contained in:
parent
55afb7adc4
commit
ef01e8807e
8 changed files with 353 additions and 101 deletions
|
@ -24,7 +24,7 @@
|
|||
},
|
||||
props: {
|
||||
value: {
|
||||
required: true,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
|
@ -52,7 +52,3 @@
|
|||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
152
src/components/lists/reusable/filters.vue
Normal file
152
src/components/lists/reusable/filters.vue
Normal file
|
@ -0,0 +1,152 @@
|
|||
<template>
|
||||
<div class="card filters">
|
||||
<div class="card-content">
|
||||
<div class="field">
|
||||
<label class="label">Show Done Tasks</label>
|
||||
<div class="control">
|
||||
<fancycheckbox @change="setDoneFilter" v-model="filters.done">
|
||||
Show Done Tasks
|
||||
</fancycheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Due Date</label>
|
||||
<div class="control">
|
||||
<flat-pickr
|
||||
class="input"
|
||||
:config="flatPickerConfig"
|
||||
placeholder="Due Date Range"
|
||||
v-model="filters.dueDate"
|
||||
@on-close="setDueDateFilter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Fancycheckbox from '../../global/fancycheckbox'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
|
||||
export default {
|
||||
name: 'filters',
|
||||
components: {
|
||||
Fancycheckbox,
|
||||
flatPickr,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
params: {
|
||||
sort_by: [],
|
||||
order_by: [],
|
||||
filter_by: [],
|
||||
filter_value: [],
|
||||
filter_comparator: [],
|
||||
},
|
||||
filters: {
|
||||
done: false,
|
||||
dueDate: '',
|
||||
},
|
||||
flatPickerConfig: {
|
||||
altFormat: 'j M Y H:i',
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
mode: 'range',
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.params = this.value
|
||||
this.prepareDone()
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.$set(this, 'params', newVal)
|
||||
this.prepareDone()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change() {
|
||||
this.$emit('input', this.params)
|
||||
this.$emit('change', this.params)
|
||||
},
|
||||
prepareDone() {
|
||||
// Set filters.done based on params
|
||||
if(typeof this.params.filter_by !== 'undefined') {
|
||||
let foundDone = false
|
||||
this.params.filter_by.forEach((f, i) => {
|
||||
if (f === 'done') {
|
||||
foundDone = i
|
||||
}
|
||||
})
|
||||
if (foundDone === false) {
|
||||
this.filters.done = true
|
||||
}
|
||||
}
|
||||
},
|
||||
setDoneFilter() {
|
||||
if (this.filters.done) {
|
||||
for (const i in this.params.filter_by) {
|
||||
if (this.params.filter_by[i] === 'done') {
|
||||
this.params.filter_by.splice(i, 1)
|
||||
this.params.filter_comparator.splice(i, 1)
|
||||
this.params.filter_value.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.params.filter_by.push('done')
|
||||
this.params.filter_comparator.push('equals')
|
||||
this.params.filter_value.push('false')
|
||||
}
|
||||
this.change()
|
||||
},
|
||||
setDueDateFilter() {
|
||||
// Only filter if we have a start and end due date
|
||||
if (this.filters.dueDate !== '') {
|
||||
|
||||
const parts = this.filters.dueDate.split(' to ')
|
||||
|
||||
if(parts.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we already have values in params and only update them if we do
|
||||
let foundStart = false
|
||||
let foundEnd = false
|
||||
this.params.filter_by.forEach((f, i) => {
|
||||
if (f === 'due_date' && this.params.filter_comparator[i] === 'greater_equals') {
|
||||
foundStart = true
|
||||
this.params.filter_value[i] = +new Date(parts[0]) / 1000
|
||||
}
|
||||
if (f === 'due_date' && this.params.filter_comparator[i] === 'less_equals') {
|
||||
foundEnd = true
|
||||
this.params.filter_value[i] = +new Date(parts[1]) / 1000
|
||||
}
|
||||
})
|
||||
|
||||
if (!foundStart) {
|
||||
this.params.filter_by.push('due_date')
|
||||
this.params.filter_comparator.push('greater_equals')
|
||||
this.params.filter_value.push(+new Date(parts[0]) / 1000)
|
||||
}
|
||||
if (!foundEnd) {
|
||||
this.params.filter_by.push('due_date')
|
||||
this.params.filter_comparator.push('less_equals')
|
||||
this.params.filter_value.push(+new Date(parts[1]) / 1000)
|
||||
}
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,7 @@
|
|||
<template>
|
||||
<div class="loader-container" :class="{ 'is-loading': taskCollectionService.loading}">
|
||||
<div class="filter-container">
|
||||
<div class="items">
|
||||
<div class="search">
|
||||
<div class="field has-addons" :class="{ 'hidden': !showTaskSearch }">
|
||||
<div class="control has-icons-left has-icons-right">
|
||||
|
@ -30,11 +32,33 @@
|
|||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="button" @click="showTaskFilter = !showTaskFilter">
|
||||
<span class="icon is-small">
|
||||
<icon icon="filter"/>
|
||||
</span>
|
||||
Filters
|
||||
</button>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<filters
|
||||
v-if="showTaskFilter"
|
||||
v-model="params"
|
||||
@change="loadTasks(1)"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="field task-add" v-if="!list.isArchived">
|
||||
<div class="field is-grouped">
|
||||
<p class="control has-icons-left is-expanded" :class="{ 'is-loading': taskService.loading}">
|
||||
<input v-focus class="input" :class="{ 'disabled': taskService.loading}" v-model="newTaskText" type="text" placeholder="Add a new task..." @keyup.enter="addTask()"/>
|
||||
<input
|
||||
v-focus
|
||||
class="input"
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
v-model="newTaskText"
|
||||
type="text"
|
||||
placeholder="Add a new task..."
|
||||
@keyup.enter="addTask()"/>
|
||||
<span class="icon is-small is-left">
|
||||
<icon icon="tasks"/>
|
||||
</span>
|
||||
|
@ -85,14 +109,36 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="pagination is-centered" role="navigation" aria-label="pagination" v-if="taskCollectionService.totalPages > 1">
|
||||
<router-link class="pagination-previous" :to="getRouteForPagination(currentPage - 1)" tag="button" :disabled="currentPage === 1">Previous</router-link>
|
||||
<router-link class="pagination-next" :to="getRouteForPagination(currentPage + 1)" tag="button" :disabled="currentPage === taskCollectionService.totalPages">Next page</router-link>
|
||||
<nav
|
||||
class="pagination is-centered"
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
v-if="taskCollectionService.totalPages > 1">
|
||||
<router-link
|
||||
class="pagination-previous"
|
||||
:to="getRouteForPagination(currentPage - 1)"
|
||||
tag="button"
|
||||
:disabled="currentPage === 1">
|
||||
Previous
|
||||
</router-link>
|
||||
<router-link
|
||||
class="pagination-next"
|
||||
:to="getRouteForPagination(currentPage + 1)"
|
||||
tag="button"
|
||||
:disabled="currentPage === taskCollectionService.totalPages">
|
||||
Next page
|
||||
</router-link>
|
||||
<ul class="pagination-list">
|
||||
<template v-for="(p, i) in pages">
|
||||
<li :key="'page'+i" v-if="p.isEllipsis"><span class="pagination-ellipsis">…</span></li>
|
||||
<li :key="'page'+i" v-else>
|
||||
<router-link :to="getRouteForPagination(p.number)" :class="{'is-current': p.number === currentPage}" class="pagination-link" :aria-label="'Goto page ' + p.number">{{ p.number }}</router-link>
|
||||
<router-link
|
||||
:to="getRouteForPagination(p.number)"
|
||||
:class="{'is-current': p.number === currentPage}"
|
||||
class="pagination-link"
|
||||
:aria-label="'Goto page ' + p.number">
|
||||
{{ p.number }}
|
||||
</router-link>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
@ -113,6 +159,7 @@
|
|||
import SingleTaskInList from '../../tasks/reusable/singleTaskInList'
|
||||
import taskList from '../../tasks/helpers/taskList'
|
||||
import {saveListView} from '../../../helpers/saveListView'
|
||||
import Filters from '../reusable/filters'
|
||||
|
||||
export default {
|
||||
name: 'List',
|
||||
|
@ -131,6 +178,7 @@
|
|||
taskList,
|
||||
],
|
||||
components: {
|
||||
Filters,
|
||||
SingleTaskInList,
|
||||
EditTask,
|
||||
},
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
<template>
|
||||
<div class="table-view loader-container" :class="{'is-loading': taskCollectionService.loading}">
|
||||
<div class="column-filter">
|
||||
<button class="button" @click="showActiveColumnsFilter = !showActiveColumnsFilter">
|
||||
<div class="filter-container">
|
||||
<div class="items">
|
||||
<button class="button" @click="() => {showActiveColumnsFilter = !showActiveColumnsFilter; showTaskFilter = false}">
|
||||
<span class="icon is-small">
|
||||
<icon icon="th"/>
|
||||
</span>
|
||||
Columns
|
||||
</button>
|
||||
<button class="button" @click="() => {showTaskFilter = !showTaskFilter; showActiveColumnsFilter = false}">
|
||||
<span class="icon is-small">
|
||||
<icon icon="filter"/>
|
||||
</span>
|
||||
Filters
|
||||
</button>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div class="card" v-if="showActiveColumnsFilter">
|
||||
<div class="card-content">
|
||||
|
@ -25,6 +33,11 @@
|
|||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.createdBy">Created By</fancycheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<filters
|
||||
v-if="showTaskFilter"
|
||||
v-model="params"
|
||||
@change="loadTasks(1)"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
|
@ -155,10 +168,12 @@
|
|||
import Fancycheckbox from '../../global/fancycheckbox'
|
||||
import Sort from '../../tasks/reusable/sort'
|
||||
import {saveListView} from '../../../helpers/saveListView'
|
||||
import Filters from '../reusable/filters'
|
||||
|
||||
export default {
|
||||
name: 'Table',
|
||||
components: {
|
||||
Filters,
|
||||
Sort,
|
||||
Fancycheckbox,
|
||||
DateTableCell,
|
||||
|
@ -202,6 +217,10 @@
|
|||
this.$set(this, 'sortBy', JSON.parse(savedSortBy))
|
||||
}
|
||||
|
||||
this.$set(this.params, 'filter_by', [])
|
||||
this.$set(this.params, 'filter_value', [])
|
||||
this.$set(this.params, 'filter_comparator', [])
|
||||
|
||||
this.initTasks(1)
|
||||
|
||||
// Save the current list view to local storage
|
||||
|
@ -210,7 +229,9 @@
|
|||
},
|
||||
methods: {
|
||||
initTasks(page, search = '') {
|
||||
let params = {sort_by: [], order_by: []}
|
||||
const params = this.params
|
||||
params.sort_by = []
|
||||
params.order_by = []
|
||||
Object.keys(this.sortBy).map(s => {
|
||||
params.sort_by.push(s)
|
||||
params.order_by.push(this.sortBy[s])
|
||||
|
|
|
@ -14,6 +14,16 @@ export default {
|
|||
|
||||
showTaskSearch: false,
|
||||
searchTerm: '',
|
||||
|
||||
showTaskFilter: false,
|
||||
params: {
|
||||
sort_by: ['done', 'id'],
|
||||
order_by: ['asc', 'desc'],
|
||||
filter_by: ['done'],
|
||||
filter_value: ['false'],
|
||||
filter_comparator: ['equals'],
|
||||
filter_concat: 'and',
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -28,7 +38,11 @@ export default {
|
|||
this.taskCollectionService = new TaskCollectionService()
|
||||
},
|
||||
methods: {
|
||||
loadTasks(page, search = '', params = {sort_by: ['done', 'id'], order_by: ['asc', 'desc']}) {
|
||||
loadTasks(
|
||||
page,
|
||||
search = '',
|
||||
params = null,
|
||||
) {
|
||||
|
||||
// Because this function is triggered every time on navigation, we're putting a condition here to only load it when we actually want to show tasks
|
||||
// FIXME: This is a bit hacky -> Cleanup.
|
||||
|
@ -41,6 +55,10 @@ export default {
|
|||
|
||||
this.$set(this, 'tasks', [])
|
||||
|
||||
if (params === null) {
|
||||
params = this.params
|
||||
}
|
||||
|
||||
if (search !== '') {
|
||||
params.s = search
|
||||
}
|
||||
|
@ -142,6 +160,6 @@ export default {
|
|||
page: page,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
|
@ -66,6 +66,7 @@ import { faSort } from '@fortawesome/free-solid-svg-icons'
|
|||
import { faSortUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faList } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faFilter } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faComments } from '@fortawesome/free-regular-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
|
@ -111,6 +112,7 @@ library.add(faSort)
|
|||
library.add(faSortUp)
|
||||
library.add(faList)
|
||||
library.add(faEllipsisV)
|
||||
library.add(faFilter)
|
||||
|
||||
Vue.component('icon', FontAwesomeIcon)
|
||||
|
||||
|
|
|
@ -37,13 +37,64 @@
|
|||
height: 40px;
|
||||
}
|
||||
|
||||
.search {
|
||||
max-width: 300px;
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: $grey-dark;
|
||||
margin-left: 1rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-list {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
min-width: 400px;
|
||||
max-width: 180px;
|
||||
position: absolute;
|
||||
right: 1.5em;
|
||||
margin-top: -58px;
|
||||
z-index: 99;
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.button:not(:last-child) {
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
height: $switch-view-height;
|
||||
}
|
||||
|
||||
.card {
|
||||
text-align: left;
|
||||
margin-top: calc(1rem - 1px);
|
||||
float: right;
|
||||
}
|
||||
|
||||
.fancycheckbox {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-right: .5em;
|
||||
|
||||
.button, .input {
|
||||
height: $switch-view-height;
|
||||
|
@ -66,22 +117,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: $grey-dark;
|
||||
margin-left: 1rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
.filters input {
|
||||
font-size: .9em;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-list {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
|
|
@ -13,26 +13,5 @@
|
|||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.column-filter {
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
position: absolute;
|
||||
right: 1.5em;
|
||||
margin-top: -58px;
|
||||
|
||||
.button {
|
||||
height: $switch-view-height;
|
||||
}
|
||||
|
||||
.card {
|
||||
text-align: left;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.fancycheckbox {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue