Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/search')
-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/index.js4
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue2
-rw-r--r--app/assets/javascripts/search/store/actions.js22
-rw-r--r--app/assets/javascripts/search/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/search/store/mutations.js11
-rw-r--r--app/assets/javascripts/search/store/state.js2
-rw-r--r--app/assets/javascripts/search/topbar/components/group_filter.vue49
-rw-r--r--app/assets/javascripts/search/topbar/components/project_filter.vue52
-rw-r--r--app/assets/javascripts/search/topbar/components/searchable_dropdown.vue144
-rw-r--r--app/assets/javascripts/search/topbar/constants.js21
-rw-r--r--app/assets/javascripts/search/topbar/index.js44
14 files changed, 352 insertions, 165 deletions
diff --git a/app/assets/javascripts/search/group_filter/components/group_filter.vue b/app/assets/javascripts/search/group_filter/components/group_filter.vue
deleted file mode 100644
index 4b7963c5187..00000000000
--- a/app/assets/javascripts/search/group_filter/components/group_filter.vue
+++ /dev/null
@@ -1,124 +0,0 @@
-<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
deleted file mode 100644
index 9bd92eaa130..00000000000
--- a/app/assets/javascripts/search/group_filter/constants.js
+++ /dev/null
@@ -1,10 +0,0 @@
-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
deleted file mode 100644
index 9b009bc0305..00000000000
--- a/app/assets/javascripts/search/group_filter/index.js
+++ /dev/null
@@ -1,28 +0,0 @@
-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/index.js b/app/assets/javascripts/search/index.js
index 781a564d077..d2bb1ccfc44 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -1,7 +1,7 @@
import { queryToObject } from '~/lib/utils/url_utility';
import createStore from './store';
+import { initTopbar } from './topbar';
import { initSidebar } from './sidebar';
-import initGroupFilter from './group_filter';
export const initSearchApp = () => {
// Similar to url_utility.decodeUrlParameter
@@ -9,6 +9,6 @@ export const initSearchApp = () => {
const sanitizedSearch = window.location.search.replace(/\+/g, '%20');
const store = createStore({ query: queryToObject(sanitizedSearch) });
+ initTopbar(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
index aa11b2025f2..e233d18b716 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -26,7 +26,7 @@ export default {
<template>
<form
- class="gl-display-flex gl-flex-direction-column col-md-3 gl-mr-4 gl-mb-6 gl-mt-5"
+ class="search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4 gl-mb-6 gl-mt-5"
@submit.prevent="applyQuery"
>
<status-filter />
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index 447278aa223..082beb5930d 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -16,6 +16,28 @@ export const fetchGroups = ({ commit }, search) => {
});
};
+export const fetchProjects = ({ commit, state }, search) => {
+ commit(types.REQUEST_PROJECTS);
+ const groupId = state.query?.group_id;
+ const callback = data => {
+ if (data) {
+ commit(types.RECEIVE_PROJECTS_SUCCESS, data);
+ } else {
+ createFlash({ message: __('There was an error fetching projects') });
+ commit(types.RECEIVE_PROJECTS_ERROR);
+ }
+ };
+
+ if (groupId) {
+ Api.groupProjects(groupId, search, {}, callback);
+ } else {
+ // The .catch() is due to the API method not handling a rejection properly
+ Api.projects(search, { order_by: 'id' }, callback).catch(() => {
+ callback();
+ });
+ }
+};
+
export const setQuery = ({ commit }, { key, value }) => {
commit(types.SET_QUERY, { key, value });
};
diff --git a/app/assets/javascripts/search/store/mutation_types.js b/app/assets/javascripts/search/store/mutation_types.js
index 2482621d4d7..a6430b53c4f 100644
--- a/app/assets/javascripts/search/store/mutation_types.js
+++ b/app/assets/javascripts/search/store/mutation_types.js
@@ -2,4 +2,8 @@ export const REQUEST_GROUPS = 'REQUEST_GROUPS';
export const RECEIVE_GROUPS_SUCCESS = 'RECEIVE_GROUPS_SUCCESS';
export const RECEIVE_GROUPS_ERROR = 'RECEIVE_GROUPS_ERROR';
+export const REQUEST_PROJECTS = 'REQUEST_PROJECTS';
+export const RECEIVE_PROJECTS_SUCCESS = 'RECEIVE_PROJECTS_SUCCESS';
+export const RECEIVE_PROJECTS_ERROR = 'RECEIVE_PROJECTS_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
index e57850b870e..91d7cf66c8f 100644
--- a/app/assets/javascripts/search/store/mutations.js
+++ b/app/assets/javascripts/search/store/mutations.js
@@ -12,6 +12,17 @@ export default {
state.fetchingGroups = false;
state.groups = [];
},
+ [types.REQUEST_PROJECTS](state) {
+ state.fetchingProjects = true;
+ },
+ [types.RECEIVE_PROJECTS_SUCCESS](state, data) {
+ state.fetchingProjects = false;
+ state.projects = data;
+ },
+ [types.RECEIVE_PROJECTS_ERROR](state) {
+ state.fetchingProjects = false;
+ state.projects = [];
+ },
[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 70a8aab9998..9a0d61d0b93 100644
--- a/app/assets/javascripts/search/store/state.js
+++ b/app/assets/javascripts/search/store/state.js
@@ -2,5 +2,7 @@ const createState = ({ query }) => ({
query,
groups: [],
fetchingGroups: false,
+ projects: [],
+ fetchingProjects: false,
});
export default createState;
diff --git a/app/assets/javascripts/search/topbar/components/group_filter.vue b/app/assets/javascripts/search/topbar/components/group_filter.vue
new file mode 100644
index 00000000000..fce9ec17d23
--- /dev/null
+++ b/app/assets/javascripts/search/topbar/components/group_filter.vue
@@ -0,0 +1,49 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { isEmpty } from 'lodash';
+import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import SearchableDropdown from './searchable_dropdown.vue';
+import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
+
+export default {
+ name: 'GroupFilter',
+ components: {
+ SearchableDropdown,
+ },
+ props: {
+ initialData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ ...mapState(['groups', 'fetchingGroups']),
+ selectedGroup() {
+ return isEmpty(this.initialData) ? ANY_OPTION : this.initialData;
+ },
+ },
+ methods: {
+ ...mapActions(['fetchGroups']),
+ handleGroupChange(group) {
+ visitUrl(
+ setUrlParams({ [GROUP_DATA.queryParam]: group.id, [PROJECT_DATA.queryParam]: null }),
+ );
+ },
+ },
+ GROUP_DATA,
+};
+</script>
+
+<template>
+ <searchable-dropdown
+ :header-text="$options.GROUP_DATA.headerText"
+ :selected-display-value="$options.GROUP_DATA.selectedDisplayValue"
+ :items-display-value="$options.GROUP_DATA.itemsDisplayValue"
+ :loading="fetchingGroups"
+ :selected-item="selectedGroup"
+ :items="groups"
+ @search="fetchGroups"
+ @change="handleGroupChange"
+ />
+</template>
diff --git a/app/assets/javascripts/search/topbar/components/project_filter.vue b/app/assets/javascripts/search/topbar/components/project_filter.vue
new file mode 100644
index 00000000000..3f1f3848ac7
--- /dev/null
+++ b/app/assets/javascripts/search/topbar/components/project_filter.vue
@@ -0,0 +1,52 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import SearchableDropdown from './searchable_dropdown.vue';
+import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
+
+export default {
+ name: 'ProjectFilter',
+ components: {
+ SearchableDropdown,
+ },
+ props: {
+ initialData: {
+ type: Object,
+ required: false,
+ default: () => null,
+ },
+ },
+ computed: {
+ ...mapState(['projects', 'fetchingProjects']),
+ selectedProject() {
+ return this.initialData ? this.initialData : ANY_OPTION;
+ },
+ },
+ methods: {
+ ...mapActions(['fetchProjects']),
+ handleProjectChange(project) {
+ // This determines if we need to update the group filter or not
+ const queryParams = {
+ ...(project.namespace_id && { [GROUP_DATA.queryParam]: project.namespace_id }),
+ [PROJECT_DATA.queryParam]: project.id,
+ };
+
+ visitUrl(setUrlParams(queryParams));
+ },
+ },
+ PROJECT_DATA,
+};
+</script>
+
+<template>
+ <searchable-dropdown
+ :header-text="$options.PROJECT_DATA.headerText"
+ :selected-display-value="$options.PROJECT_DATA.selectedDisplayValue"
+ :items-display-value="$options.PROJECT_DATA.itemsDisplayValue"
+ :loading="fetchingProjects"
+ :selected-item="selectedProject"
+ :items="projects"
+ @search="fetchProjects"
+ @change="handleProjectChange"
+ />
+</template>
diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
new file mode 100644
index 00000000000..14577fd7d7a
--- /dev/null
+++ b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
@@ -0,0 +1,144 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+ GlIcon,
+ GlButton,
+ GlSkeletonLoader,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+
+import { ANY_OPTION } from '../constants';
+
+export default {
+ name: 'SearchableDropdown',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+ GlIcon,
+ GlButton,
+ GlSkeletonLoader,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ headerText: {
+ type: String,
+ required: false,
+ default: "__('Filter')",
+ },
+ selectedDisplayValue: {
+ type: String,
+ required: false,
+ default: 'name',
+ },
+ itemsDisplayValue: {
+ type: String,
+ required: false,
+ default: 'name',
+ },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ selectedItem: {
+ type: Object,
+ required: true,
+ },
+ items: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ searchText: '',
+ };
+ },
+ methods: {
+ isSelected(selected) {
+ return selected.id === this.selectedItem.id;
+ },
+ openDropdown() {
+ this.$emit('search', this.searchText);
+ },
+ resetDropdown() {
+ this.$emit('change', ANY_OPTION);
+ },
+ },
+ ANY_OPTION,
+};
+</script>
+
+<template>
+ <gl-dropdown
+ class="gl-w-full"
+ menu-class="gl-w-full!"
+ toggle-class="gl-text-truncate"
+ :header-text="headerText"
+ @show="$emit('search', searchText)"
+ @shown="$refs.searchBox.focusInput()"
+ >
+ <template #button-content>
+ <span class="dropdown-toggle-text gl-flex-grow-1 gl-text-truncate">
+ {{ selectedItem[selectedDisplayValue] }}
+ </span>
+ <gl-loading-icon v-if="loading" inline class="gl-mr-3" />
+ <gl-button
+ v-if="!isSelected($options.ANY_OPTION)"
+ v-gl-tooltip
+ name="clear"
+ category="tertiary"
+ :title="__('Clear')"
+ class="gl-p-0! gl-mr-2"
+ @keydown.enter.stop="resetDropdown"
+ @click.stop="resetDropdown"
+ >
+ <gl-icon name="clear" class="gl-text-gray-200! gl-hover-text-blue-800!" />
+ </gl-button>
+ <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
+ ref="searchBox"
+ v-model="searchText"
+ class="gl-m-3"
+ :debounce="500"
+ @input="$emit('search', searchText)"
+ />
+ <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="isSelected($options.ANY_OPTION)"
+ @click="resetDropdown"
+ >
+ {{ $options.ANY_OPTION.name }}
+ </gl-dropdown-item>
+ </div>
+ <div v-if="!loading">
+ <gl-dropdown-item
+ v-for="item in items"
+ :key="item.id"
+ :is-check-item="true"
+ :is-checked="isSelected(item)"
+ @click="$emit('change', item)"
+ >
+ {{ item[itemsDisplayValue] }}
+ </gl-dropdown-item>
+ </div>
+ <div v-if="loading" class="gl-mx-4 gl-mt-3">
+ <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/topbar/constants.js b/app/assets/javascripts/search/topbar/constants.js
new file mode 100644
index 00000000000..3944b2c8374
--- /dev/null
+++ b/app/assets/javascripts/search/topbar/constants.js
@@ -0,0 +1,21 @@
+import { __ } from '~/locale';
+
+export const ANY_OPTION = Object.freeze({
+ id: null,
+ name: __('Any'),
+ name_with_namespace: __('Any'),
+});
+
+export const GROUP_DATA = {
+ headerText: __('Filter results by group'),
+ queryParam: 'group_id',
+ selectedDisplayValue: 'name',
+ itemsDisplayValue: 'full_name',
+};
+
+export const PROJECT_DATA = {
+ headerText: __('Filter results by project'),
+ queryParam: 'project_id',
+ selectedDisplayValue: 'name_with_namespace',
+ itemsDisplayValue: 'name_with_namespace',
+};
diff --git a/app/assets/javascripts/search/topbar/index.js b/app/assets/javascripts/search/topbar/index.js
new file mode 100644
index 00000000000..024544148a0
--- /dev/null
+++ b/app/assets/javascripts/search/topbar/index.js
@@ -0,0 +1,44 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import GroupFilter from './components/group_filter.vue';
+import ProjectFilter from './components/project_filter.vue';
+
+Vue.use(Translate);
+
+const mountSearchableDropdown = (store, { id, component }) => {
+ const el = document.getElementById(id);
+
+ if (!el) {
+ return false;
+ }
+
+ let { initialData } = el.dataset;
+
+ initialData = JSON.parse(initialData);
+
+ return new Vue({
+ el,
+ store,
+ render(createElement) {
+ return createElement(component, {
+ props: {
+ initialData,
+ },
+ });
+ },
+ });
+};
+
+const searchableDropdowns = [
+ {
+ id: 'js-search-group-dropdown',
+ component: GroupFilter,
+ },
+ {
+ id: 'js-search-project-dropdown',
+ component: ProjectFilter,
+ },
+];
+
+export const initTopbar = store =>
+ searchableDropdowns.map(dropdown => mountSearchableDropdown(store, dropdown));