feat(filters): pass timezone down when filtering with relative date math
Resolves https://community.vikunja.io/t/my-vikunja-instance-creates-tasks-with-due-date-time-of-9am-for-tasks-with-the-word-today-word-in-it/2105/8
This commit is contained in:
parent
6fc3d1e98f
commit
a66e26678e
@ -6,6 +6,7 @@ import TaskCollectionService, {getDefaultTaskFilterParams} from '@/services/task
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import {error} from '@/message'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
export type Order = 'asc' | 'desc' | 'none'
|
||||
|
||||
@ -82,10 +83,15 @@ export function useTaskList(projectIdGetter: ComputedGetter<IProject['id']>, sor
|
||||
},
|
||||
)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const getAllTasksParams = computed(() => {
|
||||
return [
|
||||
{projectId: projectId.value},
|
||||
allParams.value,
|
||||
{
|
||||
...allParams.value,
|
||||
filter_timezone: authStore.settings.timezone,
|
||||
},
|
||||
page.value,
|
||||
]
|
||||
})
|
||||
|
@ -8,6 +8,7 @@ export interface TaskFilterParams {
|
||||
order_by: ('asc' | 'desc')[],
|
||||
filter: string,
|
||||
filter_include_nulls: boolean,
|
||||
filter_timezone: string,
|
||||
s: string,
|
||||
}
|
||||
|
||||
@ -17,6 +18,7 @@ export function getDefaultTaskFilterParams(): TaskFilterParams {
|
||||
order_by: ['asc', 'desc'],
|
||||
filter: '',
|
||||
filter_include_nulls: false,
|
||||
filter_timezone: '',
|
||||
s: '',
|
||||
}
|
||||
}
|
||||
|
@ -7,13 +7,14 @@ import {i18n} from '@/i18n'
|
||||
import {success} from '@/message'
|
||||
|
||||
import BucketService from '@/services/bucket'
|
||||
import TaskCollectionService from '@/services/taskCollection'
|
||||
import TaskCollectionService, {type TaskFilterParams} from '@/services/taskCollection'
|
||||
|
||||
import {setModuleLoading} from '@/stores/helper'
|
||||
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {IBucket} from '@/modelTypes/IBucket'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
const TASKS_PER_BUCKET = 25
|
||||
|
||||
@ -44,6 +45,8 @@ const addTaskToBucketAndSort = (buckets: IBucket[], task: ITask) => {
|
||||
* It should hold only the current buckets.
|
||||
*/
|
||||
export const useKanbanStore = defineStore('kanban', () => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const buckets = ref<IBucket[]>([])
|
||||
const projectId = ref<IProject['id']>(0)
|
||||
const bucketLoading = ref<{[id: IBucket['id']]: boolean}>({})
|
||||
@ -247,7 +250,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
|
||||
async function loadNextTasksForBucket(
|
||||
projectId: IProject['id'],
|
||||
ps,
|
||||
ps: TaskFilterParams,
|
||||
bucketId: IBucket['id'],
|
||||
) {
|
||||
const isLoading = bucketLoading.value[bucketId] ?? false
|
||||
@ -265,7 +268,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
setBucketLoading({bucketId: bucketId, loading: true})
|
||||
|
||||
const params = JSON.parse(JSON.stringify(ps))
|
||||
const params: TaskFilterParams = JSON.parse(JSON.stringify(ps))
|
||||
|
||||
params.sort_by = 'kanban_position'
|
||||
params.order_by = 'asc'
|
||||
@ -287,6 +290,8 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
params.filter_comparator = [...(params.filter_comparator ?? []), 'equals']
|
||||
}
|
||||
|
||||
params.filter_timezone = authStore.settings.timezone
|
||||
|
||||
params.per_page = TASKS_PER_BUCKET
|
||||
|
||||
const taskService = new TaskCollectionService()
|
||||
|
@ -28,7 +28,7 @@ import {useKanbanStore} from '@/stores/kanban'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import ProjectUserService from '@/services/projectUsers'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import TaskCollectionService from '@/services/taskCollection'
|
||||
import TaskCollectionService, {type TaskFilterParams} from '@/services/taskCollection'
|
||||
import {getRandomColorHex} from '@/helpers/color/randomColor'
|
||||
|
||||
interface MatchedAssignee extends IUser {
|
||||
@ -124,7 +124,11 @@ export const useTaskStore = defineStore('task', () => {
|
||||
})
|
||||
}
|
||||
|
||||
async function loadTasks(params, projectId: IProject['id'] | null = null) {
|
||||
async function loadTasks(params: TaskFilterParams, projectId: IProject['id'] | null = null) {
|
||||
|
||||
if (params.filter_timezone === '') {
|
||||
params.filter_timezone = authStore.settings.timezone
|
||||
}
|
||||
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
try {
|
||||
|
@ -9,6 +9,7 @@ import TaskService from '@/services/task'
|
||||
|
||||
import TaskModel from '@/models/task'
|
||||
import {error, success} from '@/message'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
// FIXME: unify with general `useTaskList`
|
||||
export function useGanttTaskList<F extends Filters>(
|
||||
@ -21,12 +22,18 @@ export function useGanttTaskList<F extends Filters>(
|
||||
}) {
|
||||
const taskCollectionService = shallowReactive(new TaskCollectionService())
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const isLoading = computed(() => taskCollectionService.loading)
|
||||
|
||||
const tasks = ref<Map<ITask['id'], ITask>>(new Map())
|
||||
|
||||
async function fetchTasks(params: TaskFilterParams, page = 1): Promise<ITask[]> {
|
||||
|
||||
if(params.filter_timezone === '') {
|
||||
params.filter_timezone = authStore.settings.timezone
|
||||
}
|
||||
|
||||
const tasks = await taskCollectionService.getAll({projectId: filters.value.projectId}, params, page) as ITask[]
|
||||
if (options.loadAll && page < taskCollectionService.totalPages) {
|
||||
const nextTasks = await fetchTasks(params, page + 1)
|
||||
|
@ -85,6 +85,7 @@ import type {ITask} from '@/modelTypes/ITask'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import type {TaskFilterParams} from '@/services/taskCollection'
|
||||
|
||||
// Linting disabled because we explicitely enabled destructuring in vite's config, this will work.
|
||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||
@ -184,6 +185,7 @@ async function loadPendingTasks(from: string, to: string) {
|
||||
const params = {
|
||||
sortBy: ['due_date', 'id'],
|
||||
orderBy: ['asc', 'desc'],
|
||||
filterTimezone: authStore.settings.timezone,
|
||||
filterBy: ['done'],
|
||||
filterValue: ['false'],
|
||||
filterComparator: ['equals'],
|
||||
|
@ -109,6 +109,7 @@ func getDefaultBucketID(s *xorm.Session, project *Project) (bucketID int64, err
|
||||
// @Param per_page query int false "The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page."
|
||||
// @Param s query string false "Search tasks by task text."
|
||||
// @Param filter query string false "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature."
|
||||
// @Param filter_timezone query string false "The time zone which should be used for date match (statements like "now" resolve to different actual times)"
|
||||
// @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`."
|
||||
// @Success 200 {array} models.Bucket "The buckets with their tasks"
|
||||
// @Failure 500 {object} models.Message "Internal server error"
|
||||
@ -197,7 +198,7 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
||||
} else {
|
||||
filterString = "(" + originalFilter + ") && bucket_id = " + strconv.FormatInt(id, 10)
|
||||
}
|
||||
opts.parsedFilters, err = getTaskFiltersFromFilterString(filterString)
|
||||
opts.parsedFilters, err = getTaskFiltersFromFilterString(filterString, opts.filterTimezone)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -33,7 +33,10 @@ type TaskCollection struct {
|
||||
OrderBy []string `query:"order_by" json:"order_by"`
|
||||
OrderByArr []string `query:"order_by[]" json:"-"`
|
||||
|
||||
// The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature.
|
||||
Filter string `query:"filter" json:"filter"`
|
||||
// The time zone which should be used for date match (statements like "now" resolve to different actual times)
|
||||
FilterTimezone string `query:"filter_timezone" json:"filter_timezone"`
|
||||
|
||||
// If set to true, the result will also include null values
|
||||
FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"`
|
||||
@ -103,9 +106,10 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption
|
||||
sortby: sort,
|
||||
filterIncludeNulls: tf.FilterIncludeNulls,
|
||||
filter: tf.Filter,
|
||||
filterTimezone: tf.FilterTimezone,
|
||||
}
|
||||
|
||||
opts.parsedFilters, err = getTaskFiltersFromFilterString(tf.Filter)
|
||||
opts.parsedFilters, err = getTaskFiltersFromFilterString(tf.Filter, tf.FilterTimezone)
|
||||
return opts, err
|
||||
}
|
||||
|
||||
@ -122,6 +126,7 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption
|
||||
// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `project_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`."
|
||||
// @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`."
|
||||
// @Param filter query string false "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature."
|
||||
// @Param filter_timezone query string false "The time zone which should be used for date match (statements like "now" resolve to different actual times)"
|
||||
// @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.Task "The tasks"
|
||||
|
@ -92,7 +92,7 @@ func parseTimeFromUserInput(timeString string) (value time.Time, err error) {
|
||||
return value.In(config.GetTimeZone()), err
|
||||
}
|
||||
|
||||
func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error) {
|
||||
func parseFilterFromExpression(f fexpr.ExprGroup, loc *time.Location) (filter *taskFilter, err error) {
|
||||
filter = &taskFilter{
|
||||
join: filterConcatAnd,
|
||||
}
|
||||
@ -112,7 +112,7 @@ func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error
|
||||
case []fexpr.ExprGroup:
|
||||
values := make([]*taskFilter, 0, len(v))
|
||||
for _, expression := range v {
|
||||
subfilter, err := parseFilterFromExpression(expression)
|
||||
subfilter, err := parseFilterFromExpression(expression, loc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -132,7 +132,7 @@ func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error
|
||||
if filter.field == "project" {
|
||||
filter.field = "project_id"
|
||||
}
|
||||
reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, value)
|
||||
reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, value, loc)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidTaskFilterValue{
|
||||
Value: filter.field,
|
||||
@ -146,7 +146,7 @@ func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func getTaskFiltersFromFilterString(filter string) (filters []*taskFilter, err error) {
|
||||
func getTaskFiltersFromFilterString(filter string, filterTimezone string) (filters []*taskFilter, err error) {
|
||||
|
||||
if filter == "" {
|
||||
return
|
||||
@ -174,9 +174,17 @@ func getTaskFiltersFromFilterString(filter string) (filters []*taskFilter, err e
|
||||
}
|
||||
}
|
||||
|
||||
var loc *time.Location
|
||||
if filterTimezone != "" {
|
||||
loc, err = time.LoadLocation(filterTimezone)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
filters = make([]*taskFilter, 0, len(parsedFilter))
|
||||
for _, f := range parsedFilter {
|
||||
parsedFilter, err := parseFilterFromExpression(f)
|
||||
parsedFilter, err := parseFilterFromExpression(f, loc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -230,7 +238,12 @@ func getFilterComparatorFromOp(op fexpr.SignOp) (taskFilterComparator, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func getValueForField(field reflect.StructField, rawValue string) (value interface{}, err error) {
|
||||
func getValueForField(field reflect.StructField, rawValue string, loc *time.Location) (value interface{}, err error) {
|
||||
|
||||
if loc == nil {
|
||||
loc = config.GetTimeZone()
|
||||
}
|
||||
|
||||
switch field.Type.Kind() {
|
||||
case reflect.Int64:
|
||||
value, err = strconv.ParseInt(rawValue, 10, 64)
|
||||
@ -245,7 +258,7 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa
|
||||
var t datemath.Expression
|
||||
t, err = datemath.Parse(rawValue)
|
||||
if err == nil {
|
||||
value = t.Time(datemath.WithLocation(config.GetTimeZone()))
|
||||
value = t.Time(datemath.WithLocation(config.GetTimeZone())).In(loc)
|
||||
} else {
|
||||
value, err = parseTimeFromUserInput(rawValue)
|
||||
}
|
||||
@ -273,7 +286,7 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa
|
||||
return
|
||||
}
|
||||
|
||||
func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string) (reflectField *reflect.StructField, nativeValue interface{}, err error) {
|
||||
func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string, loc *time.Location) (reflectField *reflect.StructField, nativeValue interface{}, err error) {
|
||||
|
||||
realFieldName := strings.ReplaceAll(strcase.ToCamel(fieldName), "Id", "ID")
|
||||
|
||||
@ -299,7 +312,7 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
|
||||
vals := strings.Split(value, ",")
|
||||
valueSlice := []interface{}{}
|
||||
for _, val := range vals {
|
||||
v, err := getValueForField(field, val)
|
||||
v, err := getValueForField(field, val, loc)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@ -308,6 +321,6 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
|
||||
return nil, valueSlice, nil
|
||||
}
|
||||
|
||||
val, err := getValueForField(field, value)
|
||||
val, err := getValueForField(field, value, loc)
|
||||
return &field, val, err
|
||||
}
|
||||
|
@ -174,6 +174,7 @@ type taskSearchOptions struct {
|
||||
parsedFilters []*taskFilter
|
||||
filterIncludeNulls bool
|
||||
filter string
|
||||
filterTimezone string
|
||||
projectIDs []int64
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user