Skip to content
Snippets Groups Projects
Commit 733eec88 authored by Eric Eastwood's avatar Eric Eastwood
Browse files

Backport canEdit changes for conditional remove button

Backport Fix locked milestone in boards being remove-able.
See https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1963

Fix https://gitlab.com/gitlab-org/gitlab-ee/issues/2433

Conflicts:
	app/assets/javascripts/boards/boards_bundle.js
	app/assets/javascripts/boards/filtered_search_boards.js
	app/assets/javascripts/filtered_search/filtered_search_manager.js
	spec/features/boards/boards_spec.rb
parent 8e2fefc6
No related branches found
No related tags found
No related merge requests found
Showing with 88 additions and 41 deletions
Loading
Loading
@@ -70,6 +70,7 @@ $(() => {
gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
 
this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager.setup();
 
// Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
Loading
Loading
Loading
Loading
@@ -13,6 +13,7 @@ export default {
FilteredSearchContainer.container = this.$el;
 
this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.setup();
this.filteredSearch.removeTokens();
this.filteredSearch.handleInputPlaceholder();
this.filteredSearch.toggleClearSearchButton();
Loading
Loading
Loading
Loading
@@ -2,7 +2,7 @@
import FilteredSearchContainer from '../filtered_search/container';
 
export default class FilteredSearchBoards extends gl.FilteredSearchManager {
constructor(store, updateUrl = false) {
constructor(store, updateUrl = false, cantEdit = []) {
super('boards');
 
this.store = store;
Loading
Loading
@@ -11,6 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true;
this.cantEdit = cantEdit;
}
 
updateObject(path) {
Loading
Loading
@@ -40,4 +42,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Get the placeholder back if search is empty
this.filteredSearchInput.dispatchEvent(new Event('input'));
}
canEdit(tokenName) {
return this.cantEdit.indexOf(tokenName) === -1;
}
}
Loading
Loading
@@ -124,7 +124,8 @@ import ShortcutsBlob from './shortcuts_blob';
case 'projects:merge_requests:index':
case 'projects:issues:index':
if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) {
new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
Issuable.init();
new gl.IssuableBulkActions({
Loading
Loading
Loading
Loading
@@ -2,9 +2,9 @@ import './dropdown_hint';
import './dropdown_non_user';
import './dropdown_user';
import './dropdown_utils';
import './filtered_search_token_keys';
import './filtered_search_dropdown_manager';
import './filtered_search_dropdown';
import './filtered_search_manager';
import './filtered_search_token_keys';
import './filtered_search_tokenizer';
import './filtered_search_visual_tokens';
Loading
Loading
@@ -6,6 +6,7 @@ import eventHub from './event_hub';
 
class FilteredSearchManager {
constructor(page) {
this.page = page;
this.container = FilteredSearchContainer.container;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.filteredSearchInputForm = this.filteredSearchInput.form;
Loading
Loading
@@ -17,16 +18,18 @@ class FilteredSearchManager {
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
allowedKeys: this.filteredSearchTokenKeys.getKeys(),
});
const searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
const projectPath = searchHistoryDropdownElement ?
searchHistoryDropdownElement.dataset.projectFullPath : 'project';
this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
const projectPath = this.searchHistoryDropdownElement ?
this.searchHistoryDropdownElement.dataset.projectFullPath : 'project';
let recentSearchesPagePrefix = 'issue-recent-searches';
if (page === 'merge_requests') {
if (this.page === 'merge_requests') {
recentSearchesPagePrefix = 'merge-request-recent-searches';
}
const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
}
 
setup() {
// Fetch recent searches from localStorage
this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
.catch((error) => {
Loading
Loading
@@ -47,12 +50,12 @@ class FilteredSearchManager {
 
if (this.filteredSearchInput) {
this.tokenizer = gl.FilteredSearchTokenizer;
this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', this.tokenizer, page);
this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', this.tokenizer, this.page);
 
this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore,
this.recentSearchesService,
searchHistoryDropdownElement,
this.searchHistoryDropdownElement,
);
this.recentSearchesRoot.init();
 
Loading
Loading
@@ -141,7 +144,9 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
 
if (this.filteredSearchInput.value === '' && lastVisualToken) {
const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim();
const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
}
Loading
Loading
@@ -240,8 +245,10 @@ class FilteredSearchManager {
 
editToken(e) {
const token = e.target.closest('.js-visual-token');
const sanitizedTokenName = token.querySelector('.name').textContent.trim();
const canEdit = this.canEdit && this.canEdit(sanitizedTokenName);
 
if (token) {
if (token && canEdit) {
gl.FilteredSearchVisualTokens.editToken(token);
this.tokenChange();
}
Loading
Loading
@@ -391,7 +398,12 @@ class FilteredSearchManager {
 
if (condition) {
hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
const canEdit = this.canEdit && this.canEdit(condition.tokenKey);
gl.FilteredSearchVisualTokens.addFilterVisualToken(
condition.tokenKey,
condition.value,
canEdit,
);
} else {
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded +
Loading
Loading
@@ -410,18 +422,27 @@ class FilteredSearchManager {
}
 
hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
const canEdit = this.canEdit && this.canEdit(sanitizedKey);
gl.FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
canEdit,
);
} else if (!match && keyParam === 'assignee_id') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
const tokenName = 'assignee';
const canEdit = this.canEdit && this.canEdit(tokenName);
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
}
} else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
const tokenName = 'author';
const canEdit = this.canEdit && this.canEdit(tokenName);
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
}
} else if (!match && keyParam === 'search') {
hasFilteredSearch = true;
Loading
Loading
@@ -516,6 +537,11 @@ class FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
this.search();
}
// eslint-disable-next-line class-methods-use-this
canEdit() {
return true;
}
}
 
window.gl = window.gl || {};
Loading
Loading
Loading
Loading
@@ -36,15 +36,22 @@ class FilteredSearchVisualTokens {
}
}
 
static createVisualTokenElementHTML() {
static createVisualTokenElementHTML(canEdit = true) {
let removeTokenMarkup = '';
if (canEdit) {
removeTokenMarkup = `
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
`;
}
return `
<div class="selectable" role="button">
<div class="name"></div>
<div class="value-container">
<div class="value"></div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
${removeTokenMarkup}
</div>
</div>
`;
Loading
Loading
@@ -84,13 +91,13 @@ class FilteredSearchVisualTokens {
}
}
 
static addVisualTokenElement(name, value, isSearchTerm) {
static addVisualTokenElement(name, value, isSearchTerm, canEdit) {
const li = document.createElement('li');
li.classList.add('js-visual-token');
li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
 
if (value) {
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(canEdit);
FilteredSearchVisualTokens.renderVisualTokenValue(li, name, value);
} else {
li.innerHTML = '<div class="name"></div>';
Loading
Loading
@@ -114,20 +121,20 @@ class FilteredSearchVisualTokens {
}
}
 
static addFilterVisualToken(tokenName, tokenValue) {
static addFilterVisualToken(tokenName, tokenValue, canEdit) {
const { lastVisualToken, isLastVisualTokenValid }
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
 
if (isLastVisualTokenValid) {
addVisualTokenElement(tokenName, tokenValue, false);
addVisualTokenElement(tokenName, tokenValue, false, canEdit);
} else {
const previousTokenName = lastVisualToken.querySelector('.name').innerText;
const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
tokensContainer.removeChild(lastVisualToken);
 
const value = tokenValue || tokenName;
addVisualTokenElement(previousTokenName, value, false);
addVisualTokenElement(previousTokenName, value, false, canEdit);
}
}
 
Loading
Loading
Loading
Loading
@@ -104,6 +104,22 @@
padding: 2px 7px;
}
 
.name {
background-color: $filter-name-resting-color;
color: $filter-name-text-color;
border-radius: 2px 0 0 2px;
margin-right: 1px;
text-transform: capitalize;
}
.value-container {
background-color: $white-normal;
color: $filter-value-text-color;
border-radius: 0 2px 2px 0;
margin-right: 5px;
padding-right: 8px;
}
.value {
padding-right: 0;
}
Loading
Loading
@@ -111,7 +127,7 @@
.remove-token {
display: inline-block;
padding-left: 4px;
padding-right: 8px;
padding-right: 0;
 
.fa-close {
color: $gl-text-color-secondary;
Loading
Loading
@@ -132,21 +148,6 @@
}
}
 
.name {
background-color: $filter-name-resting-color;
color: $filter-name-text-color;
border-radius: 2px 0 0 2px;
margin-right: 1px;
text-transform: capitalize;
}
.value-container {
background-color: $white-normal;
color: $filter-value-text-color;
border-radius: 0 2px 2px 0;
margin-right: 5px;
}
.selected {
.name {
background-color: $filter-name-selected-color;
Loading
Loading
Loading
Loading
@@ -157,7 +157,8 @@
 
$(document).off('page:restore').on('page:restore', function (event) {
if (gl.FilteredSearchManager) {
new gl.FilteredSearchManager();
const filteredSearchManager = new gl.FilteredSearchManager();
filteredSearchManager.setup();
}
Issuable.init();
new gl.IssuableBulkActions({
Loading
Loading
Loading
Loading
@@ -57,6 +57,7 @@ describe('Filtered Search Manager', () => {
input = document.querySelector('.filtered-search');
tokensContainer = document.querySelector('.tokens-container');
manager = new gl.FilteredSearchManager();
manager.setup();
});
 
afterEach(() => {
Loading
Loading
@@ -72,6 +73,7 @@ describe('Filtered Search Manager', () => {
spyOn(recentSearchesStoreSrc, 'default');
 
filteredSearchManager = new gl.FilteredSearchManager();
filteredSearchManager.setup();
 
return filteredSearchManager;
});
Loading
Loading
@@ -89,6 +91,7 @@ describe('Filtered Search Manager', () => {
spyOn(window, 'Flash');
 
filteredSearchManager = new gl.FilteredSearchManager();
filteredSearchManager.setup();
 
expect(window.Flash).not.toHaveBeenCalled();
});
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment