Saved filters (#239)
Fix saving Cleanup Fix single value prepare Add prepare percent done stub Fix populating filters with saved values when editing for single values Fix populating filters with saved values when editing Add edit filter view page Hide adding new tasks to pseudolists Make sure all filter values are passed as strings as per requirement from the api Add redirect to list after creating it Add creating saved filter Add filter by percent done Add end date filter Add start date filter Add extra checkbox to enable/disable priority filter Add changing priority Add more filter stubs Fix dates for filters Add saved filter create form Add include nulls and concat to filter options Add new saved filter component Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/239 Co-Authored-By: konrad <konrad@kola-entertainments.de> Co-Committed-By: konrad <konrad@kola-entertainments.de>
This commit is contained in:
parent
06524b5cc9
commit
6b1ebbabb7
12 changed files with 676 additions and 44 deletions
|
@ -1,6 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card filters">
|
<div class="card filters">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
<fancycheckbox v-model="params.filter_include_nulls">
|
||||||
|
Include Tasks which don't have a value set
|
||||||
|
</fancycheckbox>
|
||||||
|
<fancycheckbox
|
||||||
|
v-model="filters.requireAllFilters"
|
||||||
|
@change="setFilterConcat()"
|
||||||
|
>
|
||||||
|
Require all filters to be true for a task to show up
|
||||||
|
</fancycheckbox>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Show Done Tasks</label>
|
<label class="label">Show Done Tasks</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
@ -21,6 +30,62 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Priority</label>
|
||||||
|
<div class="control single-value-control">
|
||||||
|
<priority-select
|
||||||
|
:disabled="!filters.usePriority"
|
||||||
|
v-model.number="filters.priority"
|
||||||
|
@change="setPriority"
|
||||||
|
/>
|
||||||
|
<fancycheckbox
|
||||||
|
v-model="filters.usePriority"
|
||||||
|
@change="setPriority"
|
||||||
|
>
|
||||||
|
Enable Filter By Priority
|
||||||
|
</fancycheckbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Start Date</label>
|
||||||
|
<div class="control">
|
||||||
|
<flat-pickr
|
||||||
|
:config="flatPickerConfig"
|
||||||
|
@on-close="setStartDateFilter"
|
||||||
|
class="input"
|
||||||
|
placeholder="Start Date Range"
|
||||||
|
v-model="filters.startDate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">End Date</label>
|
||||||
|
<div class="control">
|
||||||
|
<flat-pickr
|
||||||
|
:config="flatPickerConfig"
|
||||||
|
@on-close="setEndDateFilter"
|
||||||
|
class="input"
|
||||||
|
placeholder="End Date Range"
|
||||||
|
v-model="filters.endDate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Percent Done</label>
|
||||||
|
<div class="control single-value-control">
|
||||||
|
<percent-done-select
|
||||||
|
v-model.number="filters.percentDone"
|
||||||
|
@change="setPercentDoneFilter"
|
||||||
|
:disabled="!filters.usePercentDone"
|
||||||
|
/>
|
||||||
|
<fancycheckbox
|
||||||
|
v-model="filters.usePercentDone"
|
||||||
|
@change="setPercentDoneFilter"
|
||||||
|
>
|
||||||
|
Enable Filter By Percent Done
|
||||||
|
</fancycheckbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -30,11 +95,17 @@ import Fancycheckbox from '../../input/fancycheckbox'
|
||||||
import flatPickr from 'vue-flatpickr-component'
|
import flatPickr from 'vue-flatpickr-component'
|
||||||
import 'flatpickr/dist/flatpickr.css'
|
import 'flatpickr/dist/flatpickr.css'
|
||||||
|
|
||||||
|
import {formatISO} from 'date-fns'
|
||||||
|
import PrioritySelect from '@/components/tasks/partials/prioritySelect'
|
||||||
|
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'filters',
|
name: 'filters',
|
||||||
components: {
|
components: {
|
||||||
|
PrioritySelect,
|
||||||
Fancycheckbox,
|
Fancycheckbox,
|
||||||
flatPickr,
|
flatPickr,
|
||||||
|
PercentDoneSelect,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -44,10 +115,19 @@ export default {
|
||||||
filter_by: [],
|
filter_by: [],
|
||||||
filter_value: [],
|
filter_value: [],
|
||||||
filter_comparator: [],
|
filter_comparator: [],
|
||||||
|
filter_include_nulls: true,
|
||||||
|
filter_concat: 'or',
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
done: false,
|
done: false,
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
|
requireAllFilters: false,
|
||||||
|
priority: 0,
|
||||||
|
usePriority: false,
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
percentDone: 0,
|
||||||
|
usePercentDone: false,
|
||||||
},
|
},
|
||||||
flatPickerConfig: {
|
flatPickerConfig: {
|
||||||
altFormat: 'j M Y H:i',
|
altFormat: 'j M Y H:i',
|
||||||
|
@ -61,7 +141,8 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.params = this.value
|
this.params = this.value
|
||||||
this.prepareDone()
|
this.filters.requireAllFilters = this.params.filter_concat === 'and'
|
||||||
|
this.prepareFilters()
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
|
@ -71,7 +152,7 @@ export default {
|
||||||
watch: {
|
watch: {
|
||||||
value(newVal) {
|
value(newVal) {
|
||||||
this.$set(this, 'params', newVal)
|
this.$set(this, 'params', newVal)
|
||||||
this.prepareDone()
|
this.prepareFilters()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -79,42 +160,29 @@ export default {
|
||||||
this.$emit('input', this.params)
|
this.$emit('input', this.params)
|
||||||
this.$emit('change', this.params)
|
this.$emit('change', this.params)
|
||||||
},
|
},
|
||||||
prepareDone() {
|
prepareFilters() {
|
||||||
// Set filters.done based on params
|
this.prepareDone()
|
||||||
if (typeof this.params.filter_by !== 'undefined') {
|
this.prepareDueDate()
|
||||||
let foundDone = false
|
this.prepareStartDate()
|
||||||
this.params.filter_by.forEach((f, i) => {
|
this.prepareEndDate()
|
||||||
if (f === 'done') {
|
this.preparePriority()
|
||||||
foundDone = i
|
this.preparePercentDone()
|
||||||
}
|
},
|
||||||
})
|
removePropertyFromFilter(propertyName) {
|
||||||
if (foundDone === false) {
|
for (const i in this.params.filter_by) {
|
||||||
this.filters.done = true
|
if (this.params.filter_by[i] === propertyName) {
|
||||||
|
this.params.filter_by.splice(i, 1)
|
||||||
|
this.params.filter_comparator.splice(i, 1)
|
||||||
|
this.params.filter_value.splice(i, 1)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setDoneFilter() {
|
setDateFilter(filterName, variableName) {
|
||||||
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
|
// Only filter if we have a start and end due date
|
||||||
if (this.filters.dueDate !== '') {
|
if (this.filters[variableName] !== '') {
|
||||||
|
|
||||||
const parts = this.filters.dueDate.split(' to ')
|
const parts = this.filters[variableName].split(' to ')
|
||||||
|
|
||||||
if (parts.length < 2) {
|
if (parts.length < 2) {
|
||||||
return
|
return
|
||||||
|
@ -124,29 +192,173 @@ export default {
|
||||||
let foundStart = false
|
let foundStart = false
|
||||||
let foundEnd = false
|
let foundEnd = false
|
||||||
this.params.filter_by.forEach((f, i) => {
|
this.params.filter_by.forEach((f, i) => {
|
||||||
if (f === 'due_date' && this.params.filter_comparator[i] === 'greater_equals') {
|
if (f === filterName && this.params.filter_comparator[i] === 'greater_equals') {
|
||||||
foundStart = true
|
foundStart = true
|
||||||
this.params.filter_value[i] = +new Date(parts[0]) / 1000
|
this.$set(this.params.filter_value, i, formatISO(new Date(parts[0])))
|
||||||
}
|
}
|
||||||
if (f === 'due_date' && this.params.filter_comparator[i] === 'less_equals') {
|
if (f === filterName && this.params.filter_comparator[i] === 'less_equals') {
|
||||||
foundEnd = true
|
foundEnd = true
|
||||||
this.params.filter_value[i] = +new Date(parts[1]) / 1000
|
this.$set(this.params.filter_value, i, formatISO(new Date(parts[1])))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!foundStart) {
|
if (!foundStart) {
|
||||||
this.params.filter_by.push('due_date')
|
this.params.filter_by.push(filterName)
|
||||||
this.params.filter_comparator.push('greater_equals')
|
this.params.filter_comparator.push('greater_equals')
|
||||||
this.params.filter_value.push(+new Date(parts[0]) / 1000)
|
this.params.filter_value.push(formatISO(new Date(parts[0])))
|
||||||
}
|
}
|
||||||
if (!foundEnd) {
|
if (!foundEnd) {
|
||||||
this.params.filter_by.push('due_date')
|
this.params.filter_by.push(filterName)
|
||||||
this.params.filter_comparator.push('less_equals')
|
this.params.filter_comparator.push('less_equals')
|
||||||
this.params.filter_value.push(+new Date(parts[1]) / 1000)
|
this.params.filter_value.push(formatISO(new Date(parts[1])))
|
||||||
}
|
}
|
||||||
this.change()
|
this.change()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
prepareDate(filterName, variableName) {
|
||||||
|
if (typeof this.params.filter_by === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let foundDateStart = false
|
||||||
|
let foundDateEnd = false
|
||||||
|
for (const i in this.params.filter_by) {
|
||||||
|
if (this.params.filter_by[i] === filterName && this.params.filter_comparator[i] === 'greater_equals') {
|
||||||
|
foundDateStart = i
|
||||||
|
}
|
||||||
|
if (this.params.filter_by[i] === filterName && this.params.filter_comparator[i] === 'less_equals') {
|
||||||
|
foundDateEnd = i
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundDateStart !== false && foundDateEnd !== false) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundDateStart !== false && foundDateEnd !== false) {
|
||||||
|
const start = new Date(this.params.filter_value[foundDateStart])
|
||||||
|
const end = new Date(this.params.filter_value[foundDateEnd])
|
||||||
|
this.filters[variableName] = `${start.getFullYear()}-${start.getMonth() + 1}-${start.getDate()} to ${end.getFullYear()}-${end.getMonth() + 1}-${end.getDate()}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setSingleValueFilter(filterName, variableName, useVariableName) {
|
||||||
|
if (!this.filters[useVariableName]) {
|
||||||
|
this.removePropertyFromFilter(filterName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let found = false
|
||||||
|
this.params.filter_by.forEach((f, i) => {
|
||||||
|
if (f === filterName) {
|
||||||
|
found = true
|
||||||
|
this.$set(this.params.filter_value, i, this.filters[variableName])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
this.params.filter_by.push(filterName)
|
||||||
|
this.params.filter_comparator.push('equals')
|
||||||
|
this.params.filter_value.push(this.filters[variableName])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.change()
|
||||||
|
},
|
||||||
|
prepareSingleValue(filterName, variableName, useVariableName, isNumber = false) {
|
||||||
|
let found = false
|
||||||
|
for (const i in this.params.filter_by) {
|
||||||
|
if (this.params.filter_by[i] === filterName) {
|
||||||
|
found = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found === false) {
|
||||||
|
this.filters[useVariableName] = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNumber) {
|
||||||
|
this.filters[variableName] = Number(this.params.filter_value[found])
|
||||||
|
} else {
|
||||||
|
this.filters[variableName] = this.params.filter_value[found]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filters[useVariableName] = true
|
||||||
|
},
|
||||||
|
prepareDone() {
|
||||||
|
// Set filters.done based on params
|
||||||
|
if (typeof this.params.filter_by === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let foundDone = false
|
||||||
|
this.params.filter_by.forEach((f, i) => {
|
||||||
|
if (f === 'done') {
|
||||||
|
foundDone = i
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (foundDone === false) {
|
||||||
|
this.$set(this.filters, 'done', true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setDoneFilter() {
|
||||||
|
if (this.filters.done) {
|
||||||
|
this.removePropertyFromFilter('done')
|
||||||
|
} else {
|
||||||
|
this.params.filter_by.push('done')
|
||||||
|
this.params.filter_comparator.push('equals')
|
||||||
|
this.params.filter_value.push('false')
|
||||||
|
}
|
||||||
|
this.change()
|
||||||
|
},
|
||||||
|
setFilterConcat() {
|
||||||
|
if (this.filters.requireAllFilters) {
|
||||||
|
this.params.filter_concat = 'and'
|
||||||
|
} else {
|
||||||
|
this.params.filter_concat = 'or'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setDueDateFilter() {
|
||||||
|
this.setDateFilter('due_date', 'dueDate')
|
||||||
|
},
|
||||||
|
setPriority() {
|
||||||
|
this.setSingleValueFilter('priority', 'priority', 'usePriority')
|
||||||
|
},
|
||||||
|
setStartDateFilter() {
|
||||||
|
this.setDateFilter('start_date', 'startDate')
|
||||||
|
},
|
||||||
|
setEndDateFilter() {
|
||||||
|
this.setDateFilter('end_date', 'endDate')
|
||||||
|
},
|
||||||
|
setPercentDoneFilter() {
|
||||||
|
this.setSingleValueFilter('percent_done', 'percentDone', 'usePercentDone')
|
||||||
|
},
|
||||||
|
prepareDueDate() {
|
||||||
|
this.prepareDate('due_date', 'dueDate')
|
||||||
|
},
|
||||||
|
preparePriority() {
|
||||||
|
this.prepareSingleValue('priority', 'priority', 'usePriority', true)
|
||||||
|
},
|
||||||
|
prepareStartDate() {
|
||||||
|
this.prepareDate('start_date', 'startDate')
|
||||||
|
},
|
||||||
|
prepareEndDate() {
|
||||||
|
this.prepareDate('end_date', 'endDate')
|
||||||
|
},
|
||||||
|
preparePercentDone() {
|
||||||
|
this.prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.single-value-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.fancycheckbox {
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
9
src/helpers/savedFilter.js
Normal file
9
src/helpers/savedFilter.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
export function getSavedFilterIdFromListId(listId) {
|
||||||
|
let filterId = listId * -1 - 1
|
||||||
|
// FilterIds from listIds are always positive
|
||||||
|
if (filterId < 0) {
|
||||||
|
filterId = 0
|
||||||
|
}
|
||||||
|
return filterId
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import AbstractModel from './abstractModel'
|
import AbstractModel from './abstractModel'
|
||||||
import TaskModel from './task'
|
import TaskModel from './task'
|
||||||
import UserModel from './user'
|
import UserModel from './user'
|
||||||
|
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
||||||
|
|
||||||
export default class ListModel extends AbstractModel {
|
export default class ListModel extends AbstractModel {
|
||||||
|
|
||||||
|
@ -41,4 +42,12 @@ export default class ListModel extends AbstractModel {
|
||||||
updated: null,
|
updated: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSavedFilter() {
|
||||||
|
return this.getSavedFilterId() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
getSavedFilterId() {
|
||||||
|
return getSavedFilterIdFromListId(this.id)
|
||||||
|
}
|
||||||
}
|
}
|
47
src/models/savedFilter.js
Normal file
47
src/models/savedFilter.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import AbstractModel from '@/models/abstractModel'
|
||||||
|
import UserModel from '@/models/user'
|
||||||
|
|
||||||
|
export default class SavedFilterModel extends AbstractModel {
|
||||||
|
constructor(data) {
|
||||||
|
super(data)
|
||||||
|
|
||||||
|
this.owner = new UserModel(this.owner)
|
||||||
|
|
||||||
|
this.created = new Date(this.created)
|
||||||
|
this.updated = new Date(this.updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults() {
|
||||||
|
return {
|
||||||
|
id: 0,
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
filters: {
|
||||||
|
sortBy: ['done', 'id'],
|
||||||
|
orderBy: ['asc', 'desc'],
|
||||||
|
filterBy: ['done'],
|
||||||
|
filterValue: ['false'],
|
||||||
|
filterComparator: ['equals'],
|
||||||
|
filterConcat: 'and',
|
||||||
|
filterIncludeNulls: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
owner: {},
|
||||||
|
created: null,
|
||||||
|
updated: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the corresponding list id to this saved filter.
|
||||||
|
* This function matches the one in the api.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getListId() {
|
||||||
|
let listId = this.id * -1 - 1
|
||||||
|
if (listId > 0) {
|
||||||
|
listId = 0
|
||||||
|
}
|
||||||
|
return listId
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,8 @@ import Kanban from '../views/list/views/Kanban'
|
||||||
import List from '../views/list/views/List'
|
import List from '../views/list/views/List'
|
||||||
import Gantt from '../views/list/views/Gantt'
|
import Gantt from '../views/list/views/Gantt'
|
||||||
import Table from '../views/list/views/Table'
|
import Table from '../views/list/views/Table'
|
||||||
|
// Saved Filters
|
||||||
|
import CreateSavedFilter from '@/views/filters/CreateSavedFilter'
|
||||||
|
|
||||||
const PasswordResetComponent = () => ({
|
const PasswordResetComponent = () => ({
|
||||||
component: import(/* webpackPrefetch: true *//* webpackChunkName: "user-settings" */'../views/user/PasswordReset'),
|
component: import(/* webpackPrefetch: true *//* webpackChunkName: "user-settings" */'../views/user/PasswordReset'),
|
||||||
|
@ -54,7 +56,7 @@ const NewListComponent = () => ({
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
})
|
})
|
||||||
const EditListComponent = () => ({
|
const EditListComponent = () => ({
|
||||||
component: import(/* webpackPrefetch: true *//* webpackChunkName: "settings" */'../views/list/EditList'),
|
component: import(/* webpackPrefetch: true *//* webpackChunkName: "settings" */'../views/list/EditListView'),
|
||||||
loading: LoadingComponent,
|
loading: LoadingComponent,
|
||||||
error: ErrorComponent,
|
error: ErrorComponent,
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
|
@ -260,5 +262,10 @@ export default new Router({
|
||||||
name: 'migrate.service',
|
name: 'migrate.service',
|
||||||
component: MigrateServiceComponent,
|
component: MigrateServiceComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/filters/new',
|
||||||
|
name: 'filters.create',
|
||||||
|
component: CreateSavedFilter,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
38
src/services/savedFilter.js
Normal file
38
src/services/savedFilter.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import AbstractService from '@/services/abstractService'
|
||||||
|
import SavedFilterModel from '@/models/savedFilter'
|
||||||
|
import {objectToCamelCase} from '@/helpers/case'
|
||||||
|
|
||||||
|
export default class SavedFilterService extends AbstractService {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
get: '/filters/{id}',
|
||||||
|
create: '/filters',
|
||||||
|
update: '/filters/{id}',
|
||||||
|
delete: '/filters/{id}',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
modelFactory(data) {
|
||||||
|
return new SavedFilterModel(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
processModel(model) {
|
||||||
|
// Make filters from this.filters camelCase and set them to the model property:
|
||||||
|
// That's easier than making the whole filter component configurable since that still needs to provide
|
||||||
|
// the filter values in snake_sćase for url parameters.
|
||||||
|
model.filters = objectToCamelCase(model.filters)
|
||||||
|
|
||||||
|
// Make sure all filterValues are passes as strings. This is a requirement of the api.
|
||||||
|
model.filters.filterValue = model.filters.filterValue.map(v => String(v))
|
||||||
|
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeUpdate(model) {
|
||||||
|
return this.processModel(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeCreate(model) {
|
||||||
|
return this.processModel(model)
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ $lists-per-row: 5;
|
||||||
.namespaces-list {
|
.namespaces-list {
|
||||||
.button.new-namespace {
|
.button.new-namespace {
|
||||||
float: right;
|
float: right;
|
||||||
|
margin-left: 1rem;
|
||||||
|
|
||||||
@media screen and (max-width: $mobile) {
|
@media screen and (max-width: $mobile) {
|
||||||
float: none;
|
float: none;
|
||||||
|
|
121
src/views/filters/CreateSavedFilter.vue
Normal file
121
src/views/filters/CreateSavedFilter.vue
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal-mask keyboard-shortcuts-modal">
|
||||||
|
<div @click.self="$router.back()" class="modal-container">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="card has-background-white has-no-shadow">
|
||||||
|
<header class="card-header">
|
||||||
|
<p class="card-header-title">Create A Saved Filter</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content content">
|
||||||
|
<p>
|
||||||
|
A saved filter is a virtual list which is computed from a set of filters each time it is
|
||||||
|
accessed. Once created, it will appear in a special namespace.
|
||||||
|
</p>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="title">Title</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
v-model="savedFilter.title"
|
||||||
|
:class="{ 'disabled': savedFilterService.loading}"
|
||||||
|
:disabled="savedFilterService.loading"
|
||||||
|
class="input"
|
||||||
|
id="Title"
|
||||||
|
placeholder="The saved filter title goes here..."
|
||||||
|
type="text"
|
||||||
|
v-focus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="description">Description</label>
|
||||||
|
<div class="control">
|
||||||
|
<editor
|
||||||
|
v-model="savedFilter.description"
|
||||||
|
:class="{ 'disabled': savedFilterService.loading}"
|
||||||
|
:disabled="savedFilterService.loading"
|
||||||
|
:preview-is-default="false"
|
||||||
|
id="description"
|
||||||
|
placeholder="The description goes here..."
|
||||||
|
v-if="editorActive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="filters">Filters</label>
|
||||||
|
<div class="control">
|
||||||
|
<filters
|
||||||
|
:class="{ 'disabled': savedFilterService.loading}"
|
||||||
|
:disabled="savedFilterService.loading"
|
||||||
|
class="has-no-shadow has-no-border"
|
||||||
|
v-model="filters"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
:class="{ 'disabled': savedFilterService.loading}"
|
||||||
|
:disabled="savedFilterService.loading"
|
||||||
|
@click="create()"
|
||||||
|
class="button is-primary is-fullwidth">
|
||||||
|
Create new saved filter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LoadingComponent from '@/components/misc/loading'
|
||||||
|
import ErrorComponent from '@/components/misc/error'
|
||||||
|
import Filters from '@/components/list/partials/filters'
|
||||||
|
import SavedFilterService from '@/services/savedFilter'
|
||||||
|
import SavedFilterModel from '@/models/savedFilter'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CreateSavedFilter',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
editorActive: false,
|
||||||
|
filters: {
|
||||||
|
sort_by: ['done', 'id'],
|
||||||
|
order_by: ['asc', 'desc'],
|
||||||
|
filter_by: ['done'],
|
||||||
|
filter_value: ['false'],
|
||||||
|
filter_comparator: ['equals'],
|
||||||
|
filter_concat: 'and',
|
||||||
|
filter_include_nulls: true,
|
||||||
|
},
|
||||||
|
savedFilterService: SavedFilterService,
|
||||||
|
savedFilter: SavedFilterModel,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Filters,
|
||||||
|
editor: () => ({
|
||||||
|
component: import(/* webpackPrefetch: true *//* webpackChunkName: "editor" */ '../../components/input/editor'),
|
||||||
|
loading: LoadingComponent,
|
||||||
|
error: ErrorComponent,
|
||||||
|
timeout: 60000,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.editorActive = false
|
||||||
|
this.$nextTick(() => this.editorActive = true)
|
||||||
|
|
||||||
|
this.savedFilterService = new SavedFilterService()
|
||||||
|
this.savedFilter = new SavedFilterModel()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
create() {
|
||||||
|
this.savedFilter.filters = this.filters
|
||||||
|
this.savedFilterService.create(this.savedFilter)
|
||||||
|
.then(r => {
|
||||||
|
this.$store.dispatch('namespaces/loadNamespaces')
|
||||||
|
this.$router.push({name: 'list.index', params: {listId: r.getListId()}})
|
||||||
|
})
|
||||||
|
.catch(e => this.error(e, this))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
157
src/views/filters/EditSavedFilter.vue
Normal file
157
src/views/filters/EditSavedFilter.vue
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
<template>
|
||||||
|
<div :class="{ 'is-loading': filterService.loading}" class="loader-container edit-list is-max-width-desktop">
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header">
|
||||||
|
<p class="card-header-title">
|
||||||
|
Edit Saved Filter
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="content">
|
||||||
|
<form @submit.prevent="save()">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="listtext">Filter Name</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
:class="{ 'disabled': filterService.loading}"
|
||||||
|
:disabled="filterService.loading"
|
||||||
|
@keyup.enter="save"
|
||||||
|
class="input"
|
||||||
|
id="listtext"
|
||||||
|
placeholder="The list title goes here..."
|
||||||
|
type="text"
|
||||||
|
v-focus
|
||||||
|
v-model="filter.title"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="listdescription">Description</label>
|
||||||
|
<div class="control">
|
||||||
|
<editor
|
||||||
|
:class="{ 'disabled': filterService.loading}"
|
||||||
|
:disabled="filterService.loading"
|
||||||
|
:preview-is-default="false"
|
||||||
|
id="listdescription"
|
||||||
|
placeholder="The lists description goes here..."
|
||||||
|
v-model="filter.description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="filters">Filters</label>
|
||||||
|
<div class="control">
|
||||||
|
<filters
|
||||||
|
:class="{ 'disabled': filterService.loading}"
|
||||||
|
:disabled="filterService.loading"
|
||||||
|
class="has-no-shadow has-no-border"
|
||||||
|
v-model="filters"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="columns bigbuttons">
|
||||||
|
<div class="column">
|
||||||
|
<button :class="{ 'is-loading': filterService.loading}" @click="save()"
|
||||||
|
class="button is-primary is-fullwidth">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="column is-1">
|
||||||
|
<button :class="{ 'is-loading': filterService.loading}" @click="showDeleteModal = true"
|
||||||
|
class="button is-danger is-fullwidth">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<icon icon="trash-alt"/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<modal
|
||||||
|
@close="showDeleteModal = false"
|
||||||
|
@submit="deleteList()"
|
||||||
|
v-if="showDeleteModal">
|
||||||
|
<span slot="header">Delete this saved filter</span>
|
||||||
|
<p slot="text">
|
||||||
|
Are you sure you want to delete this saved filter?
|
||||||
|
</p>
|
||||||
|
</modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ErrorComponent from '../../components/misc/error'
|
||||||
|
import LoadingComponent from '../../components/misc/loading'
|
||||||
|
|
||||||
|
import SavedFilterModel from '@/models/savedFilter'
|
||||||
|
import SavedFilterService from '@/services/savedFilter'
|
||||||
|
import ListModel from '@/models/list'
|
||||||
|
import Filters from '@/components/list/partials/filters'
|
||||||
|
import {objectToSnakeCase} from '@/helpers/case'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditFilter',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
filter: SavedFilterModel,
|
||||||
|
filterService: SavedFilterService,
|
||||||
|
filters: {
|
||||||
|
sort_by: ['done', 'id'],
|
||||||
|
order_by: ['asc', 'desc'],
|
||||||
|
filter_by: ['done'],
|
||||||
|
filter_value: ['false'],
|
||||||
|
filter_comparator: ['equals'],
|
||||||
|
filter_concat: 'and',
|
||||||
|
filter_include_nulls: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
showDeleteModal: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Filters,
|
||||||
|
editor: () => ({
|
||||||
|
component: import(/* webpackPrefetch: true *//* webpackChunkName: "editor" */ '../../components/input/editor'),
|
||||||
|
loading: LoadingComponent,
|
||||||
|
error: ErrorComponent,
|
||||||
|
timeout: 60000,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.filterService = new SavedFilterService()
|
||||||
|
this.loadSavedFilter()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// call again the method if the route changes
|
||||||
|
'$route': 'loadSavedFilter',
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadSavedFilter() {
|
||||||
|
// We assume the listId in the route is the pseudolist
|
||||||
|
const list = new ListModel({id: this.$route.params.id})
|
||||||
|
|
||||||
|
this.filter = new SavedFilterModel({id: list.getSavedFilterId()})
|
||||||
|
this.filterService.get(this.filter)
|
||||||
|
.then(r => {
|
||||||
|
this.filter = r
|
||||||
|
this.filters = objectToSnakeCase(this.filter.filters)
|
||||||
|
})
|
||||||
|
.catch(e => this.error(e, this))
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
this.filter.filters = this.filters
|
||||||
|
this.filterService.update(this.filter)
|
||||||
|
.then(r => {
|
||||||
|
this.$store.dispatch('namespaces/loadNamespaces')
|
||||||
|
this.success({message: 'The filter was saved successfully.'}, this)
|
||||||
|
this.filter = r
|
||||||
|
this.filters = objectToSnakeCase(this.filter.filters)
|
||||||
|
})
|
||||||
|
.catch(e => this.error(e, this))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
25
src/views/list/EditListView.vue
Normal file
25
src/views/list/EditListView.vue
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<edit-filter v-if="isSavedFilter"/>
|
||||||
|
<edit-list v-else/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EditList from '@/views/list/EditList'
|
||||||
|
import EditFilter from '@/views/filters/EditSavedFilter'
|
||||||
|
import {mapState} from 'vuex'
|
||||||
|
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditListView',
|
||||||
|
components: {
|
||||||
|
EditFilter,
|
||||||
|
EditList,
|
||||||
|
},
|
||||||
|
computed: mapState({
|
||||||
|
isSavedFilter: state => getSavedFilterIdFromListId(state.currentList.id) > 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field task-add" v-if="!list.isArchived && canWrite">
|
<div class="field task-add" v-if="!list.isArchived && canWrite && list.id > 0">
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<p :class="{ 'is-loading': taskService.loading}" class="control has-icons-left is-expanded">
|
<p :class="{ 'is-loading': taskService.loading}" class="control has-icons-left is-expanded">
|
||||||
<input
|
<input
|
||||||
|
@ -183,7 +183,6 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
taskService: TaskService,
|
taskService: TaskService,
|
||||||
list: {},
|
|
||||||
isTaskEdit: false,
|
isTaskEdit: false,
|
||||||
taskEditTask: TaskModel,
|
taskEditTask: TaskModel,
|
||||||
newTaskText: '',
|
newTaskText: '',
|
||||||
|
@ -212,6 +211,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: mapState({
|
computed: mapState({
|
||||||
canWrite: state => state.currentList.maxRight > Rights.READ,
|
canWrite: state => state.currentList.maxRight > Rights.READ,
|
||||||
|
list: state => state.currentList,
|
||||||
}),
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
// 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
|
||||||
|
|
|
@ -6,6 +6,12 @@
|
||||||
</span>
|
</span>
|
||||||
Create new namespace
|
Create new namespace
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<router-link :to="{name: 'filters.create'}" class="button is-primary new-namespace">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<icon icon="filter"/>
|
||||||
|
</span>
|
||||||
|
Create a new saved filter
|
||||||
|
</router-link>
|
||||||
|
|
||||||
<fancycheckbox class="show-archived-check" v-model="showArchived">
|
<fancycheckbox class="show-archived-check" v-model="showArchived">
|
||||||
Show Archived
|
Show Archived
|
||||||
|
|
Loading…
Reference in a new issue