forked from vikunja/vikunja
Compare commits
28 Commits
main
...
feature/be
Author | SHA1 | Date |
---|---|---|
kolaente | a3a26e15bf | |
kolaente | 461910879b | |
kolaente | d73ed9c37b | |
kolaente | 8b892bb3f1 | |
kolaente | c324854a90 | |
kolaente | 7be91ee148 | |
kolaente | cd420cb2cc | |
kolaente | 414f511871 | |
kolaente | 91b152013d | |
kolaente | d5e6bef31c | |
kolaente | d2d9562af7 | |
kolaente | ddf39f3bfa | |
kolaente | 104256c4b7 | |
kolaente | cbfea1594b | |
kolaente | 74ef7f8f38 | |
kolaente | b91cff2065 | |
kolaente | ea8c129816 | |
kolaente | 1280f3e1dd | |
kolaente | 9b7a74f91e | |
kolaente | cbe4a32671 | |
kolaente | d9d2669d07 | |
kolaente | 96f2b2e761 | |
kolaente | 23946f2614 | |
kolaente | 09440cf84f | |
kolaente | d2c5327cb4 | |
kolaente | b99cd042f0 | |
kolaente | f43f246471 | |
kolaente | c22ba4576d |
|
@ -72,29 +72,30 @@ This document describes the different errors Vikunja can return.
|
|||
|
||||
| ErrorCode | HTTP Status Code | Description |
|
||||
|-----------|------------------|----------------------------------------------------------------------------|
|
||||
| 4001 | 400 | The project task text cannot be empty. |
|
||||
| 4002 | 404 | The project task does not exist. |
|
||||
| 4003 | 403 | All bulk editing tasks must belong to the same project. |
|
||||
| 4004 | 403 | Need at least one task when bulk editing tasks. |
|
||||
| 4005 | 403 | The user does not have the right to see the task. |
|
||||
| 4006 | 403 | The user tried to set a parent task as the task itself. |
|
||||
| 4007 | 400 | The user tried to create a task relation with an invalid kind of relation. |
|
||||
| 4008 | 409 | The user tried to create a task relation which already exists. |
|
||||
| 4009 | 404 | The task relation does not exist. |
|
||||
| 4010 | 400 | Cannot relate a task with itself. |
|
||||
| 4011 | 404 | The task attachment does not exist. |
|
||||
| 4012 | 400 | The task attachment is too large. |
|
||||
| 4013 | 400 | The task sort param is invalid. |
|
||||
| 4014 | 400 | The task sort order is invalid. |
|
||||
| 4015 | 404 | The task comment does not exist. |
|
||||
| 4016 | 400 | Invalid task field. |
|
||||
| 4017 | 400 | Invalid task filter comparator. |
|
||||
| 4018 | 400 | Invalid task filter concatinator. |
|
||||
| 4019 | 400 | Invalid task filter value. |
|
||||
| 4020 | 400 | The provided attachment does not belong to that task. |
|
||||
| 4021 | 400 | This user is already assigned to that task. |
|
||||
| 4022 | 400 | The task has a relative reminder which does not specify relative to what. |
|
||||
| 4023 | 409 | Tried to create a task relation which would create a cycle. |
|
||||
| 4001 | 400 | The project task text cannot be empty. |
|
||||
| 4002 | 404 | The project task does not exist. |
|
||||
| 4003 | 403 | All bulk editing tasks must belong to the same project. |
|
||||
| 4004 | 403 | Need at least one task when bulk editing tasks. |
|
||||
| 4005 | 403 | The user does not have the right to see the task. |
|
||||
| 4006 | 403 | The user tried to set a parent task as the task itself. |
|
||||
| 4007 | 400 | The user tried to create a task relation with an invalid kind of relation. |
|
||||
| 4008 | 409 | The user tried to create a task relation which already exists. |
|
||||
| 4009 | 404 | The task relation does not exist. |
|
||||
| 4010 | 400 | Cannot relate a task with itself. |
|
||||
| 4011 | 404 | The task attachment does not exist. |
|
||||
| 4012 | 400 | The task attachment is too large. |
|
||||
| 4013 | 400 | The task sort param is invalid. |
|
||||
| 4014 | 400 | The task sort order is invalid. |
|
||||
| 4015 | 404 | The task comment does not exist. |
|
||||
| 4016 | 400 | Invalid task field. |
|
||||
| 4017 | 400 | Invalid task filter comparator. |
|
||||
| 4018 | 400 | Invalid task filter concatinator. |
|
||||
| 4019 | 400 | Invalid task filter value. |
|
||||
| 4020 | 400 | The provided attachment does not belong to that task. |
|
||||
| 4021 | 400 | This user is already assigned to that task. |
|
||||
| 4022 | 400 | The task has a relative reminder which does not specify relative to what. |
|
||||
| 4023 | 409 | Tried to create a task relation which would create a cycle. |
|
||||
| 4024 | 400 | The provided filter expression is invalid. |
|
||||
|
||||
## Team
|
||||
|
||||
|
|
|
@ -19,3 +19,28 @@ export const DATE_RANGES = {
|
|||
'thisYear': ['now/y', 'now/y+1y'],
|
||||
'restOfThisYear': ['now', 'now/y+1y'],
|
||||
}
|
||||
|
||||
export const DATE_VALUES = {
|
||||
'now': 'now',
|
||||
'startOfToday': 'now/d',
|
||||
'endOfToday': 'now/d+1d',
|
||||
|
||||
'beginningOflastWeek': 'now/w-1w',
|
||||
'endOfLastWeek': 'now/w-2w',
|
||||
'beginningOfThisWeek': 'now/w',
|
||||
'endOfThisWeek': 'now/w+1w',
|
||||
'startOfNextWeek': 'now/w+1w',
|
||||
'endOfNextWeek': 'now/w+2w',
|
||||
'in7Days': 'now+7d',
|
||||
|
||||
'beginningOfLastMonth': 'now/M-1M',
|
||||
'endOfLastMonth': 'now/M-2M',
|
||||
'startOfThisMonth': 'now/M',
|
||||
'endOfThisMonth': 'now/M+1M',
|
||||
'startOfNextMonth': 'now/M+1M',
|
||||
'endOfNextMonth': 'now/M+2M',
|
||||
'in30Days': 'now+30d',
|
||||
|
||||
'startOfThisYear': 'now/y',
|
||||
'endOfThisYear': 'now/y+1y',
|
||||
}
|
||||
|
|
|
@ -75,14 +75,15 @@
|
|||
|
||||
<p>
|
||||
{{ $t('input.datemathHelp.canuse') }}
|
||||
<BaseButton
|
||||
class="has-text-primary"
|
||||
@click="showHowItWorks = true"
|
||||
>
|
||||
{{ $t('input.datemathHelp.learnhow') }}
|
||||
</BaseButton>
|
||||
</p>
|
||||
|
||||
<BaseButton
|
||||
class="has-text-primary"
|
||||
@click="showHowItWorks = true"
|
||||
>
|
||||
{{ $t('input.datemathHelp.learnhow') }}
|
||||
</BaseButton>
|
||||
|
||||
<modal
|
||||
:enabled="showHowItWorks"
|
||||
transition-name="fade"
|
||||
|
@ -90,7 +91,7 @@
|
|||
variant="hint-modal"
|
||||
@close="() => showHowItWorks = false"
|
||||
>
|
||||
<DatemathHelp />
|
||||
<DatemathHelp/>
|
||||
</modal>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -111,7 +112,7 @@ import Popup from '@/components/misc/popup.vue'
|
|||
import {DATE_RANGES} from '@/components/date/dateRanges'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import DatemathHelp from '@/components/date/datemathHelp.vue'
|
||||
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
|
||||
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
|
|
@ -0,0 +1,255 @@
|
|||
<template>
|
||||
<div class="datepicker-with-range-container">
|
||||
<Popup
|
||||
:open="open"
|
||||
@close="() => emit('close')"
|
||||
>
|
||||
<template #content="{isOpen}">
|
||||
<div
|
||||
class="datepicker-with-range"
|
||||
:class="{'is-open': isOpen}"
|
||||
>
|
||||
<div class="selections">
|
||||
<BaseButton
|
||||
:class="{'is-active': customRangeActive}"
|
||||
@click="setDate(null)"
|
||||
>
|
||||
{{ $t('misc.custom') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-for="(value, text) in DATE_VALUES"
|
||||
:key="text"
|
||||
:class="{'is-active': date === value}"
|
||||
@click="setDate(value)"
|
||||
>
|
||||
{{ $t(`input.datepickerRange.values.${text}`) }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div class="flatpickr-container input-group">
|
||||
<label class="label">
|
||||
{{ $t('input.datepickerRange.date') }}
|
||||
<div class="field has-addons">
|
||||
<div class="control is-fullwidth">
|
||||
<input
|
||||
v-model="date"
|
||||
class="input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
<x-button
|
||||
icon="calendar"
|
||||
variant="secondary"
|
||||
data-toggle
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<flat-pickr
|
||||
v-model="flatpickrDate"
|
||||
:config="flatPickerConfig"
|
||||
/>
|
||||
|
||||
<p>
|
||||
{{ $t('input.datemathHelp.canuse') }}
|
||||
</p>
|
||||
|
||||
<BaseButton
|
||||
class="has-text-primary"
|
||||
@click="showHowItWorks = true"
|
||||
>
|
||||
{{ $t('input.datemathHelp.learnhow') }}
|
||||
</BaseButton>
|
||||
|
||||
<modal
|
||||
:enabled="showHowItWorks"
|
||||
transition-name="fade"
|
||||
:overflow="true"
|
||||
variant="hint-modal"
|
||||
@close="() => showHowItWorks = false"
|
||||
>
|
||||
<DatemathHelp/>
|
||||
</modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, ref, watch} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||
|
||||
import Popup from '@/components/misc/popup.vue'
|
||||
import {DATE_VALUES} from '@/components/date/dateRanges'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import DatemathHelp from '@/components/date/datemathHelp.vue'
|
||||
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
required: false,
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const flatPickerConfig = computed(() => ({
|
||||
altFormat: t('date.altFormatLong'),
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
enableTime: false,
|
||||
wrap: true,
|
||||
locale: getFlatpickrLanguage(),
|
||||
}))
|
||||
|
||||
const showHowItWorks = ref(false)
|
||||
|
||||
const flatpickrDate = ref('')
|
||||
|
||||
const date = ref<string|Date>('')
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
newValue => {
|
||||
date.value = newValue
|
||||
// Only set the date back to flatpickr when it's an actual date.
|
||||
// Otherwise flatpickr runs in an endless loop and slows down the browser.
|
||||
const parsed = parseDateOrString(date.value, false)
|
||||
if (parsed instanceof Date) {
|
||||
flatpickrDate.value = date.value
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function emitChanged() {
|
||||
emit('update:modelValue', date.value === '' ? null : date.value)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => flatpickrDate.value,
|
||||
(newVal: string | null) => {
|
||||
if (newVal === null) {
|
||||
return
|
||||
}
|
||||
|
||||
date.value = newVal
|
||||
|
||||
emitChanged()
|
||||
},
|
||||
)
|
||||
watch(() => date.value, emitChanged)
|
||||
|
||||
function setDate(range: string | null) {
|
||||
if (range === null) {
|
||||
date.value = ''
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
date.value = range
|
||||
}
|
||||
|
||||
const customRangeActive = computed<boolean>(() => {
|
||||
return !Object.values(DATE_VALUES).some(d => date.value === d)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.datepicker-with-range-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.popup) {
|
||||
z-index: 10;
|
||||
margin-top: 1rem;
|
||||
border-radius: $radius;
|
||||
border: 1px solid var(--grey-200);
|
||||
background-color: var(--white);
|
||||
box-shadow: $shadow;
|
||||
|
||||
&.is-open {
|
||||
width: 500px;
|
||||
height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.datepicker-with-range {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
:deep(.flatpickr-calendar) {
|
||||
margin: 0 auto 8px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.flatpickr-container {
|
||||
width: 70%;
|
||||
border-left: 1px solid var(--grey-200);
|
||||
padding: 1rem;
|
||||
font-size: .9rem;
|
||||
|
||||
// Flatpickr has no option to use it without an input field so we're hiding it instead
|
||||
:deep(input.form-control.input) {
|
||||
height: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.field .control :deep(.button) {
|
||||
border: 1px solid var(--input-border-color);
|
||||
height: 2.25rem;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--input-hover-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.label, .input, :deep(.button) {
|
||||
font-size: .9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.selections {
|
||||
width: 30%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: .5rem;
|
||||
overflow-y: scroll;
|
||||
|
||||
button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: .5rem 1rem;
|
||||
transition: $transition;
|
||||
font-size: .9rem;
|
||||
color: var(--text);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.is-active {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&:hover, &.is-active {
|
||||
background-color: var(--grey-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -23,7 +23,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import {ref, watch} from 'vue'
|
||||
import {onClickOutside} from '@vueuse/core'
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -31,8 +31,19 @@ const props = defineProps({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
nowOpen => {
|
||||
open.value = nowOpen
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const open = ref(false)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import FilterInput from '@/components/project/partials/FilterInput.vue'
|
||||
|
||||
function initState(value: string) {
|
||||
return {
|
||||
value,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Filter Input">
|
||||
<Variant
|
||||
title="With date values"
|
||||
:init-state="initState('dueDate < now && done = false && dueDate > now/w+1w')"
|
||||
>
|
||||
<template #default="{state}">
|
||||
<FilterInput v-model="state.value"/>
|
||||
</template>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
|
@ -0,0 +1,284 @@
|
|||
<script setup lang="ts">
|
||||
import {computed, nextTick, ref, watch} from 'vue'
|
||||
import {useAutoHeightTextarea} from '@/composables/useAutoHeightTextarea'
|
||||
import DatepickerWithValues from '@/components/date/datepickerWithValues.vue'
|
||||
import UserService from "@/services/user";
|
||||
import {getAvatarUrl, getDisplayName} from "@/models/user";
|
||||
import {createRandomID} from "@/helpers/randomId";
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
} = defineProps<{
|
||||
modelValue: string,
|
||||
}>()
|
||||
|
||||
const filterQuery = ref('')
|
||||
const {
|
||||
textarea: filterInput,
|
||||
height,
|
||||
} = useAutoHeightTextarea(filterQuery)
|
||||
|
||||
watch(
|
||||
() => modelValue,
|
||||
() => {
|
||||
filterQuery.value = modelValue
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const userService = new UserService()
|
||||
|
||||
const dateFields = [
|
||||
'dueDate',
|
||||
'startDate',
|
||||
'endDate',
|
||||
'doneAt',
|
||||
'reminders',
|
||||
]
|
||||
|
||||
const assigneeFields = [
|
||||
'assignees',
|
||||
]
|
||||
|
||||
const availableFilterFields = [
|
||||
'done',
|
||||
'priority',
|
||||
'usePriority',
|
||||
'percentDone',
|
||||
'labels',
|
||||
...dateFields,
|
||||
...assigneeFields,
|
||||
]
|
||||
|
||||
const filterOperators = [
|
||||
'!=',
|
||||
'=',
|
||||
'>',
|
||||
'>=',
|
||||
'<',
|
||||
'<=',
|
||||
'like',
|
||||
'in',
|
||||
'?=',
|
||||
]
|
||||
|
||||
const filterJoinOperators = [
|
||||
'&&',
|
||||
'||',
|
||||
'(',
|
||||
')',
|
||||
]
|
||||
|
||||
function escapeHtml(unsafe: string): string {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function unEscapeHtml(unsafe: string): string {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
||||
const highlightedFilterQuery = computed(() => {
|
||||
let highlighted = escapeHtml(filterQuery.value)
|
||||
dateFields
|
||||
.forEach(o => {
|
||||
const pattern = new RegExp(o + '\\s*(<|>|<=|>=|=|!=)\\s*([\'"]?)([^\'"\\s]+\\1?)?', 'ig');
|
||||
highlighted = highlighted.replaceAll(pattern, (match, token, start, value, position) => {
|
||||
if (typeof value === 'undefined') {
|
||||
value = ''
|
||||
}
|
||||
return `${o} ${token} <button class="button is-primary filter-query__date_value" data-position="${position}">${value}</button><span class="filter-query__date_value_placeholder">${value}</span>`
|
||||
})
|
||||
})
|
||||
assigneeFields
|
||||
.forEach(f => {
|
||||
const pattern = new RegExp(f + '\\s*(<|>|<=|>=|=|!=)\\s*([\'"]?)([^\'"\\s]+\\1?)?', 'ig');
|
||||
highlighted = highlighted.replaceAll(pattern, (match, token, start, value) => {
|
||||
if (typeof value === 'undefined') {
|
||||
value = ''
|
||||
}
|
||||
|
||||
const id = createRandomID(32)
|
||||
|
||||
userService.getAll({}, {s: value}).then(users => {
|
||||
if (users.length > 0) {
|
||||
const displayName = getDisplayName(users[0])
|
||||
const nameTag = document.createElement('span')
|
||||
nameTag.innerText = displayName
|
||||
|
||||
const avatar = document.createElement('img')
|
||||
avatar.src = getAvatarUrl(users[0], 20)
|
||||
avatar.height = 20
|
||||
avatar.width = 20
|
||||
avatar.alt = displayName
|
||||
|
||||
// TODO: caching
|
||||
|
||||
nextTick(() => {
|
||||
const assigneeValue = document.getElementById(id)
|
||||
assigneeValue.innerText = ''
|
||||
assigneeValue?.appendChild(avatar)
|
||||
assigneeValue?.appendChild(nameTag)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return `${f} ${token} <span class="filter-query__assignee_value" id="${id}">${value}<span>`
|
||||
})
|
||||
})
|
||||
filterOperators
|
||||
.map(o => ` ${escapeHtml(o)} `)
|
||||
.forEach(o => {
|
||||
highlighted = highlighted.replaceAll(o, `<span class="filter-query__operator">${o}</span>`)
|
||||
})
|
||||
filterJoinOperators
|
||||
.map(o => escapeHtml(o))
|
||||
.forEach(o => {
|
||||
highlighted = highlighted.replaceAll(o, `<span class="filter-query__join-operator">${o}</span>`)
|
||||
})
|
||||
availableFilterFields.forEach(f => {
|
||||
highlighted = highlighted.replaceAll(f, `<span class="filter-query__field">${f}</span>`)
|
||||
})
|
||||
return highlighted
|
||||
})
|
||||
|
||||
const currentOldDatepickerValue = ref('')
|
||||
const currentDatepickerValue = ref('')
|
||||
const currentDatepickerPos = ref()
|
||||
const datePickerPopupOpen = ref(false)
|
||||
|
||||
watch(
|
||||
() => highlightedFilterQuery.value,
|
||||
async () => {
|
||||
await nextTick()
|
||||
document.querySelectorAll('button.filter-query__date_value')
|
||||
.forEach(b => {
|
||||
b.addEventListener('click', event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const button = event.target
|
||||
currentOldDatepickerValue.value = button?.innerText
|
||||
currentDatepickerValue.value = button?.innerText
|
||||
currentDatepickerPos.value = parseInt(button?.dataset.position)
|
||||
datePickerPopupOpen.value = true
|
||||
})
|
||||
})
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
function updateDateInQuery(newDate: string) {
|
||||
// Need to escape and unescape the query because the positions are based on the escaped query
|
||||
let escaped = escapeHtml(filterQuery.value)
|
||||
escaped = escaped
|
||||
.substring(0, currentDatepickerPos.value)
|
||||
+ escaped
|
||||
.substring(currentDatepickerPos.value)
|
||||
.replace(currentOldDatepickerValue.value, newDate)
|
||||
currentOldDatepickerValue.value = newDate
|
||||
filterQuery.value = unEscapeHtml(escaped)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('filters.query.title') }}</label>
|
||||
<div class="control filter-input">
|
||||
<textarea
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
v-model="filterQuery"
|
||||
class="input"
|
||||
ref="filterInput"
|
||||
></textarea>
|
||||
<div
|
||||
class="filter-input-highlight"
|
||||
:style="{'height': height}"
|
||||
v-html="highlightedFilterQuery"
|
||||
></div>
|
||||
<DatepickerWithValues
|
||||
v-model="currentDatepickerValue"
|
||||
:open="datePickerPopupOpen"
|
||||
@close="() => datePickerPopupOpen = false"
|
||||
@update:model-value="updateDateInQuery"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.filter-input-highlight {
|
||||
span {
|
||||
&.filter-query__field {
|
||||
color: var(--code-literal);
|
||||
}
|
||||
|
||||
&.filter-query__operator {
|
||||
color: var(--code-keyword);
|
||||
}
|
||||
|
||||
&.filter-query__join-operator {
|
||||
color: var(--code-section);
|
||||
}
|
||||
|
||||
&.filter-query__date_value_placeholder {
|
||||
padding: .125rem .25rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.filter-query__assignee_value {
|
||||
padding: .125rem .25rem;
|
||||
border-radius: $radius;
|
||||
background-color: var(--grey-200);
|
||||
color: var(--grey-700);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
> img {
|
||||
margin-right: .25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.filter-query__date_value {
|
||||
padding: .125rem .25rem;
|
||||
border-radius: $radius;
|
||||
position: absolute;
|
||||
margin-top: calc((0.25em - 0.125rem) * -1);
|
||||
height: 1.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter-input {
|
||||
position: relative;
|
||||
|
||||
textarea {
|
||||
position: absolute;
|
||||
text-fill-color: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background: transparent !important;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.filter-input-highlight {
|
||||
height: 2.5em;
|
||||
line-height: 1.5;
|
||||
padding: .5em .75em;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -30,6 +30,9 @@
|
|||
{{ $t('filters.attributes.sortAlphabetically') }}
|
||||
</Fancycheckbox>
|
||||
</div>
|
||||
|
||||
<FilterInput v-model="filterQuery"/>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('misc.search') }}</label>
|
||||
<div class="control">
|
||||
|
@ -227,6 +230,7 @@ import ProjectService from '@/services/project'
|
|||
|
||||
// FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS
|
||||
import {getDefaultParams} from '@/composables/useTaskList'
|
||||
import FilterInput from '@/components/project/partials/FilterInput.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
@ -252,6 +256,9 @@ const DEFAULT_PARAMS = {
|
|||
s: '',
|
||||
} as const
|
||||
|
||||
// FIXME: use params
|
||||
const filterQuery = ref('')
|
||||
|
||||
const DEFAULT_FILTERS = {
|
||||
done: false,
|
||||
dueDate: '',
|
||||
|
|
|
@ -77,7 +77,7 @@ const props = defineProps({
|
|||
const emit = defineEmits(['taskAdded'])
|
||||
|
||||
const newTaskTitle = ref('')
|
||||
const newTaskInput = useAutoHeightTextarea(newTaskTitle)
|
||||
const {textarea: newTaskInput} = useAutoHeightTextarea(newTaskTitle)
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const authStore = useAuthStore()
|
||||
|
|
|
@ -6,6 +6,7 @@ import {debouncedWatch, tryOnMounted, useWindowSize, type MaybeRef} from '@vueus
|
|||
export function useAutoHeightTextarea(value: MaybeRef<string>) {
|
||||
const textarea = ref<HTMLTextAreaElement | null>(null)
|
||||
const minHeight = ref(0)
|
||||
const height = ref('')
|
||||
|
||||
// adapted from https://github.com/LeaVerou/stretchy/blob/47f5f065c733029acccb755cae793009645809e2/src/stretchy.js#L34
|
||||
function resize(textareaEl: HTMLTextAreaElement | null) {
|
||||
|
@ -23,14 +24,13 @@ export function useAutoHeightTextarea(value: MaybeRef<string>) {
|
|||
|
||||
textareaEl.style.minHeight = ''
|
||||
textareaEl.style.height = '0'
|
||||
const offset = textareaEl.offsetHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom)
|
||||
const height = textareaEl.scrollHeight + offset + 'px'
|
||||
height.value = textareaEl.scrollHeight + 'px'
|
||||
|
||||
textareaEl.style.height = height
|
||||
textareaEl.style.height = height.value
|
||||
|
||||
// calculate min-height for the first time
|
||||
if (!minHeight.value) {
|
||||
minHeight.value = parseFloat(height)
|
||||
minHeight.value = parseFloat(height.value)
|
||||
}
|
||||
|
||||
textareaEl.style.minHeight = minHeight.value.toString()
|
||||
|
@ -68,5 +68,8 @@ export function useAutoHeightTextarea(value: MaybeRef<string>) {
|
|||
},
|
||||
)
|
||||
|
||||
return textarea
|
||||
return {
|
||||
textarea,
|
||||
height,
|
||||
}
|
||||
}
|
|
@ -415,6 +415,9 @@
|
|||
"edit": {
|
||||
"title": "Edit This Saved Filter",
|
||||
"success": "The filter was saved successfully."
|
||||
},
|
||||
"query": {
|
||||
"title": "Query"
|
||||
}
|
||||
},
|
||||
"migrate": {
|
||||
|
@ -585,6 +588,7 @@
|
|||
"to": "To",
|
||||
"from": "From",
|
||||
"fromto": "{from} to {to}",
|
||||
"date": "Date",
|
||||
"ranges": {
|
||||
"today": "Today",
|
||||
|
||||
|
@ -602,6 +606,30 @@
|
|||
|
||||
"thisYear": "This Year",
|
||||
"restOfThisYear": "The Rest of This Year"
|
||||
},
|
||||
"values": {
|
||||
"now": "Now",
|
||||
"startOfToday": "Start of today",
|
||||
"endOfToday": "End of today",
|
||||
|
||||
"beginningOflastWeek": "Beginning of last week",
|
||||
"endOfLastWeek": "End of last week",
|
||||
"beginningOfThisWeek": "Beginning of this week",
|
||||
"endOfThisWeek": "End of this week",
|
||||
"startOfNextWeek": "Start of next week",
|
||||
"endOfNextWeek": "End of next week",
|
||||
"in7Days": "In 7 days",
|
||||
|
||||
"beginningOfLastMonth": "Beginning of last month",
|
||||
"endOfLastMonth": "End of last month",
|
||||
"startOfThisMonth": "Start of this month",
|
||||
"endOfThisMonth": "End of this month",
|
||||
"startOfNextMonth": "Start of next month",
|
||||
"endOfNextMonth": "End of next month",
|
||||
"in30Days": "In 30 days",
|
||||
|
||||
"startOfThisYear": "Beginning of this year",
|
||||
"endOfThisYear": "End of this year"
|
||||
}
|
||||
},
|
||||
"datemathHelp": {
|
||||
|
|
3
go.mod
3
go.mod
|
@ -31,6 +31,7 @@ require (
|
|||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2
|
||||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/ganigeorgiev/fexpr v0.4.0
|
||||
github.com/getsentry/sentry-go v0.27.0
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.9.0
|
||||
|
@ -134,7 +135,6 @@ require (
|
|||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
|
@ -162,6 +162,7 @@ require (
|
|||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/tj/assert v0.0.3 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/urfave/cli/v2 v2.3.0 // indirect
|
||||
|
|
83
go.sum
83
go.sum
|
@ -29,10 +29,6 @@ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG
|
|||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/arran4/golang-ical v0.2.3 h1:C4Vj7+BjJBIrAJhHgi6Ku+XUkQVugRq4re5Cqj5QVdE=
|
||||
github.com/arran4/golang-ical v0.2.3/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||
github.com/arran4/golang-ical v0.2.4 h1:0/rTXn2qqEekLKec3SzRRy+z7pCLtniMb0KD/dPogUo=
|
||||
github.com/arran4/golang-ical v0.2.4/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||
github.com/arran4/golang-ical v0.2.5 h1:zaAdee/cOnOCeSuxUSgkWnF9jZl/oYq2ZgDk+LU3wGs=
|
||||
github.com/arran4/golang-ical v0.2.5/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
|
@ -111,10 +107,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
|
|||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI=
|
||||
github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/getsentry/sentry-go v0.26.0 h1:IX3++sF6/4B5JcevhdZfdKIHfyvMmAq/UnqcyT2H6mA=
|
||||
github.com/getsentry/sentry-go v0.26.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/ganigeorgiev/fexpr v0.4.0 h1:ojitI+VMNZX/odeNL1x3RzTTE8qAIVvnSSYPNAnQFDI=
|
||||
github.com/ganigeorgiev/fexpr v0.4.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
||||
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
|
||||
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
|
@ -173,8 +167,9 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL
|
|||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
|
@ -210,8 +205,6 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S3
|
|||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
|
@ -319,13 +312,9 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|||
github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c=
|
||||
github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU=
|
||||
github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
|
||||
github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM=
|
||||
github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
|
||||
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
|
||||
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk=
|
||||
github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef h1:RZnRnSID1skF35j/15KJ6hKZkdIC/teQClJK5wP5LU4=
|
||||
|
@ -373,16 +362,8 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
|||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
|
||||
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.20 h1:BAZ50Ns0OFBNxdAqFhbZqdPcht1Xlb16pDCqkq1spr0=
|
||||
github.com/mattn/go-sqlite3 v1.14.20/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.21 h1:IXocQLOykluc3xPE0Lvy8FtggMz1G+U3mEjg+0zGizc=
|
||||
github.com/mattn/go-sqlite3 v1.14.21/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
|
@ -432,24 +413,14 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds=
|
||||
github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
|
||||
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
|
@ -516,19 +487,17 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
|
|||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04=
|
||||
github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E=
|
||||
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
|
||||
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
|
||||
github.com/tkuchiki/go-timezone v0.2.2 h1:MdHR65KwgVTwWFQrota4SKzc4L5EfuH5SdZZGtk/P2Q=
|
||||
github.com/tkuchiki/go-timezone v0.2.2/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/typesense/typesense-go v0.9.0 h1:V1sk0QN6jHevHHiV3GZyL6aIb6Oa8QsmyXRUYJj2Zfg=
|
||||
github.com/typesense/typesense-go v0.9.0/go.mod h1:4mq4FYHzU7csU/KHaZoyG2bCSKl7GrCeyAr2YhXT1/0=
|
||||
github.com/typesense/typesense-go v1.0.0 h1:/8Lr1yf9YjmUKdn/xbTNy+OhwOvBd0noBTRkcB22Uhw=
|
||||
github.com/typesense/typesense-go v1.0.0/go.mod h1:4mq4FYHzU7csU/KHaZoyG2bCSKl7GrCeyAr2YhXT1/0=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
|
@ -555,8 +524,6 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7Jul
|
|||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
|
||||
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
|
||||
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
|
@ -599,17 +566,11 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
||||
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
|
@ -639,27 +600,16 @@ golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
|
||||
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
|
||||
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -690,20 +640,12 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -778,6 +720,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@ -827,12 +770,6 @@ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYm
|
|||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20231205202227-c7ed78300ce9 h1:lcNlqzNPv7WBKVRqGXWjs+nt9r5WBf2FG+eBOCUcyLM=
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20231205202227-c7ed78300ce9/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240124202215-77ac23f331fe h1:8t+5jXWFfMOxWi0OIBMpRSM5agX6xhwA5+em+P9nGTE=
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240124202215-77ac23f331fe/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240206191224-5aae65575674 h1:/uC4C2ANN3SsMZmsLSDWvfjJPP+nHisQIfD8ElkjBdI=
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240206191224-5aae65575674/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240206231429-45b9ea635e03 h1:GMq57lSFGhXrFuOJ/HuSf67Y/SfzWxlJRZus262YxXw=
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240206231429-45b9ea635e03/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
|
||||
src.techknowlogick.com/xormigrate v1.7.1 h1:RKGLLUAqJ+zO8iZ7eOc7oLH7f0cs2gfXSZSvBRBHnlY=
|
||||
|
@ -843,11 +780,5 @@ xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
|||
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/xorm v1.0.5/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
|
||||
xorm.io/xorm v1.3.3/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo=
|
||||
xorm.io/xorm v1.3.4 h1:vWFKzR3DhGUDl5b4srhUjhDwjxkZAc4C7BFszpu0swI=
|
||||
xorm.io/xorm v1.3.4/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo=
|
||||
xorm.io/xorm v1.3.6 h1:hfpWHkDIWWqUi8FRF2H2M9O8lO3Ov47rwFcS9gPzPkU=
|
||||
xorm.io/xorm v1.3.6/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo=
|
||||
xorm.io/xorm v1.3.7 h1:mLceAGu0b87r9pD4qXyxGHxifOXIIrAdVcA6k95/osw=
|
||||
xorm.io/xorm v1.3.7/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
|
||||
xorm.io/xorm v1.3.8 h1:CJmplmWqfSRpLWSPMmqz+so8toBp3m7ehuRehIWedZo=
|
||||
xorm.io/xorm v1.3.8/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- id: 1
|
||||
filters: '{"sort_by":null,"order_by":null,"filter_by":["start_date","end_date","due_date"],"filter_value":["2018-12-11T03:46:40+00:00","2018-12-13T11:20:01+00:00","2018-11-29T14:00:00+00:00"],"filter_comparator":["greater","less","greater"],"filter_concat":"","filter_include_nulls":false}'
|
||||
filters: '{"sort_by":null,"order_by":null,"filter":"start_date > \u00272018-12-11T03:46:40+00:00\u0027 || end_date < \u00272018-12-13T11:20:01+00:00\u0027 || due_date > \u00272018-11-29T14:00:00+00:00\u0027","filter_include_nulls":false}'
|
||||
title: testfilter1
|
||||
owner_id: 1
|
||||
updated: 2020-09-08 15:13:12
|
||||
|
|
|
@ -184,9 +184,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("start and end date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date", "end_date", "due_date"},
|
||||
"filter_value": []string{"2018-12-11T03:46:40+00:00", "2018-12-13T11:20:01+00:00", "2018-11-29T14:00:00+00:00"},
|
||||
"filter_comparator": []string{"greater", "less", "greater"},
|
||||
"filter": []string{"start_date > '2018-12-11T03:46:40+00:00' || end_date < '2018-12-13T11:20:01+00:00' || due_date > '2018-11-29T14:00:00+00:00'"},
|
||||
},
|
||||
urlParams,
|
||||
)
|
||||
|
@ -209,9 +207,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("start date only", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date"},
|
||||
"filter_value": []string{"2018-10-20T01:46:40+00:00"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
"filter": []string{"start_date > '2018-10-20T01:46:40+00:00'"},
|
||||
},
|
||||
urlParams,
|
||||
)
|
||||
|
@ -234,9 +230,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("end date only", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"end_date"},
|
||||
"filter_value": []string{"2018-12-13T11:20:01+00:00"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
"filter": []string{"end_date > '2018-12-13T11:20:01+00:00'"},
|
||||
},
|
||||
urlParams,
|
||||
)
|
||||
|
@ -249,9 +243,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("unix timestamps", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date", "end_date", "due_date"},
|
||||
"filter_value": []string{"1544500000", "1513164001", "1543500000"},
|
||||
"filter_comparator": []string{"greater", "less", "greater"},
|
||||
"filter": []string{"start_date > 1544500000 || end_date < 1513164001 || due_date > 1543500000"},
|
||||
},
|
||||
urlParams,
|
||||
)
|
||||
|
@ -275,9 +267,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("invalid date", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"due_date"},
|
||||
"filter_value": []string{"invalid"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
"filter": []string{"due_date > invalid"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
@ -411,9 +401,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("start and end date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date", "end_date", "due_date"},
|
||||
"filter_value": []string{"2018-12-11T03:46:40+00:00", "2018-12-13T11:20:01+00:00", "2018-11-29T14:00:00+00:00"},
|
||||
"filter_comparator": []string{"greater", "less", "greater"},
|
||||
"filter": []string{"start_date > '2018-12-11T03:46:40+00:00' || end_date < '2018-12-13T11:20:01+00:00' || due_date > '2018-11-29T14:00:00+00:00'"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
@ -436,9 +424,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("start date only", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date"},
|
||||
"filter_value": []string{"2018-10-20T01:46:40+00:00"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
"filter": []string{"start_date > '2018-10-20T01:46:40+00:00'"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
@ -461,9 +447,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("end date only", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"end_date"},
|
||||
"filter_value": []string{"2018-12-13T11:20:01+00:00"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
"filter": []string{"end_date > '2018-12-13T11:20:01+00:00'"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
@ -477,9 +461,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("invalid date", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"due_date"},
|
||||
"filter_value": []string{"invalid"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
"filter": []string{"due_date > invalid"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type taskCollectionFilter20231121191822 struct {
|
||||
SortBy []string `query:"sort_by" json:"sort_by"`
|
||||
OrderBy []string `query:"order_by" json:"order_by"`
|
||||
|
||||
FilterBy []string `query:"filter_by" json:"filter_by,omitempty"`
|
||||
FilterValue []string `query:"filter_value" json:"filter_value,omitempty"`
|
||||
FilterComparator []string `query:"filter_comparator" json:"filter_comparator,omitempty"`
|
||||
FilterConcat string `query:"filter_concat" json:"filter_concat,omitempty"`
|
||||
|
||||
Filter string `query:"filter" json:"filter"`
|
||||
FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"`
|
||||
}
|
||||
|
||||
type savedFilter20231121191822 struct {
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"filter"`
|
||||
Filters *taskCollectionFilter20231121191822 `xorm:"JSON not null" json:"filters" valid:"required"`
|
||||
}
|
||||
|
||||
func (savedFilter20231121191822) TableName() string {
|
||||
return "saved_filters"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20231121191822",
|
||||
Description: "Migrate saved filter structure",
|
||||
Migrate: func(tx *xorm.Engine) (err error) {
|
||||
allFilters := []*savedFilter20231121191822{}
|
||||
err = tx.Find(&allFilters)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, filter := range allFilters {
|
||||
var filterStrings []string
|
||||
for i, f := range filter.Filters.FilterBy {
|
||||
var comparator string
|
||||
switch filter.Filters.FilterComparator[i] {
|
||||
case "equals":
|
||||
comparator = "="
|
||||
case "greater":
|
||||
comparator = ">"
|
||||
case "greater_equals":
|
||||
comparator = ">="
|
||||
case "less":
|
||||
comparator = "<"
|
||||
case "less_equals":
|
||||
comparator = "<="
|
||||
case "not_equals":
|
||||
comparator = "!="
|
||||
case "like":
|
||||
comparator = "~"
|
||||
case "in":
|
||||
comparator = "?="
|
||||
}
|
||||
filterStrings = append(filterStrings, f+" "+comparator+" "+filter.Filters.FilterValue[i])
|
||||
}
|
||||
|
||||
filter.Filters.FilterConcat = " || "
|
||||
if filter.Filters.FilterConcat == "and" {
|
||||
filter.Filters.FilterConcat = " && "
|
||||
}
|
||||
filter.Filters.Filter = strings.Join(filterStrings, filter.Filters.FilterConcat)
|
||||
|
||||
filter.Filters.FilterBy = nil
|
||||
filter.Filters.FilterComparator = nil
|
||||
filter.Filters.FilterValue = nil
|
||||
filter.Filters.FilterConcat = ""
|
||||
|
||||
_, err = tx.Where("id = ?", filter.ID).Update(filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -1021,7 +1021,7 @@ func (err ErrTaskRelationCycle) Error() string {
|
|||
}
|
||||
|
||||
// ErrCodeTaskRelationCycle holds the unique world-error code of this error
|
||||
const ErrCodeTaskRelationCycle = 4022
|
||||
const ErrCodeTaskRelationCycle = 4023
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTaskRelationCycle) HTTPError() web.HTTPError {
|
||||
|
@ -1032,6 +1032,34 @@ func (err ErrTaskRelationCycle) HTTPError() web.HTTPError {
|
|||
}
|
||||
}
|
||||
|
||||
// ErrInvalidFilterExpression represents an error where the task filter expression was invalid
|
||||
type ErrInvalidFilterExpression struct {
|
||||
Expression string
|
||||
ExpressionError error
|
||||
}
|
||||
|
||||
// IsErrInvalidFilterExpression checks if an error is ErrInvalidFilterExpression.
|
||||
func IsErrInvalidFilterExpression(err error) bool {
|
||||
_, ok := err.(ErrInvalidFilterExpression)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidFilterExpression) Error() string {
|
||||
return fmt.Sprintf("Task filter expression '%s' is invalid [ExpressionError: %v]", err.Expression, err.ExpressionError)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidFilterExpression holds the unique world-error code of this error
|
||||
const ErrCodeInvalidFilterExpression = 4024
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidFilterExpression) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
Code: ErrCodeInvalidFilterExpression,
|
||||
Message: fmt.Sprintf("The filter expression '%s' is invalid: %v", err.Expression, err.ExpressionError),
|
||||
}
|
||||
}
|
||||
|
||||
// ============
|
||||
// Team errors
|
||||
// ============
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
@ -173,28 +175,36 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||
opts.page = page
|
||||
opts.perPage = perPage
|
||||
opts.search = search
|
||||
opts.filterConcat = filterConcatAnd
|
||||
|
||||
var bucketFilterIndex int
|
||||
for i, filter := range opts.filters {
|
||||
for _, filter := range opts.parsedFilters {
|
||||
if filter.field == taskPropertyBucketID {
|
||||
bucketFilterIndex = i
|
||||
|
||||
// Limiting the map to the one filter we're looking for is the easiest way to ensure we only
|
||||
// get tasks in this bucket
|
||||
bucketID := filter.value.(int64)
|
||||
bucket := bucketMap[bucketID]
|
||||
|
||||
bucketMap = make(map[int64]*Bucket, 1)
|
||||
bucketMap[bucketID] = bucket
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if bucketFilterIndex == 0 {
|
||||
opts.filters = append(opts.filters, &taskFilter{
|
||||
field: taskPropertyBucketID,
|
||||
value: 0,
|
||||
comparator: taskFilterComparatorEquals,
|
||||
})
|
||||
bucketFilterIndex = len(opts.filters) - 1
|
||||
}
|
||||
|
||||
originalFilter := opts.filter
|
||||
for id, bucket := range bucketMap {
|
||||
|
||||
opts.filters[bucketFilterIndex].value = id
|
||||
if !strings.Contains(originalFilter, "bucket_id") {
|
||||
var filterString string
|
||||
if originalFilter == "" {
|
||||
filterString = "bucket_id = " + strconv.FormatInt(id, 10)
|
||||
} else {
|
||||
filterString = "(" + originalFilter + ") && bucket_id = " + strconv.FormatInt(id, 10)
|
||||
}
|
||||
opts.parsedFilters, err = getTaskFiltersFromFilterString(filterString)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ts, _, total, err := getRawTasksForProjects(s, []*Project{{ID: bucket.ProjectID}}, auth, opts)
|
||||
if err != nil {
|
||||
|
|
|
@ -81,9 +81,7 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||
b := &Bucket{
|
||||
ProjectID: 1,
|
||||
TaskCollection: TaskCollection{
|
||||
FilterBy: []string{"title"},
|
||||
FilterComparator: []string{"like"},
|
||||
FilterValue: []string{"done"},
|
||||
Filter: "title ~ 'done'",
|
||||
},
|
||||
}
|
||||
bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", -1, 0)
|
||||
|
@ -94,6 +92,30 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||
assert.Equal(t, int64(2), buckets[0].Tasks[0].ID)
|
||||
assert.Equal(t, int64(33), buckets[0].Tasks[1].ID)
|
||||
})
|
||||
t.Run("filtered by bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
testuser := &user.User{ID: 1}
|
||||
b := &Bucket{
|
||||
ProjectID: 1,
|
||||
TaskCollection: TaskCollection{
|
||||
Filter: "title ~ 'task' && bucket_id = 2",
|
||||
},
|
||||
}
|
||||
bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", -1, 0)
|
||||
assert.NoError(t, err)
|
||||
|
||||
buckets := bucketsInterface.([]*Bucket)
|
||||
assert.Len(t, buckets, 3)
|
||||
assert.Len(t, buckets[0].Tasks, 0)
|
||||
assert.Len(t, buckets[1].Tasks, 3)
|
||||
assert.Len(t, buckets[2].Tasks, 0)
|
||||
assert.Equal(t, int64(3), buckets[1].Tasks[0].ID)
|
||||
assert.Equal(t, int64(4), buckets[1].Tasks[1].ID)
|
||||
assert.Equal(t, int64(5), buckets[1].Tasks[2].ID)
|
||||
})
|
||||
t.Run("accessed by link share", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
|
|
@ -65,7 +65,7 @@ func TestSavedFilter_Create(t *testing.T) {
|
|||
vals := map[string]interface{}{
|
||||
"title": "'test'",
|
||||
"description": "'Lorem Ipsum dolor sit amet'",
|
||||
"filters": "'{\"sort_by\":null,\"order_by\":null,\"filter_by\":null,\"filter_value\":null,\"filter_comparator\":null,\"filter_concat\":\"\",\"filter_include_nulls\":false}'",
|
||||
"filters": "'{\"sort_by\":null,\"order_by\":null,\"filter\":\"\",\"filter_include_nulls\":false}'",
|
||||
"owner_id": 1,
|
||||
}
|
||||
// Postgres can't compare json values directly, see https://dba.stackexchange.com/a/106290/210721
|
||||
|
|
|
@ -33,17 +33,8 @@ type TaskCollection struct {
|
|||
OrderBy []string `query:"order_by" json:"order_by"`
|
||||
OrderByArr []string `query:"order_by[]" json:"-"`
|
||||
|
||||
// The field name of the field to filter by
|
||||
FilterBy []string `query:"filter_by" json:"filter_by"`
|
||||
FilterByArr []string `query:"filter_by[]" json:"-"`
|
||||
// The value of the field name to filter by
|
||||
FilterValue []string `query:"filter_value" json:"filter_value"`
|
||||
FilterValueArr []string `query:"filter_value[]" json:"-"`
|
||||
// The comparator for field and value
|
||||
FilterComparator []string `query:"filter_comparator" json:"filter_comparator"`
|
||||
FilterComparatorArr []string `query:"filter_comparator[]" json:"-"`
|
||||
// The way all filter conditions are concatenated together, can be either "and" or "or".,
|
||||
FilterConcat string `query:"filter_concat" json:"filter_concat"`
|
||||
Filter string `query:"filter" json:"filter"`
|
||||
|
||||
// If set to true, the result will also include null values
|
||||
FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"`
|
||||
|
||||
|
@ -110,11 +101,11 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption
|
|||
|
||||
opts = &taskSearchOptions{
|
||||
sortby: sort,
|
||||
filterConcat: taskFilterConcatinator(tf.FilterConcat),
|
||||
filterIncludeNulls: tf.FilterIncludeNulls,
|
||||
filter: tf.Filter,
|
||||
}
|
||||
|
||||
opts.filters, err = getTaskFiltersByCollections(tf)
|
||||
opts.parsedFilters, err = getTaskFiltersFromFilterString(tf.Filter)
|
||||
return opts, err
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ganigeorgiev/fexpr"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
|
@ -54,6 +56,7 @@ type taskFilter struct {
|
|||
value interface{} // Needs to be an interface to be able to hold the field's native value
|
||||
comparator taskFilterComparator
|
||||
isNumeric bool
|
||||
join taskFilterConcatinator
|
||||
}
|
||||
|
||||
func parseTimeFromUserInput(timeString string) (value time.Time, err error) {
|
||||
|
@ -88,61 +91,83 @@ func parseTimeFromUserInput(timeString string) (value time.Time, err error) {
|
|||
return value.In(config.GetTimeZone()), err
|
||||
}
|
||||
|
||||
func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err error) {
|
||||
|
||||
if len(c.FilterByArr) > 0 {
|
||||
c.FilterBy = append(c.FilterBy, c.FilterByArr...)
|
||||
func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error) {
|
||||
filter = &taskFilter{
|
||||
join: filterConcatAnd,
|
||||
}
|
||||
if f.Join == fexpr.JoinOr {
|
||||
filter.join = filterConcatOr
|
||||
}
|
||||
|
||||
if len(c.FilterValueArr) > 0 {
|
||||
c.FilterValue = append(c.FilterValue, c.FilterValueArr...)
|
||||
}
|
||||
|
||||
if len(c.FilterComparatorArr) > 0 {
|
||||
c.FilterComparator = append(c.FilterComparator, c.FilterComparatorArr...)
|
||||
}
|
||||
|
||||
if c.FilterConcat != "" && c.FilterConcat != filterConcatAnd && c.FilterConcat != filterConcatOr {
|
||||
return nil, ErrInvalidTaskFilterConcatinator{
|
||||
Concatinator: taskFilterConcatinator(c.FilterConcat),
|
||||
}
|
||||
}
|
||||
|
||||
filters = make([]*taskFilter, 0, len(c.FilterBy))
|
||||
for i, f := range c.FilterBy {
|
||||
filter := &taskFilter{
|
||||
field: f,
|
||||
comparator: taskFilterComparatorEquals,
|
||||
}
|
||||
|
||||
if len(c.FilterComparator) > i {
|
||||
filter.comparator, err = getFilterComparatorFromString(c.FilterComparator[i])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = validateTaskFieldComparator(filter.comparator)
|
||||
var value string
|
||||
switch v := f.Item.(type) {
|
||||
case fexpr.Expr:
|
||||
filter.field = v.Left.Literal
|
||||
value = v.Right.Literal
|
||||
filter.comparator, err = getFilterComparatorFromOp(v.Op)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Cast the field value to its native type
|
||||
var reflectValue *reflect.StructField
|
||||
if len(c.FilterValue) > i {
|
||||
reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, c.FilterValue[i])
|
||||
case []fexpr.ExprGroup:
|
||||
values := make([]*taskFilter, 0, len(v))
|
||||
for _, expression := range v {
|
||||
subfilter, err := parseFilterFromExpression(expression)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidTaskFilterValue{
|
||||
Value: filter.field,
|
||||
Field: c.FilterValue[i],
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
values = append(values, subfilter)
|
||||
}
|
||||
if reflectValue != nil {
|
||||
filter.isNumeric = reflectValue.Type.Kind() == reflect.Int64
|
||||
}
|
||||
filter.value = values
|
||||
return
|
||||
}
|
||||
|
||||
filters = append(filters, filter)
|
||||
err = validateTaskFieldComparator(filter.comparator)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Cast the field value to its native type
|
||||
var reflectValue *reflect.StructField
|
||||
if filter.field == "project" {
|
||||
filter.field = "project_id"
|
||||
}
|
||||
reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, value)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidTaskFilterValue{
|
||||
Value: filter.field,
|
||||
Field: value,
|
||||
}
|
||||
}
|
||||
if reflectValue != nil {
|
||||
filter.isNumeric = reflectValue.Type.Kind() == reflect.Int64
|
||||
}
|
||||
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func getTaskFiltersFromFilterString(filter string) (filters []*taskFilter, err error) {
|
||||
|
||||
if filter == "" {
|
||||
return
|
||||
}
|
||||
|
||||
filter = strings.ReplaceAll(filter, " in ", " ?= ")
|
||||
|
||||
parsedFilter, err := fexpr.Parse(filter)
|
||||
if err != nil {
|
||||
return nil, &ErrInvalidFilterExpression{
|
||||
Expression: filter,
|
||||
ExpressionError: err,
|
||||
}
|
||||
}
|
||||
|
||||
filters = make([]*taskFilter, 0, len(parsedFilter))
|
||||
for _, f := range parsedFilter {
|
||||
parsedFilter, err := parseFilterFromExpression(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filters = append(filters, parsedFilter)
|
||||
}
|
||||
|
||||
return
|
||||
|
@ -167,26 +192,28 @@ func validateTaskFieldComparator(comparator taskFilterComparator) error {
|
|||
}
|
||||
}
|
||||
|
||||
func getFilterComparatorFromString(comparator string) (taskFilterComparator, error) {
|
||||
switch comparator {
|
||||
case "equals":
|
||||
func getFilterComparatorFromOp(op fexpr.SignOp) (taskFilterComparator, error) {
|
||||
switch op {
|
||||
case fexpr.SignEq:
|
||||
return taskFilterComparatorEquals, nil
|
||||
case "greater":
|
||||
case fexpr.SignGt:
|
||||
return taskFilterComparatorGreater, nil
|
||||
case "greater_equals":
|
||||
case fexpr.SignGte:
|
||||
return taskFilterComparatorGreateEquals, nil
|
||||
case "less":
|
||||
case fexpr.SignLt:
|
||||
return taskFilterComparatorLess, nil
|
||||
case "less_equals":
|
||||
case fexpr.SignLte:
|
||||
return taskFilterComparatorLessEquals, nil
|
||||
case "not_equals":
|
||||
case fexpr.SignNeq:
|
||||
return taskFilterComparatorNotEquals, nil
|
||||
case "like":
|
||||
case fexpr.SignLike:
|
||||
return taskFilterComparatorLike, nil
|
||||
case fexpr.SignAnyEq:
|
||||
fallthrough
|
||||
case "in":
|
||||
return taskFilterComparatorIn, nil
|
||||
default:
|
||||
return taskFilterComparatorInvalid, ErrInvalidTaskFilterComparator{Comparator: taskFilterComparator(comparator)}
|
||||
return taskFilterComparatorInvalid, ErrInvalidTaskFilterComparator{Comparator: taskFilterComparator(op)}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@ import (
|
|||
"gopkg.in/d4l3k/messagediff.v1"
|
||||
)
|
||||
|
||||
// To only run a selected tests: ^\QTestTaskCollection_ReadAll\E$/^\QReadAll_Tasks_with_range\E$
|
||||
|
||||
func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
// Dummy users
|
||||
user1 := &user.User{
|
||||
|
@ -663,10 +665,8 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
SortBy []string // Is a string, since this is the place where a query string comes from the user
|
||||
OrderBy []string
|
||||
|
||||
FilterBy []string
|
||||
FilterValue []string
|
||||
FilterComparator []string
|
||||
FilterIncludeNulls bool
|
||||
Filter string
|
||||
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
|
@ -778,9 +778,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "ReadAll Tasks with range",
|
||||
fields: fields{
|
||||
FilterBy: []string{"start_date", "end_date"},
|
||||
FilterValue: []string{"2018-12-11T03:46:40+00:00", "2018-12-13T11:20:01+00:00"},
|
||||
FilterComparator: []string{"greater", "less"},
|
||||
Filter: "start_date > '2018-12-11T03:46:40+00:00' || end_date < '2018-12-13T11:20:01+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -793,9 +791,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "ReadAll Tasks with different range",
|
||||
fields: fields{
|
||||
FilterBy: []string{"start_date", "end_date"},
|
||||
FilterValue: []string{"2018-12-13T11:20:00+00:00", "2018-12-16T22:40:00+00:00"},
|
||||
FilterComparator: []string{"greater", "less"},
|
||||
Filter: "start_date > '2018-12-13T11:20:00+00:00' || end_date < '2018-12-16T22:40:00+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -807,20 +803,16 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "ReadAll Tasks with range with start date only",
|
||||
fields: fields{
|
||||
FilterBy: []string{"start_date"},
|
||||
FilterValue: []string{"2018-12-12T07:33:20+00:00"},
|
||||
FilterComparator: []string{"greater"},
|
||||
Filter: "start_date > '2018-12-12T07:33:20+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks with range with start date only and greater equals",
|
||||
name: "ReadAll Tasks with range with start date only between",
|
||||
fields: fields{
|
||||
FilterBy: []string{"start_date"},
|
||||
FilterValue: []string{"2018-12-12T07:33:20+00:00"},
|
||||
FilterComparator: []string{"greater_equals"},
|
||||
Filter: "start_date > '2018-12-12T00:00:00+00:00' && start_date < '2018-12-13T00:00:00+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -829,12 +821,35 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ReadAll Tasks with range with start date only and greater equals",
|
||||
fields: fields{
|
||||
Filter: "start_date >= '2018-12-12T07:33:20+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task7,
|
||||
task9,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "range and nesting",
|
||||
fields: fields{
|
||||
Filter: "(start_date > '2018-12-12T00:00:00+00:00' && start_date < '2018-12-13T00:00:00+00:00') || end_date > '2018-12-13T00:00:00+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "undone tasks only",
|
||||
fields: fields{
|
||||
FilterBy: []string{"done"},
|
||||
FilterValue: []string{"false"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "done = false",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -878,9 +893,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "done tasks only",
|
||||
fields: fields{
|
||||
FilterBy: []string{"done"},
|
||||
FilterValue: []string{"true"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "done = true",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -891,9 +904,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "done tasks only - not equals done",
|
||||
fields: fields{
|
||||
FilterBy: []string{"done"},
|
||||
FilterValue: []string{"false"},
|
||||
FilterComparator: []string{"not_equals"},
|
||||
Filter: "done != false",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -904,10 +915,8 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "range with nulls",
|
||||
fields: fields{
|
||||
FilterBy: []string{"start_date", "end_date"},
|
||||
FilterValue: []string{"2018-12-11T03:46:40+00:00", "2018-12-13T11:20:01+00:00"},
|
||||
FilterComparator: []string{"greater", "less"},
|
||||
FilterIncludeNulls: true,
|
||||
Filter: "start_date > '2018-12-11T03:46:40+00:00' || end_date < '2018-12-13T11:20:01+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -961,9 +970,26 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "filtered with like",
|
||||
fields: fields{
|
||||
FilterBy: []string{"title"},
|
||||
FilterValue: []string{"with"},
|
||||
FilterComparator: []string{"like"},
|
||||
Filter: "title ~ with",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task27,
|
||||
task28,
|
||||
task29,
|
||||
task30,
|
||||
task31,
|
||||
task33,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filtered with like and '",
|
||||
fields: fields{
|
||||
Filter: "title ~ 'with'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -982,9 +1008,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "filtered reminder dates",
|
||||
fields: fields{
|
||||
FilterBy: []string{"reminders", "reminders"},
|
||||
FilterValue: []string{"2018-10-01T00:00:00+00:00", "2018-12-10T00:00:00+00:00"},
|
||||
FilterComparator: []string{"greater", "less"},
|
||||
Filter: "reminders > '2018-10-01T00:00:00+00:00' && reminders < '2018-12-10T00:00:00+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -993,12 +1017,22 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter in keyword",
|
||||
fields: fields{
|
||||
Filter: "id in '1,2,34'", // user does not have permission to access task 34
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter in",
|
||||
fields: fields{
|
||||
FilterBy: []string{"id"},
|
||||
FilterValue: []string{"1,2,34"}, // Task 34 is forbidden for user 1
|
||||
FilterComparator: []string{"in"},
|
||||
Filter: "id ?= '1,2,34'", // user does not have permission to access task 34
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -1010,9 +1044,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "filter assignees by username",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"user1"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "assignees = 'user1'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -1023,9 +1055,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "filter assignees by username with users field name",
|
||||
fields: fields{
|
||||
FilterBy: []string{"users"},
|
||||
FilterValue: []string{"user1"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "users = 'user1'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: nil,
|
||||
|
@ -1034,9 +1064,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "filter assignees by username with user_id field name",
|
||||
fields: fields{
|
||||
FilterBy: []string{"user_id"},
|
||||
FilterValue: []string{"user1"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "user_id = 'user1'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: nil,
|
||||
|
@ -1045,9 +1073,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "filter assignees by multiple username",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees", "assignees"},
|
||||
FilterValue: []string{"user1", "user2"},
|
||||
FilterComparator: []string{"equals", "equals"},
|
||||
Filter: "assignees = 'user1' || assignees = 'user2'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -1059,9 +1085,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "filter assignees by numbers",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"1"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "assignees = 1",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
|
@ -1070,20 +1094,50 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "filter assignees by name with like",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"user"},
|
||||
FilterComparator: []string{"like"},
|
||||
Filter: "assignees ~ 'user'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
// Same as without any filter since the filter is ignored
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task11,
|
||||
task12,
|
||||
task15,
|
||||
task16,
|
||||
task17,
|
||||
task18,
|
||||
task19,
|
||||
task20,
|
||||
task21,
|
||||
task22,
|
||||
task23,
|
||||
task24,
|
||||
task25,
|
||||
task26,
|
||||
task27,
|
||||
task28,
|
||||
task29,
|
||||
task30,
|
||||
task31,
|
||||
task32,
|
||||
task33,
|
||||
task35,
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter assignees in by id",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"1,2"},
|
||||
FilterComparator: []string{"in"},
|
||||
Filter: "assignees ?= '1,2'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
|
@ -1092,9 +1146,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "filter assignees in by username",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"user1,user2"},
|
||||
FilterComparator: []string{"in"},
|
||||
Filter: "assignees ?= 'user1,user2'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -1106,9 +1158,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
{
|
||||
name: "filter labels",
|
||||
fields: fields{
|
||||
FilterBy: []string{"labels"},
|
||||
FilterValue: []string{"4"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "labels = 4",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -1119,11 +1169,9 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter project",
|
||||
name: "filter project_id",
|
||||
fields: fields{
|
||||
FilterBy: []string{"project_id"},
|
||||
FilterValue: []string{"6"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "project_id = 6",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -1131,13 +1179,31 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter project",
|
||||
fields: fields{
|
||||
Filter: "project = 6",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task15,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter project forbidden",
|
||||
fields: fields{
|
||||
Filter: "project_id = 20", // user1 has no access to project 20
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
wantErr: false,
|
||||
},
|
||||
// TODO filter parent project?
|
||||
{
|
||||
name: "filter by index",
|
||||
fields: fields{
|
||||
FilterBy: []string{"index"},
|
||||
FilterValue: []string{"5"},
|
||||
FilterComparator: []string{"equals"},
|
||||
Filter: "index = 5",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
|
@ -1304,6 +1370,8 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
task9,
|
||||
},
|
||||
},
|
||||
// TODO unix dates
|
||||
// TODO date magic
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
@ -1317,11 +1385,10 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
SortBy: tt.fields.SortBy,
|
||||
OrderBy: tt.fields.OrderBy,
|
||||
|
||||
FilterBy: tt.fields.FilterBy,
|
||||
FilterValue: tt.fields.FilterValue,
|
||||
FilterComparator: tt.fields.FilterComparator,
|
||||
FilterIncludeNulls: tt.fields.FilterIncludeNulls,
|
||||
|
||||
Filter: tt.fields.Filter,
|
||||
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
|
|
|
@ -76,23 +76,21 @@ func getOrderByDBStatement(opts *taskSearchOptions) (orderby string, err error)
|
|||
return
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
||||
func convertFiltersToDBFilterCond(rawFilters []*taskFilter, includeNulls bool) (filterCond builder.Cond, err error) {
|
||||
|
||||
orderby, err := getOrderByDBStatement(opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Some filters need a special treatment since they are in a separate table
|
||||
reminderFilters := []builder.Cond{}
|
||||
assigneeFilters := []builder.Cond{}
|
||||
labelFilters := []builder.Cond{}
|
||||
projectFilters := []builder.Cond{}
|
||||
|
||||
var filters = make([]builder.Cond, 0, len(opts.filters))
|
||||
var dbFilters = make([]builder.Cond, 0, len(rawFilters))
|
||||
// To still find tasks with nil values, we exclude 0s when comparing with >/< values.
|
||||
for _, f := range opts.filters {
|
||||
for _, f := range rawFilters {
|
||||
|
||||
if nested, is := f.value.([]*taskFilter); is {
|
||||
nestedDBFilters, err := convertFiltersToDBFilterCond(nested, includeNulls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbFilters = append(dbFilters, nestedDBFilters)
|
||||
continue
|
||||
}
|
||||
|
||||
if f.field == "reminders" {
|
||||
filter, err := getFilterCond(&taskFilter{
|
||||
// recreating the struct here to avoid modifying it when reusing the opts struct
|
||||
|
@ -100,17 +98,17 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
}, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
reminderFilters = append(reminderFilters, filter)
|
||||
dbFilters = append(dbFilters, getFilterCondForSeparateTable("task_reminders", filter))
|
||||
continue
|
||||
}
|
||||
|
||||
if f.field == "assignees" {
|
||||
if f.comparator == taskFilterComparatorLike {
|
||||
return nil, totalCount, err
|
||||
return
|
||||
}
|
||||
filter, err := getFilterCond(&taskFilter{
|
||||
// recreating the struct here to avoid modifying it when reusing the opts struct
|
||||
|
@ -118,11 +116,17 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
}, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
assigneeFilters = append(assigneeFilters, filter)
|
||||
|
||||
assigneeFilter := builder.In("user_id",
|
||||
builder.Select("id").
|
||||
From("users").
|
||||
Where(filter),
|
||||
)
|
||||
dbFilters = append(dbFilters, getFilterCondForSeparateTable("task_assignees", assigneeFilter))
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -133,11 +137,12 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
}, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
labelFilters = append(labelFilters, filter)
|
||||
|
||||
dbFilters = append(dbFilters, getFilterCondForSeparateTable("label_tasks", filter))
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -148,19 +153,60 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
}, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
projectFilters = append(projectFilters, filter)
|
||||
|
||||
cond := builder.In(
|
||||
"project_id",
|
||||
builder.
|
||||
Select("id").
|
||||
From("projects").
|
||||
Where(filter),
|
||||
)
|
||||
dbFilters = append(dbFilters, cond)
|
||||
continue
|
||||
}
|
||||
|
||||
filter, err := getFilterCond(f, opts.filterIncludeNulls)
|
||||
filter, err := getFilterCond(f, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
filters = append(filters, filter)
|
||||
dbFilters = append(dbFilters, filter)
|
||||
}
|
||||
|
||||
if len(dbFilters) > 0 {
|
||||
if len(dbFilters) == 1 {
|
||||
filterCond = dbFilters[0]
|
||||
} else {
|
||||
for i, f := range dbFilters {
|
||||
if len(dbFilters) > i+1 {
|
||||
switch rawFilters[i+1].join {
|
||||
case filterConcatOr:
|
||||
filterCond = builder.Or(filterCond, f, dbFilters[i+1])
|
||||
case filterConcatAnd:
|
||||
filterCond = builder.And(filterCond, f, dbFilters[i+1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filterCond, nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
||||
|
||||
orderby, err := getOrderByDBStatement(opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
filterCond, err := convertFiltersToDBFilterCond(opts.parsedFilters, opts.filterIncludeNulls)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Then return all tasks for that projects
|
||||
|
@ -199,53 +245,6 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||
favoritesCond = builder.In("id", favCond)
|
||||
}
|
||||
|
||||
if len(reminderFilters) > 0 {
|
||||
filters = append(filters, getFilterCondForSeparateTable("task_reminders", opts.filterConcat, reminderFilters))
|
||||
}
|
||||
|
||||
if len(assigneeFilters) > 0 {
|
||||
assigneeFilter := []builder.Cond{
|
||||
builder.In("user_id",
|
||||
builder.Select("id").
|
||||
From("users").
|
||||
Where(builder.Or(assigneeFilters...)),
|
||||
)}
|
||||
filters = append(filters, getFilterCondForSeparateTable("task_assignees", opts.filterConcat, assigneeFilter))
|
||||
}
|
||||
|
||||
if len(labelFilters) > 0 {
|
||||
filters = append(filters, getFilterCondForSeparateTable("label_tasks", opts.filterConcat, labelFilters))
|
||||
}
|
||||
|
||||
if len(projectFilters) > 0 {
|
||||
var filtercond builder.Cond
|
||||
if opts.filterConcat == filterConcatOr {
|
||||
filtercond = builder.Or(projectFilters...)
|
||||
}
|
||||
if opts.filterConcat == filterConcatAnd {
|
||||
filtercond = builder.And(projectFilters...)
|
||||
}
|
||||
|
||||
cond := builder.In(
|
||||
"project_id",
|
||||
builder.
|
||||
Select("id").
|
||||
From("projects").
|
||||
Where(filtercond),
|
||||
)
|
||||
filters = append(filters, cond)
|
||||
}
|
||||
|
||||
var filterCond builder.Cond
|
||||
if len(filters) > 0 {
|
||||
if opts.filterConcat == filterConcatOr {
|
||||
filterCond = builder.Or(filters...)
|
||||
}
|
||||
if opts.filterConcat == filterConcatAnd {
|
||||
filterCond = builder.And(filters...)
|
||||
}
|
||||
}
|
||||
|
||||
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
||||
cond := builder.And(builder.Or(projectIDCond, favoritesCond), where, filterCond)
|
||||
|
||||
|
@ -316,41 +315,23 @@ func convertFilterValues(value interface{}) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
||||
// Parsing and rebuilding the filter for Typesense has the advantage that we have more control over
|
||||
// what Typesense finally gets to see.
|
||||
func convertParsedFilterToTypesense(rawFilters []*taskFilter) (filterBy string, err error) {
|
||||
|
||||
var sortbyFields []string
|
||||
for i, param := range opts.sortby {
|
||||
// Validate the params
|
||||
if err := param.validate(); err != nil {
|
||||
return nil, totalCount, err
|
||||
filters := []string{}
|
||||
|
||||
for _, f := range rawFilters {
|
||||
|
||||
if nested, is := f.value.([]*taskFilter); is {
|
||||
nestedDBFilters, err := convertParsedFilterToTypesense(nested)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
filters = append(filters, "("+nestedDBFilters+")")
|
||||
continue
|
||||
}
|
||||
|
||||
// Typesense does not allow sorting by ID, so we sort by created timestamp instead
|
||||
if param.sortBy == "id" {
|
||||
param.sortBy = "created"
|
||||
}
|
||||
|
||||
sortbyFields = append(sortbyFields, param.sortBy+"(missing_values:last):"+param.orderBy.String())
|
||||
|
||||
if i == 2 {
|
||||
// Typesense supports up to 3 sorting parameters
|
||||
// https://typesense.org/docs/0.25.0/api/search.html#ranking-and-sorting-parameters
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sortby := strings.Join(sortbyFields, ",")
|
||||
|
||||
projectIDStrings := []string{}
|
||||
for _, id := range opts.projectIDs {
|
||||
projectIDStrings = append(projectIDStrings, strconv.FormatInt(id, 10))
|
||||
}
|
||||
filterBy := []string{
|
||||
"project_id: [" + strings.Join(projectIDStrings, ", ") + "]",
|
||||
}
|
||||
|
||||
for _, f := range opts.filters {
|
||||
|
||||
if f.field == "reminders" {
|
||||
f.field = "reminders.reminder"
|
||||
}
|
||||
|
@ -363,6 +344,10 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
|
|||
f.field = "labels.id"
|
||||
}
|
||||
|
||||
if f.field == "project" {
|
||||
f.field = "project_id"
|
||||
}
|
||||
|
||||
filter := f.field
|
||||
|
||||
switch f.comparator {
|
||||
|
@ -394,7 +379,67 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
|
|||
filter += "]"
|
||||
}
|
||||
|
||||
filterBy = append(filterBy, filter)
|
||||
filters = append(filters, filter)
|
||||
}
|
||||
|
||||
if len(filters) > 0 {
|
||||
if len(filters) == 1 {
|
||||
filterBy = filters[0]
|
||||
} else {
|
||||
for i, f := range filters {
|
||||
if len(filters) > i+1 {
|
||||
switch rawFilters[i+1].join {
|
||||
case filterConcatOr:
|
||||
filterBy = f + " || " + filters[i+1]
|
||||
case filterConcatAnd:
|
||||
filterBy = f + " && " + filters[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
||||
|
||||
var sortbyFields []string
|
||||
for i, param := range opts.sortby {
|
||||
// Validate the params
|
||||
if err := param.validate(); err != nil {
|
||||
return nil, totalCount, err
|
||||
}
|
||||
|
||||
// Typesense does not allow sorting by ID, so we sort by created timestamp instead
|
||||
if param.sortBy == "id" {
|
||||
param.sortBy = "created"
|
||||
}
|
||||
|
||||
sortbyFields = append(sortbyFields, param.sortBy+"(missing_values:last):"+param.orderBy.String())
|
||||
|
||||
if i == 2 {
|
||||
// Typesense supports up to 3 sorting parameters
|
||||
// https://typesense.org/docs/0.25.0/api/search.html#ranking-and-sorting-parameters
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sortby := strings.Join(sortbyFields, ",")
|
||||
|
||||
projectIDStrings := []string{}
|
||||
for _, id := range opts.projectIDs {
|
||||
projectIDStrings = append(projectIDStrings, strconv.FormatInt(id, 10))
|
||||
}
|
||||
|
||||
filter, err := convertParsedFilterToTypesense(opts.parsedFilters)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
filterBy := []string{
|
||||
"project_id: [" + strings.Join(projectIDStrings, ", ") + "]",
|
||||
"(" + filter + ")",
|
||||
}
|
||||
|
||||
////////////////
|
||||
|
|
|
@ -162,8 +162,8 @@ func (t *Task) GetFrontendURL() string {
|
|||
type taskFilterConcatinator string
|
||||
|
||||
const (
|
||||
filterConcatAnd = "and"
|
||||
filterConcatOr = "or"
|
||||
filterConcatAnd taskFilterConcatinator = "and"
|
||||
filterConcatOr taskFilterConcatinator = "or"
|
||||
)
|
||||
|
||||
type taskSearchOptions struct {
|
||||
|
@ -171,9 +171,9 @@ type taskSearchOptions struct {
|
|||
page int
|
||||
perPage int
|
||||
sortby []*sortParam
|
||||
filters []*taskFilter
|
||||
filterConcat taskFilterConcatinator
|
||||
parsedFilters []*taskFilter
|
||||
filterIncludeNulls bool
|
||||
filter string
|
||||
projectIDs []int64
|
||||
}
|
||||
|
||||
|
@ -238,21 +238,13 @@ func getFilterCond(f *taskFilter, includeNulls bool) (cond builder.Cond, err err
|
|||
return
|
||||
}
|
||||
|
||||
func getFilterCondForSeparateTable(table string, concat taskFilterConcatinator, conds []builder.Cond) builder.Cond {
|
||||
var filtercond builder.Cond
|
||||
if concat == filterConcatOr {
|
||||
filtercond = builder.Or(conds...)
|
||||
}
|
||||
if concat == filterConcatAnd {
|
||||
filtercond = builder.And(conds...)
|
||||
}
|
||||
|
||||
func getFilterCondForSeparateTable(table string, cond builder.Cond) builder.Cond {
|
||||
return builder.In(
|
||||
"id",
|
||||
builder.
|
||||
Select("task_id").
|
||||
From(table).
|
||||
Where(filtercond),
|
||||
Where(cond),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -273,11 +265,6 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
|
|||
return nil, 0, 0, nil
|
||||
}
|
||||
|
||||
// Set the default concatinator of filter variables to or if none was provided
|
||||
if opts.filterConcat == "" {
|
||||
opts.filterConcat = filterConcatOr
|
||||
}
|
||||
|
||||
// Get all project IDs and get the tasks
|
||||
opts.projectIDs = []int64{}
|
||||
var hasFavoritesProject bool
|
||||
|
|
Loading…
Reference in New Issue