feat: filters script setup
continuous-integration/drone/pr Build is passing Details

This commit is contained in:
Dominik Pschenitschni 2022-10-02 20:00:36 +02:00
parent 4c0ce26f2d
commit 15f532d37a
Signed by: dpschen
GPG Key ID: B257AC0149F43A77
11 changed files with 595 additions and 489 deletions

View File

@ -20,7 +20,7 @@
:overflow="true"
variant="hint-modal"
>
<filters
<Filters
:has-title="true"
v-model="value"
ref="filters"
@ -30,14 +30,16 @@
</template>
<script setup lang="ts">
import {computed, ref, watch} from 'vue'
import {computed, ref, watch, type PropType} from 'vue'
import Filters from '@/components/list/partials/filters.vue'
import {getDefaultParams} from '@/composables/taskList'
import type {IParams} from '@/types/IParams'
const props = defineProps({
modelValue: {
type: Object as PropType<IParams>,
required: true,
},
})
@ -63,15 +65,15 @@ watch(
const hasFilters = computed(() => {
// this.value also contains the page parameter which we don't want to include in filters
// eslint-disable-next-line no-unused-vars
const {filter_by, filter_value, filter_comparator, filter_concat, s} = value.value
const {filterBy, filterValue, filterComparator, filterConcat, s} = value.value
const def = {...getDefaultParams()}
const params = {filter_by, filter_value, filter_comparator, filter_concat, s}
const params = {filterBy, filterValue, filterComparator, filterConcat, s}
const defaultParams = {
filter_by: def.filter_by,
filter_value: def.filter_value,
filter_comparator: def.filter_comparator,
filter_concat: def.filter_concat,
filterBy: def.filterBy,
filterValue: def.filterValue,
filterComparator: def.filterComparator,
filterConcat: def.filterConcat,
s: s ? def.s : undefined,
}

View File

@ -2,7 +2,7 @@
<card class="filters has-overflow" :title="hasTitle ? $t('filters.title') : ''">
<div class="field is-flex is-flex-direction-column">
<fancycheckbox
v-model="params.filter_include_nulls"
v-model="params.filterIncludeNulls"
@update:model-value="change()"
>
{{ $t('filters.attributes.includeNulls') }}
@ -39,84 +39,87 @@
<div class="field">
<label class="label">{{ $t('task.attributes.priority') }}</label>
<div class="control single-value-control">
<fancycheckbox
v-model="filters.usePriority"
@update:model-value="setPriority"
:title="$t('filters.attributes.enablePriority')"
/>
<priority-select
:disabled="!filters.usePriority || undefined"
v-model.number="filters.priority"
@update:model-value="setPriority"
/>
<fancycheckbox
v-model="filters.usePriority"
@update:model-value="setPriority"
>
{{ $t('filters.attributes.enablePriority') }}
</fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">{{ $t('task.attributes.percentDone') }}</label>
<div class="control single-value-control">
<fancycheckbox
v-model="filters.usePercentDone"
@update:model-value="setPercentDoneFilter"
:title="$t('filters.attributes.enablePercentDone')"
/>
<percent-done-select
v-model.number="filters.percentDone"
@update:model-value="setPercentDoneFilter"
:disabled="!filters.usePercentDone || undefined"
/>
<fancycheckbox
v-model="filters.usePercentDone"
@update:model-value="setPercentDoneFilter"
>
{{ $t('filters.attributes.enablePercentDone') }}
</fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">{{ $t('task.attributes.dueDate') }}</label>
<div class="control">
<datepicker-with-range
<DatepickerWithRange
v-model="filters.dueDate"
@update:model-value="values => setDateFilter('due_date', values)"
@update:model-value="values => setDateFilter('dueDate', values)"
>
<template #trigger="{toggle, buttonText}">
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
<x-button
class="mb-2"
@click.prevent.stop="toggle()"
variant="secondary"
:shadow="false"
>
{{ buttonText }}
</x-button>
</template>
</datepicker-with-range>
</DatepickerWithRange>
</div>
</div>
<div class="field">
<label class="label">{{ $t('task.attributes.startDate') }}</label>
<div class="control">
<datepicker-with-range
<DatepickerWithRange
v-model="filters.startDate"
@update:model-value="values => setDateFilter('start_date', values)"
@update:model-value="values => setDateFilter('startDate', values)"
>
<template #trigger="{toggle, buttonText}">
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
{{ buttonText }}
</x-button>
</template>
</datepicker-with-range>
</DatepickerWithRange>
</div>
</div>
<div class="field">
<label class="label">{{ $t('task.attributes.endDate') }}</label>
<div class="control">
<datepicker-with-range
<DatepickerWithRange
v-model="filters.endDate"
@update:model-value="values => setDateFilter('end_date', values)"
@update:model-value="values => setDateFilter('endDate', values)"
>
<template #trigger="{toggle, buttonText}">
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
{{ buttonText }}
</x-button>
</template>
</datepicker-with-range>
</DatepickerWithRange>
</div>
</div>
<div class="field">
<label class="label">{{ $t('task.attributes.reminders') }}</label>
<div class="control">
<datepicker-with-range
<DatepickerWithRange
v-model="filters.reminders"
@update:model-value="values => setDateFilter('reminders', values)"
>
@ -125,7 +128,7 @@
{{ buttonText }}
</x-button>
</template>
</datepicker-with-range>
</DatepickerWithRange>
</div>
</div>
@ -133,15 +136,15 @@
<label class="label">{{ $t('task.attributes.assignees') }}</label>
<div class="control">
<multiselect
:loading="usersService.loading"
:loading="service.user.loading"
:placeholder="$t('team.edit.search')"
@search="query => find('users', query)"
:search-results="foundusers"
@select="() => add('users', 'assignees')"
@search="query => find('user', query)"
:search-results="foundEntities.user"
@select="() => add('user', 'assignees')"
label="username"
:multiple="true"
@remove="() => remove('users', 'assignees')"
v-model="users"
@remove="() => remove('user', 'assignees')"
v-model="entities.user"
/>
</div>
</div>
@ -149,7 +152,7 @@
<div class="field">
<label class="label">{{ $t('task.attributes.labels') }}</label>
<div class="control labels-list">
<edit-labels v-model="labels" @update:model-value="changeLabelFilter"/>
<edit-labels v-model="entities.label" @update:model-value="changeLabelFilter"/>
</div>
</div>
@ -159,15 +162,15 @@
<label class="label">{{ $t('list.lists') }}</label>
<div class="control">
<multiselect
:loading="listsService.loading"
:loading="service.list.loading"
:placeholder="$t('list.search')"
@search="query => find('lists', query)"
:search-results="foundlists"
@select="() => add('lists', 'list_id')"
@search="query => find('list', query)"
:search-results="foundEntities.list"
@select="() => add('list', 'listId')"
label="title"
@remove="() => remove('lists', 'list_id')"
@remove="() => remove('list', 'listId')"
:multiple="true"
v-model="lists"
v-model="entities.list"
/>
</div>
</div>
@ -175,15 +178,15 @@
<label class="label">{{ $t('namespace.namespaces') }}</label>
<div class="control">
<multiselect
:loading="namespaceService.loading"
:loading="service.namespace.loading"
:placeholder="$t('namespace.search')"
@search="query => find('namespace', query)"
:search-results="foundnamespace"
:search-results="foundEntities.namespace"
@select="() => add('namespace', 'namespace')"
label="title"
@remove="() => remove('namespace', 'namespace')"
:multiple="true"
v-model="namespace"
v-model="entities.namespace"
/>
</div>
</div>
@ -192,94 +195,69 @@
</template>
<script lang="ts">
import {defineComponent} from 'vue'
export const ALPHABETICAL_SORT = 'title'
</script>
import {useLabelStore} from '@/stores/labels'
<script setup lang="ts">
import {computed, nextTick, onMounted, reactive, ref, shallowReactive, toRefs, watch} from 'vue'
import DatepickerWithRange from '@/components/date/datepickerWithRange.vue'
import EditLabels from '@/components/tasks/partials/editLabels.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import {includesById} from '@/helpers/utils'
import PrioritySelect from '@/components/tasks/partials/prioritySelect.vue'
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect.vue'
import Multiselect from '@/components/input/multiselect.vue'
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect.vue'
import PrioritySelect from '@/components/tasks/partials/prioritySelect.vue'
import UserService from '@/services/user'
import ListService from '@/services/list'
import NamespaceService from '@/services/namespace'
import EditLabels from '@/components/tasks/partials/editLabels.vue'
import {objectToSnakeCase} from '@/helpers/case'
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import {includesById} from '@/helpers/utils'
import {objectToCamelCase} from '@/helpers/case'
import {getDefaultParams} from '@/composables/taskList'
import {camelCase} from 'camel-case'
import {useLabelStore} from '@/stores/labels'
import type {IUser} from '@/modelTypes/IUser'
import type {ILabel} from '@/modelTypes/ILabel'
import type {IList} from '@/modelTypes/IList'
import type {INamespace} from '@/modelTypes/INamespace'
import type {IParams, FilterBy, FilterComparator, FilterValue} from '@/types/IParams'
import type {IFilter, IFilterKey} from '@/types/IFilter'
// FIXME: merge with DEFAULT_PARAMS in taskList.js
const DEFAULT_PARAMS = {
sort_by: [],
order_by: [],
filter_by: [],
filter_value: [],
filter_comparator: [],
filter_include_nulls: true,
filter_concat: 'or',
const DEFAULT_PARAMS: IParams = {
sortBy: [],
orderBy: [],
filterBy: [],
filterValue: [],
filterComparator: [],
filterIncludeNulls: true,
filterConcat: 'or',
s: '',
}
const DEFAULT_FILTERS = {
const DEFAULT_FILTERS: IFilter = {
done: false,
dueDate: '',
requireAllFilters: false,
priority: 0,
usePriority: false,
startDate: '',
endDate: '',
percentDone: 0,
usePercentDone: false,
reminders: '',
assignees: '',
labels: '',
list_id: '',
listId: '',
namespace: '',
}
export const ALPHABETICAL_SORT = 'title'
requireAllFilters: false,
export default defineComponent({
name: 'filters',
components: {
DatepickerWithRange,
EditLabels,
PrioritySelect,
Fancycheckbox,
PercentDoneSelect,
Multiselect,
},
data() {
return {
params: DEFAULT_PARAMS,
filters: DEFAULT_FILTERS,
usePercentDone: false,
usePriority: false,
} as const
usersService: new UserService(),
foundusers: [],
users: [],
labelQuery: '',
labels: [],
listsService: new ListService(),
foundlists: [],
lists: [],
namespaceService: new NamespaceService(),
foundnamespace: [],
namespace: [],
}
},
mounted() {
this.filters.requireAllFilters = this.params.filter_concat === 'and'
},
props: {
const props = defineProps({
modelValue: {
required: true,
},
@ -287,81 +265,137 @@ export default defineComponent({
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue'])
const labelStore = useLabelStore()
const params = ref<IParams>(DEFAULT_PARAMS)
const filters = ref<IFilter>(DEFAULT_FILTERS)
const labelQuery = ref('')
interface Entities {
user: IUser[]
label: ILabel[]
list: IList[]
namespace: INamespace[]
}
type EntityKind = keyof Entities
const entities: Entities = reactive({
user: [],
label: [],
list: [],
namespace: [],
})
interface FoundEntities extends Entities {}
const foundEntities: FoundEntities = reactive({
user: [],
// FIXME: this is readonly and thus cannot be cleared
label: computed(() => labelStore.filterLabelsByQuery(entities.label, labelQuery.value)),
list: [],
namespace: [],
})
const service = reactive({
user: shallowReactive(new UserService()),
list: shallowReactive(new ListService()),
namespace: shallowReactive(new NamespaceService()),
})
type ServiceKind = keyof typeof service
onMounted(() => {
filters.value.requireAllFilters = params.value.filterConcat === 'and'
})
const {modelValue} = toRefs(props)
watch(
modelValue,
(value) => {
// FIXME: this is just a precaution in case some values are not in camelcase yet
params.value = objectToCamelCase(value)
prepareFilters()
},
emits: ['update:modelValue'],
watch: {
modelValue: {
handler(value) {
// FIXME: filters should only be converted to snake case in
// the last moment
this.params = objectToSnakeCase(value)
this.prepareFilters()
},
immediate: true,
},
},
computed: {
sortAlphabetically: {
{immediate: true},
)
const sortAlphabetically = computed({
get() {
return this.params?.sort_by?.find(sortBy => sortBy === ALPHABETICAL_SORT) !== undefined
return params.value?.sortBy?.find(sortBy => sortBy === ALPHABETICAL_SORT) !== undefined
},
set(sortAlphabetically) {
this.params.sort_by = sortAlphabetically
params.value.sortBy = sortAlphabetically
? [ALPHABETICAL_SORT]
: getDefaultParams().sort_by
: getDefaultParams().sortBy
this.change()
},
change()
},
})
foundLabels() {
const labelStore = useLabelStore()
return labelStore.filterLabelsByQuery(this.labels, this.labelQuery)
},
},
methods: {
change() {
const params = {...this.params}
params.filter_value = params.filter_value.map(v => v instanceof Date ? v.toISOString() : v)
this.$emit('update:modelValue', params)
},
prepareFilters() {
this.prepareDone()
this.prepareDate('due_date', 'dueDate')
this.prepareDate('start_date', 'startDate')
this.prepareDate('end_date', 'endDate')
this.prepareSingleValue('priority', 'priority', 'usePriority', true)
this.prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
this.prepareDate('reminders')
this.prepareRelatedObjectFilter('users', 'assignees')
this.prepareRelatedObjectFilter('lists', 'list_id')
this.prepareRelatedObjectFilter('namespace')
function change() {
const newParams: IParams = {...params.value}
newParams.filterValue = newParams.filterValue.map(v => v instanceof Date ? v.toISOString() : v)
// FIXME: use underscore case for emitting
// take also care of values like filterComparator
emit('update:modelValue', newParams)
}
this.prepareSingleValue('labels')
function prepareFilters() {
prepareDone()
prepareDate('dueDate', 'dueDate')
prepareDate('startDate', 'startDate')
prepareDate('endDate', 'endDate')
prepareSingleValue('priority', 'priority', 'usePriority', true)
prepareSingleValue('percentDone', 'percentDone', 'usePercentDone', true)
prepareDate('reminders', 'reminders')
prepareRelatedObjectFilter('user', 'assignees')
prepareRelatedObjectFilter('list', 'listId')
prepareRelatedObjectFilter('namespace', 'namespace')
const labels = typeof this.filters.labels === 'string'
? this.filters.labels
prepareSingleValue('labels')
const labelString = typeof filters.value.labels === 'string'
? filters.value.labels
: ''
const labelIds = labels.split(',').map(i => parseInt(i))
const labelIds = labelString.split(',').map(i => Number(i))
const labelStore = useLabelStore()
this.labels = labelStore.getLabelsByIds(labelIds)
},
removePropertyFromFilter(propertyName) {
entities.label = labelStore.getLabelsByIds(labelIds)
}
function removeFilterProperty(propertyName: FilterBy) {
// Because of the way arrays work, we can only ever remove one element at once.
// To remove multiple filter elements of the same name this function has to be called multiple times.
for (const i in this.params.filter_by) {
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
params.value.filterBy.find((current, index) => {
if (current === propertyName) {
params.value.filterBy.splice(index, 1)
params.value.filterComparator.splice(index, 1)
params.value.filterValue.splice(index, 1)
return true
}
})
}
},
setDateFilter(filterName, {dateFrom, dateTo}) {
dateFrom = parseDateOrString(dateFrom, null)
dateTo = parseDateOrString(dateTo, null)
function addFilterProperty(
filterBy: FilterBy,
filterComparator: FilterComparator,
filterValue: FilterValue,
) {
params.value.filterBy.push(filterBy)
params.value.filterComparator.push(filterComparator)
params.value.filterValue.push(filterValue)
}
function setDateFilter(filterName: FilterBy, dateRange: {dateFrom: string, dateTo: string}) {
const dateFrom = parseDateOrString(dateRange.dateFrom, null)
const dateTo = parseDateOrString(dateRange.dateTo, null)
// Only filter if we have a date
if (dateFrom !== null && dateTo !== null) {
@ -369,146 +403,161 @@ export default defineComponent({
// 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 === filterName && this.params.filter_comparator[i] === 'greater_equals') {
params.value.filterBy.forEach((filterBy, index) => {
if (filterBy === filterName && params.value.filterComparator[index] === 'greaterEquals') {
foundStart = true
this.params.filter_value[i] = dateFrom
params.value.filterValue[index] = dateFrom
}
if (f === filterName && this.params.filter_comparator[i] === 'less_equals') {
if (filterBy === filterName && params.value.filterComparator[index] === 'lessEquals') {
foundEnd = true
this.params.filter_value[i] = dateTo
params.value.filterValue[index] = dateTo
}
})
if (!foundStart) {
this.params.filter_by.push(filterName)
this.params.filter_comparator.push('greater_equals')
this.params.filter_value.push(dateFrom)
addFilterProperty(filterName, 'greaterEquals', dateFrom)
}
if (!foundEnd) {
this.params.filter_by.push(filterName)
this.params.filter_comparator.push('less_equals')
this.params.filter_value.push(dateTo)
addFilterProperty(filterName, 'lessEquals', dateTo)
}
this.filters[camelCase(filterName)] = {dateFrom, dateTo}
this.change()
filters.value[filterName] = {dateFrom, dateTo}
change()
return
}
this.removePropertyFromFilter(filterName)
this.removePropertyFromFilter(filterName)
this.change()
},
prepareDate(filterName, variableName) {
if (typeof this.params.filter_by === 'undefined') {
removeFilterProperty(filterName)
removeFilterProperty(filterName)
change()
}
function prepareDate(filterName: FilterBy, variableName: IFilterKey) {
if (typeof params.value.filterBy === '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
let foundDateStart : false | number = false
let foundDateEnd : false | number = false
params.value.filterBy.find((currentFilterBy, index) => {
if (
currentFilterBy === filterName &&
params.value.filterComparator[index] === 'greaterEquals'
) {
foundDateStart = index
}
if (this.params.filter_by[i] === filterName && this.params.filter_comparator[i] === 'less_equals') {
foundDateEnd = i
if (
currentFilterBy === filterName &&
params.value.filterComparator[index] === 'lessEquals'
) {
foundDateEnd = index
}
if (foundDateStart !== false && foundDateEnd !== false) {
break
}
return true
}
})
if (foundDateStart !== false && foundDateEnd !== false) {
const startDate = new Date(this.params.filter_value[foundDateStart])
const endDate = new Date(this.params.filter_value[foundDateEnd])
this.filters[variableName] = {
const startDate = new Date(params.value.filterValue[foundDateStart])
const endDate = new Date(params.value.filterValue[foundDateEnd])
filters.value[filterName] = {
dateFrom: !isNaN(startDate)
? `${startDate.getFullYear()}-${startDate.getMonth() + 1}-${startDate.getDate()}`
: this.params.filter_value[foundDateStart],
: params.value.filterValue[foundDateStart],
dateTo: !isNaN(endDate)
? `${endDate.getFullYear()}-${endDate.getMonth() + 1}-${endDate.getDate()}`
: this.params.filter_value[foundDateEnd],
: params.value.filterValue[foundDateEnd],
}
}
},
setSingleValueFilter(filterName, variableName, useVariableName = '', comparator = 'equals') {
if (useVariableName !== '' && !this.filters[useVariableName]) {
this.removePropertyFromFilter(filterName)
}
function setSingleValueFilter(
filterName: FilterBy,
variableName: IFilterKey,
useVariableName: IFilterKey | '' = '',
comparator: FilterComparator = 'equals',
) {
if (useVariableName !== '' && !filters.value[useVariableName]) {
removeFilterProperty(filterName)
return
}
let found = false
this.params.filter_by.forEach((f, i) => {
params.value.filterBy.forEach((f, index) => {
if (f === filterName) {
found = true
this.params.filter_value[i] = this.filters[variableName]
params.value.filterValue[index] = filters.value[variableName]
}
})
if (!found) {
this.params.filter_by.push(filterName)
this.params.filter_comparator.push(comparator)
this.params.filter_value.push(this.filters[variableName])
addFilterProperty(filterName, comparator, filters.value[variableName])
}
change()
}
this.change()
},
/**
*
* @param filterName The filter name in the api.
* @param variableName The name of the variable in this.filters.
* @param variableName The name of the variable in filters.value.
* @param useVariableName The name of the variable of the "Use this filter" variable. Will only be set if the parameter is not null.
* @param isNumber Toggles if the value should be parsed as a number.
*/
prepareSingleValue(filterName, variableName = null, useVariableName = null, isNumber = false) {
function prepareSingleValue(
filterName: FilterBy,
variableName: string | null = null,
useVariableName: IFilterKey | null = null,
isNumber = false,
) {
if (variableName === null) {
variableName = filterName
}
let found = false
for (const i in this.params.filter_by) {
if (this.params.filter_by[i] === filterName) {
let found: number | false= false
for (const i in params.value.filterBy) {
if (params.value.filterBy[i] === filterName) {
found = i
break
}
}
if (found === false && useVariableName !== null) {
this.filters[useVariableName] = false
filters.value[useVariableName] = false
return
}
if (isNumber) {
this.filters[variableName] = Number(this.params.filter_value[found])
filters.value[variableName] = Number(params.value.filterValue[found])
} else {
this.filters[variableName] = this.params.filter_value[found]
filters.value[variableName] = params.value.filterValue[found]
}
if (useVariableName !== null) {
this.filters[useVariableName] = true
filters.value[useVariableName] = true
}
},
prepareDone() {
}
function prepareDone() {
// Set filters.done based on params
if (typeof this.params.filter_by === 'undefined') {
if (typeof params.value.filterBy === 'undefined') {
return
}
this.filters.done = this.params.filter_by.some((f) => f === 'done') === false
},
async prepareRelatedObjectFilter(kind, filterName = null, servicePrefix = null) {
if (filterName === null) {
filterName = kind
filters.value.done = params.value.filterBy.some((f) => f === 'done') === false
}
async function prepareRelatedObjectFilter(
kind: EntityKind,
filterName: FilterBy,
servicePrefix: ServiceKind | null = null,
) {
if (servicePrefix === null) {
servicePrefix = kind
servicePrefix = kind as ServiceKind
}
this.prepareSingleValue(filterName)
if (typeof this.filters[filterName] === 'undefined' || this.filters[filterName] === '') {
prepareSingleValue(filterName)
if (typeof filters.value[filterName] === 'undefined' || filters.value[filterName] === '') {
return
}
@ -516,113 +565,91 @@ export default defineComponent({
// This is not the most ideal solution because it prevents a re-population when filters are changed
// from the outside. It is still fine because we're not changing them from the outside, other than
// loading them initially.
if(this[kind].length > 0) {
if(entities[kind].length > 0) {
return
}
this[kind] = await this[`${servicePrefix}Service`].getAll({}, {s: this.filters[filterName]})
},
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')
entities[kind] = await service[servicePrefix].getAll({}, {s: filters.value[filterName]})
}
this.change()
},
setFilterConcat() {
if (this.filters.requireAllFilters) {
this.params.filter_concat = 'and'
} else {
this.params.filter_concat = 'or'
}
this.change()
},
setPriority() {
this.setSingleValueFilter('priority', 'priority', 'usePriority')
},
setPercentDoneFilter() {
this.setSingleValueFilter('percent_done', 'percentDone', 'usePercentDone')
},
clear(kind) {
this[`found${kind}`] = []
},
async find(kind, query) {
function setDoneFilter() {
if (filters.value.done) {
removeFilterProperty('done')
} else {
addFilterProperty('done', 'equals', 'false')
}
change()
}
function setFilterConcat() {
if (filters.value.requireAllFilters) {
params.value.filterConcat = 'and'
} else {
params.value.filterConcat = 'or'
}
change()
}
function setPriority() {
setSingleValueFilter('priority', 'priority', 'usePriority')
}
function setPercentDoneFilter() {
setSingleValueFilter('percentDone', 'percentDone', 'usePercentDone')
}
function clear(kind: EntityKind) {
foundEntities[kind] = []
}
async function find(kind: ServiceKind, query: string) {
if (query === '') {
this.clear(kind)
clear(kind)
return
}
const response = await this[`${kind}Service`].getAll({}, {s: query})
const response = await service[kind].getAll({}, {s: query})
// Filter users from the results who are already assigned
this[`found${kind}`] = response.filter(({id}) => !includesById(this[kind], id))
},
add(kind, filterName) {
this.$nextTick(() => {
this.changeMultiselectFilter(kind, filterName)
})
},
remove(kind, filterName) {
this.$nextTick(() => {
this.changeMultiselectFilter(kind, filterName)
})
},
changeMultiselectFilter(kind, filterName) {
if (this[kind].length === 0) {
this.removePropertyFromFilter(filterName)
this.change()
foundEntities[kind] = response.filter(({id}) => !includesById(entities[kind], id))
}
async function add(kind: EntityKind, filterName: FilterBy) {
await nextTick()
changeMultiselectFilter(kind, filterName)
}
async function remove(kind: EntityKind, filterName: FilterBy) {
await nextTick()
changeMultiselectFilter(kind, filterName)
}
function changeMultiselectFilter(kind: EntityKind, filterName: FilterBy) {
if (entities[kind].length === 0) {
removeFilterProperty(filterName)
change()
return
}
let ids = []
this[kind].forEach(u => {
ids.push(kind === 'users' ? u.username : u.id)
})
let ids: string[] = []
entities[kind].forEach(u => ids.push(kind === 'user' ? (u as IUser).username : String(u.id)))
this.filters[filterName] = ids.join(',')
this.setSingleValueFilter(filterName, filterName, '', 'in')
},
findLabels(query) {
this.labelQuery = query
},
addLabel() {
this.$nextTick(() => {
this.changeLabelFilter()
})
},
removeLabel(label) {
this.$nextTick(() => {
for (const l in this.labels) {
if (this.labels[l].id === label.id) {
this.labels.splice(l, 1)
}
break
filters.value[filterName] = ids.join(',')
setSingleValueFilter(filterName, filterName, '', 'in')
}
this.changeLabelFilter()
})
},
changeLabelFilter() {
if (this.labels.length === 0) {
this.removePropertyFromFilter('labels')
this.change()
function changeLabelFilter() {
if (entities.label.length === 0) {
removeFilterProperty('labels')
change()
return
}
let labelIDs = []
this.labels.forEach(u => {
labelIDs.push(u.id)
})
const labelIDs = entities.label.map(({id}) => id)
this.filters.labels = labelIDs.join(',')
this.setSingleValueFilter('labels', 'labels', '', 'in')
},
},
})
filters.value.labels = labelIDs.join(',')
setSingleValueFilter('labels', 'labels', '', 'in')
}
</script>
<style lang="scss" scoped>

View File

@ -1,52 +1,63 @@
import {ref, shallowReactive, watch, computed} from 'vue'
import {ref, shallowReactive, watch, computed, type Ref} from 'vue'
import {useRoute} from 'vue-router'
import TaskCollectionService from '@/services/taskCollection'
import type {ITask} from '@/modelTypes/ITask'
import type {IList} from '@/modelTypes/IList'
import type {IParams, OrderBy, SortBy} from '@/types/IParams'
// 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',
})
sortBy: ['position', 'id'],
orderBy: ['asc', 'desc'],
filterBy: ['done'],
filterValue: ['false'],
filterComparator: ['equals'],
filterConcat: 'and',
} as IParams)
const SORT_BY_DEFAULT = {
type FilterSortOrderMap = Partial<{[key in SortBy]: OrderBy }>
const SORT_BY_DEFAULT: FilterSortOrderMap = {
id: 'desc',
}
} as const
/**
* This mixin provides a base set of methods and properties to get tasks on a list.
*/
export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
export function useTaskList(listId: Ref<IList['id']>, sortByDefault = SORT_BY_DEFAULT) {
const params = ref({...getDefaultParams()})
const search = ref('')
const page = ref(1)
const sortBy = ref({ ...sortByDefault })
const sortBy = ref<FilterSortOrderMap>({ ...sortByDefault })
// 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.
function formatSortOrder(params) {
let hasIdFilter = false
const sortKeys = Object.keys(sortBy.value)
for (const s of sortKeys) {
if (s === 'id') {
sortKeys.splice(s, 1)
hasIdFilter = true
break
function formatSortOrder(params: IParams) {
const sortEntries = Object.entries(sortBy.value)
const sortKeys: SortBy[] = []
const orderByValue: OrderBy[] = []
let idFilterValue
sortEntries.forEach(([key, value], index) => {
if (key === 'id') {
idFilterValue = value
sortEntries.splice(index, 1)
return true
}
}
if (hasIdFilter) {
sortKeys.push(key as SortBy)
orderByValue.push(value)
})
if (idFilterValue) {
sortKeys.push('id')
}
params.sort_by = sortKeys
params.order_by = sortKeys.map(s => sortBy.value[s])
params.sortBy = sortKeys
params.orderBy = orderByValue
return params
}

View File

@ -1,6 +1,11 @@
import {camelCase} from 'camel-case'
import {snakeCase} from 'snake-case'
export {
camelCase,
snakeCase,
}
/**
* Transforms field names to camel case.
* @param object

View File

@ -1,4 +1,4 @@
export function parseDateOrString(rawValue: string | undefined, fallback: any): string | Date {
export function parseDateOrString(rawValue: string | undefined, fallback: null) {
if (typeof rawValue === 'undefined') {
return fallback
}

View File

@ -6,7 +6,7 @@ export function findById<T extends {id: string | number}>(array : T[], id : stri
return array.find(({id: currentId}) => currentId === id)
}
export function includesById(array: [], id: string | number) {
export function includesById(array: any[], id: string | number) {
return array.some(({id: currentId}) => currentId === id)
}

View File

@ -1,12 +1,12 @@
import type {IAbstract} from './IAbstract'
import type {IUser} from './IUser'
import type {IFilter} from '@/types/IFilter'
import type {IParams} from '@/types/IParams'
export interface ISavedFilter extends IAbstract {
id: number
title: string
description: string
filters: IFilter
filters: IParams
owner: IUser
created: Date

View File

@ -186,7 +186,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
* This one here is the default one, usually the service definitions for a model will override this.
*/
modelFactory(data : Partial<Model>) {
return new AbstractModel(data)
return data as Model
}
/**

View File

@ -1,9 +1,32 @@
import type { Priority } from '@/constants/priorities'
export interface IFilter {
sortBy: ('done' | 'id')[]
orderBy: ('asc' | 'desc')[]
filterBy: 'done'[]
filterValue: 'false'[]
filterComparator: 'equals'[]
filterConcat: 'and'
filterIncludeNulls: boolean
// where
listId: string
// what
assignees: string
done: boolean,
dueDate: string
endDate: string
labels: string
namespace: string
percentDone: number
priority: Priority
reminders: string
startDate: string
// filter meta
requireAllFilters: boolean
// toggle what
usePercentDone: boolean
usePriority: boolean
}
export interface IFilterToggles {
usePercentDone: boolean
usePriority: boolean
}
export type IFilterKey = keyof IFilter

38
src/types/IParams.ts Normal file
View File

@ -0,0 +1,38 @@
import type { ITask } from '@/modelTypes/ITask'
export type SortBy =
| 'id'
| 'title'
| 'description'
| 'done'
| 'doneAt'
| 'dueDate'
| 'createdById'
| 'listId'
| 'repeatAfter'
| 'priority'
| 'start_date'
| 'end_date'
| 'hex_color'
| 'percent_done'
| 'uid'
| 'created'
| 'updated'
| 'position'
export type OrderBy = 'asc' | 'desc'
export type FilterComparator = 'equals' | 'greater' | 'greaterEquals' | 'less' | 'lessEquals' | 'like' |'in'
export type FilterBy = keyof ITask | 'namespace'
export type FilterValue = 'false' | string | number | boolean | Date | {dateFrom: string | Date, dateTo: string | Date}
export interface IParams {
sortBy: SortBy[]
orderBy: OrderBy[]
filterBy: FilterBy[]
filterValue: FilterValue[]
filterComparator: FilterComparator[]
filterConcat: 'and' | 'or'
filterIncludeNulls: boolean
s: '' // search
}

View File

@ -226,7 +226,7 @@ const {
const isAlphabeticalSorting = computed(() => {
return params.value.sort_by.find(sortBy => sortBy === ALPHABETICAL_SORT) !== undefined
return params.value.sortBy.find(sortBy => sortBy === ALPHABETICAL_SORT) !== undefined
})
const firstNewPosition = computed(() => {