diff --git a/frontend/src/components/project/views/ViewEditForm.vue b/frontend/src/components/project/views/ViewEditForm.vue index 5bb20b5cb..b537f224e 100644 --- a/frontend/src/components/project/views/ViewEditForm.vue +++ b/frontend/src/components/project/views/ViewEditForm.vue @@ -3,7 +3,7 @@ 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 {onBeforeMount, ref} from 'vue' import {hasFilterQuery, transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters' import {useLabelStore} from '@/stores/labels' import {useProjectStore} from '@/stores/projects' @@ -29,27 +29,24 @@ const labelStore = useLabelStore() const projectStore = useProjectStore() onBeforeMount(() => { - const transform = (filterString: string) => transformFilterStringFromApi( - filterString, - labelId => labelStore.getLabelById(labelId)?.title || null, - projectId => projectStore.projects[projectId]?.title || null, - ) - - const filterString = transform(props.modelValue.filter.filter) + const transformFilterToString = (filter: IFilter): string => { + if (filter.s !== '') { + return filter.s + } - const filter: IFilter = {} - if (hasFilterQuery(filterString)) { - filter.filter = filterString - } else { - filter.s = filterString + return transformFilterStringFromApi( + filter.filter, + labelId => labelStore.getLabelById(labelId)?.title || null, + projectId => projectStore.projects[projectId]?.title || null, + ) } const transformed = { ...props.modelValue, - filter, + filter: transformFilterToString(props.modelValue.filter), bucketConfiguration: props.modelValue.bucketConfiguration.map(bc => ({ title: bc.title, - filter: transform(bc.filter), + filter: transformFilterToString(bc.filter), })), } @@ -59,30 +56,31 @@ onBeforeMount(() => { }) function save() { - const transformFilter = (filterQuery: string) => transformFilterStringForApi( - filterQuery, - labelTitle => labelStore.getLabelByExactTitle(labelTitle)?.id || null, - projectTitle => { - const found = projectStore.findProjectByExactname(projectTitle) - return found?.id || null - }, - ) - - const filterString = transformFilter(view.value?.filter?.filter) - - const filter: IFilter = {} - if (hasFilterQuery(filterString)) { - filter.filter = filterString - } else { - filter.s = filterString + const transformFilterForApi = (filterQuery: string): IFilter => { + const filterString = transformFilterStringForApi( + filterQuery, + labelTitle => labelStore.getLabelByExactTitle(labelTitle)?.id || null, + projectTitle => { + const found = projectStore.findProjectByExactname(projectTitle) + return found?.id || null + }, + ) + const filter: IFilter = {} + if (hasFilterQuery(filterString)) { + filter.filter = filterString + } else { + filter.s = filterString + } + + return filter } emit('update:modelValue', { ...view.value, - filter, + filter: transformFilterForApi(view.value?.filter || ''), bucketConfiguration: view.value?.bucketConfiguration.map(bc => ({ title: bc.title, - filter: transformFilter(bc.filter), + filter: transformFilterForApi(bc.filter || ''), })), }) } @@ -160,7 +158,7 @@ function handleBubbleSave() { {{ $t('project.kanban.addBucket') }} diff --git a/frontend/src/modelTypes/IProjectView.ts b/frontend/src/modelTypes/IProjectView.ts index 9302fc51d..42e1cd2f7 100644 --- a/frontend/src/modelTypes/IProjectView.ts +++ b/frontend/src/modelTypes/IProjectView.ts @@ -21,7 +21,7 @@ export type ProjectViewBucketConfigurationMode = typeof PROJECT_VIEW_BUCKET_CONF export interface IProjectViewBucketConfiguration { title: string - filter: string + filter: IFilters } export interface IProjectView extends IAbstract { diff --git a/pkg/migration/20241119115012.go b/pkg/migration/20241119115012.go new file mode 100644 index 000000000..695caa3ee --- /dev/null +++ b/pkg/migration/20241119115012.go @@ -0,0 +1,95 @@ +// 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 . + +package migration + +import ( + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type projectView20241119115012BucketConfiguration struct { + Title string `json:"title"` + Filter string `json:"filter"` +} + +type projectView20241119115012 struct { + ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"view"` + BucketConfiguration []*projectView20241119115012BucketConfiguration `xorm:"json" json:"bucket_configuration"` +} + +func (projectView20241119115012) TableName() string { + return "project_views" +} + +type projectView20241119115012BucketConfigurationNew struct { + Title string `json:"title"` + Filter *taskCollection20241118123644 `json:"filter"` +} + +type projectView20241119115012New struct { + ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"view"` + BucketConfiguration []*projectView20241119115012BucketConfigurationNew `xorm:"json" json:"bucket_configuration"` +} + +func (projectView20241119115012New) TableName() string { + return "project_views" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20241119115012", + Description: "change bucket filter format", + Migrate: func(tx *xorm.Engine) (err error) { + oldViews := []*projectView20241119115012{} + + err = tx.Where("bucket_configuration_mode = 2").Find(&oldViews) + if err != nil { + return + } + + err = tx.Sync(projectView20241119115012New{}) + if err != nil { + return + } + + for _, view := range oldViews { + newView := &projectView20241119115012New{ + ID: view.ID, + } + + for _, configuration := range view.BucketConfiguration { + newView.BucketConfiguration = append(newView.BucketConfiguration, &projectView20241119115012BucketConfigurationNew{ + Title: configuration.Title, + Filter: &taskCollection20241118123644{ + Filter: configuration.Filter, + }, + }) + } + + _, err = tx.Where("id = ?", view.ID).Update(newView) + if err != nil { + return + } + } + + return + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 826824abe..c7e0214c0 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -230,7 +230,14 @@ func GetTasksInBucketsForView(s *xorm.Session, view *ProjectView, projects []*Pr var bucketFilter = taskPropertyBucketID + " = " + strconv.FormatInt(id, 10) if view.BucketConfigurationMode == BucketConfigurationModeFilter { - bucketFilter = "(" + view.BucketConfiguration[id].Filter + ")" + bucketFilter = "" + if view.BucketConfiguration[id].Filter.Filter != "" { + bucketFilter = "(" + view.BucketConfiguration[id].Filter.Filter + ")" + } + + if view.BucketConfiguration[id].Filter.Search != "" { + opts.search = view.BucketConfiguration[id].Filter.Search + } } var filterString string diff --git a/pkg/models/project_view.go b/pkg/models/project_view.go index 02fbf12b9..99eac034a 100644 --- a/pkg/models/project_view.go +++ b/pkg/models/project_view.go @@ -116,8 +116,8 @@ func (p *BucketConfigurationModeKind) UnmarshalJSON(bytes []byte) error { } type ProjectViewBucketConfiguration struct { - Title string `json:"title"` - Filter string `json:"filter"` + Title string `json:"title"` + Filter *TaskCollection `json:"filter"` } type ProjectView struct { @@ -281,6 +281,17 @@ func createProjectView(s *xorm.Session, p *ProjectView, a web.Auth, createBacklo } } + if p.BucketConfigurationMode == BucketConfigurationModeFilter { + for _, configuration := range p.BucketConfiguration { + if configuration.Filter != nil && configuration.Filter.Filter != "" { + _, err = getTaskFiltersFromFilterString(configuration.Filter.Filter, configuration.Filter.FilterTimezone) + if err != nil { + return + } + } + } + } + p.ID = 0 _, err = s.Insert(p) if err != nil { diff --git a/pkg/models/saved_filters_test.go b/pkg/models/saved_filters_test.go index 619abd12d..e1e514d61 100644 --- a/pkg/models/saved_filters_test.go +++ b/pkg/models/saved_filters_test.go @@ -66,7 +66,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\":\"\",\"filter_include_nulls\":false}'", + "filters": `'{"s":"","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 diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index bdb2e3b01..b4f8d7f7e 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -183,6 +183,10 @@ func getRelevantProjectsFromCollection(s *xorm.Session, a web.Auth, tf *TaskColl } func getFilterValueForBucketFilter(filter string, view *ProjectView) (newFilter string, err error) { + if view.BucketConfigurationMode != BucketConfigurationModeFilter { + return filter, nil + } + re := regexp.MustCompile(`bucket_id\s*=\s*(\d+)`) match := re.FindStringSubmatch(filter) @@ -197,7 +201,7 @@ func getFilterValueForBucketFilter(filter string, view *ProjectView) (newFilter for id, bucket := range view.BucketConfiguration { if id == bucketID { - return re.ReplaceAllString(filter, `(`+bucket.Filter+`)`), nil + return re.ReplaceAllString(filter, `(`+bucket.Filter.Filter+`)`), nil } } @@ -304,11 +308,9 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa if strings.Contains(tf.Filter, taskPropertyBucketID) { filteringForBucket = true - if view.BucketConfigurationMode == BucketConfigurationModeFilter { - tf.Filter, err = getFilterValueForBucketFilter(tf.Filter, view) - if err != nil { - return nil, 0, 0, err - } + tf.Filter, err = getFilterValueForBucketFilter(tf.Filter, view) + if err != nil { + return nil, 0, 0, err } } }