Commit d4b2c53d authored by Phil Hughes's avatar Phil Hughes

Merge branch 'add-filtered-search-group-issues-ee' into 'master'

Add filtered search to group issue dashboard (EE)

See merge request !2581
parents 16c0b6e1 dde689ef
...@@ -143,6 +143,8 @@ import AuditLogs from './audit_logs'; ...@@ -143,6 +143,8 @@ import AuditLogs from './audit_logs';
.init(); .init();
} }
const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search');
switch (page) { switch (page) {
case 'profiles:preferences:show': case 'profiles:preferences:show':
initExperimentalFlags(); initExperimentalFlags();
...@@ -159,7 +161,7 @@ import AuditLogs from './audit_logs'; ...@@ -159,7 +161,7 @@ import AuditLogs from './audit_logs';
break; break;
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 (filteredSearchEnabled) {
const 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(); filteredSearchManager.setup();
} }
...@@ -187,11 +189,17 @@ import AuditLogs from './audit_logs'; ...@@ -187,11 +189,17 @@ import AuditLogs from './audit_logs';
break; break;
case 'dashboard:issues': case 'dashboard:issues':
case 'dashboard:merge_requests': case 'dashboard:merge_requests':
case 'groups:issues':
case 'groups:merge_requests': case 'groups:merge_requests':
new ProjectSelect(); new ProjectSelect();
initLegacyFilters(); initLegacyFilters();
break; break;
case 'groups:issues':
if (filteredSearchEnabled) {
const filteredSearchManager = new gl.FilteredSearchManager('issues');
filteredSearchManager.setup();
}
new ProjectSelect();
break;
case 'dashboard:todos:index': case 'dashboard:todos:index':
new Todos(); new Todos();
break; break;
......
...@@ -11,6 +11,16 @@ const Ajax = { ...@@ -11,6 +11,16 @@ const Ajax = {
if (!self.destroyed) self.hook.list[config.method].call(self.hook.list, data); if (!self.destroyed) self.hook.list[config.method].call(self.hook.list, data);
}, },
preprocessing: function preprocessing(config, data) {
let results = data;
if (config.preprocessing && !data.preprocessed) {
results = config.preprocessing(data);
AjaxCache.override(config.endpoint, results);
}
return results;
},
init: function init(hook) { init: function init(hook) {
var self = this; var self = this;
self.destroyed = false; self.destroyed = false;
...@@ -31,7 +41,8 @@ const Ajax = { ...@@ -31,7 +41,8 @@ const Ajax = {
dynamicList.outerHTML = loadingTemplate.outerHTML; dynamicList.outerHTML = loadingTemplate.outerHTML;
} }
AjaxCache.retrieve(config.endpoint) return AjaxCache.retrieve(config.endpoint)
.then(self.preprocessing.bind(null, config))
.then((data) => self._loadData(data, config, self)) .then((data) => self._loadData(data, config, self))
.catch(config.onError); .catch(config.onError);
}, },
......
...@@ -6,7 +6,7 @@ import './filtered_search_dropdown'; ...@@ -6,7 +6,7 @@ import './filtered_search_dropdown';
class DropdownNonUser extends gl.FilteredSearchDropdown { class DropdownNonUser extends gl.FilteredSearchDropdown {
constructor(options = {}) { constructor(options = {}) {
const { input, endpoint, symbol } = options; const { input, endpoint, symbol, preprocessing } = options;
super(options); super(options);
this.symbol = symbol; this.symbol = symbol;
this.config = { this.config = {
...@@ -14,6 +14,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown { ...@@ -14,6 +14,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown {
endpoint, endpoint,
method: 'setData', method: 'setData',
loadingTemplate: this.loadingTemplate, loadingTemplate: this.loadingTemplate,
preprocessing,
onError() { onError() {
/* eslint-disable no-new */ /* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.'); new Flash('An error occured fetching the dropdown data.');
......
...@@ -75,6 +75,66 @@ class DropdownUtils { ...@@ -75,6 +75,66 @@ class DropdownUtils {
return updatedItem; return updatedItem;
} }
static mergeDuplicateLabels(dataMap, newLabel) {
const updatedMap = dataMap;
const key = newLabel.title;
const hasKeyProperty = Object.prototype.hasOwnProperty.call(updatedMap, key);
if (!hasKeyProperty) {
updatedMap[key] = newLabel;
} else {
const existing = updatedMap[key];
if (!existing.multipleColors) {
existing.multipleColors = [existing.color];
}
existing.multipleColors.push(newLabel.color);
}
return updatedMap;
}
static duplicateLabelColor(labelColors) {
const colors = labelColors;
const spacing = 100 / colors.length;
// Reduce the colors to 4
colors.length = Math.min(colors.length, 4);
const color = colors.map((c, i) => {
const percentFirst = Math.floor(spacing * i);
const percentSecond = Math.floor(spacing * (i + 1));
return `${c} ${percentFirst}%, ${c} ${percentSecond}%`;
}).join(', ');
return `linear-gradient(${color})`;
}
static duplicateLabelPreprocessing(data) {
const results = [];
const dataMap = {};
data.forEach(DropdownUtils.mergeDuplicateLabels.bind(null, dataMap));
Object.keys(dataMap)
.forEach((key) => {
const label = dataMap[key];
if (label.multipleColors) {
label.color = DropdownUtils.duplicateLabelColor(label.multipleColors);
label.text_color = '#000000';
}
results.push(label);
});
results.preprocessed = true;
return results;
}
static setDataValueIfSelected(filter, selected) { static setDataValueIfSelected(filter, selected) {
const dataValue = selected.getAttribute('data-value'); const dataValue = selected.getAttribute('data-value');
......
...@@ -58,6 +58,7 @@ class FilteredSearchDropdownManager { ...@@ -58,6 +58,7 @@ class FilteredSearchDropdownManager {
extraArguments: { extraArguments: {
endpoint: `${this.baseEndpoint}/labels.json`, endpoint: `${this.baseEndpoint}/labels.json`,
symbol: '~', symbol: '~',
preprocessing: gl.DropdownUtils.duplicateLabelPreprocessing,
}, },
element: this.container.querySelector('#js-dropdown-label'), element: this.container.querySelector('#js-dropdown-label'),
}, },
......
...@@ -28,13 +28,13 @@ class FilteredSearchManager { ...@@ -28,13 +28,13 @@ class FilteredSearchManager {
allowedKeys: this.filteredSearchTokenKeys.getKeys(), allowedKeys: this.filteredSearchTokenKeys.getKeys(),
}); });
this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown'); this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
const projectPath = this.searchHistoryDropdownElement ? const fullPath = this.searchHistoryDropdownElement ?
this.searchHistoryDropdownElement.dataset.projectFullPath : 'project'; this.searchHistoryDropdownElement.dataset.fullPath : 'project';
let recentSearchesPagePrefix = 'issue-recent-searches'; let recentSearchesPagePrefix = 'issue-recent-searches';
if (this.page === 'merge_requests') { if (this.page === 'merge_requests') {
recentSearchesPagePrefix = 'merge-request-recent-searches'; recentSearchesPagePrefix = 'merge-request-recent-searches';
} }
const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`; const recentSearchesKey = `${fullPath}-${recentSearchesPagePrefix}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey); this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
} }
......
...@@ -58,12 +58,43 @@ class FilteredSearchVisualTokens { ...@@ -58,12 +58,43 @@ class FilteredSearchVisualTokens {
`; `;
} }
static setTokenStyle(tokenContainer, backgroundColor, textColor) {
const token = tokenContainer;
// Labels with linear gradient should not override default background color
if (backgroundColor.indexOf('linear-gradient') === -1) {
token.style.backgroundColor = backgroundColor;
}
token.style.color = textColor;
if (textColor === '#FFFFFF') {
const removeToken = token.querySelector('.remove-token');
removeToken.classList.add('inverted');
}
return token;
}
static preprocessLabel(labelsEndpoint, labels) {
let processed = labels;
if (!labels.preprocessed) {
processed = gl.DropdownUtils.duplicateLabelPreprocessing(labels);
AjaxCache.override(labelsEndpoint, processed);
processed.preprocessed = true;
}
return processed;
}
static updateLabelTokenColor(tokenValueContainer, tokenValue) { static updateLabelTokenColor(tokenValueContainer, tokenValue) {
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search'); const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
const baseEndpoint = filteredSearchInput.dataset.baseEndpoint; const baseEndpoint = filteredSearchInput.dataset.baseEndpoint;
const labelsEndpoint = `${baseEndpoint}/labels.json`; const labelsEndpoint = `${baseEndpoint}/labels.json`;
return AjaxCache.retrieve(labelsEndpoint) return AjaxCache.retrieve(labelsEndpoint)
.then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint))
.then((labels) => { .then((labels) => {
const matchingLabel = (labels || []).find(label => `~${gl.DropdownUtils.getEscapedText(label.title)}` === tokenValue); const matchingLabel = (labels || []).find(label => `~${gl.DropdownUtils.getEscapedText(label.title)}` === tokenValue);
...@@ -71,14 +102,8 @@ class FilteredSearchVisualTokens { ...@@ -71,14 +102,8 @@ class FilteredSearchVisualTokens {
return; return;
} }
const tokenValueStyle = tokenValueContainer.style; FilteredSearchVisualTokens
tokenValueStyle.backgroundColor = matchingLabel.color; .setTokenStyle(tokenValueContainer, matchingLabel.color, matchingLabel.text_color);
tokenValueStyle.color = matchingLabel.text_color;
if (matchingLabel.text_color === '#FFFFFF') {
const removeToken = tokenValueContainer.querySelector('.remove-token');
removeToken.classList.add('inverted');
}
}) })
.catch(() => new Flash('An error occurred while fetching label colors.')); .catch(() => new Flash('An error occurred while fetching label colors.'));
} }
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
/* global ListLabel */ /* global ListLabel */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils';
(function() { (function() {
this.LabelsSelect = (function() { this.LabelsSelect = (function() {
...@@ -218,18 +219,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; ...@@ -218,18 +219,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
} }
} }
if (label.duplicate) { if (label.duplicate) {
spacing = 100 / label.color.length; color = gl.DropdownUtils.duplicateLabelColor(label.color);
// Reduce the colors to 4
label.color = label.color.filter(function(color, i) {
return i < 4;
});
color = _.map(label.color, function(color, i) {
var percentFirst, percentSecond;
percentFirst = Math.floor(spacing * i);
percentSecond = Math.floor(spacing * (i + 1));
return color + " " + percentFirst + "%," + color + " " + percentSecond + "% ";
}).join(',');
color = "linear-gradient(" + color + ")";
} }
else { else {
if (label.color != null) { if (label.color != null) {
......
...@@ -6,6 +6,10 @@ class AjaxCache extends Cache { ...@@ -6,6 +6,10 @@ class AjaxCache extends Cache {
this.pendingRequests = { }; this.pendingRequests = { };
} }
override(endpoint, data) {
this.internalStorage[endpoint] = data;
}
retrieve(endpoint, forceRetrieve) { retrieve(endpoint, forceRetrieve) {
if (this.hasData(endpoint) && !forceRetrieve) { if (this.hasData(endpoint) && !forceRetrieve) {
return Promise.resolve(this.get(endpoint)); return Promise.resolve(this.get(endpoint));
......
...@@ -10,8 +10,10 @@ module EE ...@@ -10,8 +10,10 @@ module EE
private private
def search_multiple_assignees?(type) def search_multiple_assignees?(type)
context = @project.presence || @group
type == :issues && type == :issues &&
@project.feature_available?(:multiple_issue_assignees) context.feature_available?(:multiple_issue_assignees)
end end
end end
end end
...@@ -137,15 +137,23 @@ module SearchHelper ...@@ -137,15 +137,23 @@ module SearchHelper
end end
def search_filter_input_options(type) def search_filter_input_options(type)
{ opts = {
id: "filtered-search-#{type}", id: "filtered-search-#{type}",
placeholder: 'Search or filter results...', placeholder: 'Search or filter results...',
data: { data: {
'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username])
'username-params' => @users.to_json(only: [:id, :username]),
'base-endpoint' => project_path(@project)
} }
} }
if @project.present?
opts[:data]['project-id'] = @project.id
opts[:data]['base-endpoint'] = project_path(@project)
else
# Group context
opts[:data]['base-endpoint'] = group_canonical_path(@group)
end
opts
end end
# Sanitize a HTML field for search display. Most tags are stripped out and the # Sanitize a HTML field for search display. Most tags are stripped out and the
......
...@@ -4,6 +4,11 @@ ...@@ -4,6 +4,11 @@
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues") = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
= webpack_bundle_tag 'issues'
- if show_new_nav? && group_issues_exists - if show_new_nav? && group_issues_exists
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10' do = link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10' do
...@@ -20,7 +25,7 @@ ...@@ -20,7 +25,7 @@
Subscribe Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue" = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue"
= render 'shared/issuable/filter', type: :issues = render 'shared/issuable/search_bar', type: :issues
.row-content-block.second-block .row-content-block.second-block
Only issues from the Only issues from the
......
- type = local_assigns.fetch(:type) - type = local_assigns.fetch(:type)
- board = local_assigns.fetch(:board, nil) - board = local_assigns.fetch(:board, nil)
- block_css_class = type != :boards_modal ? 'row-content-block second-block' : '' - block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
- full_path = @project.present? ? @project.full_path : @group.full_path
.issues-filters .issues-filters
.issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal } .issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal }
...@@ -22,7 +23,7 @@ ...@@ -22,7 +23,7 @@
dropdown_class: "filtered-search-history-dropdown", dropdown_class: "filtered-search-history-dropdown",
content_class: "filtered-search-history-dropdown-content", content_class: "filtered-search-history-dropdown-content",
title: "Recent searches" }) do title: "Recent searches" }) do
.js-filtered-search-history-dropdown{ data: { project_full_path: @project.full_path } } .js-filtered-search-history-dropdown{ data: { full_path: full_path } }
.filtered-search-box-input-container.droplab-dropdown .filtered-search-box-input-container.droplab-dropdown
.scroll-container .scroll-container
%ul.tokens-container.list-unstyled %ul.tokens-container.list-unstyled
......
require 'spec_helper' require 'spec_helper'
feature 'Group issues page' do feature 'Group issues page' do
include FilteredSearchHelpers
let(:path) { issues_group_path(group) } let(:path) { issues_group_path(group) }
let(:issuable) { create(:issue, project: project, title: "this is my created issuable")} let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
...@@ -31,12 +33,10 @@ feature 'Group issues page' do ...@@ -31,12 +33,10 @@ feature 'Group issues page' do
let(:path) { issues_group_path(group) } let(:path) { issues_group_path(group) }
it 'filters by only group users' do it 'filters by only group users' do
click_button('Assignee') filtered_search.set('assignee:')
wait_for_requests
expect(find('.dropdown-menu-assignee')).to have_link(user.name) expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name)
expect(find('.dropdown-menu-assignee')).not_to have_link(user2.name) expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name)
end end
end end
end end
...@@ -101,4 +101,38 @@ describe SearchHelper do ...@@ -101,4 +101,38 @@ describe SearchHelper do
end end
end end
end end
describe 'search_filter_input_options' do
context 'project' do
before do
@project = create(:project, :repository)
end
it 'includes id with type' do
expect(search_filter_input_options('type')[:id]).to eq('filtered-search-type')
end
it 'includes project-id' do
expect(search_filter_input_options('')[:data]['project-id']).to eq(@project.id)
end
it 'includes project base-endpoint' do
expect(search_filter_input_options('')[:data]['base-endpoint']).to eq(project_path(@project))
end
end
context 'group' do
before do
@group = create(:group, name: 'group')
end
it 'does not includes project-id' do
expect(search_filter_input_options('')[:data]['project-id']).to eq(nil)
end
it 'includes group base-endpoint' do
expect(search_filter_input_options('')[:data]['base-endpoint']).to eq("/groups#{group_path(@group)}")
end
end
end
end end
import AjaxCache from '~/lib/utils/ajax_cache';
import Ajax from '~/droplab/plugins/ajax';
describe('Ajax', () => {
describe('preprocessing', () => {
const config = {};
describe('is not configured', () => {
it('passes the data through', () => {
const data = ['data'];
expect(Ajax.preprocessing(config, data)).toEqual(data);
});
});
describe('is configured', () => {
const processedArray = ['processed'];
beforeEach(() => {
config.preprocessing = () => processedArray;
spyOn(config, 'preprocessing').and.callFake(() => processedArray);
});
it('calls preprocessing', () => {
Ajax.preprocessing(config, []);
expect(config.preprocessing.calls.count()).toBe(1);
});
it('overrides AjaxCache', () => {
spyOn(AjaxCache, 'override').and.callFake((endpoint, results) => expect(results).toEqual(processedArray));
Ajax.preprocessing(config, []);
expect(AjaxCache.override.calls.count()).toBe(1);
});
});
});
});
...@@ -191,6 +191,102 @@ describe('Dropdown Utils', () => { ...@@ -191,6 +191,102 @@ describe('Dropdown Utils', () => {
}); });
}); });
describe('mergeDuplicateLabels', () => {
const dataMap = {
label: {
title: 'label',
color: '#FFFFFF',
},
};
it('should add label to dataMap if it is not a duplicate', () => {
const newLabel = {
title: 'new-label',
color: '#000000',
};
const updated = gl.DropdownUtils.mergeDuplicateLabels(dataMap, newLabel);
expect(updated[newLabel.title]).toEqual(newLabel);
});
it('should merge colors if label is a duplicate', () => {
const duplicate = {
title: 'label',
color: '#000000',
};
const updated = gl.DropdownUtils.mergeDuplicateLabels(dataMap, duplicate);
expect(updated.label.multipleColors).toEqual([dataMap.label.color, duplicate.color]);
});
});
describe('duplicateLabelColor', () => {
it('should linear-gradient 2 colors', () => {
const gradient = gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000']);
expect(gradient).toEqual('linear-gradient(#FFFFFF 0%, #FFFFFF 50%, #000000 50%, #000000 100%)');
});
it('should linear-gradient 3 colors', () => {
const gradient = gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333']);
expect(gradient).toEqual('linear-gradient(#FFFFFF 0%, #FFFFFF 33%, #000000 33%, #000000 66%, #333333 66%, #333333 100%)');
});
it('should linear-gradient 4 colors', () => {
const gradient = gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333', '#DDDDDD']);
expect(gradient).toEqual('linear-gradient(#FFFFFF 0%, #FFFFFF 25%, #000000 25%, #000000 50%, #333333 50%, #333333 75%, #DDDDDD 75%, #DDDDDD 100%)');
});
it('should not linear-gradient more than 4 colors', () => {
const gradient = gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333', '#DDDDDD', '#EEEEEE']);
expect(gradient.indexOf('#EEEEEE') === -1).toEqual(true);
});
});
describe('duplicateLabelPreprocessing', () => {
it('should set preprocessed to true', () => {
const results = gl.DropdownUtils.duplicateLabelPreprocessing([]);
expect(results.preprocessed).toEqual(true);
});
it('should not mutate existing data if there are no duplicates', () => {
const data = [{
title: 'label1',
color: '#FFFFFF',
}, {
title: 'label2',
color: '#000000',
}];
const results = gl.DropdownUtils.duplicateLabelPreprocessing(data);
expect(results.length).toEqual(2);
expect(results[0]).toEqual(data[0]);
expect(results[1]).toEqual(data[1]);
});
describe('duplicate labels', () => {
const data = [{
title: 'label',
color: '#FFFFFF',
}, {
title: 'label',
color: '#000000',
}];
const results = gl.DropdownUtils.duplicateLabelPreprocessing(data);
it('should merge duplicate labels', () => {
expect(results.length).toEqual(1);
});
it('should convert multiple colored labels into linear-gradient', () => {
expect(results[0].color).toEqual(gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000']));
});
it('should set multiple colored label text color to black', () => {
expect(results[0].text_color).toEqual('#000000');
});
});
});
describe('setDataValueIfSelected', () => { describe('setDataValueIfSelected', () => {
beforeEach(() => { beforeEach(() => {
spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput') spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput')
......
...@@ -797,6 +797,69 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -797,6 +797,69 @@ describe('Filtered Search Visual Tokens', () => {
}); });
}); });
describe('setTokenStyle', () => {
let originalTextColor;
beforeEach(() => {
originalTextColor = bugLabelToken.style.color;
});
it('should set backgroundColor', () => {
const originalBackgroundColor = bugLabelToken.style.backgroundColor;
const token = subject.setTokenStyle(bugLabelToken, 'blue', 'white');
expect(token.style.backgroundColor).toEqual('blue');
expect(token.style.backgroundColor).not.toEqual(originalBackgroundColor);
});
it('should not set backgroundColor when it is a linear-gradient', () => {
const token = subject.setTokenStyle(bugLabelToken, 'linear-gradient(135deg, red, blue)', 'white');
expect(token.style.backgroundColor).toEqual(bugLabelToken.style.backgroundColor);
});
it('should set textColor', () => {
const token = subject.setTokenStyle(bugLabelToken, 'white', 'black');
expect(token.style.color).toEqual('black');
expect(token.style.color).not.toEqual(originalTextColor);
});
it('should add inverted class when textColor is #FFFFFF', () => {
const token = subject.setTokenStyle(bugLabelToken, 'black', '#FFFFFF');
expect(token.style.color).toEqual('rgb(255, 255, 255)');
expect(token.style.color).not.toEqual(originalTextColor);
expect(token.querySelector('.remove-token').classList.contains('inverted')).toEqual(true);
});
});
describe('preprocessLabel', () => {
const endpoint = 'endpoint';
it('does not preprocess more than once', () => {
let labels = [];
spyOn(gl.DropdownUtils, 'duplicateLabelPreprocessing').and.callFake(() => []);
labels = gl.FilteredSearchVisualTokens.preprocessLabel(endpoint, labels);
gl.FilteredSearchVisualTokens.preprocessLabel(endpoint, labels);
expect(gl.DropdownUtils.duplicateLabelPreprocessing.calls.count()).toEqual(1);
});
describe('not preprocessed before', () => {
it('returns preprocessed labels', () => {
let labels = [];
expect(labels.preprocessed).not.toEqual(true);
labels = gl.FilteredSearchVisualTokens.preprocessLabel(endpoint, labels);
expect(labels.preprocessed).toEqual(true);
});
it('overrides AjaxCache with preprocessed results', () => {
spyOn(AjaxCache, 'override').and.callFake(() => {});
gl.FilteredSearchVisualTokens.preprocessLabel(endpoint, []);
expect(AjaxCache.override.calls.count()).toEqual(1);
});
});
});
describe('updateLabelTokenColor', () => { describe('updateLabelTokenColor', () => {
const jsonFixtureName = 'labels/project_labels.json'; const jsonFixtureName = 'labels/project_labels.json';
const dummyEndpoint = '/dummy/endpoint'; const dummyEndpoint = '/dummy/endpoint';
......
...@@ -77,6 +77,15 @@ describe('AjaxCache', () => { ...@@ -77,6 +77,15 @@ describe('AjaxCache', () => {
}); });
}); });
describe('override', () => {
it('overrides existing cache', () => {
AjaxCache.internalStorage.endpoint = 'existing-endpoint';
AjaxCache.override('endpoint', 'new-endpoint');
expect(AjaxCache.internalStorage.endpoint).toEqual('new-endpoint');
});
});
describe('retrieve', () => { describe('retrieve', () => {
let ajaxSpy; let ajaxSpy;
......
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