feat(filters): make date values in filter query editable

This commit is contained in:
kolaente 2024-02-17 17:48:22 +01:00
parent 3bd639a110
commit c22daab28c
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
5 changed files with 156 additions and 131 deletions

View File

@ -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: {

View File

@ -1,13 +1,9 @@
<template>
<div class="datepicker-with-range-container">
<Popup>
<template #trigger="{toggle}">
<slot
name="trigger"
:toggle="toggle"
:button-text="buttonText"
/>
</template>
<Popup
:open="open"
@close="() => emit('close')"
>
<template #content="{isOpen}">
<div
class="datepicker-with-range"
@ -16,45 +12,26 @@
<div class="selections">
<BaseButton
:class="{'is-active': customRangeActive}"
@click="setDateRange(null)"
@click="setDate(null)"
>
{{ $t('misc.custom') }}
</BaseButton>
<BaseButton
v-for="(value, text) in DATE_RANGES"
:key="text"
:class="{'is-active': from === value[0] && to === value[1]}"
@click="setDateRange(value)"
:class="{'is-active': date === value[0]}"
@click="setDate(value[0])"
>
{{ $t(`input.datepickerRange.ranges.${text}`) }}
</BaseButton>
</div>
<div class="flatpickr-container input-group">
<label class="label">
{{ $t('input.datepickerRange.from') }}
{{ $t('input.datepickerRange.date') }}
<div class="field has-addons">
<div class="control is-fullwidth">
<input
v-model="from"
class="input"
type="text"
>
</div>
<div class="control">
<x-button
icon="calendar"
variant="secondary"
data-toggle
/>
</div>
</div>
</label>
<label class="label">
{{ $t('input.datepickerRange.to') }}
<div class="field has-addons">
<div class="control is-fullwidth">
<input
v-model="to"
v-model="date"
class="input"
type="text"
>
@ -69,20 +46,21 @@
</div>
</label>
<flat-pickr
v-model="flatpickrRange"
v-model="flatpickrDate"
:config="flatPickerConfig"
/>
<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 +68,7 @@
variant="hint-modal"
@close="() => showHowItWorks = false"
>
<DatemathHelp />
<DatemathHelp/>
</modal>
</div>
</div>
@ -111,15 +89,19 @@ 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: {
required: false,
},
open: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'close'])
const {t} = useI18n({useScope: 'global'})
@ -129,87 +111,58 @@ const flatPickerConfig = computed(() => ({
dateFormat: 'Y-m-d H:i',
enableTime: false,
wrap: true,
mode: 'range',
locale: getFlatpickrLanguage(),
}))
const showHowItWorks = ref(false)
const flatpickrRange = ref('')
const flatpickrDate = ref('')
const from = ref('')
const to = ref('')
const date = ref<string|Date>('')
watch(
() => props.modelValue,
newValue => {
from.value = newValue.dateFrom
to.value = newValue.dateTo
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 dateFrom = parseDateOrString(from.value, false)
const dateTo = parseDateOrString(to.value, false)
if (dateFrom instanceof Date && dateTo instanceof Date) {
flatpickrRange.value = `${from.value} to ${to.value}`
const parsed = parseDateOrString(date.value, false)
if (parsed instanceof Date) {
flatpickrDate.value = date.value
}
},
)
function emitChanged() {
const args = {
dateFrom: from.value === '' ? null : from.value,
dateTo: to.value === '' ? null : to.value,
}
emit('update:modelValue', args)
emit('update:modelValue', date.value === '' ? null : date.value)
}
watch(
() => flatpickrRange.value,
() => flatpickrDate.value,
(newVal: string | null) => {
if (newVal === null) {
return
}
const [fromDate, toDate] = newVal.split(' to ')
if (typeof fromDate === 'undefined' || typeof toDate === 'undefined') {
return
}
from.value = fromDate
to.value = toDate
date.value = newVal
emitChanged()
},
)
watch(() => from.value, emitChanged)
watch(() => to.value, emitChanged)
watch(() => date.value, emitChanged)
function setDateRange(range: string[] | null) {
function setDate(range: string | null) {
if (range === null) {
from.value = ''
to.value = ''
date.value = ''
return
}
from.value = range[0]
to.value = range[1]
date.value = range
}
const customRangeActive = computed<boolean>(() => {
return !Object.values(DATE_RANGES).some(range => from.value === range[0] && to.value === range[1])
})
const buttonText = computed<string>(() => {
if (from.value !== '' && to.value !== '') {
return t('input.datepickerRange.fromto', {
from: from.value,
to: to.value,
})
}
return t('task.show.select')
return !Object.values(DATE_RANGES).some(d => date.value === d)
})
</script>

View File

@ -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)

View File

@ -1,18 +1,22 @@
<script setup lang="ts">
import FilterInput from '@/components/project/partials/FilterInput.vue'
import {ref} from "vue";
function initState(value: string) {
return {
value,
}
}
const value = ref('')
</script>
<template>
<Story title="Filter Input">
<FilterInput v-model="value"/>
<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>

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import {computed, ref, watch} from 'vue'
import {computed, nextTick, ref, watch} from 'vue'
import {useAutoHeightTextarea} from '@/composables/useAutoHeightTextarea'
import DatepickerWithValues from '@/components/date/datepickerWithValues.vue'
const {
modelValue,
@ -59,16 +60,36 @@ const filterJoinOperators = [
')',
]
function escapeHtml(unsafe: string): string {
return unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
function unEscapeHtml(unsafe: string): string {
return unsafe
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot/g, '"')
.replace(/&#039;/g, "'")
}
const highlightedFilterQuery = computed(() => {
let highlighted = escapeHtml(filterQuery.value)
dateFields
.map(o => escapeHtml(o))
.forEach(o => {
const pattern = new RegExp(o + '\\s*(&lt;|&gt;|&lt;=|&gt;=|=|!=)\\s*([\'"]?)([^\'"\\s]+\\1?)?', 'ig');
highlighted = highlighted.replaceAll(pattern, (match, token, start, value, position) => {
console.log({match, token, value, position})
return `${o} ${token} <span class="filter-query__special_value">${value}</span><span class="filter-query__special_value_placeholder">${value}</span>`
// TODO: make special value a button with datepicker popup
highlighted = highlighted.replaceAll(pattern, (match, token, start, value, position, last) => {
console.log({position, last})
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>`
})
})
filterOperators
@ -87,13 +108,41 @@ const highlightedFilterQuery = computed(() => {
return highlighted
})
function escapeHtml(unsafe: string): string {
return unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
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
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>
@ -110,41 +159,48 @@ function escapeHtml(unsafe: string): string {
class="input"
ref="filterInput"
></textarea>
<div
class="filter-input-highlight"
<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-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__operator {
color: var(--code-keyword);
}
&.filter-query__join-operator {
color: var(--code-section);
}
&.filter-query__special_value_placeholder {
button.filter-query__date_value {
padding: .125rem .25rem;
display: inline-block;
}
&.filter-query__special_value {
background: var(--primary);
padding: .125rem .25rem;
color: white;
border-radius: $radius;
position: absolute;
margin-top: calc((0.25em - 0.125rem) * -1);
height: 1.75rem;
}
}
</style>