Commit 4e7d4b4d authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'issue-boards-modal-filter-bar' into 'master'

Added filter bar into add issues modal

See merge request !9856
parents 4413d97b addbf88c
/* global Vue */ import FilteredSearchBoards from '../../filtered_search_boards';
const userFilter = require('./filters/user'); import FilteredSearchContainer from '../../../filtered_search/container';
const milestoneFilter = require('./filters/milestone');
const labelFilter = require('./filters/label');
module.exports = Vue.extend({ export default {
name: 'modal-filters', name: 'modal-filters',
props: { props: {
projectId: { store: {
type: Number, type: Object,
required: true, required: true,
}, },
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
}, },
mounted() {
FilteredSearchContainer.container = this.$el;
this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.removeTokens();
}, },
destroyed() { beforeDestroy() {
gl.issueBoards.ModalStore.setDefaultFilter(); this.filteredSearch.cleanup();
}, FilteredSearchContainer.container = document;
components: { this.store.path = '';
userFilter,
milestoneFilter,
labelFilter,
}, },
template: ` template: '#js-board-modal-filter',
<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>
`,
});
/* 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>
`,
});
/* 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-show-started="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>
`,
});
/* 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>
`,
});
/* global Vue */ import Vue from 'vue';
import modalFilters from './filters';
require('./tabs'); require('./tabs');
const modalFilters = require('./filters');
(() => { (() => {
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;
...@@ -66,16 +67,7 @@ const modalFilters = require('./filters'); ...@@ -66,16 +67,7 @@ const modalFilters = require('./filters');
<div <div
class="add-issues-search append-bottom-10" class="add-issues-search append-bottom-10"
v-if="showSearch"> v-if="showSearch">
<modal-filters <modal-filters :store="filter" />
:project-id="projectId"
:milestone-path="milestonePath"
:label-path="labelPath">
</modal-filters>
<input
placeholder="Search issues..."
class="form-control"
type="search"
v-model="searchTerm" />
<button <button
type="button" type="button"
class="btn btn-success btn-inverted prepend-left-10" class="btn btn-success btn-inverted prepend-left-10"
......
/* global Vue */ /* global Vue */
/* global ListIssue */ /* global ListIssue */
import queryData from '../../utils/query_data';
require('./header'); require('./header');
require('./list'); require('./list');
...@@ -47,9 +48,6 @@ require('./empty_state'); ...@@ -47,9 +48,6 @@ require('./empty_state');
page() { page() {
this.loadIssues(); this.loadIssues();
}, },
searchTerm() {
this.searchOperation();
},
showAddIssuesModal() { showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) { if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true; this.loading = true;
...@@ -72,19 +70,13 @@ require('./empty_state'); ...@@ -72,19 +70,13 @@ require('./empty_state');
}, },
}, },
methods: { methods: {
searchOperation: _.debounce(function searchOperationDebounce() {
this.loadIssues(true);
}, 500),
loadIssues(clearIssues = false) { loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false; if (!this.showAddIssuesModal) return false;
const queryData = Object.assign({}, this.filter, { return gl.boardService.getBacklog(queryData(this.filter.path, {
search: this.searchTerm,
page: this.page, page: this.page,
per: this.perPage, per: this.perPage,
}); })).then((res) => {
return gl.boardService.getBacklog(queryData).then((res) => {
const data = res.json(); const data = res.json();
if (clearIssues) { if (clearIssues) {
......
/* eslint-disable class-methods-use-this */
import FilteredSearchContainer from '../filtered_search/container';
export default class FilteredSearchBoards extends gl.FilteredSearchManager { export default class FilteredSearchBoards extends gl.FilteredSearchManager {
constructor(store, updateUrl = false) { constructor(store, updateUrl = false) {
super('boards'); super('boards');
...@@ -18,13 +21,17 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -18,13 +21,17 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
} }
} }
updateTokens() { removeTokens() {
const tokens = document.querySelectorAll('.js-visual-token'); const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token');
// Remove all the tokens as they will be replaced by the search manager // Remove all the tokens as they will be replaced by the search manager
[].forEach.call(tokens, (el) => { [].forEach.call(tokens, (el) => {
el.parentNode.removeChild(el); el.parentNode.removeChild(el);
}); });
}
updateTokens() {
this.removeTokens();
this.loadSearchParamsFromURL(); this.loadSearchParamsFromURL();
......
/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */ /* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
/* global ListIssue */ /* global ListIssue */
/* global ListLabel */ /* global ListLabel */
import queryData from '../utils/query_data';
class List { class List {
constructor (obj) { constructor (obj) {
...@@ -64,25 +65,7 @@ class List { ...@@ -64,25 +65,7 @@ class List {
} }
getIssues (emptyIssues = true) { getIssues (emptyIssues = true) {
const data = gl.issueBoards.BoardsStore.filter.path.split('&').reduce((data, filterParam) => { const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page });
if (filterParam === '') return data;
const paramSplit = filterParam.split('=');
const paramKeyNormalized = paramSplit[0].replace('[]', '');
const isArray = paramSplit[0].indexOf('[]');
const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' ');
if (isArray !== -1) {
if (!data[paramKeyNormalized]) {
data[paramKeyNormalized] = [];
}
data[paramKeyNormalized].push(value);
} else {
data[paramKeyNormalized] = value;
}
return data;
}, { page: this.page });
if (this.label && data.label_name) { if (this.label && data.label_name) {
data.label_name = data.label_name.filter(label => label !== this.label.title); data.label_name = data.label_name.filter(label => label !== this.label.title);
......
...@@ -17,17 +17,9 @@ ...@@ -17,17 +17,9 @@
loadingNewPage: false, loadingNewPage: false,
page: 1, page: 1,
perPage: 50, perPage: 50,
}; filter: {
path: '',
this.setDefaultFilter(); },
}
setDefaultFilter() {
this.store.filter = {
author_id: '',
assignee_id: '',
milestone_title: '',
label_name: [],
}; };
} }
......
export default (path, extraData) => path.split('&').reduce((dataParam, filterParam) => {
if (filterParam === '') return dataParam;
const data = dataParam;
const paramSplit = filterParam.split('=');
const paramKeyNormalized = paramSplit[0].replace('[]', '');
const isArray = paramSplit[0].indexOf('[]');
const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' ');
if (isArray !== -1) {
if (!data[paramKeyNormalized]) {
data[paramKeyNormalized] = [];
}
data[paramKeyNormalized].push(value);
} else {
data[paramKeyNormalized] = value;
}
return data;
}, extraData);
/* eslint-disable class-methods-use-this */
let container = document;
class FilteredSearchContainerClass {
set container(containerParam) {
container = containerParam;
}
get container() {
return container;
}
}
export default new FilteredSearchContainerClass();
...@@ -45,7 +45,7 @@ require('./filtered_search_dropdown'); ...@@ -45,7 +45,7 @@ require('./filtered_search_dropdown');
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' ')); gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
} }
gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', '')); gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
} }
this.dismissDropdown(); this.dismissDropdown();
this.dispatchInputEvent(); this.dispatchInputEvent();
......
import FilteredSearchContainer from './container';
(() => { (() => {
class DropdownUtils { class DropdownUtils {
static getEscapedText(text) { static getEscapedText(text) {
...@@ -85,7 +87,8 @@ ...@@ -85,7 +87,8 @@
// Determines the full search query (visual tokens + input) // Determines the full search query (visual tokens + input)
static getSearchQuery(untilInput = false) { static getSearchQuery(untilInput = false) {
const tokens = [].slice.call(document.querySelectorAll('.tokens-container li')); const container = FilteredSearchContainer.container;
const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
const values = []; const values = [];
if (untilInput) { if (untilInput) {
......
/* global DropLab */ /* global DropLab */
import FilteredSearchContainer from './container';
(() => { (() => {
class FilteredSearchDropdownManager { class FilteredSearchDropdownManager {
constructor(baseEndpoint = '', page) { constructor(baseEndpoint = '', page) {
this.container = FilteredSearchContainer.container;
this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = gl.FilteredSearchTokenizer; this.tokenizer = gl.FilteredSearchTokenizer;
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
this.filteredSearchInput = document.querySelector('.filtered-search'); this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page; this.page = page;
this.setupMapping(); this.setupMapping();
...@@ -31,35 +33,35 @@ ...@@ -31,35 +33,35 @@
author: { author: {
reference: null, reference: null,
gl: 'DropdownUser', gl: 'DropdownUser',
element: document.querySelector('#js-dropdown-author'), element: this.container.querySelector('#js-dropdown-author'),
}, },
assignee: { assignee: {
reference: null, reference: null,
gl: 'DropdownUser', gl: 'DropdownUser',
element: document.querySelector('#js-dropdown-assignee'), element: this.container.querySelector('#js-dropdown-assignee'),
}, },
milestone: { milestone: {
reference: null, reference: null,
gl: 'DropdownNonUser', gl: 'DropdownNonUser',
extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'], extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
element: document.querySelector('#js-dropdown-milestone'), element: this.container.querySelector('#js-dropdown-milestone'),
}, },
label: { label: {
reference: null, reference: null,
gl: 'DropdownNonUser', gl: 'DropdownNonUser',
extraArguments: [`${this.baseEndpoint}/labels.json`, '~'], extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
element: document.querySelector('#js-dropdown-label'), element: this.container.querySelector('#js-dropdown-label'),
}, },
hint: { hint: {
reference: null, reference: null,
gl: 'DropdownHint', gl: 'DropdownHint',
element: document.querySelector('#js-dropdown-hint'), element: this.container.querySelector('#js-dropdown-hint'),
}, },
}; };
} }
static addWordToInput(tokenName, tokenValue = '', clicked = false) { static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = document.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue); gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
input.value = ''; input.value = '';
...@@ -75,13 +77,13 @@ ...@@ -75,13 +77,13 @@
updateDropdownOffset(key) { updateDropdownOffset(key) {
// Always align dropdown with the input field // Always align dropdown with the input field
let offset = this.filteredSearchInput.getBoundingClientRect().left - document.querySelector('.scroll-container').getBoundingClientRect().left; let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
const maxInputWidth = 240; const maxInputWidth = 240;
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth; const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
// Make sure offset never exceeds the input container // Make sure offset never exceeds the input container
const offsetMaxWidth = document.querySelector('.scroll-container').clientWidth - currentDropdownWidth; const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) { if (offsetMaxWidth < offset) {
offset = offsetMaxWidth; offset = offsetMaxWidth;
} }
......
import FilteredSearchContainer from './container';
(() => { (() => {
class FilteredSearchManager { class FilteredSearchManager {
constructor(page) { constructor(page) {
this.filteredSearchInput = document.querySelector('.filtered-search'); this.container = FilteredSearchContainer.container;
this.clearSearchButton = document.querySelector('.clear-search'); this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.tokensContainer = document.querySelector('.tokens-container'); this.clearSearchButton = this.container.querySelector('.clear-search');
this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
if (this.filteredSearchInput) { if (this.filteredSearchInput) {
...@@ -132,7 +135,7 @@ ...@@ -132,7 +135,7 @@
} }
unselectEditTokens(e) { unselectEditTokens(e) {
const inputContainer = document.querySelector('.filtered-search-input-container'); const inputContainer = this.container.querySelector('.filtered-search-input-container');
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null; const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
const isElementTokensContainer = e.target.classList.contains('tokens-container'); const isElementTokensContainer = e.target.classList.contains('tokens-container');
......
import FilteredSearchContainer from './container';
class FilteredSearchVisualTokens { class FilteredSearchVisualTokens {
static getLastVisualTokenBeforeInput() { static getLastVisualTokenBeforeInput() {
const inputLi = document.querySelector('.input-token'); const inputLi = FilteredSearchContainer.container.querySelector('.input-token');
const lastVisualToken = inputLi && inputLi.previousElementSibling; const lastVisualToken = inputLi && inputLi.previousElementSibling;
return { return {
...@@ -10,7 +12,7 @@ class FilteredSearchVisualTokens { ...@@ -10,7 +12,7 @@ class FilteredSearchVisualTokens {
} }
static unselectTokens() { static unselectTokens() {
const otherTokens = document.querySelectorAll('.js-visual-token .selectable.selected'); const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected');
[].forEach.call(otherTokens, t => t.classList.remove('selected')); [].forEach.call(otherTokens, t => t.classList.remove('selected'));
} }
...@@ -24,7 +26,7 @@ class FilteredSearchVisualTokens { ...@@ -24,7 +26,7 @@ class FilteredSearchVisualTokens {
} }
static removeSelectedToken() { static removeSelectedToken() {
const selected = document.querySelector('.js-visual-token .selected'); const selected = FilteredSearchContainer.container.querySelector('.js-visual-token .selected');
if (selected) { if (selected) {
const li = selected.closest('.js-visual-token'); const li = selected.closest('.js-visual-token');
...@@ -54,8 +56,8 @@ class FilteredSearchVisualTokens { ...@@ -54,8 +56,8 @@ class FilteredSearchVisualTokens {
} }
li.querySelector('.name').innerText = name; li.querySelector('.name').innerText = name;
const tokensContainer = document.querySelector('.tokens-container'); const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
const input = document.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
tokensContainer.insertBefore(li, input.parentElement); tokensContainer.insertBefore(li, input.parentElement);
} }
...@@ -77,14 +79,14 @@ class FilteredSearchVisualTokens { ...@@ -77,14 +79,14 @@ class FilteredSearchVisualTokens {
const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement; const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
if (isLastVisualTokenValid) { if (isLastVisualTokenValid) {
addVisualTokenElement(tokenName, tokenValue); addVisualTokenElement(tokenName, tokenValue, false);
} else { } else {
const previousTokenName = lastVisualToken.querySelector('.name').innerText; const previousTokenName = lastVisualToken.querySelector('.name').innerText;
const tokensContainer = document.querySelector('.tokens-container'); const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
tokensContainer.removeChild(lastVisualToken); tokensContainer.removeChild(lastVisualToken);
const value = tokenValue || tokenName; const value = tokenValue || tokenName;
addVisualTokenElement(previousTokenName, value); addVisualTokenElement(previousTokenName, value, false);
} }
} }
...@@ -129,7 +131,7 @@ class FilteredSearchVisualTokens { ...@@ -129,7 +131,7 @@ class FilteredSearchVisualTokens {
} }
static tokenizeInput() { static tokenizeInput() {
const input = document.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
const { isLastVisualTokenValid } = const { isLastVisualTokenValid } =
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
...@@ -145,7 +147,7 @@ class FilteredSearchVisualTokens { ...@@ -145,7 +147,7 @@ class FilteredSearchVisualTokens {
} }
static editToken(token) { static editToken(token) {
const input = document.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
FilteredSearchVisualTokens.tokenizeInput(); FilteredSearchVisualTokens.tokenizeInput();
...@@ -174,9 +176,9 @@ class FilteredSearchVisualTokens { ...@@ -174,9 +176,9 @@ class FilteredSearchVisualTokens {
} }
static moveInputToTheRight() { static moveInputToTheRight() {
const input = document.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
const inputLi = input.parentElement; const inputLi = input.parentElement;
const tokenContainer = document.querySelector('.tokens-container'); const tokenContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
FilteredSearchVisualTokens.tokenizeInput(); FilteredSearchVisualTokens.tokenizeInput();
......
...@@ -420,12 +420,9 @@ ...@@ -420,12 +420,9 @@
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
.form-control { .issues-filters {
margin-left: auto; -webkit-flex: 1;
flex: 1;
@media (min-width: $screen-sm-min) {
max-width: 200px;
}
} }
} }
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list" %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
= render "projects/issues/head" = render "projects/issues/head"
......
- type = local_assigns.fetch(:type) - type = local_assigns.fetch(:type)
- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
.issues-filters .issues-filters
.issues-details-filters.row-content-block.second-block.filtered-search-block .issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal }
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
- if params[:search].present? - if params[:search].present?
= hidden_field_tag :search, params[:search] = hidden_field_tag :search, params[:search]
...@@ -14,7 +15,7 @@ ...@@ -14,7 +15,7 @@
.scroll-container .scroll-container
%ul.tokens-container.list-unstyled %ul.tokens-container.list-unstyled
%li.input-token %li.input-token
%input.form-control.filtered-search{ placeholder: 'Search or filter results...', data: { id: 'filtered-search', 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } } %input.form-control.filtered-search{ placeholder: 'Search or filter results...', data: { id: "filtered-search-#{type.to_s}", 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } }
= icon('filter') = icon('filter')
%button.clear-search.hidden{ type: 'button' } %button.clear-search.hidden{ type: 'button' }
= icon('times') = icon('times')
...@@ -100,7 +101,7 @@ ...@@ -100,7 +101,7 @@
= render partial: "shared/issuable/label_page_create" = render partial: "shared/issuable/label_page_create"
= dropdown_loading = dropdown_loading
#js-add-issues-btn.prepend-left-10 #js-add-issues-btn.prepend-left-10
- else - elsif type != :boards_modal
= render 'shared/sort_dropdown' = render 'shared/sort_dropdown'
- if @bulk_edit - if @bulk_edit
...@@ -133,7 +134,8 @@ ...@@ -133,7 +134,8 @@
.filter-item.inline.update-issues-btn .filter-item.inline.update-issues-btn
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
:javascript - unless type === :boards_modal
:javascript
new UsersSelect(); new UsersSelect();
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
......
...@@ -107,6 +107,7 @@ describe 'Issue Boards add issue modal', :feature, :js do ...@@ -107,6 +107,7 @@ describe 'Issue Boards add issue modal', :feature, :js do
it 'returns issues' do it 'returns issues' do
page.within('.add-issues-modal') do page.within('.add-issues-modal') do
find('.form-control').native.send_keys(issue.title) find('.form-control').native.send_keys(issue.title)
find('.form-control').native.send_keys(:enter)
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
end end
...@@ -115,6 +116,7 @@ describe 'Issue Boards add issue modal', :feature, :js do ...@@ -115,6 +116,7 @@ describe 'Issue Boards add issue modal', :feature, :js do
it 'returns no issues' do it 'returns no issues' do
page.within('.add-issues-modal') do page.within('.add-issues-modal') do
find('.form-control').native.send_keys('testing search') find('.form-control').native.send_keys('testing search')
find('.form-control').native.send_keys(:enter)
expect(page).not_to have_selector('.card') expect(page).not_to have_selector('.card')
expect(page).not_to have_content("You haven't added any issues to your project yet") expect(page).not_to have_content("You haven't added any issues to your project yet")
......
require 'rails_helper' require 'rails_helper'
describe 'Issue Boards add issue modal filtering', :feature, :js do describe 'Issue Boards add issue modal filtering', :feature, :js do
include WaitForAjax
include WaitForVueResource include WaitForVueResource
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
...@@ -23,6 +22,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do ...@@ -23,6 +22,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
page.within('.add-issues-modal') do page.within('.add-issues-modal') do
find('.form-control').native.send_keys('testing empty state') find('.form-control').native.send_keys('testing empty state')
find('.form-control').native.send_keys(:enter)
wait_for_vue_resource wait_for_vue_resource
...@@ -33,13 +33,11 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do ...@@ -33,13 +33,11 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
it 'restores filters when closing' do it 'restores filters when closing' do
visit_board visit_board
page.within('.add-issues-modal') do set_filter('milestone')
click_button 'Milestone' click_filter_link('Upcoming')
submit_filter
wait_for_ajax
click_link 'Upcoming'
page.within('.add-issues-modal') do
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.card', count: 0) expect(page).to have_selector('.card', count: 0)
...@@ -56,39 +54,44 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do ...@@ -56,39 +54,44 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
end end
end end
context 'author' do it 'resotres filters after clicking clear button' do
let!(:issue) { create(:issue, project: project, author: user2) }
before do
project.team << [user2, :developer]
visit_board visit_board
end
it 'filters by any author' do set_filter('milestone')
click_filter_link('Upcoming')
submit_filter
page.within('.add-issues-modal') do page.within('.add-issues-modal') do
click_button 'Author' wait_for_vue_resource
wait_for_ajax expect(page).to have_selector('.card', count: 0)
click_link 'Any Author' find('.clear-search').click
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.card', count: 2) expect(page).to have_selector('.card', count: 1)
end end
end end
it 'filters by selected user' do context 'author' do
page.within('.add-issues-modal') do let!(:issue) { create(:issue, project: project, author: user2) }
click_button 'Author'
wait_for_ajax before do
project.team << [user2, :developer]
click_link user2.name visit_board
end
it 'filters by selected user' do
set_filter('author')
click_filter_link(user2.name)
submit_filter
page.within('.add-issues-modal') do
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.js-visual-token', text: user2.username)
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
end end
end end
...@@ -103,46 +106,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do ...@@ -103,46 +106,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
visit_board visit_board
end 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 it 'filters by unassigned' do
page.within('.add-issues-modal') do set_filter('assignee')
click_button 'Assignee' click_filter_link('No Assignee')
submit_filter
wait_for_ajax
click_link 'Unassigned'
page.within('.add-issues-modal') do
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.js-visual-token', text: 'none')
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
end end
end end
it 'filters by selected user' do it 'filters by selected user' do
page.within('.add-issues-modal') do set_filter('assignee')
click_button 'Assignee' click_filter_link(user2.name)
submit_filter
wait_for_ajax
page.within '.dropdown-menu-user' do
click_link user2.name
end
page.within('.add-issues-modal') do
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.js-visual-token', text: user2.username)
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
end end
end end
...@@ -156,44 +141,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do ...@@ -156,44 +141,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
visit_board visit_board
end 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 it 'filters by upcoming milestone' do
page.within('.add-issues-modal') do set_filter('milestone')
click_button 'Milestone' click_filter_link('Upcoming')
submit_filter
wait_for_ajax
click_link 'Upcoming'
page.within('.add-issues-modal') do
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.js-visual-token', text: 'upcoming')
expect(page).to have_selector('.card', count: 0) expect(page).to have_selector('.card', count: 0)
end end
end end
it 'filters by selected milestone' do it 'filters by selected milestone' do
page.within('.add-issues-modal') do set_filter('milestone')
click_button 'Milestone' click_filter_link(milestone.name)
submit_filter
wait_for_ajax
click_link milestone.name
page.within('.add-issues-modal') do
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.js-visual-token', text: milestone.name)
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
end end
end end
...@@ -207,44 +176,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do ...@@ -207,44 +176,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
visit_board visit_board
end 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 it 'filters by no label' do
page.within('.add-issues-modal') do set_filter('label')
click_button 'Label' click_filter_link('No Label')
submit_filter
wait_for_ajax
click_link 'No Label'
page.within('.add-issues-modal') do
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.js-visual-token', text: 'none')
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
end end
end end
it 'filters by label' do it 'filters by label' do
page.within('.add-issues-modal') do set_filter('label')
click_button 'Label' click_filter_link(label.title)
submit_filter
wait_for_ajax
click_link label.title
page.within('.add-issues-modal') do
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.js-visual-token', text: label.title)
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
end end
end end
...@@ -256,4 +209,20 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do ...@@ -256,4 +209,20 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
click_button('Add issues') click_button('Add issues')
end end
def set_filter(type, text = '')
find('.add-issues-modal .filtered-search').native.send_keys("#{type}:#{text}")
end
def submit_filter
find('.add-issues-modal .filtered-search').native.send_keys(:enter)
end
def click_filter_link(link_text)
page.within('.add-issues-modal .filtered-search-input-container') do
expect(page).to have_button(link_text)
click_button(link_text)
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment