Merge branch 'issue-boards-modal-filter-bar' into 'master'
Added filter bar into add issues modal See merge request !9856
This commit is contained in:
commit
4e7d4b4dc1
21 changed files with 210 additions and 459 deletions
|
@ -1,49 +1,24 @@
|
|||
/* global Vue */
|
||||
const userFilter = require('./filters/user');
|
||||
const milestoneFilter = require('./filters/milestone');
|
||||
const labelFilter = require('./filters/label');
|
||||
import FilteredSearchBoards from '../../filtered_search_boards';
|
||||
import FilteredSearchContainer from '../../../filtered_search/container';
|
||||
|
||||
module.exports = Vue.extend({
|
||||
export default {
|
||||
name: 'modal-filters',
|
||||
props: {
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
milestonePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelPath: {
|
||||
type: String,
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
destroyed() {
|
||||
gl.issueBoards.ModalStore.setDefaultFilter();
|
||||
mounted() {
|
||||
FilteredSearchContainer.container = this.$el;
|
||||
|
||||
this.filteredSearch = new FilteredSearchBoards(this.store);
|
||||
this.filteredSearch.removeTokens();
|
||||
},
|
||||
components: {
|
||||
userFilter,
|
||||
milestoneFilter,
|
||||
labelFilter,
|
||||
beforeDestroy() {
|
||||
this.filteredSearch.cleanup();
|
||||
FilteredSearchContainer.container = document;
|
||||
this.store.path = '';
|
||||
},
|
||||
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>
|
||||
`,
|
||||
});
|
||||
template: '#js-board-modal-filter',
|
||||
};
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
/* 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>
|
||||
`,
|
||||
});
|
|
@ -1,56 +0,0 @@
|
|||
/* 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>
|
||||
`,
|
||||
});
|
|
@ -1,96 +0,0 @@
|
|||
/* 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>
|
||||
`,
|
||||
});
|
|
@ -1,6 +1,7 @@
|
|||
/* global Vue */
|
||||
import Vue from 'vue';
|
||||
import modalFilters from './filters';
|
||||
|
||||
require('./tabs');
|
||||
const modalFilters = require('./filters');
|
||||
|
||||
(() => {
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
@ -66,16 +67,7 @@ const modalFilters = require('./filters');
|
|||
<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"
|
||||
type="search"
|
||||
v-model="searchTerm" />
|
||||
<modal-filters :store="filter" />
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-inverted prepend-left-10"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* global Vue */
|
||||
/* global ListIssue */
|
||||
import queryData from '../../utils/query_data';
|
||||
|
||||
require('./header');
|
||||
require('./list');
|
||||
|
@ -47,9 +48,6 @@ require('./empty_state');
|
|||
page() {
|
||||
this.loadIssues();
|
||||
},
|
||||
searchTerm() {
|
||||
this.searchOperation();
|
||||
},
|
||||
showAddIssuesModal() {
|
||||
if (this.showAddIssuesModal && !this.issues.length) {
|
||||
this.loading = true;
|
||||
|
@ -72,19 +70,13 @@ require('./empty_state');
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
searchOperation: _.debounce(function searchOperationDebounce() {
|
||||
this.loadIssues(true);
|
||||
}, 500),
|
||||
loadIssues(clearIssues = false) {
|
||||
if (!this.showAddIssuesModal) return false;
|
||||
|
||||
const queryData = Object.assign({}, this.filter, {
|
||||
search: this.searchTerm,
|
||||
return gl.boardService.getBacklog(queryData(this.filter.path, {
|
||||
page: this.page,
|
||||
per: this.perPage,
|
||||
});
|
||||
|
||||
return gl.boardService.getBacklog(queryData).then((res) => {
|
||||
})).then((res) => {
|
||||
const data = res.json();
|
||||
|
||||
if (clearIssues) {
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
import FilteredSearchContainer from '../filtered_search/container';
|
||||
|
||||
export default class FilteredSearchBoards extends gl.FilteredSearchManager {
|
||||
constructor(store, updateUrl = false) {
|
||||
super('boards');
|
||||
|
@ -18,13 +21,17 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
|
|||
}
|
||||
}
|
||||
|
||||
updateTokens() {
|
||||
const tokens = document.querySelectorAll('.js-visual-token');
|
||||
removeTokens() {
|
||||
const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token');
|
||||
|
||||
// Remove all the tokens as they will be replaced by the search manager
|
||||
[].forEach.call(tokens, (el) => {
|
||||
el.parentNode.removeChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
updateTokens() {
|
||||
this.removeTokens();
|
||||
|
||||
this.loadSearchParamsFromURL();
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* 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 ListLabel */
|
||||
import queryData from '../utils/query_data';
|
||||
|
||||
class List {
|
||||
constructor (obj) {
|
||||
|
@ -64,25 +65,7 @@ class List {
|
|||
}
|
||||
|
||||
getIssues (emptyIssues = true) {
|
||||
const data = gl.issueBoards.BoardsStore.filter.path.split('&').reduce((data, filterParam) => {
|
||||
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 });
|
||||
const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page });
|
||||
|
||||
if (this.label && data.label_name) {
|
||||
data.label_name = data.label_name.filter(label => label !== this.label.title);
|
||||
|
|
|
@ -17,17 +17,9 @@
|
|||
loadingNewPage: false,
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
};
|
||||
|
||||
this.setDefaultFilter();
|
||||
}
|
||||
|
||||
setDefaultFilter() {
|
||||
this.store.filter = {
|
||||
author_id: '',
|
||||
assignee_id: '',
|
||||
milestone_title: '',
|
||||
label_name: [],
|
||||
filter: {
|
||||
path: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
21
app/assets/javascripts/boards/utils/query_data.js
Normal file
21
app/assets/javascripts/boards/utils/query_data.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
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);
|
14
app/assets/javascripts/filtered_search/container.js
Normal file
14
app/assets/javascripts/filtered_search/container.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/* 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');
|
|||
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
|
||||
}
|
||||
|
||||
gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''));
|
||||
gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
|
||||
}
|
||||
this.dismissDropdown();
|
||||
this.dispatchInputEvent();
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import FilteredSearchContainer from './container';
|
||||
|
||||
(() => {
|
||||
class DropdownUtils {
|
||||
static getEscapedText(text) {
|
||||
|
@ -85,7 +87,8 @@
|
|||
|
||||
// Determines the full search query (visual tokens + input)
|
||||
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 = [];
|
||||
|
||||
if (untilInput) {
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
/* global DropLab */
|
||||
import FilteredSearchContainer from './container';
|
||||
|
||||
(() => {
|
||||
class FilteredSearchDropdownManager {
|
||||
constructor(baseEndpoint = '', page) {
|
||||
this.container = FilteredSearchContainer.container;
|
||||
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
|
||||
this.tokenizer = gl.FilteredSearchTokenizer;
|
||||
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
|
||||
this.filteredSearchInput = document.querySelector('.filtered-search');
|
||||
this.filteredSearchInput = this.container.querySelector('.filtered-search');
|
||||
this.page = page;
|
||||
|
||||
this.setupMapping();
|
||||
|
@ -31,35 +33,35 @@
|
|||
author: {
|
||||
reference: null,
|
||||
gl: 'DropdownUser',
|
||||
element: document.querySelector('#js-dropdown-author'),
|
||||
element: this.container.querySelector('#js-dropdown-author'),
|
||||
},
|
||||
assignee: {
|
||||
reference: null,
|
||||
gl: 'DropdownUser',
|
||||
element: document.querySelector('#js-dropdown-assignee'),
|
||||
element: this.container.querySelector('#js-dropdown-assignee'),
|
||||
},
|
||||
milestone: {
|
||||
reference: null,
|
||||
gl: 'DropdownNonUser',
|
||||
extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
|
||||
element: document.querySelector('#js-dropdown-milestone'),
|
||||
element: this.container.querySelector('#js-dropdown-milestone'),
|
||||
},
|
||||
label: {
|
||||
reference: null,
|
||||
gl: 'DropdownNonUser',
|
||||
extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
|
||||
element: document.querySelector('#js-dropdown-label'),
|
||||
element: this.container.querySelector('#js-dropdown-label'),
|
||||
},
|
||||
hint: {
|
||||
reference: null,
|
||||
gl: 'DropdownHint',
|
||||
element: document.querySelector('#js-dropdown-hint'),
|
||||
element: this.container.querySelector('#js-dropdown-hint'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static addWordToInput(tokenName, tokenValue = '', clicked = false) {
|
||||
const input = document.querySelector('.filtered-search');
|
||||
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
||||
|
||||
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
|
||||
input.value = '';
|
||||
|
@ -75,13 +77,13 @@
|
|||
|
||||
updateDropdownOffset(key) {
|
||||
// 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 currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
|
||||
|
||||
// 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) {
|
||||
offset = offsetMaxWidth;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import FilteredSearchContainer from './container';
|
||||
|
||||
(() => {
|
||||
class FilteredSearchManager {
|
||||
constructor(page) {
|
||||
this.filteredSearchInput = document.querySelector('.filtered-search');
|
||||
this.clearSearchButton = document.querySelector('.clear-search');
|
||||
this.tokensContainer = document.querySelector('.tokens-container');
|
||||
this.container = FilteredSearchContainer.container;
|
||||
this.filteredSearchInput = this.container.querySelector('.filtered-search');
|
||||
this.clearSearchButton = this.container.querySelector('.clear-search');
|
||||
this.tokensContainer = this.container.querySelector('.tokens-container');
|
||||
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
|
||||
|
||||
if (this.filteredSearchInput) {
|
||||
|
@ -132,7 +135,7 @@
|
|||
}
|
||||
|
||||
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 isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
|
||||
const isElementTokensContainer = e.target.classList.contains('tokens-container');
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import FilteredSearchContainer from './container';
|
||||
|
||||
class FilteredSearchVisualTokens {
|
||||
static getLastVisualTokenBeforeInput() {
|
||||
const inputLi = document.querySelector('.input-token');
|
||||
const inputLi = FilteredSearchContainer.container.querySelector('.input-token');
|
||||
const lastVisualToken = inputLi && inputLi.previousElementSibling;
|
||||
|
||||
return {
|
||||
|
@ -10,7 +12,7 @@ class FilteredSearchVisualTokens {
|
|||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
|
@ -24,7 +26,7 @@ class FilteredSearchVisualTokens {
|
|||
}
|
||||
|
||||
static removeSelectedToken() {
|
||||
const selected = document.querySelector('.js-visual-token .selected');
|
||||
const selected = FilteredSearchContainer.container.querySelector('.js-visual-token .selected');
|
||||
|
||||
if (selected) {
|
||||
const li = selected.closest('.js-visual-token');
|
||||
|
@ -54,8 +56,8 @@ class FilteredSearchVisualTokens {
|
|||
}
|
||||
li.querySelector('.name').innerText = name;
|
||||
|
||||
const tokensContainer = document.querySelector('.tokens-container');
|
||||
const input = document.querySelector('.filtered-search');
|
||||
const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
|
||||
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
||||
tokensContainer.insertBefore(li, input.parentElement);
|
||||
}
|
||||
|
||||
|
@ -77,14 +79,14 @@ class FilteredSearchVisualTokens {
|
|||
const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
|
||||
|
||||
if (isLastVisualTokenValid) {
|
||||
addVisualTokenElement(tokenName, tokenValue);
|
||||
addVisualTokenElement(tokenName, tokenValue, false);
|
||||
} else {
|
||||
const previousTokenName = lastVisualToken.querySelector('.name').innerText;
|
||||
const tokensContainer = document.querySelector('.tokens-container');
|
||||
const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
|
||||
tokensContainer.removeChild(lastVisualToken);
|
||||
|
||||
const value = tokenValue || tokenName;
|
||||
addVisualTokenElement(previousTokenName, value);
|
||||
addVisualTokenElement(previousTokenName, value, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,7 +131,7 @@ class FilteredSearchVisualTokens {
|
|||
}
|
||||
|
||||
static tokenizeInput() {
|
||||
const input = document.querySelector('.filtered-search');
|
||||
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
||||
const { isLastVisualTokenValid } =
|
||||
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
||||
|
||||
|
@ -145,7 +147,7 @@ class FilteredSearchVisualTokens {
|
|||
}
|
||||
|
||||
static editToken(token) {
|
||||
const input = document.querySelector('.filtered-search');
|
||||
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
||||
|
||||
FilteredSearchVisualTokens.tokenizeInput();
|
||||
|
||||
|
@ -174,9 +176,9 @@ class FilteredSearchVisualTokens {
|
|||
}
|
||||
|
||||
static moveInputToTheRight() {
|
||||
const input = document.querySelector('.filtered-search');
|
||||
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
||||
const inputLi = input.parentElement;
|
||||
const tokenContainer = document.querySelector('.tokens-container');
|
||||
const tokenContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
|
||||
|
||||
FilteredSearchVisualTokens.tokenizeInput();
|
||||
|
||||
|
|
|
@ -420,12 +420,9 @@
|
|||
display: -webkit-flex;
|
||||
display: flex;
|
||||
|
||||
.form-control {
|
||||
margin-left: auto;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
max-width: 200px;
|
||||
}
|
||||
.issues-filters {
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
%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-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
|
||||
|
||||
= render "projects/issues/head"
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
- type = local_assigns.fetch(:type)
|
||||
- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
|
||||
|
||||
.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
|
||||
- if params[:search].present?
|
||||
= hidden_field_tag :search, params[:search]
|
||||
|
@ -14,7 +15,7 @@
|
|||
.scroll-container
|
||||
%ul.tokens-container.list-unstyled
|
||||
%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')
|
||||
%button.clear-search.hidden{ type: 'button' }
|
||||
= icon('times')
|
||||
|
@ -100,7 +101,7 @@
|
|||
= render partial: "shared/issuable/label_page_create"
|
||||
= dropdown_loading
|
||||
#js-add-issues-btn.prepend-left-10
|
||||
- else
|
||||
- elsif type != :boards_modal
|
||||
= render 'shared/sort_dropdown'
|
||||
|
||||
- if @bulk_edit
|
||||
|
@ -133,7 +134,8 @@
|
|||
.filter-item.inline.update-issues-btn
|
||||
= 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 LabelsSelect();
|
||||
new MilestoneSelect();
|
||||
|
|
|
@ -107,6 +107,7 @@ describe 'Issue Boards add issue modal', :feature, :js do
|
|||
it 'returns issues' do
|
||||
page.within('.add-issues-modal') do
|
||||
find('.form-control').native.send_keys(issue.title)
|
||||
find('.form-control').native.send_keys(:enter)
|
||||
|
||||
expect(page).to have_selector('.card', count: 1)
|
||||
end
|
||||
|
@ -115,6 +116,7 @@ describe 'Issue Boards add issue modal', :feature, :js do
|
|||
it 'returns no issues' do
|
||||
page.within('.add-issues-modal') do
|
||||
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_content("You haven't added any issues to your project yet")
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe 'Issue Boards add issue modal filtering', :feature, :js do
|
||||
include WaitForAjax
|
||||
include WaitForVueResource
|
||||
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
|
@ -23,6 +22,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
|
|||
|
||||
page.within('.add-issues-modal') do
|
||||
find('.form-control').native.send_keys('testing empty state')
|
||||
find('.form-control').native.send_keys(:enter)
|
||||
|
||||
wait_for_vue_resource
|
||||
|
||||
|
@ -33,13 +33,11 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
|
|||
it 'restores filters when closing' do
|
||||
visit_board
|
||||
|
||||
set_filter('milestone')
|
||||
click_filter_link('Upcoming')
|
||||
submit_filter
|
||||
|
||||
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)
|
||||
|
@ -56,6 +54,26 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
|
|||
end
|
||||
end
|
||||
|
||||
it 'resotres filters after clicking clear button' do
|
||||
visit_board
|
||||
|
||||
set_filter('milestone')
|
||||
click_filter_link('Upcoming')
|
||||
submit_filter
|
||||
|
||||
page.within('.add-issues-modal') do
|
||||
wait_for_vue_resource
|
||||
|
||||
expect(page).to have_selector('.card', count: 0)
|
||||
|
||||
find('.clear-search').click
|
||||
|
||||
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) }
|
||||
|
||||
|
@ -65,30 +83,15 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
|
|||
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
|
||||
set_filter('author')
|
||||
click_filter_link(user2.name)
|
||||
submit_filter
|
||||
|
||||
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('.js-visual-token', text: user2.username)
|
||||
expect(page).to have_selector('.card', count: 1)
|
||||
end
|
||||
end
|
||||
|
@ -103,46 +106,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
|
|||
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
|
||||
set_filter('assignee')
|
||||
click_filter_link('No Assignee')
|
||||
submit_filter
|
||||
|
||||
page.within('.add-issues-modal') do
|
||||
click_button 'Assignee'
|
||||
|
||||
wait_for_ajax
|
||||
|
||||
click_link 'Unassigned'
|
||||
|
||||
wait_for_vue_resource
|
||||
|
||||
expect(page).to have_selector('.js-visual-token', text: 'none')
|
||||
expect(page).to have_selector('.card', count: 1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'filters by selected user' do
|
||||
set_filter('assignee')
|
||||
click_filter_link(user2.name)
|
||||
submit_filter
|
||||
|
||||
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('.js-visual-token', text: user2.username)
|
||||
expect(page).to have_selector('.card', count: 1)
|
||||
end
|
||||
end
|
||||
|
@ -156,44 +141,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js 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
|
||||
set_filter('milestone')
|
||||
click_filter_link('Upcoming')
|
||||
submit_filter
|
||||
|
||||
page.within('.add-issues-modal') do
|
||||
click_button 'Milestone'
|
||||
|
||||
wait_for_ajax
|
||||
|
||||
click_link 'Upcoming'
|
||||
|
||||
wait_for_vue_resource
|
||||
|
||||
expect(page).to have_selector('.js-visual-token', text: 'upcoming')
|
||||
expect(page).to have_selector('.card', count: 0)
|
||||
end
|
||||
end
|
||||
|
||||
it 'filters by selected milestone' do
|
||||
set_filter('milestone')
|
||||
click_filter_link(milestone.name)
|
||||
submit_filter
|
||||
|
||||
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('.js-visual-token', text: milestone.name)
|
||||
expect(page).to have_selector('.card', count: 1)
|
||||
end
|
||||
end
|
||||
|
@ -207,44 +176,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js 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
|
||||
set_filter('label')
|
||||
click_filter_link('No Label')
|
||||
submit_filter
|
||||
|
||||
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('.js-visual-token', text: 'none')
|
||||
expect(page).to have_selector('.card', count: 1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'filters by label' do
|
||||
set_filter('label')
|
||||
click_filter_link(label.title)
|
||||
submit_filter
|
||||
|
||||
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('.js-visual-token', text: label.title)
|
||||
expect(page).to have_selector('.card', count: 1)
|
||||
end
|
||||
end
|
||||
|
@ -256,4 +209,20 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
|
|||
|
||||
click_button('Add issues')
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue