fix: make search in saved filter work

This commit is contained in:
kolaente
2024-11-19 09:51:05 +01:00
committed by konrad
parent f9a6b4f1bb
commit 624907ad6a
10 changed files with 169 additions and 19 deletions

View File

@@ -65,7 +65,11 @@ import {useRoute} from 'vue-router'
import type {TaskFilterParams} from '@/services/taskCollection'
import {useLabelStore} from '@/stores/labels'
import {useProjectStore} from '@/stores/projects'
import {FILTER_OPERATORS, transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
import {
hasFilterQuery,
transformFilterStringForApi,
transformFilterStringFromApi,
} from '@/helpers/filters'
import FilterInputDocs from '@/components/project/partials/FilterInputDocs.vue'
const props = withDefaults(defineProps<{
@@ -161,8 +165,7 @@ function change(event: 'blur' | 'modelValue' | 'always') {
let s = ''
// When the filter does not contain any filter tokens, assume a simple search and redirect the input
const hasFilterQueries = FILTER_OPERATORS.find(o => filter.includes(o)) || false
if (!hasFilterQueries) {
if (!hasFilterQuery(filter)) {
s = filter
}

View File

@@ -1,9 +1,10 @@
<script setup lang="ts">
import type {IProjectView} from '@/modelTypes/IProjectView'
import type {IFilter} from '@/modelTypes/ISavedFilter'
import XButton from '@/components/input/Button.vue'
import FilterInput from '@/components/project/partials/FilterInput.vue'
import {ref, onBeforeMount} from 'vue'
import {transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
import {hasFilterQuery, transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
import {useLabelStore} from '@/stores/labels'
import {useProjectStore} from '@/stores/projects'
import FilterInputDocs from '@/components/project/partials/FilterInputDocs.vue'
@@ -33,10 +34,19 @@ onBeforeMount(() => {
labelId => labelStore.getLabelById(labelId)?.title || null,
projectId => projectStore.projects[projectId]?.title || null,
)
const filterString = transform(props.modelValue.filter.filter)
const filter: IFilter = {}
if (hasFilterQuery(filterString)) {
filter.filter = filterString
} else {
filter.s = filterString
}
const transformed = {
...props.modelValue,
filter: transform(props.modelValue.filter),
filter,
bucketConfiguration: props.modelValue.bucketConfiguration.map(bc => ({
title: bc.title,
filter: transform(bc.filter),
@@ -57,10 +67,19 @@ function save() {
return found?.id || null
},
)
const filterString = transformFilter(view.value?.filter?.filter)
const filter: IFilter = {}
if (hasFilterQuery(filterString)) {
filter.filter = filterString
} else {
filter.s = filterString
}
emit('update:modelValue', {
...view.value,
filter: transformFilter(view.value?.filter),
filter,
bucketConfiguration: view.value?.bucketConfiguration.map(bc => ({
title: bc.title,
filter: transformFilter(bc.filter),
@@ -141,7 +160,7 @@ function handleBubbleSave() {
</div>
<FilterInput
v-model="view.filter"
v-model="view.filter.filter"
:project-id="view.projectId"
:input-label="$t('project.views.filter')"
class="mb-1"

View File

@@ -60,6 +60,10 @@ export const FILTER_JOIN_OPERATOR = [
export const FILTER_OPERATORS_REGEX = '(&lt;|&gt;|&lt;=|&gt;=|=|!=|not in|in)'
export function hasFilterQuery(filter: string): boolean {
return FILTER_OPERATORS.find(o => filter.includes(o)) || false
}
export function getFilterFieldRegexPattern(field: string): RegExp {
return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()<]+\\1?)?', 'ig')
}

View File

@@ -1,5 +1,6 @@
import type {IAbstract} from './IAbstract'
import type {IProject} from '@/modelTypes/IProject'
import type {IFilters} from '@/modelTypes/ISavedFilter'
export const PROJECT_VIEW_KINDS = {
LIST: 'list',
@@ -29,7 +30,7 @@ export interface IProjectView extends IAbstract {
projectId: IProject['id']
viewKind: ProjectViewKind
filter: string
filter: IFilters
position: number
bucketConfigurationMode: ProjectViewBucketConfigurationMode

View File

@@ -2,7 +2,7 @@ import type {IAbstract} from './IAbstract'
import type {IUser} from './IUser'
// FIXME: what makes this different from TaskFilterParams?
interface Filters {
export interface IFilters {
sort_by: ('start_date' | 'done' | 'id' | 'position')[],
order_by: ('asc' | 'desc')[],
filter: string,
@@ -14,7 +14,7 @@ export interface ISavedFilter extends IAbstract {
id: number
title: string
description: string
filters: Filters
filters: IFilters
owner: IUser
created: Date

View File

@@ -7,7 +7,13 @@ export default class ProjectViewModel extends AbstractModel<IProjectView> implem
projectId = 0
viewKind: ProjectViewKind = 'list'
filter = ''
filter: IProjectView['filters'] = {
sort_by: ['done', 'id'],
order_by: ['asc', 'desc'],
filter: 'done = false',
filter_include_nulls: true,
s: '',
}
position = 0
bucketConfiguration = []

View File

@@ -0,0 +1,83 @@
// 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 (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type taskCollection20241118123644 struct {
Filter string `query:"filter" json:"filter"`
}
type projectViews20241118123644New struct {
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"view"`
Filter *taskCollection20241118123644 `xorm:"json null default null" query:"filter" json:"filter"`
}
func (*projectViews20241118123644New) TableName() string {
return "project_views"
}
type projectViews20241118123644 struct {
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"view"`
Filter string `xorm:"json null default null" query:"filter" json:"filter"`
}
func (*projectViews20241118123644) TableName() string {
return "project_views"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20241118123644",
Description: "change filter format",
Migrate: func(tx *xorm.Engine) (err error) {
oldViews := []*projectViews20241118123644{}
err = tx.Where("filter != '' AND filter IS NOT NULL").Find(&oldViews)
if err != nil {
return
}
err = tx.Sync(projectViews20241118123644New{})
if err != nil {
return
}
for _, view := range oldViews {
newView := &projectViews20241118123644New{
ID: view.ID,
Filter: &taskCollection20241118123644{
Filter: view.Filter,
},
}
_, err = tx.Where("id = ?", view.ID).Update(newView)
if err != nil {
return
}
}
return
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@@ -136,7 +136,7 @@ var FavoritesPseudoProject = Project{
Title: "List",
ViewKind: ProjectViewKindList,
Position: 100,
Filter: "done = false",
Filter: &TaskCollection{Filter: "done = false"},
},
{
ID: -2,

View File

@@ -22,6 +22,7 @@ import (
"time"
"code.vikunja.io/api/pkg/web"
"xorm.io/xorm"
)
@@ -130,7 +131,7 @@ type ProjectView struct {
ViewKind ProjectViewKind `xorm:"not null" json:"view_kind"`
// The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation.
Filter string `xorm:"text null default null" query:"filter" json:"filter"`
Filter *TaskCollection `xorm:"json null default null" query:"filter" json:"filter"`
// The position of this view in the list. The list of all views will be sorted by this parameter.
Position float64 `xorm:"double null" json:"position"`
@@ -273,6 +274,13 @@ func (pv *ProjectView) Create(s *xorm.Session, a web.Auth) (err error) {
}
func createProjectView(s *xorm.Session, p *ProjectView, a web.Auth, createBacklogBucket bool, addExistingTasksToView bool) (err error) {
if p.Filter != nil && p.Filter.Filter != "" {
_, err = getTaskFiltersFromFilterString(p.Filter.Filter, p.Filter.FilterTimezone)
if err != nil {
return
}
}
p.ID = 0
_, err = s.Insert(p)
if err != nil {
@@ -348,6 +356,13 @@ func addTasksToView(s *xorm.Session, a web.Auth, pv *ProjectView, b *Bucket) (er
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{project}/views/{id} [post]
func (pv *ProjectView) Update(s *xorm.Session, _ web.Auth) (err error) {
if pv.Filter != nil && pv.Filter.Filter != "" {
_, err = getTaskFiltersFromFilterString(pv.Filter.Filter, pv.Filter.FilterTimezone)
if err != nil {
return
}
}
// Check if the project view exists
_, err = GetProjectViewByIDAndProject(s, pv.ID, pv.ProjectID)
if err != nil {
@@ -428,7 +443,9 @@ func CreateDefaultViewsForProject(s *xorm.Session, project *Project, a web.Auth,
Position: 100,
}
if createDefaultListFilter {
list.Filter = "done = false"
list.Filter = &TaskCollection{
Filter: "done = false",
}
}
err = createProjectView(s, list, a, createBacklogBucket, true)
if err != nil {

View File

@@ -32,6 +32,8 @@ type TaskCollection struct {
ProjectID int64 `param:"project" json:"-"`
ProjectViewID int64 `param:"view" json:"-"`
Search string `query:"s" json:"s"`
// The query parameter to sort by. This is for ex. done, priority, etc.
SortBy []string `query:"sort_by" json:"sort_by"`
SortByArr []string `query:"sort_by[]" json:"-"`
@@ -277,11 +279,26 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
return nil, 0, 0, err
}
if view.Filter != "" {
if tf.Filter != "" {
tf.Filter = "(" + tf.Filter + ") && (" + view.Filter + ")"
} else {
tf.Filter = view.Filter
if view.Filter != nil {
if view.Filter.Filter != "" {
if tf.Filter != "" {
tf.Filter = "(" + tf.Filter + ") && (" + view.Filter.Filter + ")"
} else {
tf.Filter = view.Filter.Filter
}
tf.FilterIncludeNulls = view.Filter.FilterIncludeNulls
}
if view.Filter.FilterTimezone != "" {
tf.FilterTimezone = view.Filter.FilterTimezone
}
if view.Filter.FilterIncludeNulls {
tf.FilterIncludeNulls = view.Filter.FilterIncludeNulls
}
if view.Filter.Search != "" {
search = view.Filter.Search
}
}