Merge 72c38456ff into b947e892d0
This commit is contained in:
commit
42a5dc6928
|
|
@ -8,6 +8,7 @@ import FilterCommandsList from './FilterCommandsList.vue'
|
|||
import {
|
||||
ASSIGNEE_FIELDS,
|
||||
AUTOCOMPLETE_FIELDS,
|
||||
CREATOR_FIELDS,
|
||||
FILTER_OPERATORS_REGEX,
|
||||
isMultiValueOperator,
|
||||
LABEL_FIELDS,
|
||||
|
|
@ -45,7 +46,7 @@ interface SuggestionItem {
|
|||
name?: string
|
||||
}
|
||||
|
||||
export type AutocompleteField = 'labels' | 'assignees' | 'projects'
|
||||
export type AutocompleteField = 'labels' | 'users' | 'projects'
|
||||
|
||||
/**
|
||||
* Calculates the replacement range for autocomplete selection.
|
||||
|
|
@ -222,7 +223,7 @@ export default Extension.create<FilterAutocompleteOptions>({
|
|||
return labelStore.filterLabelsByQuery([], autocompleteContext.search).filter((label): label is ILabel => label !== undefined) as SuggestionItem[]
|
||||
}
|
||||
|
||||
if (fieldType === 'assignees') {
|
||||
if (fieldType === 'users') {
|
||||
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer)
|
||||
|
|
@ -230,23 +231,23 @@ export default Extension.create<FilterAutocompleteOptions>({
|
|||
|
||||
return new Promise((resolve) => {
|
||||
debounceTimer = setTimeout(async () => {
|
||||
let assigneeSuggestions: SuggestionItem[] = []
|
||||
let userSuggestions: SuggestionItem[] = []
|
||||
try {
|
||||
if (this.options.projectId) {
|
||||
// @ts-expect-error - projectId is used for URL replacement but not part of IAbstract
|
||||
assigneeSuggestions = await projectUserService.getAll({projectId: this.options.projectId}, {s: autocompleteContext.search}) as SuggestionItem[]
|
||||
userSuggestions = await projectUserService.getAll({projectId: this.options.projectId}, {s: autocompleteContext.search}) as SuggestionItem[]
|
||||
} else {
|
||||
assigneeSuggestions = await userService.getAll({} as IUser, {s: autocompleteContext.search}) as SuggestionItem[]
|
||||
userSuggestions = await userService.getAll({} as IUser, {s: autocompleteContext.search}) as SuggestionItem[]
|
||||
}
|
||||
// For assignees, show suggestions even with empty search, but limit if we have many
|
||||
if (autocompleteContext.search === '' && assigneeSuggestions.length > 10) {
|
||||
assigneeSuggestions = assigneeSuggestions.slice(0, 10)
|
||||
// Show suggestions even with empty search, but limit if we have many
|
||||
if (autocompleteContext.search === '' && userSuggestions.length > 10) {
|
||||
userSuggestions = userSuggestions.slice(0, 10)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching assignee suggestions:', error)
|
||||
assigneeSuggestions = []
|
||||
console.error('Error fetching user suggestions:', error)
|
||||
userSuggestions = []
|
||||
}
|
||||
resolve(assigneeSuggestions)
|
||||
resolve(userSuggestions)
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
|
@ -338,8 +339,8 @@ export default Extension.create<FilterAutocompleteOptions>({
|
|||
|
||||
if (LABEL_FIELDS.includes(field)) {
|
||||
fieldType = 'labels'
|
||||
} else if (ASSIGNEE_FIELDS.includes(field)) {
|
||||
fieldType = 'assignees'
|
||||
} else if (ASSIGNEE_FIELDS.includes(field) || CREATOR_FIELDS.includes(field)) {
|
||||
fieldType = 'users'
|
||||
} else if (PROJECT_FIELDS.includes(field)) {
|
||||
fieldType = 'projects'
|
||||
}
|
||||
|
|
@ -363,8 +364,8 @@ export default Extension.create<FilterAutocompleteOptions>({
|
|||
|
||||
const items = suggestions.map(item => ({
|
||||
id: item.id,
|
||||
title: fieldType === 'assignees' ? item.username : item.title,
|
||||
description: fieldType === 'assignees' ? `${item.name || item.username}` : item.title,
|
||||
title: fieldType === 'users' ? item.username : item.title,
|
||||
description: fieldType === 'users' ? `${item.name || item.username}` : item.title,
|
||||
item,
|
||||
fieldType,
|
||||
context: autocompleteContext,
|
||||
|
|
@ -388,7 +389,7 @@ export default Extension.create<FilterAutocompleteOptions>({
|
|||
items,
|
||||
command: (item: AutocompleteItem) => {
|
||||
// Handle selection
|
||||
const newValue = item.fieldType === 'assignees'
|
||||
const newValue = item.fieldType === 'users'
|
||||
? (item.item as IUser).username
|
||||
: (item.item as IProject | ILabel).title
|
||||
// Use currentAutocompleteContext (outer variable) for up-to-date positions
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
class="filter-autocomplete__label"
|
||||
/>
|
||||
<User
|
||||
v-else-if="item.fieldType === 'assignees'"
|
||||
v-else-if="item.fieldType === 'users'"
|
||||
:user="(item.item as unknown as IUser)"
|
||||
:avatar-size="20"
|
||||
class="filter-autocomplete__user"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const showDocs = ref(false)
|
|||
<li><code>endDate</code>: {{ $t('filters.query.help.fields.endDate') }}</li>
|
||||
<li><code>doneAt</code>: {{ $t('filters.query.help.fields.doneAt') }}</li>
|
||||
<li><code>assignees</code>: {{ $t('filters.query.help.fields.assignees') }}</li>
|
||||
<li><code>creator</code>: {{ $t('filters.query.help.fields.creator') }}</li>
|
||||
<li><code>labels</code>: {{ $t('filters.query.help.fields.labels') }}</li>
|
||||
<li><code>project</code>: {{ $t('filters.query.help.fields.project') }}</li>
|
||||
<li><code>reminders</code>: {{ $t('filters.query.help.fields.reminders') }}</li>
|
||||
|
|
@ -62,6 +63,7 @@ const showDocs = ref(false)
|
|||
{{ $t('filters.query.help.examples.undoneHighPriority') }}
|
||||
</li>
|
||||
<li><code>assignees in user1, user2</code>: {{ $t('filters.query.help.examples.assigneesIn') }}</li>
|
||||
<li><code>creator = user1</code>: {{ $t('filters.query.help.examples.creatorEqual') }}</li>
|
||||
<li>
|
||||
<code>(priority = 1 || priority = 2) && dueDate <= now</code>:
|
||||
{{ $t('filters.query.help.examples.priorityOneOrTwoPastDue') }}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ describe('Filter Transformation', () => {
|
|||
'doneAt': 'done_at',
|
||||
'reminders': 'reminders',
|
||||
'assignees': 'assignees',
|
||||
'creator': 'creator',
|
||||
'labels': 'labels',
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ export const ASSIGNEE_FIELDS = [
|
|||
'assignees',
|
||||
]
|
||||
|
||||
// Creator is matched by username and autocompletes to users, exactly like assignees.
|
||||
export const CREATOR_FIELDS = [
|
||||
'creator',
|
||||
]
|
||||
|
||||
export const LABEL_FIELDS = [
|
||||
'labels',
|
||||
]
|
||||
|
|
@ -36,12 +41,14 @@ export const PROJECT_FIELDS = [
|
|||
export const AUTOCOMPLETE_FIELDS = [
|
||||
...LABEL_FIELDS,
|
||||
...ASSIGNEE_FIELDS,
|
||||
...CREATOR_FIELDS,
|
||||
...PROJECT_FIELDS,
|
||||
]
|
||||
|
||||
export const AVAILABLE_FILTER_FIELDS = [
|
||||
...DATE_FIELDS,
|
||||
...ASSIGNEE_FIELDS,
|
||||
...CREATOR_FIELDS,
|
||||
...LABEL_FIELDS,
|
||||
...PROJECT_FIELDS,
|
||||
'done',
|
||||
|
|
|
|||
|
|
@ -610,6 +610,7 @@
|
|||
"endDate": "The end date of the task",
|
||||
"doneAt": "The date and time when the task was completed",
|
||||
"assignees": "The assignees of the task",
|
||||
"creator": "The user who created the task (matched by username)",
|
||||
"labels": "The labels associated with the task",
|
||||
"project": "The project the task belongs to (only available for saved filters, not on a project level)",
|
||||
"reminders": "The reminders of the task as a date field, will return all tasks with at least one reminder matching the query",
|
||||
|
|
@ -640,6 +641,7 @@
|
|||
"dueDatePast": "Matches tasks with a due date in the past",
|
||||
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
|
||||
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
|
||||
"creatorEqual": "Matches tasks created by \"user1\"",
|
||||
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,8 @@ func validateTaskField(fieldName string) error {
|
|||
case
|
||||
taskPropertyAssignees,
|
||||
taskPropertyLabels,
|
||||
taskPropertyReminders:
|
||||
taskPropertyReminders,
|
||||
taskPropertyCreator:
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -360,9 +360,16 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
|
|||
|
||||
realFieldName := strings.ReplaceAll(strcase.ToCamel(fieldName), "Id", "ID")
|
||||
|
||||
if realFieldName == "Assignees" {
|
||||
if realFieldName == "Assignees" || realFieldName == "Creator" {
|
||||
vals := strings.Split(value, ",")
|
||||
valueSlice := append([]string{}, vals...)
|
||||
valueSlice := make([]string, 0, len(vals))
|
||||
for _, val := range vals {
|
||||
val = strings.TrimSpace(val)
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
valueSlice = append(valueSlice, val)
|
||||
}
|
||||
return nil, valueSlice, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -229,6 +229,33 @@ func TestParseFilter(t *testing.T) {
|
|||
assert.Equal(t, taskFilterComparatorNotEquals, result[0].comparator)
|
||||
assert.Equal(t, int64(3), result[0].value)
|
||||
})
|
||||
t.Run("creator resolves to usernames, not an int64 id", func(t *testing.T) {
|
||||
result, err := getTaskFiltersFromFilterString("creator in 'user1,user6'", "UTC")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 1)
|
||||
assert.Equal(t, "creator", result[0].field)
|
||||
assert.Equal(t, taskFilterComparatorIn, result[0].comparator)
|
||||
assert.Equal(t, []string{"user1", "user6"}, result[0].value)
|
||||
})
|
||||
t.Run("creator usernames are trimmed of surrounding whitespace", func(t *testing.T) {
|
||||
result, err := getTaskFiltersFromFilterString("creator in 'user1, user6'", "UTC")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 1)
|
||||
assert.Equal(t, "creator", result[0].field)
|
||||
assert.Equal(t, taskFilterComparatorIn, result[0].comparator)
|
||||
assert.Equal(t, []string{"user1", "user6"}, result[0].value)
|
||||
})
|
||||
t.Run("creator usernames drop empty entries from stray commas", func(t *testing.T) {
|
||||
result, err := getTaskFiltersFromFilterString("creator in 'user1, , user6,'", "UTC")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 1)
|
||||
assert.Equal(t, "creator", result[0].field)
|
||||
assert.Equal(t, taskFilterComparatorIn, result[0].comparator)
|
||||
assert.Equal(t, []string{"user1", "user6"}, result[0].value)
|
||||
})
|
||||
t.Run("less than or equal comparator", func(t *testing.T) {
|
||||
result, err := getTaskFiltersFromFilterString("percent_done <= 50", "UTC")
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ const (
|
|||
taskPropertyAssignees string = "assignees"
|
||||
taskPropertyLabels string = "labels"
|
||||
taskPropertyReminders string = "reminders"
|
||||
taskPropertyCreator string = "creator"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -1327,6 +1327,91 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter by creator username",
|
||||
fields: fields{
|
||||
Filter: "creator = 'user6'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task15,
|
||||
task16,
|
||||
task17,
|
||||
task18,
|
||||
task19,
|
||||
task20,
|
||||
task21,
|
||||
task22,
|
||||
task23,
|
||||
task24,
|
||||
task25,
|
||||
task26,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter by creator username in list",
|
||||
fields: fields{
|
||||
Filter: "creator ?= 'user6'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task15,
|
||||
task16,
|
||||
task17,
|
||||
task18,
|
||||
task19,
|
||||
task20,
|
||||
task21,
|
||||
task22,
|
||||
task23,
|
||||
task24,
|
||||
task25,
|
||||
task26,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter by creator username not equals",
|
||||
fields: fields{
|
||||
Filter: "creator != 'user6'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task11,
|
||||
task12,
|
||||
task27,
|
||||
task28,
|
||||
task29,
|
||||
task30,
|
||||
task31,
|
||||
task32,
|
||||
task33,
|
||||
task39,
|
||||
task47,
|
||||
task48,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter by creator unknown username",
|
||||
fields: fields{
|
||||
Filter: "creator = 'nonexistentuser'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter labels",
|
||||
fields: fields{
|
||||
|
|
|
|||
|
|
@ -62,6 +62,12 @@ var subTableFilters = SubTableFilters{
|
|||
FilterableField: "username",
|
||||
AllowNullCheck: true,
|
||||
},
|
||||
"creator": {
|
||||
Table: "users",
|
||||
BaseFilter: "tasks.created_by_id = users.id",
|
||||
FilterableField: "username",
|
||||
AllowNullCheck: false,
|
||||
},
|
||||
"parent_project": {
|
||||
Table: "projects",
|
||||
BaseFilter: "tasks.project_id = id",
|
||||
|
|
@ -182,7 +188,7 @@ func convertFiltersToDBFilterCond(rawFilters []*taskFilter, includeNulls bool) (
|
|||
|
||||
subTableFilterParams, ok := subTableFilters[f.field]
|
||||
if ok {
|
||||
if f.field == "assignees" && (f.comparator == taskFilterComparatorLike) {
|
||||
if (f.field == "assignees" || f.field == "creator") && (f.comparator == taskFilterComparatorLike) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue