Commit 733eec88 authored by Eric Eastwood's avatar Eric Eastwood

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