summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-01-25 12:25:58 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-01-25 12:25:58 +0000
commit21fb7a5e5b6be5c58845460e3a2f9de0c1cfab8c (patch)
tree0a83af79fb27c52ecfd168ddc24648d45943d952
parentc052f86b6b4d2428b62a2baac77aee4cc91fc2b1 (diff)
downloadgitlab-ce-21fb7a5e5b6be5c58845460e3a2f9de0c1cfab8c.tar.gz
Add latest changes from gitlab-org/gitlab@15-8-stable-ee
-rw-r--r--app/assets/javascripts/projects/commit/components/branches_dropdown.vue67
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue13
-rw-r--r--app/assets/javascripts/projects/commit/components/projects_dropdown.vue57
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb4
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js115
-rw-r--r--spec/frontend/projects/commit/components/projects_dropdown_spec.js64
6 files changed, 267 insertions, 53 deletions
diff --git a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
index a1fc3f1a731..a037e721677 100644
--- a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
@@ -1,5 +1,11 @@
<script>
-import { GlCollapsibleListbox } from '@gitlab/ui';
+import {
+ GlDropdown,
+ GlSearchBoxByType,
+ GlDropdownItem,
+ GlDropdownText,
+ GlLoadingIcon,
+} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import {
I18N_NO_RESULTS_MESSAGE,
@@ -10,7 +16,11 @@ import {
export default {
name: 'BranchesDropdown',
components: {
- GlCollapsibleListbox,
+ GlDropdown,
+ GlSearchBoxByType,
+ GlDropdownItem,
+ GlDropdownText,
+ GlLoadingIcon,
},
props: {
value: {
@@ -36,16 +46,13 @@ export default {
},
computed: {
...mapGetters(['joinedBranches']),
- ...mapState(['isFetching']),
+ ...mapState(['isFetching', 'branch', 'branches']),
filteredResults() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
return this.joinedBranches.filter((resultString) =>
resultString.toLowerCase().includes(lowerCasedSearchTerm),
);
},
- listboxItems() {
- return this.filteredResults.map((value) => ({ value, text: value }));
- },
},
watch: {
// Parent component can set the branch value (e.g. when the user selects a different project)
@@ -61,6 +68,10 @@ export default {
...mapActions(['fetchBranches']),
selectBranch(branch) {
this.$emit('selectBranch', branch);
+ this.searchTerm = branch; // enables isSelected to work as expected
+ },
+ isSelected(selectedBranch) {
+ return selectedBranch === this.branch;
},
searchTermChanged(value) {
this.searchTerm = value;
@@ -70,16 +81,36 @@ export default {
};
</script>
<template>
- <gl-collapsible-listbox
- :header-text="$options.i18n.branchHeaderTitle"
- :toggle-text="value"
- :items="listboxItems"
- searchable
- :search-placeholder="$options.i18n.branchSearchPlaceholder"
- :searching="isFetching"
- :selected="value"
- :no-results-text="$options.i18n.noResultsMessage"
- @search="searchTermChanged"
- @select="selectBranch"
- />
+ <gl-dropdown :text="value" :header-text="$options.i18n.branchHeaderTitle">
+ <gl-search-box-by-type
+ :value="searchTerm"
+ trim
+ autocomplete="off"
+ :debounce="250"
+ :placeholder="$options.i18n.branchSearchPlaceholder"
+ data-testid="dropdown-search-box"
+ @input="searchTermChanged"
+ />
+ <gl-dropdown-item
+ v-for="branch in filteredResults"
+ v-show="!isFetching"
+ :key="branch"
+ :name="branch"
+ :is-checked="isSelected(branch)"
+ is-check-item
+ data-testid="dropdown-item"
+ @click="selectBranch(branch)"
+ >
+ {{ branch }}
+ </gl-dropdown-item>
+ <gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon">
+ <gl-loading-icon size="sm" class="gl-mx-auto" />
+ </gl-dropdown-text>
+ <gl-dropdown-text
+ v-if="!filteredResults.length && !isFetching"
+ data-testid="empty-result-message"
+ >
+ <span class="gl-text-gray-500">{{ $options.i18n.noResultsMessage }}</span>
+ </gl-dropdown-text>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index b31ba4a100c..1febe8ceaab 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -141,7 +141,11 @@ export default {
:value="targetProjectId"
/>
- <projects-dropdown :value="targetProjectName" @selectProject="setSelectedProject" />
+ <projects-dropdown
+ class="gl-w-half"
+ :value="targetProjectName"
+ @selectProject="setSelectedProject"
+ />
</gl-form-group>
<gl-form-group
@@ -151,7 +155,12 @@ export default {
>
<input id="start_branch" type="hidden" name="start_branch" :value="branch" />
- <branches-dropdown :value="branch" :blanked="isRevert" @selectBranch="setBranch" />
+ <branches-dropdown
+ class="gl-w-half"
+ :value="branch"
+ :blanked="isRevert"
+ @selectBranch="setBranch"
+ />
</gl-form-group>
<gl-form-checkbox
diff --git a/app/assets/javascripts/projects/commit/components/projects_dropdown.vue b/app/assets/javascripts/projects/commit/components/projects_dropdown.vue
index d43f5b99e2c..6288bcdaad0 100644
--- a/app/assets/javascripts/projects/commit/components/projects_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/projects_dropdown.vue
@@ -1,5 +1,5 @@
<script>
-import { GlCollapsibleListbox } from '@gitlab/ui';
+import { GlDropdown, GlSearchBoxByType, GlDropdownItem, GlDropdownText } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import {
I18N_NO_RESULTS_MESSAGE,
@@ -10,7 +10,10 @@ import {
export default {
name: 'ProjectsDropdown',
components: {
- GlCollapsibleListbox,
+ GlDropdown,
+ GlSearchBoxByType,
+ GlDropdownItem,
+ GlDropdownText,
},
props: {
value: {
@@ -38,20 +41,17 @@ export default {
project.name.toLowerCase().includes(lowerCasedFilterTerm),
);
},
- listboxItems() {
- return this.filteredResults.map(({ id, name }) => ({ value: id, text: name }));
- },
selectedProject() {
return this.sortedProjects.find((project) => project.id === this.targetProjectId) || {};
},
},
methods: {
- selectProject(value) {
- this.$emit('selectProject', value);
-
- // when we select a project, we want the dropdown to filter to the selected project
- const project = this.listboxItems.find((x) => x.value === value);
- this.filterTerm = project?.text || '';
+ selectProject(project) {
+ this.$emit('selectProject', project.id);
+ this.filterTerm = project.name; // when we select a project, we want the dropdown to filter to the selected project
+ },
+ isSelected(selectedProject) {
+ return selectedProject === this.selectedProject;
},
filterTermChanged(value) {
this.filterTerm = value;
@@ -60,15 +60,28 @@ export default {
};
</script>
<template>
- <gl-collapsible-listbox
- :header-text="$options.i18n.projectHeaderTitle"
- :items="listboxItems"
- searchable
- :search-placeholder="$options.i18n.projectSearchPlaceholder"
- :selected="selectedProject.id"
- :toggle-text="selectedProject.name"
- :no-results-text="$options.i18n.noResultsMessage"
- @search="filterTermChanged"
- @select="selectProject"
- />
+ <gl-dropdown :text="selectedProject.name" :header-text="$options.i18n.projectHeaderTitle">
+ <gl-search-box-by-type
+ :value="filterTerm"
+ trim
+ autocomplete="off"
+ :placeholder="$options.i18n.projectSearchPlaceholder"
+ data-testid="dropdown-search-box"
+ @input="filterTermChanged"
+ />
+ <gl-dropdown-item
+ v-for="project in filteredResults"
+ :key="project.name"
+ :name="project.name"
+ :is-checked="isSelected(project)"
+ is-check-item
+ data-testid="dropdown-item"
+ @click="selectProject(project)"
+ >
+ {{ project.name }}
+ </gl-dropdown-item>
+ <gl-dropdown-text v-if="!filteredResults.length" data-testid="empty-result-message">
+ <span class="gl-text-gray-500">{{ $options.i18n.noResultsMessage }}</span>
+ </gl-dropdown-text>
+ </gl-dropdown>
</template>
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 4b9b692b652..dc8b84283a1 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -78,9 +78,9 @@ RSpec.describe 'Cherry-pick Commits', :js, feature_category: :source_code_manage
end
page.within("#{modal_selector} .dropdown-menu") do
- fill_in 'Search branches', with: 'feature'
+ find('[data-testid="dropdown-search-box"]').set('feature')
wait_for_requests
- find('.gl-dropdown-item-text-wrapper', exact_text: 'feature').click
+ click_button 'feature'
end
submit_cherry_pick
diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
index 7334e007e18..a84dd246f5d 100644
--- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
@@ -1,8 +1,9 @@
-import { GlCollapsibleListbox } from '@gitlab/ui';
+import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
Vue.use(Vuex);
@@ -33,7 +34,12 @@ describe('BranchesDropdown', () => {
}),
);
};
- const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
+
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
+ const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
+ const findNoResults = () => wrapper.findByTestId('empty-result-message');
+ const findLoading = () => wrapper.findByTestId('dropdown-text-loading-icon');
afterEach(() => {
wrapper.destroy();
@@ -49,6 +55,72 @@ describe('BranchesDropdown', () => {
it('invokes fetchBranches', () => {
expect(spyFetchBranches).toHaveBeenCalled();
});
+
+ describe('with a value but visually blanked', () => {
+ beforeEach(() => {
+ createComponent({ value: '_main_', blanked: true }, { branch: '_main_' });
+ });
+
+ it('renders all branches', () => {
+ expect(findAllDropdownItems()).toHaveLength(3);
+ expect(findDropdownItemByIndex(0).text()).toBe('_main_');
+ expect(findDropdownItemByIndex(1).text()).toBe('_branch_1_');
+ expect(findDropdownItemByIndex(2).text()).toBe('_branch_2_');
+ });
+
+ it('selects the active branch', () => {
+ expect(wrapper.vm.isSelected('_main_')).toBe(true);
+ });
+ });
+ });
+
+ describe('Loading states', () => {
+ it('shows loading icon while fetching', () => {
+ createComponent({ value: '' }, { isFetching: true });
+
+ expect(findLoading().isVisible()).toBe(true);
+ });
+
+ it('does not show loading icon', () => {
+ createComponent({ value: '' });
+
+ expect(findLoading().isVisible()).toBe(false);
+ });
+ });
+
+ describe('No branches found', () => {
+ beforeEach(() => {
+ createComponent({ value: '_non_existent_branch_' });
+ });
+
+ it('renders empty results message', () => {
+ expect(findNoResults().text()).toBe('No matching results');
+ });
+
+ it('shows GlSearchBoxByType with default attributes', () => {
+ expect(findSearchBoxByType().exists()).toBe(true);
+ expect(findSearchBoxByType().vm.$attrs).toMatchObject({
+ placeholder: 'Search branches',
+ debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
+ });
+ });
+ });
+
+ describe('Search term is empty', () => {
+ beforeEach(() => {
+ createComponent({ value: '' });
+ });
+
+ it('renders all branches when search term is empty', () => {
+ expect(findAllDropdownItems()).toHaveLength(3);
+ expect(findDropdownItemByIndex(0).text()).toBe('_main_');
+ expect(findDropdownItemByIndex(1).text()).toBe('_branch_1_');
+ expect(findDropdownItemByIndex(2).text()).toBe('_branch_2_');
+ });
+
+ it('should not be selected on the inactive branch', () => {
+ expect(wrapper.vm.isSelected('_main_')).toBe(false);
+ });
});
describe('When searching', () => {
@@ -59,7 +131,7 @@ describe('BranchesDropdown', () => {
it('invokes fetchBranches', async () => {
const spy = jest.spyOn(wrapper.vm, 'fetchBranches');
- findDropdown().vm.$emit('search', '_anything_');
+ findSearchBoxByType().vm.$emit('input', '_anything_');
await nextTick();
@@ -68,13 +140,46 @@ describe('BranchesDropdown', () => {
});
});
+ describe('Branches found', () => {
+ beforeEach(() => {
+ createComponent({ value: '_branch_1_' }, { branch: '_branch_1_' });
+ });
+
+ it('renders only the branch searched for', () => {
+ expect(findAllDropdownItems()).toHaveLength(1);
+ expect(findDropdownItemByIndex(0).text()).toBe('_branch_1_');
+ });
+
+ it('should not display empty results message', () => {
+ expect(findNoResults().exists()).toBe(false);
+ });
+
+ it('should signify this branch is selected', () => {
+ expect(wrapper.vm.isSelected('_branch_1_')).toBe(true);
+ });
+
+ it('should signify the branch is not selected', () => {
+ expect(wrapper.vm.isSelected('_not_selected_branch_')).toBe(false);
+ });
+
+ describe('Custom events', () => {
+ it('should emit selectBranch if an branch is clicked', () => {
+ findDropdownItemByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('selectBranch')).toEqual([['_branch_1_']]);
+ expect(wrapper.vm.searchTerm).toBe('_branch_1_');
+ });
+ });
+ });
+
describe('Case insensitive for search term', () => {
beforeEach(() => {
createComponent({ value: '_BrAnCh_1_' });
});
- it('returns only the branch searched for', () => {
- expect(findDropdown().props('items')).toEqual([{ text: '_branch_1_', value: '_branch_1_' }]);
+ it('renders only the branch searched for', () => {
+ expect(findAllDropdownItems()).toHaveLength(1);
+ expect(findDropdownItemByIndex(0).text()).toBe('_branch_1_');
});
});
});
diff --git a/spec/frontend/projects/commit/components/projects_dropdown_spec.js b/spec/frontend/projects/commit/components/projects_dropdown_spec.js
index 0e213ff388a..bb20918e0cd 100644
--- a/spec/frontend/projects/commit/components/projects_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/projects_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlCollapsibleListbox } from '@gitlab/ui';
+import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
@@ -35,23 +35,78 @@ describe('ProjectsDropdown', () => {
);
};
- const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
+ const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
+ const findNoResults = () => wrapper.findByTestId('empty-result-message');
afterEach(() => {
wrapper.destroy();
spyFetchProjects.mockReset();
});
+ describe('No projects found', () => {
+ beforeEach(() => {
+ createComponent('_non_existent_project_');
+ });
+
+ it('renders empty results message', () => {
+ expect(findNoResults().text()).toBe('No matching results');
+ });
+
+ it('shows GlSearchBoxByType with default attributes', () => {
+ expect(findSearchBoxByType().exists()).toBe(true);
+ expect(findSearchBoxByType().vm.$attrs).toMatchObject({
+ placeholder: 'Search projects',
+ });
+ });
+ });
+
+ describe('Search term is empty', () => {
+ beforeEach(() => {
+ createComponent('');
+ });
+
+ it('renders all projects when search term is empty', () => {
+ expect(findAllDropdownItems()).toHaveLength(3);
+ expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
+ expect(findDropdownItemByIndex(1).text()).toBe('_project_2_');
+ expect(findDropdownItemByIndex(2).text()).toBe('_project_3_');
+ });
+
+ it('should not be selected on the inactive project', () => {
+ expect(wrapper.vm.isSelected('_project_1_')).toBe(false);
+ });
+ });
+
describe('Projects found', () => {
beforeEach(() => {
createComponent('_project_1_', { targetProjectId: '1' });
});
+ it('renders only the project searched for', () => {
+ expect(findAllDropdownItems()).toHaveLength(1);
+ expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
+ });
+
+ it('should not display empty results message', () => {
+ expect(findNoResults().exists()).toBe(false);
+ });
+
+ it('should signify this project is selected', () => {
+ expect(findDropdownItemByIndex(0).props('isChecked')).toBe(true);
+ });
+
+ it('should signify the project is not selected', () => {
+ expect(wrapper.vm.isSelected('_not_selected_project_')).toBe(false);
+ });
+
describe('Custom events', () => {
it('should emit selectProject if a project is clicked', () => {
- findDropdown().vm.$emit('select', '1');
+ findDropdownItemByIndex(0).vm.$emit('click');
expect(wrapper.emitted('selectProject')).toEqual([['1']]);
+ expect(wrapper.vm.filterTerm).toBe('_project_1_');
});
});
});
@@ -62,7 +117,8 @@ describe('ProjectsDropdown', () => {
});
it('renders only the project searched for', () => {
- expect(findDropdown().props('items')).toEqual([{ text: '_project_1_', value: '1' }]);
+ expect(findAllDropdownItems()).toHaveLength(1);
+ expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
});
});
});