Commit 649c095a authored by Clement Ho's avatar Clement Ho

Add filtered search to MR page

parent b596dd8f
...@@ -74,7 +74,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -74,7 +74,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues:index': case 'projects:issues:index':
if (gl.FilteredSearchManager) { if (gl.FilteredSearchManager) {
new gl.FilteredSearchManager(); new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
} }
Issuable.init(); Issuable.init();
new gl.IssuableBulkActions({ new gl.IssuableBulkActions({
......
...@@ -37,23 +37,18 @@ require('./filtered_search_dropdown'); ...@@ -37,23 +37,18 @@ require('./filtered_search_dropdown');
} }
renderContent() { renderContent() {
const dropdownData = [{ const dropdownData = [];
icon: 'fa-pencil',
hint: 'author:', [].forEach.call(this.input.parentElement.querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
tag: '<@author>', const { icon, hint, tag } = dropdownMenu.dataset;
}, { if (icon && hint && tag) {
icon: 'fa-user', dropdownData.push({
hint: 'assignee:', icon: `fa-${icon}`,
tag: '<@assignee>', hint,
}, { tag: `<${tag}>`,
icon: 'fa-clock-o', });
hint: 'milestone:', }
tag: '<%milestone>', });
}, {
icon: 'fa-tag',
hint: 'label:',
tag: '<~label>',
}];
this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config);
this.droplab.setData(this.hookId, dropdownData); this.droplab.setData(this.hookId, dropdownData);
......
...@@ -52,8 +52,9 @@ ...@@ -52,8 +52,9 @@
} }
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
if (forceShowList && this.getCurrentHook().list.hidden) { const currentHook = this.getCurrentHook();
this.getCurrentHook().list.show(); if (forceShowList && currentHook && currentHook.list.hidden) {
currentHook.list.show();
} }
} }
...@@ -92,18 +93,24 @@ ...@@ -92,18 +93,24 @@
} }
hideDropdown() { hideDropdown() {
this.getCurrentHook().list.hide(); const currentHook = this.getCurrentHook();
if (currentHook) {
currentHook.list.hide();
}
} }
resetFilters() { resetFilters() {
const hook = this.getCurrentHook(); const hook = this.getCurrentHook();
const data = hook.list.data;
const results = data.map((o) => { if (hook) {
const updated = o; const data = hook.list.data;
updated.droplab_hidden = false; const results = data.map((o) => {
return updated; const updated = o;
}); updated.droplab_hidden = false;
hook.list.render(results); return updated;
});
hook.list.render(results);
}
} }
} }
......
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
(() => { (() => {
class FilteredSearchDropdownManager { class FilteredSearchDropdownManager {
constructor(baseEndpoint = '') { constructor(baseEndpoint = '', page) {
this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = gl.FilteredSearchTokenizer; this.tokenizer = gl.FilteredSearchTokenizer;
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
this.filteredSearchInput = document.querySelector('.filtered-search'); this.filteredSearchInput = document.querySelector('.filtered-search');
this.page = page;
this.setupMapping(); this.setupMapping();
...@@ -150,7 +152,7 @@ ...@@ -150,7 +152,7 @@
this.droplab = new DropLab(); this.droplab = new DropLab();
} }
const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
&& this.mapping[match.key]; && this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
......
(() => { (() => {
class FilteredSearchManager { class FilteredSearchManager {
constructor() { constructor(page) {
this.filteredSearchInput = document.querySelector('.filtered-search'); this.filteredSearchInput = document.querySelector('.filtered-search');
this.clearSearchButton = document.querySelector('.clear-search'); this.clearSearchButton = document.querySelector('.clear-search');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
if (this.filteredSearchInput) { if (this.filteredSearchInput) {
this.tokenizer = gl.FilteredSearchTokenizer; this.tokenizer = gl.FilteredSearchTokenizer;
this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || ''); this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
this.bindEvents(); this.bindEvents();
this.loadSearchParamsFromURL(); this.loadSearchParamsFromURL();
...@@ -117,8 +118,8 @@ ...@@ -117,8 +118,8 @@
const keyParam = decodeURIComponent(split[0]); const keyParam = decodeURIComponent(split[0]);
const value = split[1]; const value = split[1];
// Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys // Check if it matches edge conditions listed in this.filteredSearchTokenKeys
const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p); const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
if (condition) { if (condition) {
inputValues.push(`${condition.tokenKey}:${condition.value}`); inputValues.push(`${condition.tokenKey}:${condition.value}`);
...@@ -126,7 +127,7 @@ ...@@ -126,7 +127,7 @@
// Sanitize value since URL converts spaces into + // Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded + // Replace before decode so that we know what was originally + versus the encoded +
const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam); const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
if (match) { if (match) {
const indexOf = keyParam.indexOf('_'); const indexOf = keyParam.indexOf('_');
...@@ -171,9 +172,9 @@ ...@@ -171,9 +172,9 @@
paths.push(`state=${currentState}`); paths.push(`state=${currentState}`);
tokens.forEach((token) => { tokens.forEach((token) => {
const condition = gl.FilteredSearchTokenKeys const condition = this.filteredSearchTokenKeys
.searchByConditionKeyValue(token.key, token.value.toLowerCase()); .searchByConditionKeyValue(token.key, token.value.toLowerCase());
const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key) || {}; const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const keyParam = param ? `${token.key}_${param}` : token.key; const keyParam = param ? `${token.key}_${param}` : token.key;
let tokenPath = ''; let tokenPath = '';
......
...@@ -169,10 +169,10 @@ ...@@ -169,10 +169,10 @@
url: issuesPath + "/?author_username=" + userName url: issuesPath + "/?author_username=" + userName
}, 'separator', { }, 'separator', {
text: 'Merge requests assigned to me', text: 'Merge requests assigned to me',
url: mrPath + "/?assignee_id=" + userId url: mrPath + "/?assignee_username=" + userName
}, { }, {
text: "Merge requests I've created", text: "Merge requests I've created",
url: mrPath + "/?author_id=" + userId url: mrPath + "/?author_username=" + userName
} }
]; ];
if (!name) { if (!name) {
......
...@@ -50,6 +50,17 @@ def index ...@@ -50,6 +50,17 @@ def index
@labels = LabelsFinder.new(current_user, labels_params).execute @labels = LabelsFinder.new(current_user, labels_params).execute
end end
@users = []
if params[:assignee_id].present?
assignee = User.find_by_id(params[:assignee_id])
@users.push(assignee) if assignee
end
if params[:author_id].present?
author = User.find_by_id(params[:author_id])
@users.push(author) if author
end
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
......
...@@ -5,18 +5,19 @@ ...@@ -5,18 +5,19 @@
= render "projects/issues/head" = render "projects/issues/head"
= render 'projects/last_push' = render 'projects/last_push'
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('filtered_search')
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests
.nav-controls .nav-controls
= render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- if merge_project - if merge_project
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do
New Merge Request New Merge Request
= render 'shared/issuable/filter', type: :merge_requests = render 'shared/issuable/search_bar', type: :merge_requests
.merge-requests-holder .merge-requests-holder
= render 'merge_requests' = render 'merge_requests'
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
{{hint}} {{hint}}
%span.js-filter-tag.dropdown-light-content %span.js-filter-tag.dropdown-light-content
{{tag}} {{tag}}
#js-dropdown-author.dropdown-menu #js-dropdown-author.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } }
%ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
%li.filter-dropdown-item %li.filter-dropdown-item
%button.btn.btn-link.dropdown-user %button.btn.btn-link.dropdown-user
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
{{name}} {{name}}
%span.dropdown-light-content %span.dropdown-light-content
@{{username}} @{{username}}
#js-dropdown-assignee.dropdown-menu #js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } }
%ul{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' } %li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link %button.btn.btn-link
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
{{name}} {{name}}
%span.dropdown-light-content %span.dropdown-light-content
@{{username}} @{{username}}
#js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } #js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } }
%ul{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' } %li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link %button.btn.btn-link
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
%li.filter-dropdown-item %li.filter-dropdown-item
%button.btn.btn-link.js-data-value %button.btn.btn-link.js-data-value
{{title}} {{title}}
#js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } }
%ul{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' } %li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link %button.btn.btn-link
......
---
title: Add filtered search to MR page
merge_request:
author:
...@@ -293,13 +293,6 @@ Feature: Project Merge Requests ...@@ -293,13 +293,6 @@ Feature: Project Merge Requests
And I preview a description text like "Bug fixed :smile:" And I preview a description text like "Bug fixed :smile:"
Then I should see the Markdown write tab Then I should see the Markdown write tab
@javascript
Scenario: I search merge request
Given I click link "All"
When I fill in merge request search with "Fe"
Then I should see "Feature NS-03" in merge requests
And I should not see "Bug NS-04" in merge requests
@javascript @javascript
Scenario: I can unsubscribe from merge request Scenario: I can unsubscribe from merge request
Given I visit merge request page "Bug NS-04" Given I visit merge request page "Bug NS-04"
......
require 'spec_helper' require 'spec_helper'
describe 'Dropdown label', js: true, feature: true do describe 'Dropdown label', js: true, feature: true do
include FilteredSearchHelpers
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:filtered_search) { find('.filtered-search') } let(:filtered_search) { find('.filtered-search') }
...@@ -17,12 +19,6 @@ ...@@ -17,12 +19,6 @@
let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title') } let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title') }
end end
def init_label_search
filtered_search.set('label:')
# This ensures the dropdown is shown
expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading')
end
def search_for_label(label) def search_for_label(label)
init_label_search init_label_search
filtered_search.send_keys(label) filtered_search.send_keys(label)
......
require 'rails_helper' require 'rails_helper'
describe 'Filter issues', js: true, feature: true do describe 'Filter issues', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax include WaitForAjax
let!(:group) { create(:group) } let!(:group) { create(:group) }
...@@ -17,19 +18,6 @@ ...@@ -17,19 +18,6 @@
let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) } let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) }
let(:filtered_search) { find('.filtered-search') }
def input_filtered_search(search_term, submit: true)
filtered_search.set(search_term)
if submit
filtered_search.send_keys(:enter)
end
end
def expect_filtered_search_input(input)
expect(find('.filtered-search').value).to eq(input)
end
def expect_no_issues_list def expect_no_issues_list
page.within '.issues-list' do page.within '.issues-list' do
......
require 'rails_helper' require 'rails_helper'
feature 'Issue filtering by Labels', feature: true, js: true do feature 'Issue filtering by Labels', feature: true, js: true do
include FilteredSearchHelpers
include MergeRequestHelpers
include WaitForAjax include WaitForAjax
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
...@@ -32,123 +34,77 @@ ...@@ -32,123 +34,77 @@
context 'filter by label bug' do context 'filter by label bug' do
before do before do
select_labels('bug') input_filtered_search('label:~bug')
end end
it 'apply the filter' do it 'apply the filter' do
expect(page).to have_content "Bugfix1" expect(page).to have_content "Bugfix1"
expect(page).to have_content "Bugfix2" expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1" expect(page).not_to have_content "Feature1"
expect(find('.filtered-labels')).to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "feature"
expect(find('.filtered-labels')).not_to have_content "enhancement"
find('.js-label-filter-remove').click
wait_for_ajax
expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
end end
end end
context 'filter by label feature' do context 'filter by label feature' do
before do before do
select_labels('feature') input_filtered_search('label:~feature')
end end
it 'applies the filter' do it 'applies the filter' do
expect(page).to have_content "Feature1" expect(page).to have_content "Feature1"
expect(page).not_to have_content "Bugfix2" expect(page).not_to have_content "Bugfix2"
expect(page).not_to have_content "Bugfix1" expect(page).not_to have_content "Bugfix1"
expect(find('.filtered-labels')).to have_content "feature"
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "enhancement"
end end
end end
context 'filter by label enhancement' do context 'filter by label enhancement' do
before do before do
select_labels('enhancement') input_filtered_search('label:~enhancement')
end end
it 'applies the filter' do it 'applies the filter' do
expect(page).to have_content "Bugfix2" expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1" expect(page).not_to have_content "Feature1"
expect(page).not_to have_content "Bugfix1" expect(page).not_to have_content "Bugfix1"
expect(find('.filtered-labels')).to have_content "enhancement"
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "feature"
end end
end end
context 'filter by label enhancement and bug in issues list' do context 'filter by label enhancement and bug in issues list' do
before do before do
select_labels('bug', 'enhancement') input_filtered_search('label:~bug label:~enhancement')
end end
it 'applies the filters' do it 'applies the filters' do
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content "Bugfix2" expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1" expect(page).not_to have_content "Feature1"
expect(find('.filtered-labels')).to have_content "bug"
expect(find('.filtered-labels')).to have_content "enhancement"
expect(find('.filtered-labels')).not_to have_content "feature"
find('.js-label-filter-remove', match: :first).click
wait_for_ajax
expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1"
expect(page).not_to have_content "Bugfix1"
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).to have_content "enhancement"
expect(find('.filtered-labels')).not_to have_content "feature"
end end
end end
context 'remove filtered labels' do context 'clear button' do
before do before do
page.within '.labels-filter' do input_filtered_search('label:~bug')
click_button 'Label'
wait_for_ajax
click_link 'bug'
find('.dropdown-menu-close').click
end
page.within '.filtered-labels' do
expect(page).to have_content 'bug'
end
end end
it 'allows user to remove filtered labels' do it 'allows user to remove filtered labels' do
first('.js-label-filter-remove').click first('.clear-search').click
wait_for_ajax filtered_search.send_keys(:enter)
expect(find('.filtered-labels', visible: false)).not_to have_content 'bug' expect(page).to have_issuable_counts(open: 3, closed: 0, all: 3)
expect(find('.labels-filter')).not_to have_content 'bug'