Prettify environments feature_highlight and filtered_search modules

This commit is contained in:
Mike Greiling 2018-10-10 01:18:49 -05:00
parent 4d0db16f97
commit 8b090caf82
No known key found for this signature in database
GPG key ID: 0303DF507FA67596
22 changed files with 452 additions and 398 deletions

View file

@ -1,40 +1,40 @@
<script> <script>
import tablePagination from '../../vue_shared/components/table_pagination.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue'; import environmentTable from '../components/environments_table.vue';
export default { export default {
components: { components: {
environmentTable, environmentTable,
tablePagination, tablePagination,
},
props: {
isLoading: {
type: Boolean,
required: true,
}, },
props: { environments: {
isLoading: { type: Array,
type: Boolean, required: true,
required: true,
},
environments: {
type: Array,
required: true,
},
pagination: {
type: Object,
required: true,
},
canCreateDeployment: {
type: Boolean,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
}, },
methods: { pagination: {
onChangePage(page) { type: Object,
this.$emit('onChangePage', page); required: true,
},
}, },
}; canCreateDeployment: {
type: Boolean,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
},
methods: {
onChangePage(page) {
this.$emit('onChangePage', page);
},
},
};
</script> </script>
<template> <template>

View file

@ -1,21 +1,21 @@
<script> <script>
export default { export default {
name: 'EnvironmentsEmptyState', name: 'EnvironmentsEmptyState',
props: { props: {
newPath: { newPath: {
type: String, type: String,
required: true, required: true,
},
canCreateEnvironment: {
type: Boolean,
required: true,
},
helpPath: {
type: String,
required: true,
},
}, },
}; canCreateEnvironment: {
type: Boolean,
required: true,
},
helpPath: {
type: String,
required: true,
},
},
};
</script> </script>
<template> <template>
<div class="blank-state-row"> <div class="blank-state-row">

View file

@ -38,7 +38,9 @@ export default {
computed: { computed: {
title() { title() {
return this.isLastDeployment ? s__('Environments|Re-deploy to environment') : s__('Environments|Rollback environment'); return this.isLastDeployment
? s__('Environments|Re-deploy to environment')
: s__('Environments|Rollback environment');
}, },
}, },

View file

@ -5,31 +5,32 @@ import Translate from '../../vue_shared/translate';
Vue.use(Translate); Vue.use(Translate);
export default () => new Vue({ export default () =>
el: '#environments-folder-list-view', new Vue({
components: { el: '#environments-folder-list-view',
environmentsFolderApp, components: {
}, environmentsFolderApp,
data() { },
const environmentsData = document.querySelector(this.$options.el).dataset; data() {
const environmentsData = document.querySelector(this.$options.el).dataset;
return { return {
endpoint: environmentsData.endpoint, endpoint: environmentsData.endpoint,
folderName: environmentsData.folderName, folderName: environmentsData.folderName,
cssContainerClass: environmentsData.cssClass, cssContainerClass: environmentsData.cssClass,
canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment), canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment),
canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment), canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment),
}; };
}, },
render(createElement) { render(createElement) {
return createElement('environments-folder-app', { return createElement('environments-folder-app', {
props: { props: {
endpoint: this.endpoint, endpoint: this.endpoint,
folderName: this.folderName, folderName: this.folderName,
cssContainerClass: this.cssContainerClass, cssContainerClass: this.cssContainerClass,
canCreateDeployment: this.canCreateDeployment, canCreateDeployment: this.canCreateDeployment,
canReadEnvironment: this.canReadEnvironment, canReadEnvironment: this.canReadEnvironment,
}, },
}); });
}, },
}); });

View file

@ -1,46 +1,43 @@
<script> <script>
import environmentsMixin from '../mixins/environments_mixin'; import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from '../components/stop_environment_modal.vue'; import StopEnvironmentModal from '../components/stop_environment_modal.vue';
export default { export default {
components: { components: {
StopEnvironmentModal, StopEnvironmentModal,
}, },
mixins: [ mixins: [environmentsMixin, CIPaginationMixin],
environmentsMixin,
CIPaginationMixin,
],
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
required: true, required: true,
},
folderName: {
type: String,
required: true,
},
cssContainerClass: {
type: String,
required: true,
},
canCreateDeployment: {
type: Boolean,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
}, },
methods: { folderName: {
successCallback(resp) { type: String,
this.saveData(resp); required: true,
},
}, },
}; cssContainerClass: {
type: String,
required: true,
},
canCreateDeployment: {
type: Boolean,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
},
methods: {
successCallback(resp) {
this.saveData(resp);
},
},
};
</script> </script>
<template> <template>
<div :class="cssContainerClass"> <div :class="cssContainerClass">

View file

@ -5,35 +5,36 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate); Vue.use(Translate);
export default () => new Vue({ export default () =>
el: '#environments-list-view', new Vue({
components: { el: '#environments-list-view',
environmentsComponent, components: {
}, environmentsComponent,
data() { },
const environmentsData = document.querySelector(this.$options.el).dataset; data() {
const environmentsData = document.querySelector(this.$options.el).dataset;
return { return {
endpoint: environmentsData.environmentsDataEndpoint, endpoint: environmentsData.environmentsDataEndpoint,
newEnvironmentPath: environmentsData.newEnvironmentPath, newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath, helpPagePath: environmentsData.helpPagePath,
cssContainerClass: environmentsData.cssClass, cssContainerClass: environmentsData.cssClass,
canCreateEnvironment: convertPermissionToBoolean(environmentsData.canCreateEnvironment), canCreateEnvironment: convertPermissionToBoolean(environmentsData.canCreateEnvironment),
canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment), canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment),
canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment), canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment),
}; };
}, },
render(createElement) { render(createElement) {
return createElement('environments-component', { return createElement('environments-component', {
props: { props: {
endpoint: this.endpoint, endpoint: this.endpoint,
newEnvironmentPath: this.newEnvironmentPath, newEnvironmentPath: this.newEnvironmentPath,
helpPagePath: this.helpPagePath, helpPagePath: this.helpPagePath,
cssContainerClass: this.cssContainerClass, cssContainerClass: this.cssContainerClass,
canCreateEnvironment: this.canCreateEnvironment, canCreateEnvironment: this.canCreateEnvironment,
canCreateDeployment: this.canCreateDeployment, canCreateDeployment: this.canCreateDeployment,
canReadEnvironment: this.canReadEnvironment, canReadEnvironment: this.canReadEnvironment,
}, },
}); });
}, },
}); });

View file

