Filter merge requests by target branch
This commit is contained in:
		
							parent
							
								
									6908c5f70e
								
							
						
					
					
						commit
						de784ac105
					
				|  | @ -13,4 +13,16 @@ export default IssuableTokenKeys => { | ||||||
| 
 | 
 | ||||||
|   IssuableTokenKeys.tokenKeys.push(wipToken); |   IssuableTokenKeys.tokenKeys.push(wipToken); | ||||||
|   IssuableTokenKeys.tokenKeysWithAlternative.push(wipToken); |   IssuableTokenKeys.tokenKeysWithAlternative.push(wipToken); | ||||||
|  | 
 | ||||||
|  |   const targetBranchToken = { | ||||||
|  |     key: 'target-branch', | ||||||
|  |     type: 'string', | ||||||
|  |     param: '', | ||||||
|  |     symbol: '', | ||||||
|  |     icon: 'arrow-right', | ||||||
|  |     tag: 'branch', | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   IssuableTokenKeys.tokenKeys.push(targetBranchToken); | ||||||
|  |   IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import DropdownEmoji from './dropdown_emoji'; | ||||||
| import NullDropdown from './null_dropdown'; | import NullDropdown from './null_dropdown'; | ||||||
| import DropdownAjaxFilter from './dropdown_ajax_filter'; | import DropdownAjaxFilter from './dropdown_ajax_filter'; | ||||||
| import DropdownUtils from './dropdown_utils'; | import DropdownUtils from './dropdown_utils'; | ||||||
|  | import { mergeUrlParams } from '../lib/utils/url_utility'; | ||||||
| 
 | 
 | ||||||
| export default class AvailableDropdownMappings { | export default class AvailableDropdownMappings { | ||||||
|   constructor(container, baseEndpoint, groupsOnly, includeAncestorGroups, includeDescendantGroups) { |   constructor(container, baseEndpoint, groupsOnly, includeAncestorGroups, includeDescendantGroups) { | ||||||
|  | @ -13,6 +14,7 @@ export default class AvailableDropdownMappings { | ||||||
|     this.groupsOnly = groupsOnly; |     this.groupsOnly = groupsOnly; | ||||||
|     this.includeAncestorGroups = includeAncestorGroups; |     this.includeAncestorGroups = includeAncestorGroups; | ||||||
|     this.includeDescendantGroups = includeDescendantGroups; |     this.includeDescendantGroups = includeDescendantGroups; | ||||||
|  |     this.filteredSearchInput = this.container.querySelector('.filtered-search'); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   getAllowedMappings(supportedTokens) { |   getAllowedMappings(supportedTokens) { | ||||||
|  | @ -102,6 +104,15 @@ export default class AvailableDropdownMappings { | ||||||
|         }, |         }, | ||||||
|         element: this.container.querySelector('#js-dropdown-runner-tag'), |         element: this.container.querySelector('#js-dropdown-runner-tag'), | ||||||
|       }, |       }, | ||||||
|  |       'target-branch': { | ||||||
|  |         reference: null, | ||||||
|  |         gl: DropdownNonUser, | ||||||
|  |         extraArguments: { | ||||||
|  |           endpoint: this.getMergeRequestTargetBranchesEndpoint(), | ||||||
|  |           symbol: '', | ||||||
|  |         }, | ||||||
|  |         element: this.container.querySelector('#js-dropdown-target-branch'), | ||||||
|  |       }, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -130,4 +141,24 @@ export default class AvailableDropdownMappings { | ||||||
|   getRunnerTagsEndpoint() { |   getRunnerTagsEndpoint() { | ||||||
|     return `${this.baseEndpoint}/admin/runners/tag_list.json`; |     return `${this.baseEndpoint}/admin/runners/tag_list.json`; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   getMergeRequestTargetBranchesEndpoint() { | ||||||
|  |     const endpoint = `${gon.relative_url_root || | ||||||
|  |       ''}/autocomplete/merge_request_target_branches.json`;
 | ||||||
|  | 
 | ||||||
|  |     const params = { | ||||||
|  |       group_id: this.getGroupId(), | ||||||
|  |       project_id: this.getProjectId(), | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return mergeUrlParams(params, endpoint); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getGroupId() { | ||||||
|  |     return this.filteredSearchInput.getAttribute('data-group-id') || ''; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getProjectId() { | ||||||
|  |     return this.filteredSearchInput.getAttribute('data-project-id') || ''; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -504,14 +504,7 @@ export default class FilteredSearchManager { | ||||||
|         const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam); |         const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam); | ||||||
| 
 | 
 | ||||||
|         if (match) { |         if (match) { | ||||||
|           // Use lastIndexOf because the token key is allowed to contain underscore
 |           const { key, symbol } = match; | ||||||
|           // e.g. 'my_reaction' is the token key of 'my_reaction_emoji'
 |  | ||||||
|           const lastIndexOf = keyParam.lastIndexOf('_'); |  | ||||||
|           let sanitizedKey = lastIndexOf !== -1 ? keyParam.slice(0, lastIndexOf) : keyParam; |  | ||||||
|           // Replace underscore with hyphen in the sanitizedkey.
 |  | ||||||
|           // e.g. 'my_reaction' => 'my-reaction'
 |  | ||||||
|           sanitizedKey = sanitizedKey.replace('_', '-'); |  | ||||||
|           const { symbol } = match; |  | ||||||
|           let quotationsToUse = ''; |           let quotationsToUse = ''; | ||||||
| 
 | 
 | ||||||
|           if (sanitizedValue.indexOf(' ') !== -1) { |           if (sanitizedValue.indexOf(' ') !== -1) { | ||||||
|  | @ -520,10 +513,10 @@ export default class FilteredSearchManager { | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           hasFilteredSearch = true; |           hasFilteredSearch = true; | ||||||
|           const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue); |           const canEdit = this.canEdit && this.canEdit(key, sanitizedValue); | ||||||
|           const { uppercaseTokenName, capitalizeTokenValue } = match; |           const { uppercaseTokenName, capitalizeTokenValue } = match; | ||||||
|           FilteredSearchVisualTokens.addFilterVisualToken( |           FilteredSearchVisualTokens.addFilterVisualToken( | ||||||
|             sanitizedKey, |             key, | ||||||
|             `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, |             `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, | ||||||
|             { |             { | ||||||
|               canEdit, |               canEdit, | ||||||
|  |  | ||||||
|  | @ -69,11 +69,21 @@ export default class FilteredSearchVisualTokens { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   static addVisualTokenElement(name, value, options = {}) { |   static addVisualTokenElement(name, value, options = {}) { | ||||||
|     const { isSearchTerm = false, canEdit, uppercaseTokenName, capitalizeTokenValue } = options; |     const { | ||||||
|  |       isSearchTerm = false, | ||||||
|  |       canEdit, | ||||||
|  |       uppercaseTokenName, | ||||||
|  |       capitalizeTokenValue, | ||||||
|  |       tokenClass = `search-token-${name.toLowerCase()}`, | ||||||
|  |     } = options; | ||||||
|     const li = document.createElement('li'); |     const li = document.createElement('li'); | ||||||
|     li.classList.add('js-visual-token'); |     li.classList.add('js-visual-token'); | ||||||
|     li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token'); |     li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token'); | ||||||
| 
 | 
 | ||||||
|  |     if (!isSearchTerm) { | ||||||
|  |       li.classList.add(tokenClass); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (value) { |     if (value) { | ||||||
|       li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({ |       li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({ | ||||||
|         canEdit, |         canEdit, | ||||||
|  |  | ||||||
|  | @ -108,6 +108,8 @@ | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .value-container { |   .value-container { | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|     background-color: $white-normal; |     background-color: $white-normal; | ||||||
|     color: $filter-value-text-color; |     color: $filter-value-text-color; | ||||||
|     border-radius: 0 2px 2px 0; |     border-radius: 0 2px 2px 0; | ||||||
|  | @ -121,7 +123,7 @@ | ||||||
| 
 | 
 | ||||||
|   .remove-token { |   .remove-token { | ||||||
|     display: inline-block; |     display: inline-block; | ||||||
|     padding-left: 4px; |     padding-left: 8px; | ||||||
|     padding-right: 0; |     padding-right: 0; | ||||||
| 
 | 
 | ||||||
|     .fa-close { |     .fa-close { | ||||||
|  | @ -412,3 +414,10 @@ | ||||||
|   padding: 8px 16px; |   padding: 8px 16px; | ||||||
|   text-align: center; |   text-align: center; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .search-token-target-branch { | ||||||
|  |   .value { | ||||||
|  |     font-family: $monospace-font; | ||||||
|  |     font-size: 13px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class AutocompleteController < ApplicationController | class AutocompleteController < ApplicationController | ||||||
|   skip_before_action :authenticate_user!, only: [:users, :award_emojis] |   skip_before_action :authenticate_user!, only: [:users, :award_emojis, :merge_request_target_branches] | ||||||
| 
 | 
 | ||||||
|   def users |   def users | ||||||
|     project = Autocomplete::ProjectFinder |     project = Autocomplete::ProjectFinder | ||||||
|  | @ -38,4 +38,11 @@ class AutocompleteController < ApplicationController | ||||||
|   def award_emojis |   def award_emojis | ||||||
|     render json: AwardedEmojiFinder.new(current_user).execute |     render json: AwardedEmojiFinder.new(current_user).execute | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   def merge_request_target_branches | ||||||
|  |     merge_requests = MergeRequestsFinder.new(current_user, params).execute | ||||||
|  |     target_branches = merge_requests.recent_target_branches | ||||||
|  | 
 | ||||||
|  |     render json: target_branches.map { |target_branch| { title: target_branch } } | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -29,7 +29,7 @@ | ||||||
| # | # | ||||||
| class MergeRequestsFinder < IssuableFinder | class MergeRequestsFinder < IssuableFinder | ||||||
|   def self.scalar_params |   def self.scalar_params | ||||||
|     @scalar_params ||= super + [:wip] |     @scalar_params ||= super + [:wip, :target_branch] | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def klass |   def klass | ||||||
|  |  | ||||||
|  | @ -203,6 +203,22 @@ class MergeRequest < ActiveRecord::Base | ||||||
|     '!' |     '!' | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   # Returns the top 100 target branches | ||||||
|  |   # | ||||||
|  |   # The returned value is a Array containing branch names | ||||||
|  |   # sort by updated_at of merge request: | ||||||
|  |   # | ||||||
|  |   #     ['master', 'develop', 'production'] | ||||||
|  |   # | ||||||
|  |   # limit - The maximum number of target branch to return. | ||||||
|  |   def self.recent_target_branches(limit: 100) | ||||||
|  |     group(:target_branch) | ||||||
|  |       .select(:target_branch) | ||||||
|  |       .reorder('MAX(merge_requests.updated_at) DESC') | ||||||
|  |       .limit(limit) | ||||||
|  |       .pluck(:target_branch) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def rebase_in_progress? |   def rebase_in_progress? | ||||||
|     strong_memoize(:rebase_in_progress) do |     strong_memoize(:rebase_in_progress) do | ||||||
|       # The source project can be deleted |       # The source project can be deleted | ||||||
|  |  | ||||||
|  | @ -137,6 +137,11 @@ | ||||||
|                 %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } |                 %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } | ||||||
|                   %button.btn.btn-link{ type: 'button' } |                   %button.btn.btn-link{ type: 'button' } | ||||||
|                     = _('No') |                     = _('No') | ||||||
|  |             #js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu | ||||||
|  |               %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } | ||||||
|  |                 %li.filter-dropdown-item | ||||||
|  |                   %button.btn.btn-link.js-data-value.monospace | ||||||
|  |                     {{title}} | ||||||
| 
 | 
 | ||||||
|             = render_if_exists 'shared/issuable/filter_weight', type: type |             = render_if_exists 'shared/issuable/filter_weight', type: type | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Add target branch filter to merge requests search bar | ||||||
|  | merge_request: 24380 | ||||||
|  | author: Hiroyuki Sato | ||||||
|  | type: added | ||||||
|  | @ -43,6 +43,7 @@ Rails.application.routes.draw do | ||||||
|   get '/autocomplete/users/:id' => 'autocomplete#user' |   get '/autocomplete/users/:id' => 'autocomplete#user' | ||||||
|   get '/autocomplete/projects' => 'autocomplete#projects' |   get '/autocomplete/projects' => 'autocomplete#projects' | ||||||
|   get '/autocomplete/award_emojis' => 'autocomplete#award_emojis' |   get '/autocomplete/award_emojis' => 'autocomplete#award_emojis' | ||||||
|  |   get '/autocomplete/merge_request_target_branches' => 'autocomplete#merge_request_target_branches' | ||||||
| 
 | 
 | ||||||
|   # Search |   # Search | ||||||
|   get 'search' => 'search#show' |   get 'search' => 'search#show' | ||||||
|  |  | ||||||
|  | @ -371,5 +371,36 @@ describe AutocompleteController do | ||||||
|         expect(json_response[3]).to match('name' => 'thumbsdown') |         expect(json_response[3]).to match('name' => 'thumbsdown') | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     context 'Get merge_request_target_branches' do | ||||||
|  |       let(:user2) { create(:user) } | ||||||
|  |       let!(:merge_request1) { create(:merge_request, source_project: project, target_branch: 'feature') } | ||||||
|  | 
 | ||||||
|  |       context 'unauthorized user' do | ||||||
|  |         it 'returns empty json' do | ||||||
|  |           get :merge_request_target_branches | ||||||
|  | 
 | ||||||
|  |           expect(json_response).to be_empty | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'sign in as user without any accesible merge requests' do | ||||||
|  |         it 'returns empty json' do | ||||||
|  |           sign_in(user2) | ||||||
|  |           get :merge_request_target_branches | ||||||
|  | 
 | ||||||
|  |           expect(json_response).to be_empty | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'sign in as user with a accesible merge request' do | ||||||
|  |         it 'returns json' do | ||||||
|  |           sign_in(user) | ||||||
|  |           get :merge_request_target_branches | ||||||
|  | 
 | ||||||
|  |           expect(json_response).to contain_exactly({ 'title' => 'feature' }) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,45 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | describe 'Merge Requests > User filters by target branch', :js do | ||||||
|  |   include FilteredSearchHelpers | ||||||
|  | 
 | ||||||
|  |   let!(:project) { create(:project, :public, :repository) } | ||||||
|  |   let!(:user)    { project.creator } | ||||||
|  |   let!(:mr1) { create(:merge_request, source_project: project, target_project: project, source_branch: 'feature', target_branch: 'master') } | ||||||
|  |   let!(:mr2) { create(:merge_request, source_project: project, target_project: project, source_branch: 'feature', target_branch: 'merged-target') } | ||||||
|  | 
 | ||||||
|  |   before do | ||||||
|  |     sign_in(user) | ||||||
|  |     visit project_merge_requests_path(project) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   context 'filtering by target-branch:master' do | ||||||
|  |     it 'applies the filter' do | ||||||
|  |       input_filtered_search('target-branch:master') | ||||||
|  | 
 | ||||||
|  |       expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) | ||||||
|  |       expect(page).to have_content mr1.title | ||||||
|  |       expect(page).not_to have_content mr2.title | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   context 'filtering by target-branch:merged-target' do | ||||||
|  |     it 'applies the filter' do | ||||||
|  |       input_filtered_search('target-branch:merged-target') | ||||||
|  | 
 | ||||||
|  |       expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) | ||||||
|  |       expect(page).not_to have_content mr1.title | ||||||
|  |       expect(page).to have_content mr2.title | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   context 'filtering by target-branch:feature' do | ||||||
|  |     it 'applies the filter' do | ||||||
|  |       input_filtered_search('target-branch:feature') | ||||||
|  | 
 | ||||||
|  |       expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0) | ||||||
|  |       expect(page).not_to have_content mr1.title | ||||||
|  |       expect(page).not_to have_content mr2.title | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -36,7 +36,7 @@ describe MergeRequestsFinder do | ||||||
|     let(:project5) { create_project_without_n_plus_1(group: subgroup) } |     let(:project5) { create_project_without_n_plus_1(group: subgroup) } | ||||||
|     let(:project6) { create_project_without_n_plus_1(group: subgroup) } |     let(:project6) { create_project_without_n_plus_1(group: subgroup) } | ||||||
| 
 | 
 | ||||||
|     let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) } |     let!(:merge_request1) { create(:merge_request, author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') } | ||||||
|     let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') } |     let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') } | ||||||
|     let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') } |     let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') } | ||||||
|     let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') } |     let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') } | ||||||
|  |  | ||||||
|  | @ -293,6 +293,7 @@ describe('Filtered Search Visual Tokens', () => { | ||||||
|       subject.addVisualTokenElement('milestone'); |       subject.addVisualTokenElement('milestone'); | ||||||
|       const token = tokensContainer.querySelector('.js-visual-token'); |       const token = tokensContainer.querySelector('.js-visual-token'); | ||||||
| 
 | 
 | ||||||
|  |       expect(token.classList.contains('search-token-milestone')).toEqual(true); | ||||||
|       expect(token.classList.contains('filtered-search-token')).toEqual(true); |       expect(token.classList.contains('filtered-search-token')).toEqual(true); | ||||||
|       expect(token.querySelector('.name').innerText).toEqual('milestone'); |       expect(token.querySelector('.name').innerText).toEqual('milestone'); | ||||||
|       expect(token.querySelector('.value')).toEqual(null); |       expect(token.querySelector('.value')).toEqual(null); | ||||||
|  | @ -302,6 +303,7 @@ describe('Filtered Search Visual Tokens', () => { | ||||||
|       subject.addVisualTokenElement('label', 'Frontend'); |       subject.addVisualTokenElement('label', 'Frontend'); | ||||||
|       const token = tokensContainer.querySelector('.js-visual-token'); |       const token = tokensContainer.querySelector('.js-visual-token'); | ||||||
| 
 | 
 | ||||||
|  |       expect(token.classList.contains('search-token-label')).toEqual(true); | ||||||
|       expect(token.classList.contains('filtered-search-token')).toEqual(true); |       expect(token.classList.contains('filtered-search-token')).toEqual(true); | ||||||
|       expect(token.querySelector('.name').innerText).toEqual('label'); |       expect(token.querySelector('.name').innerText).toEqual('label'); | ||||||
|       expect(token.querySelector('.value').innerText).toEqual('Frontend'); |       expect(token.querySelector('.value').innerText).toEqual('Frontend'); | ||||||
|  | @ -317,10 +319,12 @@ describe('Filtered Search Visual Tokens', () => { | ||||||
|       const labelToken = tokens[0]; |       const labelToken = tokens[0]; | ||||||
|       const assigneeToken = tokens[1]; |       const assigneeToken = tokens[1]; | ||||||
| 
 | 
 | ||||||
|  |       expect(labelToken.classList.contains('search-token-label')).toEqual(true); | ||||||
|       expect(labelToken.classList.contains('filtered-search-token')).toEqual(true); |       expect(labelToken.classList.contains('filtered-search-token')).toEqual(true); | ||||||
|       expect(labelToken.querySelector('.name').innerText).toEqual('label'); |       expect(labelToken.querySelector('.name').innerText).toEqual('label'); | ||||||
|       expect(labelToken.querySelector('.value').innerText).toEqual('Frontend'); |       expect(labelToken.querySelector('.value').innerText).toEqual('Frontend'); | ||||||
| 
 | 
 | ||||||
|  |       expect(assigneeToken.classList.contains('search-token-assignee')).toEqual(true); | ||||||
|       expect(assigneeToken.classList.contains('filtered-search-token')).toEqual(true); |       expect(assigneeToken.classList.contains('filtered-search-token')).toEqual(true); | ||||||
|       expect(assigneeToken.querySelector('.name').innerText).toEqual('assignee'); |       expect(assigneeToken.querySelector('.name').innerText).toEqual('assignee'); | ||||||
|       expect(assigneeToken.querySelector('.value').innerText).toEqual('@root'); |       expect(assigneeToken.querySelector('.value').innerText).toEqual('@root'); | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ export default class FilteredSearchSpecHelper { | ||||||
| 
 | 
 | ||||||
|   static createFilterVisualToken(name, value, isSelected = false) { |   static createFilterVisualToken(name, value, isSelected = false) { | ||||||
|     const li = document.createElement('li'); |     const li = document.createElement('li'); | ||||||
|     li.classList.add('js-visual-token', 'filtered-search-token'); |     li.classList.add('js-visual-token', 'filtered-search-token', `search-token-${name}`); | ||||||
| 
 | 
 | ||||||
|     li.innerHTML = ` |     li.innerHTML = ` | ||||||
|       <div class="selectable ${isSelected ? 'selected' : ''}" role="button"> |       <div class="selectable ${isSelected ? 'selected' : ''}" role="button"> | ||||||
|  |  | ||||||
|  | @ -270,6 +270,25 @@ describe MergeRequest do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   describe '.recent_target_branches' do | ||||||
|  |     let(:project) { create(:project) } | ||||||
|  |     let!(:merge_request1) { create(:merge_request, :opened, source_project: project, target_branch: 'feature') } | ||||||
|  |     let!(:merge_request2) { create(:merge_request, :closed, source_project: project, target_branch: 'merge-test') } | ||||||
|  |     let!(:merge_request3) { create(:merge_request, :opened, source_project: project, target_branch: 'fix') } | ||||||
|  |     let!(:merge_request4) { create(:merge_request, :closed, source_project: project, target_branch: 'feature') } | ||||||
|  | 
 | ||||||
|  |     before do | ||||||
|  |       merge_request1.update_columns(updated_at: 1.day.since) | ||||||
|  |       merge_request2.update_columns(updated_at: 2.days.since) | ||||||
|  |       merge_request3.update_columns(updated_at: 3.days.since) | ||||||
|  |       merge_request4.update_columns(updated_at: 4.days.since) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns target branches sort by updated at desc' do | ||||||
|  |       expect(described_class.recent_target_branches).to match_array(['feature', 'merge-test', 'fix']) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe '#target_branch_sha' do |   describe '#target_branch_sha' do | ||||||
|     let(:project) { create(:project, :repository) } |     let(:project) { create(:project, :repository) } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue