Commit 17b0afee authored by Phil Hughes's avatar Phil Hughes

Merge branch '2433-update-locked-milestone-to-not-be-removable' into 'master'

Fix locked milestone in boards being remove-able

Closes #2433

See merge request !1963
parents 03c7b548 0162c514
......@@ -87,6 +87,7 @@ $(() => {
Store.rootPath = this.endpoint;
this.filterManager = new FilteredSearchBoards(Store.filter, true, [(this.milestoneTitle ? 'milestone' : null)]);
this.filterManager.setup();
// Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
......
......@@ -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();
......
......@@ -42,9 +42,7 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new Event('input'));
}
canEdit(token) {
const tokenName = token.querySelector('.name').textContent.trim();
canEdit(tokenName) {
return this.cantEdit.indexOf(tokenName) === -1;
}
}
......@@ -130,7 +130,10 @@ import ApproversSelect from './approvers_select';
case 'projects:merge_requests:index':
case 'projects:issues:index':
if (gl.FilteredSearchManager) {
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({
......
......@@ -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;
......@@ -13,7 +14,7 @@ class FilteredSearchManager {
this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
if (page === 'issues' || page === 'boards') {
if (this.page === 'issues' || this.page === 'boards') {
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysIssuesEE;
}
......@@ -21,16 +22,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) => {
......@@ -51,12 +54,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();
......@@ -145,9 +148,9 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (this.filteredSearchInput.value === '' && lastVisualToken) {
if (this.canEdit && !this.canEdit(lastVisualToken)) return;
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();
}
......@@ -246,10 +249,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 (this.canEdit && !this.canEdit(token)) return;
if (token) {
if (token && canEdit) {
gl.FilteredSearchVisualTokens.editToken(token);
this.tokenChange();
}
......@@ -399,7 +402,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 +
......@@ -418,18 +426,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;
......@@ -524,6 +541,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 || {};
......
......@@ -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>
`;
......@@ -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>';
......@@ -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);
}
}
......
......@@ -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;
}
......@@ -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;
......@@ -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;
......
......@@ -177,7 +177,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({
......
......@@ -4,7 +4,9 @@ describe 'Issue Boards', feature: true, js: true do
include DragTo
let(:project) { create(:empty_project, :public) }
let(:milestone) { create(:milestone, title: "v2.2", project: project) }
let!(:board) { create(:board, project: project) }
let!(:board_with_milestone) { create(:board, project: project, milestone: milestone) }
let(:user) { create(:user) }
let!(:user2) { create(:user) }
......@@ -509,6 +511,22 @@ describe 'Issue Boards', feature: true, js: true do
end
end
context 'locked milestone' do
before do
visit namespace_project_board_path(project.namespace, project, board_with_milestone)
wait_for_requests
end
it 'should not have remove button' do
expect(page).to have_selector('.js-visual-token .remove-token', count: 0)
end
it 'should not be able to be backspaced' do
find('.input-token .filtered-search').native.send_key(:backspace)
expect(page).to have_selector('.js-visual-token', count: 1)
end
end
context 'keyboard shortcuts' do
before do
visit namespace_project_boards_path(project.namespace, project)
......
......@@ -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(() => {
......@@ -72,6 +73,7 @@ describe('Filtered Search Manager', () => {
spyOn(recentSearchesStoreSrc, 'default');
filteredSearchManager = new gl.FilteredSearchManager();
filteredSearchManager.setup();
return filteredSearchManager;
});
......@@ -89,6 +91,7 @@ describe('Filtered Search Manager', () => {
spyOn(window, 'Flash');
filteredSearchManager = new gl.FilteredSearchManager();
filteredSearchManager.setup();
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