diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-17 11:59:07 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-17 11:59:07 +0000 |
commit | 8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch) | |
tree | 544930fb309b30317ae9797a9683768705d664c4 /app/assets/javascripts/search | |
parent | 4b1de649d0168371549608993deac953eb692019 (diff) | |
download | gitlab-ce-8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca.tar.gz |
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'app/assets/javascripts/search')
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)); |