feat: add button to clear active filters (#924)

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/924
Reviewed-by: dpschen <dpschen@noreply.kolaente.de>
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
This commit is contained in:
konrad 2021-11-13 19:48:06 +00:00
parent 73651ef964
commit 31f0c384ac
11 changed files with 227 additions and 174 deletions

View file

@ -219,10 +219,10 @@ describe('Lists', () => {
cy.get('.table-view .filter-container .items .button') cy.get('.table-view .filter-container .items .button')
.contains('Columns') .contains('Columns')
.click() .click()
cy.get('.table-view .filter-container .card .card-content .fancycheckbox .check') cy.get('.table-view .filter-container .card.columns-filter .card-content .fancycheckbox .check')
.contains('Priority') .contains('Priority')
.click() .click()
cy.get('.table-view .filter-container .card .card-content .fancycheckbox .check') cy.get('.table-view .filter-container .card.columns-filter .card-content .fancycheckbox .check')
.contains('Done') .contains('Done')
.click() .click()

View file

@ -1,37 +1,49 @@
<template> <template>
<transition name="fade"> <x-button
<filters v-if="hasFilters"
v-if="visibleInternal" type="secondary"
v-model="value" @click="clearFilters"
ref="filters" >
/> {{ $t('filters.clear') }}
</transition> </x-button>
<popup>
<template #trigger="{toggle}">
<x-button
@click.prevent.stop="toggle()"
type="secondary"
icon="filter"
>
{{ $t('filters.title') }}
</x-button>
</template>
<template #content="{isOpen}">
<filters
v-model="value"
ref="filters"
class="filter-popup"
:class="{'is-open': isOpen}"
/>
</template>
</popup>
</template> </template>
<script> <script>
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import Filters from '@/components/list/partials/filters'
import Filters from '../../../components/list/partials/filters' import {getDefaultParams} from '@/components/tasks/mixins/taskList'
import Popup from '@/components/misc/popup'
export default { export default {
name: 'filter-popup', name: 'filter-popup',
components: { components: {
Popup,
Filters, Filters,
}, },
props: { props: {
modelValue: { modelValue: {
required: true, required: true,
}, },
visible: {
type: Boolean,
default: false,
},
}, },
emits: ['update:modelValue'], emits: ['update:modelValue'],
data() {
return {
visibleInternal: false,
}
},
computed: { computed: {
value: { value: {
get() { get() {
@ -41,34 +53,46 @@ export default {
this.$emit('update:modelValue', value) this.$emit('update:modelValue', value)
}, },
}, },
}, hasFilters() {
mounted() { // this.value also contains the page parameter which we don't want to include in filters
document.addEventListener('click', this.hidePopup) // eslint-disable-next-line no-unused-vars
}, const {filter_by, filter_value, filter_comparator, filter_concat, s} = this.value
beforeUnmount() { const def = {...getDefaultParams()}
document.removeEventListener('click', this.hidePopup)
const params = {filter_by, filter_value, filter_comparator, filter_concat, s}
const defaultParams = {
filter_by: def.filter_by,
filter_value: def.filter_value,
filter_comparator: def.filter_comparator,
filter_concat: def.filter_concat,
s: s ? def.s : undefined,
}
return JSON.stringify(params) !== JSON.stringify(defaultParams)
},
}, },
watch: { watch: {
modelValue: { modelValue: {
handler(value) { handler(value) {
this.params = value this.value = value
}, },
immediate: true, immediate: true,
}, },
visible() {
this.visibleInternal = !this.visibleInternal
},
}, },
methods: { methods: {
hidePopup(e) { clearFilters() {
if (!this.visibleInternal) { this.value = {...getDefaultParams()}
return
}
closeWhenClickedOutside(e, this.$refs.filters.$el, () => {
this.visibleInternal = false
})
}, },
}, },
} }
</script> </script>
<style scoped lang="scss">
.filter-popup {
margin: 0;
&.is-open {
margin: 2rem 0 1rem;
}
}
</style>

View file

@ -458,15 +458,7 @@ export default {
return return
} }
let foundDone = false this.filters.done = this.params.filter_by.some((f) => f === 'done') === false
this.params.filter_by.forEach((f, i) => {
if (f === 'done') {
foundDone = i
}
})
if (foundDone === false) {
this.filters.done = true
}
}, },
async prepareRelatedObjectFilter(kind, filterName = null, servicePrefix = null) { async prepareRelatedObjectFilter(kind, filterName = null, servicePrefix = null) {
if (filterName === null) { if (filterName === null) {

View file

@ -0,0 +1,54 @@
<template>
<slot name="trigger" :isOpen="open" :toggle="toggle"></slot>
<div class="popup" :class="{'is-open': open}" ref="popup">
<slot name="content" :isOpen="open"/>
</div>
</template>
<script setup>
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {onBeforeUnmount, onMounted, ref} from 'vue'
const open = ref(false)
const popup = ref(null)
const toggle = () => {
open.value = !open.value
}
function hidePopup(e) {
if (!open.value) {
return
}
// we actually want to use popup.$el, not its value.
// eslint-disable-next-line vue/no-ref-as-operand
closeWhenClickedOutside(e, popup.value, () => {
open.value = false
})
}
onMounted(() => {
document.addEventListener('click', hidePopup)
})
onBeforeUnmount(() => {
document.removeEventListener('click', hidePopup)
})
</script>
<style scoped lang="scss">
.popup {
transition: opacity $transition;
opacity: 0;
height: 0;
overflow: hidden;
position: absolute;
top: 1rem;
&.is-open {
opacity: 1;
height: auto;
}
}
</style>

View file

@ -9,12 +9,12 @@
> >
{{ $t('filters.title') }} {{ $t('filters.title') }}
</x-button> </x-button>
<filter-popup
:visible="showTaskFilter"
v-model="params"
@update:modelValue="loadTasks()"
/>
</div> </div>
<filter-popup
:visible="showTaskFilter"
v-model="params"
@update:modelValue="loadTasks()"
/>
</div> </div>
<div class="dates"> <div class="dates">
<template v-for="(y, yk) in days" :key="yk + 'year'"> <template v-for="(y, yk) in days" :key="yk + 'year'">
@ -347,7 +347,7 @@ export default {
return return
} }
let newTask = { ...taskDragged } let newTask = {...taskDragged}
const didntHaveDates = newTask.startDate === null ? true : false const didntHaveDates = newTask.startDate === null ? true : false

View file

@ -1,14 +1,14 @@
import TaskCollectionService from '@/services/taskCollection' import TaskCollectionService from '@/services/taskCollection'
// FIXME: merge with DEFAULT_PARAMS in filters.vue // FIXME: merge with DEFAULT_PARAMS in filters.vue
const DEFAULT_PARAMS = { export const getDefaultParams = () => ({
sort_by: ['position', 'id'], sort_by: ['position', 'id'],
order_by: ['asc', 'desc'], order_by: ['asc', 'desc'],
filter_by: ['done'], filter_by: ['done'],
filter_value: ['false'], filter_value: ['false'],
filter_comparator: ['equals'], filter_comparator: ['equals'],
filter_concat: 'and', filter_concat: 'and',
} })
/** /**
* 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.
@ -26,7 +26,7 @@ export default {
searchTerm: '', searchTerm: '',
showTaskFilter: false, showTaskFilter: false,
params: DEFAULT_PARAMS, params: {...getDefaultParams()},
} }
}, },
watch: { watch: {
@ -94,7 +94,7 @@ export default {
this.initTasks(page, search) this.initTasks(page, search)
}, },
loadTasksOnSavedFilter() { loadTasksOnSavedFilter() {
if(typeof this.$route.params.listId !== 'undefined' && parseInt(this.$route.params.listId) < 0) { if (typeof this.$route.params.listId !== 'undefined' && parseInt(this.$route.params.listId) < 0) {
this.loadTasks(1, '', null, true) this.loadTasks(1, '', null, true)
} }
}, },

View file

@ -344,6 +344,7 @@
}, },
"filters": { "filters": {
"title": "Filters", "title": "Filters",
"clear": "Clear Filters",
"attributes": { "attributes": {
"title": "Title", "title": "Title",
"titlePlaceholder": "The saved filter title goes here…", "titlePlaceholder": "The saved filter title goes here…",

View file

@ -34,7 +34,6 @@ $filter-container-top-link-share-list: -47px;
.card { .card {
text-align: left; text-align: left;
margin-top: calc(1rem - 1px);
} }
.fancycheckbox { .fancycheckbox {
@ -47,10 +46,6 @@ $filter-container-top-link-share-list: -47px;
justify-content: space-between; justify-content: space-between;
margin-right: .5rem; margin-right: .5rem;
.button, .input {
height: $switch-view-height;
}
.field { .field {
transition: width $transition; transition: width $transition;
width: 100%; width: 100%;

View file

@ -2,18 +2,11 @@
<div class="kanban-view"> <div class="kanban-view">
<div class="filter-container" v-if="isSavedFilter"> <div class="filter-container" v-if="isSavedFilter">
<div class="items"> <div class="items">
<x-button <filter-popup
@click.prevent.stop="toggleFilterPopup" v-model="params"
icon="filter" @update:modelValue="loadBuckets"
type="secondary" />
>
{{ $t('filters.title') }}
</x-button>
</div> </div>
<filter-popup
:visible="showFilters"
v-model="params"
/>
</div> </div>
<div <div
:class="{ 'is-loading': loading && !oneTaskUpdating}" :class="{ 'is-loading': loading && !oneTaskUpdating}"
@ -143,7 +136,7 @@
:component-data="taskDraggableTaskComponentData" :component-data="taskDraggableTaskComponentData"
> >
<template #item="{element: task}"> <template #item="{element: task}">
<kanban-card :task="task" /> <kanban-card :task="task"/>
</template> </template>
</draggable> </draggable>
</div> </div>
@ -213,7 +206,7 @@
<!-- This router view is used to show the task popup while keeping the kanban board itself --> <!-- This router view is used to show the task popup while keeping the kanban board itself -->
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<transition name="modal"> <transition name="modal">
<component :is="Component" /> <component :is="Component"/>
</transition> </transition>
</router-view> </router-view>
@ -224,10 +217,10 @@
v-if="showBucketDeleteModal" v-if="showBucketDeleteModal"
> >
<template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template> <template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template>
<template #text> <template #text>
<p>{{ $t('list.kanban.deleteBucketText1') }}<br/> <p>{{ $t('list.kanban.deleteBucketText1') }}<br/>
{{ $t('list.kanban.deleteBucketText2') }}</p> {{ $t('list.kanban.deleteBucketText2') }}</p>
</template> </template>
</modal> </modal>
</transition> </transition>
@ -300,7 +293,6 @@ export default {
filter_comparator: [], filter_comparator: [],
filter_concat: 'and', filter_concat: 'and',
}, },
showFilters: false,
} }
}, },
created() { created() {
@ -328,10 +320,10 @@ export default {
return { return {
type: 'transition', type: 'transition',
tag: 'div', tag: 'div',
name: !this.dragBucket ? 'move-bucket': null, name: !this.dragBucket ? 'move-bucket' : null,
class: [ class: [
'kanban-bucket-container', 'kanban-bucket-container',
{ 'dragging-disabled': !this.canWrite }, {'dragging-disabled': !this.canWrite},
], ],
} }
}, },
@ -339,10 +331,10 @@ export default {
return { return {
type: 'transition', type: 'transition',
tag: 'div', tag: 'div',
name: !this.drag ? 'move-card': null, name: !this.drag ? 'move-card' : null,
class: [ class: [
'dropper', 'dropper',
{ 'dragging-disabled': !this.canWrite }, {'dragging-disabled': !this.canWrite},
], ],
} }
}, },
@ -357,19 +349,15 @@ export default {
list: state => state.currentList, list: state => state.currentList,
}), }),
}, },
methods: {
toggleFilterPopup() {
this.showFilters = !this.showFilters
},
methods: {
loadBuckets() { loadBuckets() {
// Prevent trying to load buckets if the task popup view is active // Prevent trying to load buckets if the task popup view is active
if (this.$route.name !== 'list.kanban') { if (this.$route.name !== 'list.kanban') {
return return
} }
const { listId, params } = this.loadBucketParameter const {listId, params} = this.loadBucketParameter
this.collapsedBuckets = getCollapsedBucketState(listId) this.collapsedBuckets = getCollapsedBucketState(listId)
@ -424,7 +412,7 @@ export default {
const newTask = cloneDeep(task) // cloning the task to avoid vuex store mutations const newTask = cloneDeep(task) // cloning the task to avoid vuex store mutations
newTask.bucketId = newBucket.id, newTask.bucketId = newBucket.id,
newTask.kanbanPosition = calculateItemPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null) newTask.kanbanPosition = calculateItemPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null)
try { try {
await this.$store.dispatch('tasks/update', newTask) await this.$store.dispatch('tasks/update', newTask)

View file

@ -41,19 +41,11 @@
v-if="!showTaskSearch" v-if="!showTaskSearch"
/> />
</div> </div>
<x-button <filter-popup
@click.prevent.stop="showTaskFilter = !showTaskFilter" v-model="params"
type="secondary" @update:modelValue="loadTasks()"
icon="filter" />
>
{{ $t('filters.title') }}
</x-button>
</div> </div>
<filter-popup
:visible="showTaskFilter"
v-model="params"
@update:modelValue="loadTasks()"
/>
</div> </div>
<card :padding="false" :has-content="false" class="has-overflow"> <card :padding="false" :has-content="false" class="has-overflow">
@ -126,7 +118,7 @@
/> />
</div> </div>
<Pagination <Pagination
:total-pages="taskCollectionService.totalPages" :total-pages="taskCollectionService.totalPages"
:current-page="currentPage" :current-page="currentPage"
/> />
@ -135,7 +127,7 @@
<!-- This router view is used to show the task popup while keeping the kanban board itself --> <!-- This router view is used to show the task popup while keeping the kanban board itself -->
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<transition name="modal"> <transition name="modal">
<component :is="Component" /> <component :is="Component"/>
</transition> </transition>
</router-view> </router-view>
</div> </div>
@ -155,6 +147,7 @@ import FilterPopup from '@/components/list/partials/filter-popup.vue'
import {HAS_TASKS} from '@/store/mutation-types' import {HAS_TASKS} from '@/store/mutation-types'
import Nothing from '@/components/misc/nothing.vue' import Nothing from '@/components/misc/nothing.vue'
import Pagination from '@/components/misc/pagination.vue' import Pagination from '@/components/misc/pagination.vue'
import Popup from '@/components/misc/popup'
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
import {calculateItemPosition} from '../../../helpers/calculateItemPosition' import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
@ -198,6 +191,7 @@ export default {
taskList, taskList,
], ],
components: { components: {
Popup,
Nothing, Nothing,
FilterPopup, FilterPopup,
SingleTaskInList, SingleTaskInList,
@ -294,11 +288,11 @@ export default {
async saveTaskPosition(e) { async saveTaskPosition(e) {
this.drag = false this.drag = false
const task = this.tasks[e.newIndex] const task = this.tasks[e.newIndex]
const taskBefore = this.tasks[e.newIndex - 1] ?? null const taskBefore = this.tasks[e.newIndex - 1] ?? null
const taskAfter = this.tasks[e.newIndex + 1] ?? null const taskAfter = this.tasks[e.newIndex + 1] ?? null
const newTask = { const newTask = {
...task, ...task,
position: calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null), position: calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null),

View file

@ -2,67 +2,63 @@
<div :class="{'is-loading': taskCollectionService.loading}" class="table-view loader-container"> <div :class="{'is-loading': taskCollectionService.loading}" class="table-view loader-container">
<div class="filter-container"> <div class="filter-container">
<div class="items"> <div class="items">
<x-button <popup>
@click.prevent.stop="() => {showActiveColumnsFilter = !showActiveColumnsFilter; showTaskFilter = false}" <template #trigger="{toggle}">
icon="th" <x-button
type="secondary" @click.prevent.stop="toggle()"
> icon="th"
{{ $t('list.table.columns') }} type="secondary"
</x-button> >
<x-button {{ $t('list.table.columns') }}
@click.prevent.stop="() => {showTaskFilter = !showTaskFilter; showActiveColumnsFilter = false}" </x-button>
icon="filter" </template>
type="secondary" <template #content="{isOpen}">
> <card class="columns-filter" :class="{'is-open': isOpen}">
{{ $t('filters.title') }} <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.id">#</fancycheckbox>
</x-button> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.done">
{{ $t('task.attributes.done') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.title">
{{ $t('task.attributes.title') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.priority">
{{ $t('task.attributes.priority') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.labels">
{{ $t('task.attributes.labels') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.assignees">
{{ $t('task.attributes.assignees') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.dueDate">
{{ $t('task.attributes.dueDate') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.startDate">
{{ $t('task.attributes.startDate') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.endDate">
{{ $t('task.attributes.endDate') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.percentDone">
{{ $t('task.attributes.percentDone') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.created">
{{ $t('task.attributes.created') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.updated">
{{ $t('task.attributes.updated') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.createdBy">
{{ $t('task.attributes.createdBy') }}
</fancycheckbox>
</card>
</template>
</popup>
<filter-popup
v-model="params"
@update:modelValue="loadTasks()"
/>
</div> </div>
<transition name="fade">
<card v-if="showActiveColumnsFilter">
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.id">#</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.done">
{{ $t('task.attributes.done') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.title">
{{ $t('task.attributes.title') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.priority">
{{ $t('task.attributes.priority') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.labels">
{{ $t('task.attributes.labels') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.assignees">
{{ $t('task.attributes.assignees') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.dueDate">
{{ $t('task.attributes.dueDate') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.startDate">
{{ $t('task.attributes.startDate') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.endDate">
{{ $t('task.attributes.endDate') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.percentDone">
{{ $t('task.attributes.percentDone') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.created">
{{ $t('task.attributes.created') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.updated">
{{ $t('task.attributes.updated') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.createdBy">
{{ $t('task.attributes.createdBy') }}
</fancycheckbox>
</card>
</transition>
<filter-popup
:visible="showTaskFilter"
v-model="params"
@update:modelValue="loadTasks()"
/>
</div> </div>
<card :padding="false" :has-content="false"> <card :padding="false" :has-content="false">
@ -189,21 +185,23 @@
</template> </template>
<script> <script>
import taskList from '../../../components/tasks/mixins/taskList' import taskList from '@/components/tasks/mixins/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'
import Labels from '../../../components/tasks/partials/labels' import Labels from '@/components/tasks/partials/labels'
import DateTableCell from '../../../components/tasks/partials/date-table-cell' import DateTableCell from '@/components/tasks/partials/date-table-cell'
import Fancycheckbox from '../../../components/input/fancycheckbox' import Fancycheckbox from '@/components/input/fancycheckbox'
import Sort from '../../../components/tasks/partials/sort' import Sort from '@/components/tasks/partials/sort'
import {saveListView} from '@/helpers/saveListView' import {saveListView} from '@/helpers/saveListView'
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'
export default { export default {
name: 'Table', name: 'Table',
components: { components: {
Popup,
Done, Done,
FilterPopup, FilterPopup,
Sort, Sort,
@ -219,7 +217,6 @@ export default {
], ],
data() { data() {
return { return {
showActiveColumnsFilter: false,
activeColumns: { activeColumns: {
id: true, id: true,
done: true, done: true,
@ -323,4 +320,12 @@ export default {
} }
} }
} }
.columns-filter {
margin: 0;
&.is-open {
margin: 2rem 0 1rem;
}
}
</style> </style>