diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..6de06811d94482a8a7bb8f0f84263567303ce634 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters.js.es6 @@ -0,0 +1,49 @@ +/* global Vue */ +const userFilter = require('./filters/user'); +const milestoneFilter = require('./filters/milestone'); +const labelFilter = require('./filters/label'); + +module.exports = Vue.extend({ + name: 'modal-filters', + props: { + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, + destroyed() { + gl.issueBoards.ModalStore.setDefaultFilter(); + }, + components: { + userFilter, + milestoneFilter, + labelFilter, + }, + template: ` + <div class="modal-filters"> + <user-filter + dropdown-class-name="dropdown-menu-author" + toggle-class-name="js-user-search js-author-search" + toggle-label="Author" + field-name="author_id" + :project-id="projectId"></user-filter> + <user-filter + dropdown-class-name="dropdown-menu-author" + toggle-class-name="js-assignee-search" + toggle-label="Assignee" + field-name="assignee_id" + :null-user="true" + :project-id="projectId"></user-filter> + <milestone-filter :milestone-path="milestonePath"></milestone-filter> + <label-filter :label-path="labelPath"></label-filter> + </div> + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 b/app/assets/javascripts/boards/components/modal/filters/label.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..4fc8f72a145673c8423f1ea032371a5af2c87adb --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters/label.js.es6 @@ -0,0 +1,54 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global LabelsSelect */ +module.exports = Vue.extend({ + name: 'filter-label', + props: { + labelPath: { + type: String, + required: true, + }, + }, + mounted() { + new LabelsSelect(this.$refs.dropdown); + }, + template: ` + <div class="dropdown"> + <button + class="dropdown-menu-toggle js-label-select js-multiselect js-extra-options" + type="button" + data-toggle="dropdown" + data-show-any="true" + data-show-no="true" + :data-labels="labelPath" + ref="dropdown"> + <span class="dropdown-toggle-text"> + Label + </span> + <i class="fa fa-chevron-down"></i> + </button> + <div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"> + <div class="dropdown-title"> + Filter by label + <button + class="dropdown-title-button dropdown-menu-close" + aria-label="Close" + type="button"> + <i class="fa fa-times dropdown-menu-close-icon"></i> + </button> + </div> + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + placeholder="Search" + autocomplete="off" /> + <i class="fa fa-search dropdown-input-search"></i> + <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> + </div> + </div> + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..d555599d300684307ddba009165d18ab5edb66fd --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 @@ -0,0 +1,55 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global MilestoneSelect */ +module.exports = Vue.extend({ + name: 'filter-milestone', + props: { + milestonePath: { + type: String, + required: true, + }, + }, + mounted() { + new MilestoneSelect(null, this.$refs.dropdown); + }, + template: ` + <div class="dropdown"> + <button + class="dropdown-menu-toggle js-milestone-select" + type="button" + data-toggle="dropdown" + data-show-any="true" + data-show-upcoming="true" + data-field-name="milestone_title" + :data-milestones="milestonePath" + ref="dropdown"> + <span class="dropdown-toggle-text"> + Milestone + </span> + <i class="fa fa-chevron-down"></i> + </button> + <div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone"> + <div class="dropdown-title"> + <span>Filter by milestone</span> + <button + class="dropdown-title-button dropdown-menu-close" + aria-label="Close" + type="button"> + <i class="fa fa-times dropdown-menu-close-icon"></i> + </button> + </div> + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + placeholder="Search milestones" + autocomplete="off" /> + <i class="fa fa-search dropdown-input-search"></i> + <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> + </div> + </div> + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 b/app/assets/javascripts/boards/components/modal/filters/user.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..8523028c29c11998cbb3d30ef374f9b2bdceded2 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters/user.js.es6 @@ -0,0 +1,96 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global UsersSelect */ +module.exports = Vue.extend({ + name: 'filter-user', + props: { + toggleClassName: { + type: String, + required: true, + }, + dropdownClassName: { + type: String, + required: false, + default: '', + }, + toggleLabel: { + type: String, + required: true, + }, + fieldName: { + type: String, + required: true, + }, + nullUser: { + type: Boolean, + required: false, + default: false, + }, + projectId: { + type: Number, + required: true, + }, + }, + mounted() { + new UsersSelect(null, this.$refs.dropdown); + }, + computed: { + currentUsername() { + return gon.current_username; + }, + dropdownTitle() { + return `Filter by ${this.toggleLabel.toLowerCase()}`; + }, + inputPlaceholder() { + return `Search ${this.toggleLabel.toLowerCase()}`; + }, + }, + template: ` + <div class="dropdown"> + <button + class="dropdown-menu-toggle js-user-search" + :class="toggleClassName" + type="button" + data-toggle="dropdown" + data-current-user="true" + :data-any-user="'Any ' + toggleLabel" + :data-null-user="nullUser" + :data-field-name="fieldName" + :data-project-id="projectId" + :data-first-user="currentUsername" + ref="dropdown"> + <span class="dropdown-toggle-text"> + {{ toggleLabel }} + </span> + <i class="fa fa-chevron-down"></i> + </button> + <div + class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable" + :class="dropdownClassName"> + <div class="dropdown-title"> + {{ dropdownTitle }} + <button + class="dropdown-title-button dropdown-menu-close" + aria-label="Close" + type="button"> + <i class="fa fa-times dropdown-menu-close-icon"></i> + </button> + </div> + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + autocomplete="off" + :placeholder="inputPlaceholder" /> + <i class="fa fa-search dropdown-input-search"></i> + <i + role="button" + class="fa fa-times dropdown-input-clear js-dropdown-input-clear"> + </i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> + </div> + </div> + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index ab903722ba4bf38b8121402736f1bcf41eeb3e9a..70c088f905414d195cf9f7be4521cc5ff143ddfd 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -1,12 +1,26 @@ /* global Vue */ - require('./tabs'); +const modalFilters = require('./filters'); (() => { const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.ModalHeader = Vue.extend({ mixins: [gl.issueBoards.ModalMixins], + props: { + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, data() { return ModalStore.store; }, @@ -31,6 +45,7 @@ require('./tabs'); }, components: { 'modal-tabs': gl.issueBoards.ModalTabs, + modalFilters, }, template: ` <div> @@ -51,6 +66,11 @@ require('./tabs'); <div class="add-issues-search append-bottom-10" v-if="showSearch"> + <modal-filters + :project-id="projectId" + :milestone-path="milestonePath" + :label-path="labelPath"> + </modal-filters> <input placeholder="Search issues..." class="form-control" diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index d367b7e42466739741a54feff5a6e4f0f10ee0df..f290cd13763d318268d8ce80d07507eae6832448 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -27,6 +27,18 @@ require('./empty_state'); type: String, required: true, }, + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, }, data() { return ModalStore.store; @@ -52,17 +64,27 @@ require('./empty_state'); this.issuesCount = false; } }, + filter: { + handler() { + this.loadIssues(true); + }, + deep: true, + }, }, methods: { searchOperation: _.debounce(function searchOperationDebounce() { this.loadIssues(true); }, 500), loadIssues(clearIssues = false) { - return gl.boardService.getBacklog({ + if (!this.showAddIssuesModal) return false; + + const queryData = Object.assign({}, this.filter, { search: this.searchTerm, page: this.page, per: this.perPage, - }).then((res) => { + }); + + return gl.boardService.getBacklog(queryData).then((res) => { const data = res.json(); if (clearIssues) { @@ -112,8 +134,13 @@ require('./empty_state'); class="add-issues-modal" v-if="showAddIssuesModal"> <div class="add-issues-container"> - <modal-header></modal-header> + <modal-header + :project-id="projectId" + :milestone-path="milestonePath" + :label-path="labelPath"> + </modal-header> <modal-list + :image="blankStateImage" :issue-link-base="issueLinkBase" :root-path="rootPath" v-if="!loading && showList"></modal-list> diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index d0901219216e02e880060c107b4f4b06c4217289..3730c1ecaeb2abab384aa55a4fc25df1c4bade78 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -14,6 +14,10 @@ type: String, required: true, }, + image: { + type: String, + required: true, + }, }, data() { return ModalStore.store; @@ -110,6 +114,19 @@ <section class="add-issues-list add-issues-list-columns" ref="list"> + <div + class="empty-state add-issues-empty-state-filter text-center" + v-if="issuesCount > 0 && issues.length === 0"> + <div + class="svg-content" + v-html="image"> + </div> + <div class="text-content"> + <h4> + There are no issues to show. + </h4> + </div> + </div> <div v-for="group in groupedIssues" class="add-issues-list-column"> diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index 73518b42b8445af678019b5456f777a6173a4d08..15fc6c79e8d3891d744f235542b1c681a46d7f49 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -18,6 +18,17 @@ page: 1, perPage: 50, }; + + this.setDefaultFilter(); + } + + setDefaultFilter() { + this.store.filter = { + author_id: '', + assignee_id: '', + milestone_title: '', + label_name: [], + }; } selectedCount() { diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 70dc0d06b7b0311729086436d07782ae20124f9d..e4cf9057e6da7113201d1a7cfd80bd17bff5ca33 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,10 +4,17 @@ (function() { this.LabelsSelect = (function() { - function LabelsSelect() { - var _this; + function LabelsSelect(els) { + var _this, $els; _this = this; - $('.js-label-select').each(function(i, dropdown) { + + $els = $(els); + + if (!els) { + $els = $('.js-label-select'); + } + + $els.each(function(i, dropdown) { var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; $dropdown = $(dropdown); $dropdownContainer = $dropdown.closest('.labels-filter'); @@ -324,7 +331,7 @@ multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: function(label, $el, e, isMarking) { - var isIssueIndex, isMRIndex, page; + var isIssueIndex, isMRIndex, page, boardsModel; page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; @@ -346,22 +353,31 @@ return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { + if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && + !$dropdown.closest('.add-issues-modal').length) { + boardsModel = gl.issueBoards.BoardsStore.state.filters; + } else if ($dropdown.closest('.add-issues-modal').length) { + boardsModel = gl.issueBoards.ModalStore.store.filter; + } + + if (boardsModel) { if (label.isAny) { - gl.issueBoards.BoardsStore.state.filters['label_name'] = []; + boardsModel['label_name'] = []; } else if ($el.hasClass('is-active')) { - gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title); + boardsModel['label_name'].push(label.title); } else { - var filters = gl.issueBoards.BoardsStore.state.filters['label_name']; + var filters = boardsModel['label_name']; filters = filters.filter(function (filteredLabel) { return filteredLabel !== label.title; }); - gl.issueBoards.BoardsStore.state.filters['label_name'] = filters; + boardsModel['label_name'] = filters; } - gl.issueBoards.BoardsStore.updateFiltersUrl(); + if (!$dropdown.closest('.add-issues-modal').length) { + gl.issueBoards.BoardsStore.updateFiltersUrl(); + } e.preventDefault(); return; } diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 7ab39ffbd05b27149d4b8d9b60ec21d872223f15..2f08aa7fe8bac5ddaa78e98f6351b060ea4f4331 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -5,13 +5,20 @@ (function() { this.MilestoneSelect = (function() { - function MilestoneSelect(currentProject) { - var _this; + function MilestoneSelect(currentProject, els) { + var _this, $els; if (currentProject != null) { _this = this; this.currentProject = JSON.parse(currentProject); } - $('.js-milestone-select').each(function(i, dropdown) { + + $els = $(els); + + if (!els) { + $els = $('.js-milestone-select'); + } + + $els.each(function(i, dropdown) { var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove; $dropdown = $(dropdown); projectId = $dropdown.data('project-id'); @@ -108,7 +115,7 @@ }, vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: function(selected, $el, e) { - var data, isIssueIndex, isMRIndex, page; + var data, isIssueIndex, isMRIndex, page, boardsStore; page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); @@ -116,9 +123,19 @@ e.preventDefault(); return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { - gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name; - gl.issueBoards.BoardsStore.updateFiltersUrl(); + + if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && + !$dropdown.closest('.add-issues-modal').length) { + boardsStore = gl.issueBoards.BoardsStore.state.filters; + } else if ($dropdown.closest('.add-issues-modal').length) { + boardsStore = gl.issueBoards.ModalStore.store.filter; + } + + if (boardsStore) { + boardsStore[$dropdown.data('field-name')] = selected.name; + if (!$dropdown.closest('.add-issues-modal').length) { + gl.issueBoards.BoardsStore.updateFiltersUrl(); + } e.preventDefault(); } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { if (selected.name != null) { diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 77d2764cdf0077121b704a86f001e4848a474074..d4b24d132995cf7a2a0a2e8edb98313f865ca757 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -8,7 +8,8 @@ slice = [].slice; this.UsersSelect = (function() { - function UsersSelect(currentUser) { + function UsersSelect(currentUser, els) { + var $els; this.users = bind(this.users, this); this.user = bind(this.user, this); this.usersPath = "/autocomplete/users.json"; @@ -20,7 +21,14 @@ this.currentUser = JSON.parse(currentUser); } } - $('.js-user-search').each((function(_this) { + + $els = $(els); + + if (!els) { + $els = $('.js-user-search'); + } + + $els.each((function(_this) { return function(i, dropdown) { var options = {}; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove; @@ -193,7 +201,9 @@ selectedId = user.id; return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { + if ($el.closest('.add-issues-modal').length) { + gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; + } else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { selectedId = user.id; gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id; gl.issueBoards.BoardsStore.updateFiltersUrl(); diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 9b413f3e61c7741ea1b8afcc2e71c27112e1158c..b362cc758ccab9a0d956db8623ff7b79b5b4b4d9 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -389,6 +389,13 @@ flex: 1; margin-top: 0; + &.add-issues-empty-state-filter { + -webkit-flex-direction: column; + flex-direction: column; + -webkit-justify-content: center; + justify-content: center; + } + > .row { width: 100%; margin: auto 0; @@ -416,6 +423,14 @@ .add-issues-search { display: -webkit-flex; display: flex; + + .form-control { + margin-left: auto; + + @media (min-width: $screen-sm-min) { + max-width: 200px; + } + } } .add-issues-list-column { @@ -486,3 +501,24 @@ line-height: 15px; border-radius: 50%; } + +.modal-filters { + display: flex; + + > .dropdown { + display: none; + margin-right: 10px; + + @media (min-width: $screen-sm-min) { + display: block; + } + } + + .dropdown-menu-toggle { + width: 100px; + + @media (min-width: $screen-md-min) { + width: 140px; + } + } +} diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index f0c76af29dc4a11637fb569ff4f96def6c3d76a8..05fe504d1c9f7da53b41484522d4f424ce86d481 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -29,5 +29,8 @@ = render "projects/boards/components/sidebar" %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project), + "milestone-path" => milestones_filter_dropdown_path, + "label-path" => labels_filter_path, ":issue-link-base" => "issueLinkBase", - ":root-path" => "rootPath" } + ":root-path" => "rootPath", + ":project-id" => @project.try(:id) } diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1cf0d11d4480a73a9cacf56cfccc6d93fccefb18 --- /dev/null +++ b/spec/features/boards/modal_filter_spec.rb @@ -0,0 +1,259 @@ +require 'rails_helper' + +describe 'Issue Boards add issue modal filtering', :feature, :js do + include WaitForAjax + include WaitForVueResource + + let(:project) { create(:empty_project, :public) } + let(:board) { create(:board, project: project) } + let(:planning) { create(:label, project: project, name: 'Planning') } + let!(:list1) { create(:list, board: board, label: planning, position: 0) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:issue1) { create(:issue, project: project) } + + before do + project.team << [user, :master] + + login_as(user) + end + + it 'shows empty state when no results found' do + visit_board + + page.within('.add-issues-modal') do + find('.form-control').native.send_keys('testing empty state') + + wait_for_vue_resource + + expect(page).to have_content('There are no issues to show.') + end + end + + it 'restores filters when closing' do + visit_board + + page.within('.add-issues-modal') do + click_button 'Milestone' + + wait_for_ajax + + click_link 'Upcoming' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 0) + + click_button 'Cancel' + end + + click_button('Add issues') + + page.within('.add-issues-modal') do + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + + context 'author' do + let!(:issue) { create(:issue, project: project, author: user2) } + + before do + project.team << [user2, :developer] + + visit_board + end + + it 'filters by any author' do + page.within('.add-issues-modal') do + click_button 'Author' + + wait_for_ajax + + click_link 'Any Author' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'filters by selected user' do + page.within('.add-issues-modal') do + click_button 'Author' + + wait_for_ajax + + click_link user2.name + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + end + + context 'assignee' do + let!(:issue) { create(:issue, project: project, assignee: user2) } + + before do + project.team << [user2, :developer] + + visit_board + end + + it 'filters by any assignee' do + page.within('.add-issues-modal') do + click_button 'Assignee' + + wait_for_ajax + + click_link 'Any Assignee' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'filters by unassigned' do + page.within('.add-issues-modal') do + click_button 'Assignee' + + wait_for_ajax + + click_link 'Unassigned' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + + it 'filters by selected user' do + page.within('.add-issues-modal') do + click_button 'Assignee' + + wait_for_ajax + + page.within '.dropdown-menu-user' do + click_link user2.name + end + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + end + + context 'milestone' do + let(:milestone) { create(:milestone, project: project) } + let!(:issue) { create(:issue, project: project, milestone: milestone) } + + before do + visit_board + end + + it 'filters by any milestone' do + page.within('.add-issues-modal') do + click_button 'Milestone' + + wait_for_ajax + + click_link 'Any Milestone' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'filters by upcoming milestone' do + page.within('.add-issues-modal') do + click_button 'Milestone' + + wait_for_ajax + + click_link 'Upcoming' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 0) + end + end + + it 'filters by selected milestone' do + page.within('.add-issues-modal') do + click_button 'Milestone' + + wait_for_ajax + + click_link milestone.name + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + end + + context 'label' do + let(:label) { create(:label, project: project) } + let!(:issue) { create(:labeled_issue, project: project, labels: [label]) } + + before do + visit_board + end + + it 'filters by any label' do + page.within('.add-issues-modal') do + click_button 'Label' + + wait_for_ajax + + click_link 'Any Label' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'filters by no label' do + page.within('.add-issues-modal') do + click_button 'Label' + + wait_for_ajax + + click_link 'No Label' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + + it 'filters by label' do + page.within('.add-issues-modal') do + click_button 'Label' + + wait_for_ajax + + click_link label.title + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + end + + def visit_board + visit namespace_project_board_path(project.namespace, project, board) + wait_for_vue_resource + + click_button('Add issues') + end +end