summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/search
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 08:27:35 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 08:27:35 +0000
commit7e9c479f7de77702622631cff2628a9c8dcbc627 (patch)
treec8f718a08e110ad7e1894510980d2155a6549197 /app/assets/javascripts/search
parente852b0ae16db4052c1c567d9efa4facc81146e88 (diff)
downloadgitlab-ce-7e9c479f7de77702622631cff2628a9c8dcbc627.tar.gz
Add latest changes from gitlab-org/gitlab@13-6-stable-eev13.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/search')
-rw-r--r--app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue100
-rw-r--r--app/assets/javascripts/search/dropdown_filter/index.js38
-rw-r--r--app/assets/javascripts/search/group_filter/components/group_filter.vue124
-rw-r--r--app/assets/javascripts/search/group_filter/constants.js10
-rw-r--r--app/assets/javascripts/search/group_filter/index.js28
-rw-r--r--app/assets/javascripts/search/highlight_blob_search_result.js15
-rw-r--r--app/assets/javascripts/search/index.js13
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue41
-rw-r--r--app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue26
-rw-r--r--app/assets/javascripts/search/sidebar/components/radio_filter.vue68
-rw-r--r--app/assets/javascripts/search/sidebar/components/status_filter.vue26
-rw-r--r--app/assets/javascripts/search/sidebar/constants/confidential_filter_data.js (renamed from app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js)2
-rw-r--r--app/assets/javascripts/search/sidebar/constants/state_filter_data.js (renamed from app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js)2
-rw-r--r--app/assets/javascripts/search/sidebar/index.js19
-rw-r--r--app/assets/javascripts/search/store/actions.js29
-rw-r--r--app/assets/javascripts/search/store/index.js4
-rw-r--r--app/assets/javascripts/search/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/search/store/mutations.js18
-rw-r--r--app/assets/javascripts/search/store/state.js2
19 files changed, 426 insertions, 144 deletions
diff --git a/app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue b/app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue
deleted file mode 100644
index b6e2dd46358..00000000000
--- a/app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue
+++ /dev/null
@@ -1,100 +0,0 @@
-<script>
-import { mapState } from 'vuex';
-import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
-import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
-import { sprintf, s__ } from '~/locale';
-
-export default {
- name: 'DropdownFilter',
- components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- },
- props: {
- filterData: {
- type: Object,
- required: true,
- },
- },
- computed: {
- ...mapState(['query']),
- scope() {
- return this.query.scope;
- },
- supportedScopes() {
- return Object.values(this.filterData.scopes);
- },
- initialFilter() {
- return this.query[this.filterData.filterParam];
- },
- filter() {
- return this.initialFilter || this.filterData.filters.ANY.value;
- },
- filtersArray() {
- return this.filterData.filterByScope[this.scope];
- },
- selectedFilter: {
- get() {
- if (this.filtersArray.some(({ value }) => value === this.filter)) {
- return this.filter;
- }
-
- return this.filterData.filters.ANY.value;
- },
- set(filter) {
- visitUrl(setUrlParams({ [this.filterData.filterParam]: filter }));
- },
- },
- selectedFilterText() {
- const f = this.filtersArray.find(({ value }) => value === this.selectedFilter);
- if (!f || f === this.filterData.filters.ANY) {
- return sprintf(s__('Any %{header}'), { header: this.filterData.header });
- }
-
- return f.label;
- },
- showDropdown() {
- return this.supportedScopes.includes(this.scope);
- },
- },
- methods: {
- dropDownItemClass(filter) {
- return {
- 'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2':
- filter === this.filterData.filters.ANY,
- };
- },
- isFilterSelected(filter) {
- return filter === this.selectedFilter;
- },
- handleFilterChange(filter) {
- this.selectedFilter = filter;
- },
- },
-};
-</script>
-
-<template>
- <gl-dropdown
- v-if="showDropdown"
- :text="selectedFilterText"
- class="col-3 gl-pt-4 gl-pl-0 gl-pr-0 gl-mr-4"
- menu-class="gl-w-full! gl-pl-0"
- >
- <header class="gl-text-center gl-font-weight-bold gl-font-lg">
- {{ filterData.header }}
- </header>
- <gl-dropdown-divider />
- <gl-dropdown-item
- v-for="f in filtersArray"
- :key="f.value"
- :is-check-item="true"
- :is-checked="isFilterSelected(f.value)"
- :class="dropDownItemClass(f)"
- @click="handleFilterChange(f.value)"
- >
- {{ f.label }}
- </gl-dropdown-item>
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/search/dropdown_filter/index.js b/app/assets/javascripts/search/dropdown_filter/index.js
deleted file mode 100644
index e5e0745d990..00000000000
--- a/app/assets/javascripts/search/dropdown_filter/index.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
-import DropdownFilter from './components/dropdown_filter.vue';
-import stateFilterData from './constants/state_filter_data';
-import confidentialFilterData from './constants/confidential_filter_data';
-
-Vue.use(Translate);
-
-const mountDropdownFilter = (store, { id, filterData }) => {
- const el = document.getElementById(id);
-
- if (!el) return false;
-
- return new Vue({
- el,
- store,
- render(createElement) {
- return createElement(DropdownFilter, {
- props: {
- filterData,
- },
- });
- },
- });
-};
-
-const dropdownFilters = [
- {
- id: 'js-search-filter-by-state',
- filterData: stateFilterData,
- },
- {
- id: 'js-search-filter-by-confidential',
- filterData: confidentialFilterData,
- },
-];
-
-export default store => [...dropdownFilters].map(filter => mountDropdownFilter(store, filter));
diff --git a/app/assets/javascripts/search/group_filter/components/group_filter.vue b/app/assets/javascripts/search/group_filter/components/group_filter.vue
new file mode 100644
index 00000000000..4b7963c5187
--- /dev/null
+++ b/app/assets/javascripts/search/group_filter/components/group_filter.vue
@@ -0,0 +1,124 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+ GlIcon,
+ GlSkeletonLoader,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { isEmpty } from 'lodash';
+import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import { ANY_GROUP, GROUP_QUERY_PARAM, PROJECT_QUERY_PARAM } from '../constants';
+
+export default {
+ name: 'GroupFilter',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+ GlIcon,
+ GlSkeletonLoader,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ initialGroup: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ groupSearch: '',
+ };
+ },
+ computed: {
+ ...mapState(['groups', 'fetchingGroups']),
+ selectedGroup: {
+ get() {
+ return isEmpty(this.initialGroup) ? ANY_GROUP : this.initialGroup;
+ },
+ set(group) {
+ visitUrl(setUrlParams({ [GROUP_QUERY_PARAM]: group.id, [PROJECT_QUERY_PARAM]: null }));
+ },
+ },
+ },
+ methods: {
+ ...mapActions(['fetchGroups']),
+ isGroupSelected(group) {
+ return group.id === this.selectedGroup.id;
+ },
+ handleGroupChange(group) {
+ this.selectedGroup = group;
+ },
+ },
+ ANY_GROUP,
+};
+</script>
+
+<template>
+ <gl-dropdown
+ ref="groupFilter"
+ class="gl-w-full"
+ menu-class="gl-w-full!"
+ toggle-class="gl-text-truncate gl-reset-line-height!"
+ :header-text="__('Filter results by group')"
+ @show="fetchGroups(groupSearch)"
+ >
+ <template #button-content>
+ <span class="dropdown-toggle-text gl-flex-grow-1 gl-text-truncate">
+ {{ selectedGroup.name }}
+ </span>
+ <gl-loading-icon v-if="fetchingGroups" inline class="mr-2" />
+ <gl-icon
+ v-if="!isGroupSelected($options.ANY_GROUP)"
+ v-gl-tooltip
+ name="clear"
+ :title="__('Clear')"
+ class="gl-text-gray-200! gl-hover-text-blue-800!"
+ @click.stop="handleGroupChange($options.ANY_GROUP)"
+ />
+ <gl-icon name="chevron-down" />
+ </template>
+ <div class="gl-sticky gl-top-0 gl-z-index-1 gl-bg-white">
+ <gl-search-box-by-type
+ v-model="groupSearch"
+ class="m-2"
+ :debounce="500"
+ @input="fetchGroups"
+ />
+ <gl-dropdown-item
+ class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2"
+ :is-check-item="true"
+ :is-checked="isGroupSelected($options.ANY_GROUP)"
+ @click="handleGroupChange($options.ANY_GROUP)"
+ >
+ {{ $options.ANY_GROUP.name }}
+ </gl-dropdown-item>
+ </div>
+ <div v-if="!fetchingGroups">
+ <gl-dropdown-item
+ v-for="group in groups"
+ :key="group.id"
+ :is-check-item="true"
+ :is-checked="isGroupSelected(group)"
+ @click="handleGroupChange(group)"
+ >
+ {{ group.full_name }}
+ </gl-dropdown-item>
+ </div>
+ <div v-if="fetchingGroups" class="mx-3 mt-2">
+ <gl-skeleton-loader :height="100">
+ <rect y="0" width="90%" height="20" rx="4" />
+ <rect y="40" width="70%" height="20" rx="4" />
+ <rect y="80" width="80%" height="20" rx="4" />
+ </gl-skeleton-loader>
+ </div>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/search/group_filter/constants.js b/app/assets/javascripts/search/group_filter/constants.js
new file mode 100644
index 00000000000..9bd92eaa130
--- /dev/null
+++ b/app/assets/javascripts/search/group_filter/constants.js
@@ -0,0 +1,10 @@
+import { __ } from '~/locale';
+
+export const ANY_GROUP = Object.freeze({
+ id: null,
+ name: __('Any'),
+});
+
+export const GROUP_QUERY_PARAM = 'group_id';
+
+export const PROJECT_QUERY_PARAM = 'project_id';
diff --git a/app/assets/javascripts/search/group_filter/index.js b/app/assets/javascripts/search/group_filter/index.js
new file mode 100644
index 00000000000..9b009bc0305
--- /dev/null
+++ b/app/assets/javascripts/search/group_filter/index.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import GroupFilter from './components/group_filter.vue';
+
+Vue.use(Translate);
+
+export default store => {
+ let initialGroup;
+ const el = document.getElementById('js-search-group-dropdown');
+
+ const { initialGroupData } = el.dataset;
+
+ initialGroup = JSON.parse(initialGroupData);
+ initialGroup = convertObjectPropsToCamelCase(initialGroup, { deep: true });
+
+ return new Vue({
+ el,
+ store,
+ render(createElement) {
+ return createElement(GroupFilter, {
+ props: {
+ initialGroup,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/search/highlight_blob_search_result.js b/app/assets/javascripts/search/highlight_blob_search_result.js
new file mode 100644
index 00000000000..e17c87735b4
--- /dev/null
+++ b/app/assets/javascripts/search/highlight_blob_search_result.js
@@ -0,0 +1,15 @@
+export default () => {
+ const highlightLineClass = 'hll';
+ const contentBody = document.getElementById('content-body');
+ const searchTerm = contentBody.querySelector('.js-search-input').value.toLowerCase();
+ const blobs = contentBody.querySelectorAll('.blob-result');
+
+ blobs.forEach(blob => {
+ const lines = blob.querySelectorAll('.line');
+ lines.forEach(line => {
+ if (line.textContent.toLowerCase().includes(searchTerm)) {
+ line.classList.add(highlightLineClass);
+ }
+ });
+ });
+};
diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js
index 780d3ff0d25..781a564d077 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -1,9 +1,14 @@
import { queryToObject } from '~/lib/utils/url_utility';
import createStore from './store';
-import initDropdownFilters from './dropdown_filter';
+import { initSidebar } from './sidebar';
+import initGroupFilter from './group_filter';
-export default () => {
- const store = createStore({ query: queryToObject(window.location.search) });
+export const initSearchApp = () => {
+ // Similar to url_utility.decodeUrlParameter
+ // Our query treats + as %20. This replaces the query + symbols with %20.
+ const sanitizedSearch = window.location.search.replace(/\+/g, '%20');
+ const store = createStore({ query: queryToObject(sanitizedSearch) });
- initDropdownFilters(store);
+ initSidebar(store);
+ initGroupFilter(store);
};
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
new file mode 100644
index 00000000000..aa11b2025f2
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -0,0 +1,41 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import { GlButton, GlLink } from '@gitlab/ui';
+import StatusFilter from './status_filter.vue';
+import ConfidentialityFilter from './confidentiality_filter.vue';
+
+export default {
+ name: 'GlobalSearchSidebar',
+ components: {
+ GlButton,
+ GlLink,
+ StatusFilter,
+ ConfidentialityFilter,
+ },
+ computed: {
+ ...mapState(['query']),
+ showReset() {
+ return this.query.state || this.query.confidential;
+ },
+ },
+ methods: {
+ ...mapActions(['applyQuery', 'resetQuery']),
+ },
+};
+</script>
+
+<template>
+ <form
+ class="gl-display-flex gl-flex-direction-column col-md-3 gl-mr-4 gl-mb-6 gl-mt-5"
+ @submit.prevent="applyQuery"
+ >
+ <status-filter />
+ <confidentiality-filter />
+ <div class="gl-display-flex gl-align-items-center gl-mt-3">
+ <gl-button variant="success" type="submit">{{ __('Apply') }}</gl-button>
+ <gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{
+ __('Reset filters')
+ }}</gl-link>
+ </div>
+ </form>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
new file mode 100644
index 00000000000..38dccb9675d
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
@@ -0,0 +1,26 @@
+<script>
+import { mapState } from 'vuex';
+import { confidentialFilterData } from '../constants/confidential_filter_data';
+import RadioFilter from './radio_filter.vue';
+
+export default {
+ name: 'ConfidentialityFilter',
+ components: {
+ RadioFilter,
+ },
+ computed: {
+ ...mapState(['query']),
+ showDropdown() {
+ return Object.values(confidentialFilterData.scopes).includes(this.query.scope);
+ },
+ },
+ confidentialFilterData,
+};
+</script>
+
+<template>
+ <div v-if="showDropdown">
+ <radio-filter :filter-data="$options.confidentialFilterData" />
+ <hr class="gl-my-5 gl-border-gray-100" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/radio_filter.vue b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
new file mode 100644
index 00000000000..b27c4e26fb5
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
@@ -0,0 +1,68 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
+import { sprintf, s__ } from '~/locale';
+
+export default {
+ name: 'RadioFilter',
+ components: {
+ GlFormRadioGroup,
+ GlFormRadio,
+ },
+ props: {
+ filterData: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['query']),
+ ANY() {
+ return this.filterData.filters.ANY;
+ },
+ scope() {
+ return this.query.scope;
+ },
+ initialFilter() {
+ return this.query[this.filterData.filterParam];
+ },
+ filter() {
+ return this.initialFilter || this.ANY.value;
+ },
+ filtersArray() {
+ return this.filterData.filterByScope[this.scope];
+ },
+ selectedFilter: {
+ get() {
+ if (this.filtersArray.some(({ value }) => value === this.filter)) {
+ return this.filter;
+ }
+
+ return this.ANY.value;
+ },
+ set(value) {
+ this.setQuery({ key: this.filterData.filterParam, value });
+ },
+ },
+ },
+ methods: {
+ ...mapActions(['setQuery']),
+ radioLabel(filter) {
+ return filter.value === this.ANY.value
+ ? sprintf(s__('Any %{header}'), { header: this.filterData.header.toLowerCase() })
+ : filter.label;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h5 class="gl-mt-0">{{ filterData.header }}</h5>
+ <gl-form-radio-group v-model="selectedFilter">
+ <gl-form-radio v-for="f in filtersArray" :key="f.value" :value="f.value">
+ {{ radioLabel(f) }}
+ </gl-form-radio>
+ </gl-form-radio-group>
+ </div>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/status_filter.vue b/app/assets/javascripts/search/sidebar/components/status_filter.vue
new file mode 100644
index 00000000000..5cec2090906
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/status_filter.vue
@@ -0,0 +1,26 @@
+<script>
+import { mapState } from 'vuex';
+import { stateFilterData } from '../constants/state_filter_data';
+import RadioFilter from './radio_filter.vue';
+
+export default {
+ name: 'StatusFilter',
+ components: {
+ RadioFilter,
+ },
+ computed: {
+ ...mapState(['query']),
+ showDropdown() {
+ return Object.values(stateFilterData.scopes).includes(this.query.scope);
+ },
+ },
+ stateFilterData,
+};
+</script>
+
+<template>
+ <div v-if="showDropdown">
+ <radio-filter :filter-data="$options.stateFilterData" />
+ <hr class="gl-my-5 gl-border-gray-100" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js b/app/assets/javascripts/search/sidebar/constants/confidential_filter_data.js
index b29daca89cb..ecb63ed9eea 100644
--- a/app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js
+++ b/app/assets/javascripts/search/sidebar/constants/confidential_filter_data.js
@@ -27,7 +27,7 @@ const filterByScope = {
const filterParam = 'confidential';
-export default {
+export const confidentialFilterData = {
header,
filters,
scopes,
diff --git a/app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js b/app/assets/javascripts/search/sidebar/constants/state_filter_data.js
index 0b93aa0be29..7c9a029ffe4 100644
--- a/app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js
+++ b/app/assets/javascripts/search/sidebar/constants/state_filter_data.js
@@ -33,7 +33,7 @@ const filterByScope = {
const filterParam = 'state';
-export default {
+export const stateFilterData = {
header,
filters,
scopes,
diff --git a/app/assets/javascripts/search/sidebar/index.js b/app/assets/javascripts/search/sidebar/index.js
new file mode 100644
index 00000000000..6419e8ac2c6
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/index.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import GlobalSearchSidebar from './components/app.vue';
+
+Vue.use(Translate);
+
+export const initSidebar = store => {
+ const el = document.getElementById('js-search-sidebar');
+
+ if (!el) return false;
+
+ return new Vue({
+ el,
+ store,
+ render(createElement) {
+ return createElement(GlobalSearchSidebar);
+ },
+ });
+};
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
new file mode 100644
index 00000000000..447278aa223
--- /dev/null
+++ b/app/assets/javascripts/search/store/actions.js
@@ -0,0 +1,29 @@
+import Api from '~/api';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import * as types from './mutation_types';
+
+export const fetchGroups = ({ commit }, search) => {
+ commit(types.REQUEST_GROUPS);
+ Api.groups(search)
+ .then(data => {
+ commit(types.RECEIVE_GROUPS_SUCCESS, data);
+ })
+ .catch(() => {
+ createFlash({ message: __('There was a problem fetching groups.') });
+ commit(types.RECEIVE_GROUPS_ERROR);
+ });
+};
+
+export const setQuery = ({ commit }, { key, value }) => {
+ commit(types.SET_QUERY, { key, value });
+};
+
+export const applyQuery = ({ state }) => {
+ visitUrl(setUrlParams({ ...state.query, page: null }));
+};
+
+export const resetQuery = ({ state }) => {
+ visitUrl(setUrlParams({ ...state.query, page: null, state: null, confidential: null }));
+};
diff --git a/app/assets/javascripts/search/store/index.js b/app/assets/javascripts/search/store/index.js
index 10cfb647a92..e0a7e488f9f 100644
--- a/app/assets/javascripts/search/store/index.js
+++ b/app/assets/javascripts/search/store/index.js
@@ -1,10 +1,14 @@
import Vue from 'vue';
import Vuex from 'vuex';
+import * as actions from './actions';
+import mutations from './mutations';
import createState from './state';
Vue.use(Vuex);
export const getStoreConfig = ({ query }) => ({
+ actions,
+ mutations,
state: createState({ query }),
});
diff --git a/app/assets/javascripts/search/store/mutation_types.js b/app/assets/javascripts/search/store/mutation_types.js
new file mode 100644
index 00000000000..2482621d4d7
--- /dev/null
+++ b/app/assets/javascripts/search/store/mutation_types.js
@@ -0,0 +1,5 @@
+export const REQUEST_GROUPS = 'REQUEST_GROUPS';
+export const RECEIVE_GROUPS_SUCCESS = 'RECEIVE_GROUPS_SUCCESS';
+export const RECEIVE_GROUPS_ERROR = 'RECEIVE_GROUPS_ERROR';
+
+export const SET_QUERY = 'SET_QUERY';
diff --git a/app/assets/javascripts/search/store/mutations.js b/app/assets/javascripts/search/store/mutations.js
new file mode 100644
index 00000000000..e57850b870e
--- /dev/null
+++ b/app/assets/javascripts/search/store/mutations.js
@@ -0,0 +1,18 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.REQUEST_GROUPS](state) {
+ state.fetchingGroups = true;
+ },
+ [types.RECEIVE_GROUPS_SUCCESS](state, data) {
+ state.fetchingGroups = false;
+ state.groups = data;
+ },
+ [types.RECEIVE_GROUPS_ERROR](state) {
+ state.fetchingGroups = false;
+ state.groups = [];
+ },
+ [types.SET_QUERY](state, { key, value }) {
+ state.query[key] = value;
+ },
+};
diff --git a/app/assets/javascripts/search/store/state.js b/app/assets/javascripts/search/store/state.js
index 9115a613767..70a8aab9998 100644
--- a/app/assets/javascripts/search/store/state.js
+++ b/app/assets/javascripts/search/store/state.js
@@ -1,4 +1,6 @@
const createState = ({ query }) => ({
query,
+ groups: [],
+ fetchingGroups: false,
});
export default createState;