diff --git a/frontend/src/components/input/filter/FilterAutocomplete.ts b/frontend/src/components/input/filter/FilterAutocomplete.ts index a4b0d64fc..b5e771b7e 100644 --- a/frontend/src/components/input/filter/FilterAutocomplete.ts +++ b/frontend/src/components/input/filter/FilterAutocomplete.ts @@ -8,6 +8,7 @@ import FilterCommandsList from './FilterCommandsList.vue' import { ASSIGNEE_FIELDS, AUTOCOMPLETE_FIELDS, + CREATOR_FIELDS, FILTER_OPERATORS_REGEX, isMultiValueOperator, LABEL_FIELDS, @@ -338,7 +339,7 @@ export default Extension.create({ if (LABEL_FIELDS.includes(field)) { fieldType = 'labels' - } else if (ASSIGNEE_FIELDS.includes(field)) { + } else if (ASSIGNEE_FIELDS.includes(field) || CREATOR_FIELDS.includes(field)) { fieldType = 'assignees' } else if (PROJECT_FIELDS.includes(field)) { fieldType = 'projects' diff --git a/frontend/src/components/input/filter/FilterInputDocs.vue b/frontend/src/components/input/filter/FilterInputDocs.vue index 492b670f6..26499bf2b 100644 --- a/frontend/src/components/input/filter/FilterInputDocs.vue +++ b/frontend/src/components/input/filter/FilterInputDocs.vue @@ -28,6 +28,7 @@ const showDocs = ref(false)
  • endDate: {{ $t('filters.query.help.fields.endDate') }}
  • doneAt: {{ $t('filters.query.help.fields.doneAt') }}
  • assignees: {{ $t('filters.query.help.fields.assignees') }}
  • +
  • creator: {{ $t('filters.query.help.fields.creator') }}
  • labels: {{ $t('filters.query.help.fields.labels') }}
  • project: {{ $t('filters.query.help.fields.project') }}
  • reminders: {{ $t('filters.query.help.fields.reminders') }}
  • @@ -62,6 +63,7 @@ const showDocs = ref(false) {{ $t('filters.query.help.examples.undoneHighPriority') }}
  • assignees in user1, user2: {{ $t('filters.query.help.examples.assigneesIn') }}
  • +
  • creator = user1: {{ $t('filters.query.help.examples.creatorEqual') }}
  • (priority = 1 || priority = 2) && dueDate <= now: {{ $t('filters.query.help.examples.priorityOneOrTwoPastDue') }} diff --git a/frontend/src/helpers/filters.test.ts b/frontend/src/helpers/filters.test.ts index ec64bf62a..cb9288fae 100644 --- a/frontend/src/helpers/filters.test.ts +++ b/frontend/src/helpers/filters.test.ts @@ -15,6 +15,7 @@ describe('Filter Transformation', () => { 'doneAt': 'done_at', 'reminders': 'reminders', 'assignees': 'assignees', + 'creator': 'creator', 'labels': 'labels', } diff --git a/frontend/src/helpers/filters.ts b/frontend/src/helpers/filters.ts index 53cb34183..5c02aa60f 100644 --- a/frontend/src/helpers/filters.ts +++ b/frontend/src/helpers/filters.ts @@ -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', diff --git a/frontend/src/i18n/lang/en.json b/frontend/src/i18n/lang/en.json index a48d793ac..d77953e7d 100644 --- a/frontend/src/i18n/lang/en.json +++ b/frontend/src/i18n/lang/en.json @@ -608,6 +608,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", @@ -638,6 +639,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" } } diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index bc217f7ca..80df2e316 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -105,7 +105,8 @@ func validateTaskField(fieldName string) error { case taskPropertyAssignees, taskPropertyLabels, - taskPropertyReminders: + taskPropertyReminders, + taskPropertyCreator: return nil } diff --git a/pkg/models/task_collection_filter.go b/pkg/models/task_collection_filter.go index 6828b0e07..7cabbeff9 100644 --- a/pkg/models/task_collection_filter.go +++ b/pkg/models/task_collection_filter.go @@ -360,7 +360,7 @@ 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...) return nil, valueSlice, nil diff --git a/pkg/models/task_collection_filter_test.go b/pkg/models/task_collection_filter_test.go index 577cf79e6..b34f47bb0 100644 --- a/pkg/models/task_collection_filter_test.go +++ b/pkg/models/task_collection_filter_test.go @@ -229,6 +229,15 @@ 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("less than or equal comparator", func(t *testing.T) { result, err := getTaskFiltersFromFilterString("percent_done <= 50", "UTC") diff --git a/pkg/models/task_collection_sort.go b/pkg/models/task_collection_sort.go index 3bcd878b2..7e36aef5c 100644 --- a/pkg/models/task_collection_sort.go +++ b/pkg/models/task_collection_sort.go @@ -51,6 +51,7 @@ const ( taskPropertyAssignees string = "assignees" taskPropertyLabels string = "labels" taskPropertyReminders string = "reminders" + taskPropertyCreator string = "creator" ) const ( diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index e66d945b2..5cf8c739d 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -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{ diff --git a/pkg/models/task_search.go b/pkg/models/task_search.go index 07da3f809..13f1a5769 100644 --- a/pkg/models/task_search.go +++ b/pkg/models/task_search.go @@ -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 }