@ -4,9 +4,7 @@
import _ from 'underscore'; import _ from 'underscore';
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
import { import { getParameterByName } from '../../lib/utils/common_utils';
getParameterByName,
} from '../../lib/utils/common_utils';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import Flash from '../../flash'; import Flash from '../../flash';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
@ -19,7 +17,6 @@ import tabs from '../../vue_shared/components/navigation_tabs.vue';
import container from '../components/container.vue'; import container from '../components/container.vue';
export default { export default {
components: { components: {
environmentTable, environmentTable,
container, container,
@ -65,7 +62,8 @@ export default {
updateContent(parameters) { updateContent(parameters) {
this.updateInternalState(parameters); this.updateInternalState(parameters);
// fetch new data // fetch new data
return this.service.fetchEnvironments(this.requestData) return this.service
.fetchEnvironments(this.requestData)
.then(response => this.successCallback(response)) .then(response => this.successCallback(response))
.then(() => { .then(() => {
// restart polling // restart polling
@ -88,7 +86,8 @@ export default {
if (!this.isMakingRequest) { if (!this.isMakingRequest) {
this.isLoading = true; this.isLoading = true;
this.service.postAction(endpoint) this.service
.postAction(endpoint)
.then(() => this.fetchEnvironments()) .then(() => this.fetchEnvironments())
.catch(() => { .catch(() => {
this.isLoading = false; this.isLoading = false;
@ -100,7 +99,8 @@ export default {
fetchEnvironments() { fetchEnvironments() {
this.isLoading = true; this.isLoading = true;
return this.service.fetchEnvironments(this.requestData) return this.service
.fetchEnvironments(this.requestData)
.then(this.successCallback) .then(this.successCallback)
.catch(this.errorCallback); .catch(this.errorCallback);
}, },
@ -111,7 +111,9 @@ export default {
stopEnvironment(environment) { stopEnvironment(environment) {
const endpoint = environment.stop_path; const endpoint = environment.stop_path;
const errorMessage = s__('Environments|An error occurred while stopping the environment, please try again'); const errorMessage = s__(
'Environments|An error occurred while stopping the environment, please try again',
);
this.postAction({ endpoint, errorMessage }); this.postAction({ endpoint, errorMessage });
}, },
}, },
@ -149,7 +151,7 @@ export default {
data: this.requestData, data: this.requestData,
successCallback: this.successCallback, successCallback: this.successCallback,
errorCallback: this.errorCallback, errorCallback: this.errorCallback,
notificationCallback: (isMakingRequest) => { notificationCallback: isMakingRequest => {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
}, },
}); });

View file

@ -1,13 +1,6 @@
import $ from 'jquery'; import $ from 'jquery';
import { import { getSelector, inserted } from './feature_highlight_helper';
getSelector, import { togglePopover, mouseenter, debouncedMouseleave } from '../shared/popover';
inserted,
} from './feature_highlight_helper';
import {
togglePopover,
mouseenter,
debouncedMouseleave,
} from '../shared/popover';
export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
const $selector = $(getSelector(id)); const $selector = $(getSelector(id));
@ -41,8 +34,9 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
export function findHighestPriorityFeature() { export function findHighestPriorityFeature() {
let priorityFeature; let priorityFeature;
const sortedFeatureEls = [].slice.call(document.querySelectorAll('.js-feature-highlight')).sort((a, b) => const sortedFeatureEls = [].slice
(a.dataset.highlightPriority || 0) < (b.dataset.highlightPriority || 0)); .call(document.querySelectorAll('.js-feature-highlight'))
.sort((a, b) => (a.dataset.highlightPriority || 0) < (b.dataset.highlightPriority || 0));
const [priorityFeatureEl] = sortedFeatureEls; const [priorityFeatureEl] = sortedFeatureEls;
if (priorityFeatureEl) { if (priorityFeatureEl) {

View file

@ -8,10 +8,17 @@ import { togglePopover } from '../shared/popover';
export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`; export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
export function dismiss(highlightId) { export function dismiss(highlightId) {
axios.post(this.attr('data-dismiss-endpoint'), { axios
feature_name: highlightId, .post(this.attr('data-dismiss-endpoint'), {
}) feature_name: highlightId,
.catch(() => Flash(__('An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.'))); })
.catch(() =>
Flash(
__(
'An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.',
),
),
);
togglePopover.call(this, false); togglePopover.call(this, false);
this.hide(); this.hide();
@ -23,8 +30,7 @@ export function inserted() {
const $popover = $(this); const $popover = $(this);
const dismissWrapper = dismiss.bind($popover, highlightId); const dismissWrapper = dismiss.bind($popover, highlightId);
$(`#${popoverId} .dismiss-feature-highlight`) $(`#${popoverId} .dismiss-feature-highlight`).on('click', dismissWrapper);
.on('click', dismissWrapper);
const lazyImg = $(`#${popoverId} .feature-highlight-illustration`)[0]; const lazyImg = $(`#${popoverId} .feature-highlight-illustration`)[0];
if (lazyImg) { if (lazyImg) {

View file

@ -1,20 +1,23 @@
import FilteredSearchTokenKeys from './filtered_search_token_keys'; import FilteredSearchTokenKeys from './filtered_search_token_keys';
const tokenKeys = [{ const tokenKeys = [
key: 'status', {
type: 'string', key: 'status',
param: 'status', type: 'string',
symbol: '', param: 'status',
icon: 'messages', symbol: '',
tag: 'status', icon: 'messages',
}, { tag: 'status',
key: 'type', },
type: 'string', {
param: 'type', key: 'type',
symbol: '', type: 'string',
icon: 'cube', param: 'type',
tag: 'type', symbol: '',
}]; icon: 'cube',
tag: 'type',
},
];
const AdminRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys); const AdminRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys);

View file

@ -21,9 +21,11 @@ export default {
}, },
computed: { computed: {
processedItems() { processedItems() {
return this.items.map((item) => { return this.items.map(item => {
const { tokens, searchToken } const { tokens, searchToken } = FilteredSearchTokenizer.processTokens(
= FilteredSearchTokenizer.processTokens(item, this.allowedKeys); item,
this.allowedKeys,
);
const resultantTokens = tokens.map(token => ({ const resultantTokens = tokens.map(token => ({
prefix: `${token.key}:`, prefix: `${token.key}:`,

View file

@ -24,8 +24,12 @@ export default class DropdownEmoji extends FilteredSearchDropdown {
}; };
import(/* webpackChunkName: 'emoji' */ '~/emoji') import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(({ glEmojiTag }) => { this.glEmojiTag = glEmojiTag; }) .then(({ glEmojiTag }) => {
.catch(() => { /* ignore error and leave emoji name in the search bar */ }); this.glEmojiTag = glEmojiTag;
})
.catch(() => {
/* ignore error and leave emoji name in the search bar */
});
this.unbindEvents(); this.unbindEvents();
this.bindEvents(); this.bindEvents();
@ -48,7 +52,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown {
} }
itemClicked(e) { itemClicked(e) {
super.itemClicked(e, (selected) => { super.itemClicked(e, selected => {
const name = selected.querySelector('.js-data-value').innerText.trim(); const name = selected.querySelector('.js-data-value').innerText.trim();
return DropdownUtils.getEscapedText(name); return DropdownUtils.getEscapedText(name);
}); });
@ -64,7 +68,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown {
// Replace empty gl-emoji tag to real content // Replace empty gl-emoji tag to real content
const dropdownItems = [...this.dropdown.querySelectorAll('.filter-dropdown-item')]; const dropdownItems = [...this.dropdown.querySelectorAll('.filter-dropdown-item')];
dropdownItems.forEach((dropdownItem) => { dropdownItems.forEach(dropdownItem => {
const name = dropdownItem.querySelector('.js-data-value').innerText; const name = dropdownItem.querySelector('.js-data-value').innerText;
const emojiTag = this.glEmojiTag(name); const emojiTag = this.glEmojiTag(name);
const emojiElement = dropdownItem.querySelector('gl-emoji'); const emojiElement = dropdownItem.querySelector('gl-emoji');
@ -73,7 +77,6 @@ export default class DropdownEmoji extends FilteredSearchDropdown {
} }
init() { init() {
this.droplab this.droplab.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
} }
} }

View file

@ -41,8 +41,10 @@ export default class DropdownHint extends FilteredSearchDropdown {
previousInputValues.forEach((value, index) => { previousInputValues.forEach((value, index) => {
searchTerms.push(value); searchTerms.push(value);
if (index === previousInputValues.length - 1 if (
&& token.indexOf(value.toLowerCase()) !== -1) { index === previousInputValues.length - 1 &&
token.indexOf(value.toLowerCase()) !== -1
) {
searchTerms.pop(); searchTerms.pop();
} }
}); });
@ -64,13 +66,12 @@ export default class DropdownHint extends FilteredSearchDropdown {
} }
renderContent() { renderContent() {
const dropdownData = this.tokenKeys.get() const dropdownData = this.tokenKeys.get().map(tokenKey => ({
.map(tokenKey => ({ icon: `${gon.sprite_icons}#${tokenKey.icon}`,
icon: `${gon.sprite_icons}#${tokenKey.icon}`, hint: tokenKey.key,
hint: tokenKey.key, tag: `:${tokenKey.tag}`,
tag: `:${tokenKey.tag}`, type: tokenKey.type,
type: tokenKey.type, }));
}));
this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config); this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
this.droplab.setData(this.hookId, dropdownData); this.droplab.setData(this.hookId, dropdownData);

View file

@ -29,20 +29,18 @@ export default class DropdownNonUser extends FilteredSearchDropdown {
} }
itemClicked(e) { itemClicked(e) {
super.itemClicked(e, (selected) => { super.itemClicked(e, selected => {
const title = selected.querySelector('.js-data-value').innerText.trim(); const title = selected.querySelector('.js-data-value').innerText.trim();
return `${this.symbol}${DropdownUtils.getEscapedText(title)}`; return `${this.symbol}${DropdownUtils.getEscapedText(title)}`;
}); });
} }
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
this.droplab this.droplab.changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
.changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
super.renderContent(forceShowList); super.renderContent(forceShowList);
} }
init() { init() {
this.droplab this.droplab.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
} }
} }

View file

@ -41,7 +41,7 @@ export default class DropdownUtils {
// Removes the first character if it is a quotation so that we can search // Removes the first character if it is a quotation so that we can search
// with multiple words // with multiple words
if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { if ((value[0] === '"' || value[0] === "'") && title.indexOf(' ') !== -1) {
value = value.slice(1); value = value.slice(1);
} }
@ -82,11 +82,13 @@ export default class DropdownUtils {
// Reduce the colors to 4 // Reduce the colors to 4
colors.length = Math.min(colors.length, 4); colors.length = Math.min(colors.length, 4);
const color = colors.map((c, i) => { const color = colors
const percentFirst = Math.floor(spacing * i); .map((c, i) => {
const percentSecond = Math.floor(spacing * (i + 1)); const percentFirst = Math.floor(spacing * i);
return `${c} ${percentFirst}%, ${c} ${percentSecond}%`; const percentSecond = Math.floor(spacing * (i + 1));
}).join(', '); return `${c} ${percentFirst}%, ${c} ${percentSecond}%`;
})
.join(', ');
return `linear-gradient(${color})`; return `linear-gradient(${color})`;
} }
@ -97,17 +99,16 @@ export default class DropdownUtils {
data.forEach(DropdownUtils.mergeDuplicateLabels.bind(null, dataMap)); data.forEach(DropdownUtils.mergeDuplicateLabels.bind(null, dataMap));
Object.keys(dataMap) Object.keys(dataMap).forEach(key => {
.forEach((key) => { const label = dataMap[key];
const label = dataMap[key];
if (label.multipleColors) { if (label.multipleColors) {
label.color = DropdownUtils.duplicateLabelColor(label.multipleColors); label.color = DropdownUtils.duplicateLabelColor(label.multipleColors);
label.text_color = '#000000'; label.text_color = '#000000';
} }
results.push(label); results.push(label);
}); });
results.preprocessed = true; results.preprocessed = true;
@ -118,8 +119,7 @@ export default class DropdownUtils {
const { input, allowedKeys } = config; const { input, allowedKeys } = config;
const updatedItem = item; const updatedItem = item;
const searchInput = DropdownUtils.getSearchQuery(input); const searchInput = DropdownUtils.getSearchQuery(input);
const { lastToken, tokens } = const { lastToken, tokens } = FilteredSearchTokenizer.processTokens(searchInput, allowedKeys);
FilteredSearchTokenizer.processTokens(searchInput, allowedKeys);
const lastKey = lastToken.key || lastToken || ''; const lastKey = lastToken.key || lastToken || '';
const allowMultiple = item.type === 'array'; const allowMultiple = item.type === 'array';
const itemInExistingTokens = tokens.some(t => t.key === item.hint); const itemInExistingTokens = tokens.some(t => t.key === item.hint);
@ -154,7 +154,10 @@ export default class DropdownUtils {
static getVisualTokenValues(visualToken) { static getVisualTokenValues(visualToken) {
const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim(); const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim();
let tokenValue = visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim(); let tokenValue =
visualToken &&
visualToken.querySelector('.value') &&
visualToken.querySelector('.value').textContent.trim();
if (tokenName === 'label' && tokenValue) { if (tokenName === 'label' && tokenValue) {
// remove leading symbol and wrapping quotes // remove leading symbol and wrapping quotes
tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, ''); tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, '');
@ -174,7 +177,7 @@ export default class DropdownUtils {
tokens.splice(inputIndex + 1); tokens.splice(inputIndex + 1);
} }
tokens.forEach((token) => { tokens.forEach(token => {
if (token.classList.contains('js-visual-token')) { if (token.classList.contains('js-visual-token')) {
const name = token.querySelector('.name'); const name = token.querySelector('.name');
const value = token.querySelector('.value'); const value = token.querySelector('.value');
@ -194,8 +197,9 @@ export default class DropdownUtils {
values.push(name.innerText); values.push(name.innerText);
} }
} else if (token.classList.contains('input-token')) { } else if (token.classList.contains('input-token')) {
const { isLastVisualTokenValid } = const {
FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); isLastVisualTokenValid,
} = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
const inputValue = input && input.value; const inputValue = input && input.value;
@ -209,9 +213,7 @@ export default class DropdownUtils {
} }
}); });
return values return values.map(value => value.trim()).join(' ');
.map(value => value.trim())
.join(' ');
} }
static getSearchInput(filteredSearchInput) { static getSearchInput(filteredSearchInput) {
@ -227,7 +229,9 @@ export default class DropdownUtils {
// Replace all spaces inside quote marks with underscores // Replace all spaces inside quote marks with underscores
// (will continue to match entire string until an end quote is found if any) // (will continue to match entire string until an end quote is found if any)
// This helps with matching the beginning & end of a token:key // This helps with matching the beginning & end of a token:key
inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_')); inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str =>
str.replace(/\s/g, '_'),
);
// Get the right position for the word selected // Get the right position for the word selected
// Regex matches first space // Regex matches first space

View file

@ -87,10 +87,12 @@ export default class FilteredSearchDropdown {
dispatchInputEvent() { dispatchInputEvent() {
// Propogate input change to FilteredSearchDropdownManager // Propogate input change to FilteredSearchDropdownManager
// so that it can determine which dropdowns to open // so that it can determine which dropdowns to open
this.input.dispatchEvent(new CustomEvent('input', { this.input.dispatchEvent(
bubbles: true, new CustomEvent('input', {
cancelable: true, bubbles: true,
})); cancelable: true,
}),
);
} }
dispatchFormSubmitEvent() { dispatchFormSubmitEvent() {
@ -114,7 +116,7 @@ export default class FilteredSearchDropdown {
if (!data) return; if (!data) return;
const results = data.map((o) => { const results = data.map(o => {
const updated = o; const updated = o;
updated.droplab_hidden = false; updated.droplab_hidden = false;
return updated; return updated;

View file

@ -42,19 +42,21 @@ export default class FilteredSearchTokenKeys {
} }
searchByKeyParam(keyParam) { searchByKeyParam(keyParam) {
return this.tokenKeysWithAlternative.find((tokenKey) => { return (
let tokenKeyParam = tokenKey.key; this.tokenKeysWithAlternative.find(tokenKey => {
let tokenKeyParam = tokenKey.key;
// Replace hyphen with underscore to compare keyParam with tokenKeyParam // Replace hyphen with underscore to compare keyParam with tokenKeyParam
// e.g. 'my-reaction' => 'my_reaction' // e.g. 'my-reaction' => 'my_reaction'
tokenKeyParam = tokenKeyParam.replace('-', '_'); tokenKeyParam = tokenKeyParam.replace('-', '_');
if (tokenKey.param) { if (tokenKey.param) {
tokenKeyParam += `_${tokenKey.param}`; tokenKeyParam += `_${tokenKey.param}`;
} }
return keyParam === tokenKeyParam; return keyParam === tokenKeyParam;
}) || null; }) || null
);
} }
searchByConditionUrl(url) { searchByConditionUrl(url) {
@ -62,8 +64,10 @@ export default class FilteredSearchTokenKeys {
} }
searchByConditionKeyValue(key, value) { searchByConditionKeyValue(key, value) {
return this.conditions return (
.find(condition => condition.tokenKey === key && condition.value === value) || null; this.conditions.find(condition => condition.tokenKey === key && condition.value === value) ||
null
);
} }
addExtraTokensForMergeRequests() { addExtraTokensForMergeRequests() {

View file

@ -4,41 +4,48 @@ export default class FilteredSearchTokenizer {
static processTokens(input, allowedKeys) { static processTokens(input, allowedKeys) {
// Regex extracts `(token):(symbol)(value)` // Regex extracts `(token):(symbol)(value)`
// Values that start with a double quote must end in a double quote (same for single) // Values that start with a double quote must end in a double quote (same for single)
const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g'); const tokenRegex = new RegExp(
`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`,
'g',
);
const tokens = []; const tokens = [];
const tokenIndexes = []; // stores key+value for simple search const tokenIndexes = []; // stores key+value for simple search
let lastToken = null; let lastToken = null;
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { const searchToken =
let tokenValue = v1 || v2 || v3; input
let tokenSymbol = symbol; .replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
let tokenIndex = ''; let tokenValue = v1 || v2 || v3;
let tokenSymbol = symbol;
let tokenIndex = '';
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
tokenSymbol = tokenValue; tokenSymbol = tokenValue;
tokenValue = ''; tokenValue = '';
} }
tokenIndex = `${key}:${tokenValue}`; tokenIndex = `${key}:${tokenValue}`;
// Prevent adding duplicates // Prevent adding duplicates
if (tokenIndexes.indexOf(tokenIndex) === -1) { if (tokenIndexes.indexOf(tokenIndex) === -1) {
tokenIndexes.push(tokenIndex); tokenIndexes.push(tokenIndex);
tokens.push({ tokens.push({
key, key,
value: tokenValue || '', value: tokenValue || '',
symbol: tokenSymbol || '', symbol: tokenSymbol || '',
}); });
} }
return ''; return '';
}).replace(/\s{2,}/g, ' ').trim() || ''; })
.replace(/\s{2,}/g, ' ')
.trim() || '';
if (tokens.length > 0) { if (tokens.length > 0) {
const last = tokens[tokens.length - 1]; const last = tokens[tokens.length - 1];
const lastString = `${last.key}:${last.symbol}${last.value}`; const lastString = `${last.key}:${last.symbol}${last.value}`;
lastToken = input.lastIndexOf(lastString) === lastToken =
input.length - lastString.length ? last : searchToken; input.lastIndexOf(lastString) === input.length - lastString.length ? last : searchToken;
} else { } else {
lastToken = searchToken; lastToken = searchToken;
} }

View file

@ -13,7 +13,10 @@ export default class FilteredSearchVisualTokens {
return { return {
lastVisualToken, lastVisualToken,
isLastVisualTokenValid: lastVisualToken === null || lastVisualToken.className.indexOf('filtered-search-term') !== -1 || (lastVisualToken && lastVisualToken.querySelector('.value') !== null), isLastVisualTokenValid:
lastVisualToken === null ||
lastVisualToken.className.indexOf('filtered-search-term') !== -1 ||
(lastVisualToken && lastVisualToken.querySelector('.value') !== null),
}; };
} }
@ -33,7 +36,9 @@ export default class FilteredSearchVisualTokens {
} }
static unselectTokens() { static unselectTokens() {
const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected'); const otherTokens = FilteredSearchContainer.container.querySelectorAll(
'.js-visual-token .selectable.selected',
);
[].forEach.call(otherTokens, t => t.classList.remove('selected')); [].forEach.call(otherTokens, t => t.classList.remove('selected'));
} }
@ -56,11 +61,7 @@ export default class FilteredSearchVisualTokens {
} }
static createVisualTokenElementHTML(options = {}) { static createVisualTokenElementHTML(options = {}) {
const { const { canEdit = true, uppercaseTokenName = false, capitalizeTokenValue = false } = options;
canEdit = true,
uppercaseTokenName = false,
capitalizeTokenValue = false,
} = options;
return ` return `
<div class="${canEdit ? 'selectable' : 'hidden'}" role="button"> <div class="${canEdit ? 'selectable' : 'hidden'}" role="button">
@ -115,15 +116,20 @@ export default class FilteredSearchVisualTokens {
return AjaxCache.retrieve(labelsEndpoint) return AjaxCache.retrieve(labelsEndpoint)
.then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint)) .then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint))
.then((labels) => { .then(labels => {
const matchingLabel = (labels || []).find(label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue); const matchingLabel = (labels || []).find(
label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue,
);
if (!matchingLabel) { if (!matchingLabel) {
return; return;
} }
FilteredSearchVisualTokens FilteredSearchVisualTokens.setTokenStyle(
.setTokenStyle(tokenValueContainer, matchingLabel.color, matchingLabel.text_color); tokenValueContainer,
matchingLabel.color,
matchingLabel.text_color,
);
}) })
.catch(() => new Flash('An error occurred while fetching label colors.')); .catch(() => new Flash('An error occurred while fetching label colors.'));
} }
@ -134,39 +140,43 @@ export default class FilteredSearchVisualTokens {
} }
const username = tokenValue.replace(/^@/, ''); const username = tokenValue.replace(/^@/, '');
return UsersCache.retrieve(username) return (
.then((user) => { UsersCache.retrieve(username)
if (!user) { .then(user => {
return; if (!user) {
} return;
}
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
tokenValueContainer.dataset.originalValue = tokenValue; tokenValueContainer.dataset.originalValue = tokenValue;
tokenValueElement.innerHTML = ` tokenValueElement.innerHTML = `
<img class="avatar s20" src="${user.avatar_url}" alt=""> <img class="avatar s20" src="${user.avatar_url}" alt="">
${_.escape(user.name)} ${_.escape(user.name)}
`; `;
/* eslint-enable no-param-reassign */ /* eslint-enable no-param-reassign */
}) })
// ignore error and leave username in the search bar // ignore error and leave username in the search bar
.catch(() => { }); .catch(() => {})
);
} }
static updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) { static updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
const container = tokenValueContainer; const container = tokenValueContainer;
const element = tokenValueElement; const element = tokenValueElement;
return import(/* webpackChunkName: 'emoji' */ '../emoji') return (
.then((Emoji) => { import(/* webpackChunkName: 'emoji' */ '../emoji')
if (!Emoji.isEmojiNameValid(tokenValue)) { .then(Emoji => {
return; if (!Emoji.isEmojiNameValid(tokenValue)) {
} return;
}
container.dataset.originalValue = tokenValue; container.dataset.originalValue = tokenValue;
element.innerHTML = Emoji.glEmojiTag(tokenValue); element.innerHTML = Emoji.glEmojiTag(tokenValue);
}) })
// ignore error and leave emoji name in the search bar // ignore error and leave emoji name in the search bar
.catch(() => { }); .catch(() => {})
);
} }
static renderVisualTokenValue(parentElement, tokenName, tokenValue) { static renderVisualTokenValue(parentElement, tokenName, tokenValue) {
@ -177,24 +187,23 @@ export default class FilteredSearchVisualTokens {
const tokenType = tokenName.toLowerCase(); const tokenType = tokenName.toLowerCase();
if (tokenType === 'label') { if (tokenType === 'label') {
FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue); FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue);
} else if ((tokenType === 'author') || (tokenType === 'assignee')) { } else if (tokenType === 'author' || tokenType === 'assignee') {
FilteredSearchVisualTokens.updateUserTokenAppearance( FilteredSearchVisualTokens.updateUserTokenAppearance(
tokenValueContainer, tokenValueElement, tokenValue, tokenValueContainer,
tokenValueElement,
tokenValue,
); );
} else if (tokenType === 'my-reaction') { } else if (tokenType === 'my-reaction') {
FilteredSearchVisualTokens.updateEmojiTokenAppearance( FilteredSearchVisualTokens.updateEmojiTokenAppearance(
tokenValueContainer, tokenValueElement, tokenValue, tokenValueContainer,
tokenValueElement,
tokenValue,
); );
} }
} }
static addVisualTokenElement(name, value, options = {}) { static addVisualTokenElement(name, value, options = {}) {
const { const { isSearchTerm = false, canEdit, uppercaseTokenName, capitalizeTokenValue } = options;
isSearchTerm = false,
canEdit,
uppercaseTokenName,
capitalizeTokenValue,
} = options;
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');
@ -217,8 +226,10 @@ export default class FilteredSearchVisualTokens {
} }
static addValueToPreviousVisualTokenElement(value) { static addValueToPreviousVisualTokenElement(value) {
const { lastVisualToken, isLastVisualTokenValid } = const {
FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); lastVisualToken,
isLastVisualTokenValid,
} = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) { if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) {
const name = FilteredSearchVisualTokens.getLastTokenPartial(); const name = FilteredSearchVisualTokens.getLastTokenPartial();
@ -228,13 +239,15 @@ export default class FilteredSearchVisualTokens {
} }
} }
static addFilterVisualToken(tokenName, tokenValue, { static addFilterVisualToken(
canEdit, tokenName,
uppercaseTokenName = false, tokenValue,
capitalizeTokenValue = false, { canEdit, uppercaseTokenName = false, capitalizeTokenValue = false } = {},
} = {}) { ) {
const { lastVisualToken, isLastVisualTokenValid } const {
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); lastVisualToken,
isLastVisualTokenValid,
} = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const { addVisualTokenElement } = FilteredSearchVisualTokens; const { addVisualTokenElement } = FilteredSearchVisualTokens;
if (isLastVisualTokenValid) { if (isLastVisualTokenValid) {
@ -308,8 +321,7 @@ export default class FilteredSearchVisualTokens {
static tokenizeInput() { static tokenizeInput() {
const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
const { isLastVisualTokenValid } = const { isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (input.value) { if (input.value) {
if (isLastVisualTokenValid) { if (isLastVisualTokenValid) {
@ -375,8 +387,7 @@ export default class FilteredSearchVisualTokens {
FilteredSearchVisualTokens.tokenizeInput(); FilteredSearchVisualTokens.tokenizeInput();
if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) { if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) {
const { isLastVisualTokenValid } = const { isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (!isLastVisualTokenValid) { if (!isLastVisualTokenValid) {
const lastPartial = FilteredSearchVisualTokens.getLastTokenPartial(); const lastPartial = FilteredSearchVisualTokens.getLastTokenPartial();

View file

@ -1,34 +1,39 @@
import FilteredSearchTokenKeys from './filtered_search_token_keys'; import FilteredSearchTokenKeys from './filtered_search_token_keys';
export const tokenKeys = [{ export const tokenKeys = [
key: 'author', {
type: 'string', key: 'author',
param: 'username', type: 'string',
symbol: '@', param: 'username',
icon: 'pencil', symbol: '@',
tag: '@author', icon: 'pencil',
}, { tag: '@author',
key: 'assignee', },
type: 'string', {
param: 'username', key: 'assignee',
symbol: '@', type: 'string',
icon: 'user', param: 'username',
tag: '@assignee', symbol: '@',
}, { icon: 'user',
key: 'milestone', tag: '@assignee',
type: 'string', },
param: 'title', {
symbol: '%', key: 'milestone',
icon: 'clock', type: 'string',
tag: '%milestone', param: 'title',
}, { symbol: '%',
key: 'label', icon: 'clock',
type: 'array', tag: '%milestone',
param: 'name[]', },
symbol: '~', {
icon: 'labels', key: 'label',
tag: '~label', type: 'array',
}]; param: 'name[]',
symbol: '~',
icon: 'labels',
tag: '~label',
},
];
if (gon.current_user_id) { if (gon.current_user_id) {
// Appending tokenkeys only logged-in // Appending tokenkeys only logged-in
@ -42,36 +47,47 @@ if (gon.current_user_id) {
}); });
} }
export const alternativeTokenKeys = [{ export const alternativeTokenKeys = [
key: 'label', {
type: 'string', key: 'label',
param: 'name', type: 'string',
symbol: '~', param: 'name',
}]; symbol: '~',
},
];
export const conditions = [{ export const conditions = [
url: 'assignee_id=0', {
tokenKey: 'assignee', url: 'assignee_id=0',
value: 'none', tokenKey: 'assignee',
}, { value: 'none',
url: 'milestone_title=No+Milestone', },
tokenKey: 'milestone', {
value: 'none', url: 'milestone_title=No+Milestone',
}, { tokenKey: 'milestone',
url: 'milestone_title=%23upcoming', value: 'none',
tokenKey: 'milestone', },
value: 'upcoming', {
}, { url: 'milestone_title=%23upcoming',
url: 'milestone_title=%23started', tokenKey: 'milestone',
tokenKey: 'milestone', value: 'upcoming',
value: 'started', },
}, { {
url: 'label_name[]=No+Label', url: 'milestone_title=%23started',
tokenKey: 'label', tokenKey: 'milestone',
value: 'none', value: 'started',
}]; },
{
url: 'label_name[]=No+Label',
tokenKey: 'label',
value: 'none',
},
];
const IssuableFilteredSearchTokenKeys = const IssuableFilteredSearchTokenKeys = new FilteredSearchTokenKeys(
new FilteredSearchTokenKeys(tokenKeys, alternativeTokenKeys, conditions); tokenKeys,
alternativeTokenKeys,
conditions,
);
export default IssuableFilteredSearchTokenKeys; export default IssuableFilteredSearchTokenKeys;

View file

@ -3,11 +3,7 @@ import RecentSearchesDropdownContent from './components/recent_searches_dropdown
import eventHub from './event_hub'; import eventHub from './event_hub';
class RecentSearchesRoot { class RecentSearchesRoot {
constructor( constructor(recentSearchesStore, recentSearchesService, wrapperElement) {
recentSearchesStore,
recentSearchesService,
wrapperElement,
) {
this.store = recentSearchesStore; this.store = recentSearchesStore;
this.service = recentSearchesService; this.service = recentSearchesService;
this.wrapperElement = wrapperElement; this.wrapperElement = wrapperElement;
@ -35,7 +31,9 @@ class RecentSearchesRoot {
components: { components: {
RecentSearchesDropdownContent, RecentSearchesDropdownContent,
}, },
data() { return state; }, data() {
return state;
},
template: ` template: `
<recent-searches-dropdown-content <recent-searches-dropdown-content
:items="recentSearches" :items="recentSearches"
@ -57,7 +55,6 @@ class RecentSearchesRoot {
this.vm.$destroy(); this.vm.$destroy();
} }
} }
} }
export default RecentSearchesRoot; export default RecentSearchesRoot;

View file

@ -2,11 +2,14 @@ import _ from 'underscore';
class RecentSearchesStore { class RecentSearchesStore {
constructor(initialState = {}, allowedKeys) { constructor(initialState = {}, allowedKeys) {
this.state = Object.assign({ this.state = Object.assign(
isLocalStorageAvailable: true, {
recentSearches: [], isLocalStorageAvailable: true,
allowedKeys, recentSearches: [],
}, initialState); allowedKeys,
},
initialState,
);
} }
addRecentSearch(newSearch) { addRecentSearch(newSearch) {