feat: support filter_include_nulls in project view configuration

This commit is contained in:
Timh
2026-04-24 11:34:24 +02:00
committed by kolaente
parent 9852aff4ee
commit e97b629d6c
4 changed files with 82 additions and 8 deletions

View File

@@ -9,6 +9,7 @@ import {useLabelStore} from '@/stores/labels'
import {useProjectStore} from '@/stores/projects'
import XButton from '@/components/input/Button.vue'
import FancyCheckbox from '@/components/input/FancyCheckbox.vue'
import FilterInputDocs from '@/components/input/filter/FilterInputDocs.vue'
import FilterInput from '@/components/input/filter/FilterInput.vue'
import FormField from '@/components/input/FormField.vue'
@@ -58,6 +59,16 @@ onBeforeMount(() => {
filter.filter = filter.s
}
// AbstractModel.assignData() runs objectToCamelCase recursively on all
// nested objects, which converts filter_include_nulls to filterIncludeNulls
// inside the filter object. IFilters intentionally uses snake_case keys to
// match the API query param format. We check both key forms here to handle
// data coming from either the API response (camelCased by assignData) or
// from a freshly constructed filter object (snake_case).
filter.filter_include_nulls = filterInput.filter_include_nulls
?? (filterInput as Record<string, unknown>).filterIncludeNulls as boolean
?? false
return filter
}
@@ -76,16 +87,18 @@ onBeforeMount(() => {
})
function save() {
const transformFilterForApi = (filterQuery: string): IFilters => {
const transformFilterForApi = (filterInput: IFilters): IFilters => {
const filterString = transformFilterStringForApi(
filterQuery,
filterInput?.filter || '',
labelTitle => labelStore.getLabelByExactTitle(labelTitle)?.id || null,
projectTitle => {
const found = projectStore.findProjectByExactname(projectTitle)
return found?.id || null
},
)
const filter: IFilters = {}
const filter: IFilters = {
filter_include_nulls: filterInput?.filter_include_nulls ?? false,
}
if (hasFilterQuery(filterString)) {
filter.filter = filterString
} else {
@@ -97,10 +110,10 @@ function save() {
emit('update:modelValue', {
...view.value,
filter: transformFilterForApi(view.value?.filter?.filter || ''),
filter: transformFilterForApi(view.value?.filter),
bucketConfiguration: view.value?.bucketConfiguration.map(bc => ({
title: bc.title,
filter: transformFilterForApi(bc.filter?.filter || ''),
filter: transformFilterForApi(bc.filter),
})),
})
}
@@ -172,10 +185,18 @@ function handleBubbleSave() {
class="mbe-1"
/>
<div class="is-size-7 mbe-3">
<div class="is-size-7 mbe-2">
<FilterInputDocs />
</div>
<div class="field mbe-3">
<FancyCheckbox
v-model="view.filter.filter_include_nulls"
>
{{ $t('filters.attributes.includeNulls') }}
</FancyCheckbox>
</div>
<div
v-if="view.viewKind === 'kanban'"
class="field"
@@ -245,16 +266,24 @@ function handleBubbleSave() {
class="mbe-2"
/>
<div class="is-size-7">
<div class="is-size-7 mbe-2">
<FilterInputDocs />
</div>
<div class="field mbe-3">
<FancyCheckbox
v-model="view.bucketConfiguration[index].filter.filter_include_nulls"
>
{{ $t('filters.attributes.includeNulls') }}
</FancyCheckbox>
</div>
</div>
</div>
<div class="is-flex is-justify-content-end">
<XButton
variant="secondary"
icon="plus"
@click="() => view.bucketConfiguration.push({title: '', filter: {filter: ''}})"
@click="() => view.bucketConfiguration.push({title: '', filter: {filter: '', filter_include_nulls: false}})"
>
{{ $t('project.kanban.addBucket') }}
</XButton>

View File

@@ -1007,3 +1007,11 @@
updated: '2024-03-18 15:14:13'
created: '2018-03-18 15:14:13'
bucket_configuration_mode: 1
- id: 161
title: List with include nulls
project_id: 1
view_kind: 0
position: 5
filter: '{"filter":"start_date > ''2018-12-11T03:46:40+00:00'' || end_date < ''2018-12-13T11:20:01+00:00''","filter_include_nulls":true}'
updated: '2024-03-18 15:14:13'
created: '2018-03-18 15:14:13'

View File

@@ -335,6 +335,10 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
if view.Filter.Search != "" {
search = view.Filter.Search
}
if view.Filter.FilterIncludeNulls {
tf.FilterIncludeNulls = true
}
}
if strings.Contains(tf.Filter, taskPropertyBucketID) {

View File

@@ -1007,6 +1007,39 @@ func TestTaskCollection_ReadAll(t *testing.T) {
},
wantErr: false,
},
{
// Tests that FilterIncludeNulls set on a view's saved filter config
// is properly applied when loading tasks through that view.
name: "range with nulls from view filter",
fields: fields{
ProjectViewID: 161,
ProjectID: 1,
},
args: defaultArgs,
want: []*Task{
task1, // has nil dates
task2, // has nil dates
task3, // has nil dates
task4, // has nil dates
task5, // has nil dates
task6, // has nil dates
task7, // matches start_date filter
task8, // matches end_date filter
task9, // matches both
task10, // has nil dates
task11, // has nil dates
task12, // has nil dates
task27, // has start_date, matches filter
task28, // has dates, matches filter
task29, // has nil dates
task30, // has nil dates
task31, // has nil dates
task33, // has nil dates
task47, // has nil dates
task48, // has nil dates
},
wantErr: false,
},
{
name: "favorited tasks",
args: defaultArgs,