diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js index 2a1b0e4275639efbabc74064a1a83e9c63b18e59..a7779dc878b390bc63e850e84c85c3ab24f66c42 100644 --- a/app/assets/javascripts/issues/list/constants.js +++ b/app/assets/javascripts/issues/list/constants.js @@ -12,6 +12,7 @@ import { OPERATOR_AFTER, OPERATOR_BEFORE, TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_MR_ASSIGNEE, TOKEN_TYPE_AUTHOR, TOKEN_TYPE_CONFIDENTIAL, TOKEN_TYPE_CONTACT, @@ -214,6 +215,26 @@ export const filtersMap = { }, }, }, + [TOKEN_TYPE_MR_ASSIGNEE]: { + [API_PARAM]: { + [NORMAL_FILTER]: 'assigneeUsername', + [SPECIAL_FILTER]: 'assigneeWildcardId', + [ALTERNATIVE_FILTER]: 'assigneeId', + }, + [URL_PARAM]: { + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'mr_assignee_username', + [SPECIAL_FILTER]: 'mr_assignee_id', + [ALTERNATIVE_FILTER]: 'mr_assignee_username', + }, + [OPERATOR_NOT]: { + [NORMAL_FILTER]: 'not[mr_assignee_username]', + }, + [OPERATOR_OR]: { + [NORMAL_FILTER]: 'or[mr_assignee_username]', + }, + }, + }, [TOKEN_TYPE_ASSIGNEE]: { [API_PARAM]: { [NORMAL_FILTER]: 'assigneeUsernames', diff --git a/app/assets/javascripts/merge_requests/list/components/merge_requests_list_app.vue b/app/assets/javascripts/merge_requests/list/components/merge_requests_list_app.vue index 039c9689e303369cf582c87d1eef2447930a646f..0221c9be83ae39340a0207b7c9284930a4b922e6 100644 --- a/app/assets/javascripts/merge_requests/list/components/merge_requests_list_app.vue +++ b/app/assets/javascripts/merge_requests/list/components/merge_requests_list_app.vue @@ -23,6 +23,8 @@ import { TOKEN_TYPE_TARGET_BRANCH, TOKEN_TITLE_SOURCE_BRANCH, TOKEN_TYPE_SOURCE_BRANCH, + TOKEN_TITLE_ASSIGNEE, + TOKEN_TYPE_MR_ASSIGNEE, } from '~/vue_shared/components/filtered_search_bar/constants'; import { convertToApiParams, @@ -171,6 +173,20 @@ export default { } return [ + { + type: TOKEN_TYPE_MR_ASSIGNEE, + title: TOKEN_TITLE_ASSIGNEE, + icon: 'user', + token: UserToken, + dataType: 'user', + operators: OPERATORS_IS, + fullPath: this.fullPath, + isProject: true, + recentSuggestionsStorageKey: `${this.fullPath}-merge-requests-recent-tokens-assignee`, + preloadedUsers, + multiSelect: false, + unique: true, + }, { type: TOKEN_TYPE_AUTHOR, title: TOKEN_TITLE_AUTHOR, diff --git a/app/assets/javascripts/merge_requests/list/queries/get_merge_requests.query.graphql b/app/assets/javascripts/merge_requests/list/queries/get_merge_requests.query.graphql index b7811523b0b62490c9a7690b9f9255856d9a0624..bd472856ad157ee54d2a9acf103a4b5126028e3b 100644 --- a/app/assets/javascripts/merge_requests/list/queries/get_merge_requests.query.graphql +++ b/app/assets/javascripts/merge_requests/list/queries/get_merge_requests.query.graphql @@ -7,6 +7,7 @@ query getMergeRequests( $fullPath: ID! $sort: MergeRequestSort $state: MergeRequestState + $assigneeUsername: String $authorUsername: String $draft: Boolean $sourceBranches: [String!] @@ -21,6 +22,7 @@ query getMergeRequests( mergeRequests( sort: $sort state: $state + assigneeUsername: $assigneeUsername authorUsername: $authorUsername draft: $draft sourceBranches: $sourceBranches diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js index aeebe312e9d08a76be77a41c17100da017014945..32e9c424b8762087e2e4f06996ac9e24b68e6731 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js @@ -90,6 +90,7 @@ export const TOKEN_TITLE_CLOSED = __('Closed date'); export const TOKEN_TYPE_APPROVED_BY = 'approved-by'; export const TOKEN_TYPE_MERGE_USER = 'merge-user'; export const TOKEN_TYPE_ASSIGNEE = 'assignee'; +export const TOKEN_TYPE_MR_ASSIGNEE = 'mr-assignee'; export const TOKEN_TYPE_AUTHOR = 'author'; export const TOKEN_TYPE_CONFIDENTIAL = 'confidential'; export const TOKEN_TYPE_CONTACT = 'contact'; diff --git a/spec/frontend/merge_requests/list/components/merge_requests_list_app_spec.js b/spec/frontend/merge_requests/list/components/merge_requests_list_app_spec.js index 1e1239d4b28be9e293ce9a17d3133a486a88df9b..7fa8c062671de8abb17522fccfe080a484928b4b 100644 --- a/spec/frontend/merge_requests/list/components/merge_requests_list_app_spec.js +++ b/spec/frontend/merge_requests/list/components/merge_requests_list_app_spec.js @@ -12,6 +12,7 @@ import { TOKEN_TYPE_DRAFT, TOKEN_TYPE_SOURCE_BRANCH, TOKEN_TYPE_TARGET_BRANCH, + TOKEN_TYPE_MR_ASSIGNEE, } from '~/vue_shared/components/filtered_search_bar/constants'; import { mergeRequestListTabs } from '~/vue_shared/issuable/list/constants'; import { getSortOptions } from '~/issues/list/utils'; @@ -107,6 +108,7 @@ describe('Merge requests list app', () => { it('does not have preloaded users when gon.current_user_id does not exist', () => { expect(findIssuableList().props('searchTokens')).toMatchObject([ + { type: TOKEN_TYPE_MR_ASSIGNEE }, { type: TOKEN_TYPE_AUTHOR, preloadedUsers: [] }, { type: TOKEN_TYPE_DRAFT }, { type: TOKEN_TYPE_TARGET_BRANCH }, @@ -116,8 +118,18 @@ describe('Merge requests list app', () => { }); describe('when all tokens are available', () => { + const urlParams = { + mr_assignee_username: 'bob', + draft: 'yes', + 'target_branches[]': 'branch-a', + 'source_branches[]': 'branch-b', + }; + const paramString = Object.entries(urlParams) + .map(([k, v]) => `${k}=${v}`) + .join('&'); + beforeEach(async () => { - setWindowLocation('?draft=yes&target_branches[]=branch-a&source_branches[]=branch-b'); + setWindowLocation(`?${paramString}`); window.gon = { current_user_id: mockCurrentUser.id, current_user_fullname: mockCurrentUser.name, @@ -141,6 +153,7 @@ describe('Merge requests list app', () => { ]; expect(findIssuableList().props('searchTokens')).toMatchObject([ + { type: TOKEN_TYPE_MR_ASSIGNEE }, { type: TOKEN_TYPE_AUTHOR, preloadedUsers }, { type: TOKEN_TYPE_DRAFT }, { type: TOKEN_TYPE_TARGET_BRANCH }, @@ -150,6 +163,7 @@ describe('Merge requests list app', () => { it('pre-displays tokens that are in the url search parameters', () => { expect(findIssuableList().props('initialFilterValue')).toMatchObject([ + { type: TOKEN_TYPE_MR_ASSIGNEE }, { type: TOKEN_TYPE_DRAFT }, { type: TOKEN_TYPE_TARGET_BRANCH }, { type: TOKEN_TYPE_SOURCE_BRANCH },