diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-05 00:14:01 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-05 00:14:01 +0300 |
commit | f4056ff4474daf3da66ceaf4473306b0c4652897 (patch) | |
tree | ba6ca64ede0a7ec8d2c6971c7f3f5b3d8ab5f81d | |
parent | 795b6eb292706d577c13556a3583897f082dda6e (diff) |
Add latest changes from gitlab-org/gitlab@master
38 files changed, 566 insertions, 652 deletions
@@ -399,11 +399,11 @@ group :development do gem 'listen', '~> 3.7' # rubocop:todo Gemfile/MissingFeatureCategory - gem 'ruby-lsp', "~> 0.12.3", feature_category: :tooling + gem 'ruby-lsp', "~> 0.13.0", require: false, feature_category: :tooling - gem 'ruby-lsp-rails', "~> 0.2.7", feature_category: :tooling + gem 'ruby-lsp-rails', "~> 0.2.8", feature_category: :tooling - gem 'ruby-lsp-rspec', "~> 0.1.5", feature_category: :tooling + gem 'ruby-lsp-rspec', "~> 0.1.8", require: false, feature_category: :tooling end group :development, :test do diff --git a/Gemfile.checksum b/Gemfile.checksum index 031a8943ef8..4ef3a47e3d2 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -455,7 +455,7 @@ {"name":"premailer","version":"1.16.0","platform":"ruby","checksum":"03e4402c448e6bae13fb5f6301a8bde4f3508e1bff90ae7c0972c7be94694786"}, {"name":"premailer-rails","version":"1.10.3","platform":"ruby","checksum":"7cdcb97027866f7a81c490c6d15ada7f39666b5f6375f0821b7e97e0483b112f"}, {"name":"prime","version":"0.1.2","platform":"ruby","checksum":"d4e956cadfaf04de036dc7dc74f95bf6a285a62cc509b28b7a66b245d19fe3a4"}, -{"name":"prism","version":"0.17.1","platform":"ruby","checksum":"e63f86df2c36aecd578431ee0c9d1f66cdef98a406f0a11e7da949514212cbcd"}, +{"name":"prism","version":"0.18.0","platform":"ruby","checksum":"bae73ccaed950e830e136be38cdb9461f9f645f8ef306217ff1d66ff83eb589c"}, {"name":"proc_to_ast","version":"0.1.0","platform":"ruby","checksum":"92a73fa66e2250a83f8589f818b0751bcf227c68f85916202df7af85082f8691"}, {"name":"prometheus-client-mmap","version":"1.0.0","platform":"aarch64-linux","checksum":"6a4bb32e7f7c554bf9d7d1c6c1a40ad3cd94d8bcb8265f6da4fe7601761d9347"}, {"name":"prometheus-client-mmap","version":"1.0.0","platform":"arm64-darwin","checksum":"e92ac0806393640dd91d6048d9ab8cfec0d7b6f40555ea80c930414968c38b94"}, @@ -555,9 +555,9 @@ {"name":"rubocop-rails","version":"2.22.1","platform":"ruby","checksum":"db673cdb6321d8bb7627cd6cfb2cb36114acaa0e89581e4694b7304ce2acbd46"}, {"name":"rubocop-rspec","version":"2.25.0","platform":"ruby","checksum":"083f8a0481dbb9969b2a9eae85670a454fe91d46812e6ec97b34e7f6227b99f3"}, {"name":"ruby-fogbugz","version":"0.3.0","platform":"ruby","checksum":"5e04cde474648f498a71cf1e1a7ab42c66b953862fbe224f793ec0a7a1d5f657"}, -{"name":"ruby-lsp","version":"0.12.3","platform":"ruby","checksum":"e49d82cdcb20c16f3b78556e3107af813f785c05d2d02658f810d03852db4567"}, -{"name":"ruby-lsp-rails","version":"0.2.7","platform":"ruby","checksum":"722c4613d212aa136733b36674e5773e2352de9b3c1a05cafec86dc589a47811"}, -{"name":"ruby-lsp-rspec","version":"0.1.5","platform":"ruby","checksum":"d26dcfcc0ad3e9690f22354a8b1c12e0eb5cc03949c7afa846af805f4fc842e5"}, +{"name":"ruby-lsp","version":"0.13.0","platform":"ruby","checksum":"80c148ee5eff6d729ff9bef52e58cb1d6a506a4feaaba9ed7963ef0430b9568f"}, +{"name":"ruby-lsp-rails","version":"0.2.8","platform":"ruby","checksum":"1730cafa65c04c9bc3b6e28b3454afb561ae71859be1f26f36b065975a5a57c8"}, +{"name":"ruby-lsp-rspec","version":"0.1.8","platform":"ruby","checksum":"21db2255bad7ecf7297945c453d8e84af167d01776853f47aacb3bb50caa0ea3"}, {"name":"ruby-magic","version":"0.6.0","platform":"ruby","checksum":"7b2138877b7d23aff812c95564eba6473b74b815ef85beb0eb792e729a2b6101"}, {"name":"ruby-openai","version":"3.7.0","platform":"ruby","checksum":"fb735d4c055e282ade264cab9864944c05a8a10e0cddd45a0551e8a9851b1850"}, {"name":"ruby-progressbar","version":"1.11.0","platform":"ruby","checksum":"cc127db3866dc414ffccbf92928a241e585b3aa2b758a5563e74a6ee0f57d50a"}, @@ -604,7 +604,7 @@ {"name":"snaky_hash","version":"2.0.0","platform":"ruby","checksum":"fe8b2e39e8ff69320f7812af73ea06401579e29ff1734a7009567391600687de"}, {"name":"snowplow-tracker","version":"0.8.0","platform":"ruby","checksum":"7ba6f4f1443a829845fd28e63eda72d9d3d247f485310ddcccaebbc52b734a38"}, {"name":"solargraph","version":"0.47.2","platform":"ruby","checksum":"87ca4b799b9155c2c31c15954c483e952fdacd800f52d6709b901dd447bcac6a"}, -{"name":"sorbet-runtime","version":"0.5.11120","platform":"ruby","checksum":"73112246db6c28ac93befb7335dfbf1ec96e583ee8724f2c1c177dc027586bd2"}, +{"name":"sorbet-runtime","version":"0.5.11144","platform":"ruby","checksum":"cb36dfc4ede6d206fa6f7587d4be7c8b4fcd3cc9fd5792614fb9b6c7030548a0"}, {"name":"sorted_set","version":"1.0.3","platform":"java","checksum":"996283f2e5c6e838825bcdcee31d6306515ae5f24bcb0ee4ce09dfff32919b8c"}, {"name":"sorted_set","version":"1.0.3","platform":"ruby","checksum":"4f2b8bee6e8c59cbd296228c0f1f81679357177a8b6859dcc2a99e86cce6372f"}, {"name":"spamcheck","version":"1.3.0","platform":"ruby","checksum":"a46082752257838d8484c844736e309ec499f85dcc51283a5f973b33f1c994f5"}, diff --git a/Gemfile.lock b/Gemfile.lock index 50e152536dc..efce0df699f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1242,7 +1242,7 @@ GEM prime (0.1.2) forwardable singleton - prism (0.17.1) + prism (0.18.0) proc_to_ast (0.1.0) coderay parser @@ -1456,16 +1456,18 @@ GEM ruby-fogbugz (0.3.0) crack (~> 0.4) multipart-post (~> 2.0) - ruby-lsp (0.12.3) + ruby-lsp (0.13.0) language_server-protocol (~> 3.17.0) - prism (>= 0.17.1, < 0.18) + prism (>= 0.18.0, < 0.19) sorbet-runtime (>= 0.5.5685) - ruby-lsp-rails (0.2.7) - rails (>= 6.0) - ruby-lsp (>= 0.12.0, < 0.13.0) + ruby-lsp-rails (0.2.8) + actionpack (>= 6.0) + activerecord (>= 6.0) + railties (>= 6.0) + ruby-lsp (>= 0.13.0, < 0.14.0) sorbet-runtime (>= 0.5.9897) - ruby-lsp-rspec (0.1.5) - ruby-lsp (~> 0.12.0) + ruby-lsp-rspec (0.1.8) + ruby-lsp (~> 0.13.0) ruby-magic (0.6.0) mini_portile2 (~> 2.8) ruby-openai (3.7.0) @@ -1573,7 +1575,7 @@ GEM thor (~> 1.0) tilt (~> 2.0) yard (~> 0.9, >= 0.9.24) - sorbet-runtime (0.5.11120) + sorbet-runtime (0.5.11144) sorted_set (1.0.3) rbtree set (~> 1.0) @@ -2031,9 +2033,9 @@ DEPENDENCIES rspec_profiling (~> 0.0.6) rubocop ruby-fogbugz (~> 0.3.0) - ruby-lsp (~> 0.12.3) - ruby-lsp-rails (~> 0.2.7) - ruby-lsp-rspec (~> 0.1.5) + ruby-lsp (~> 0.13.0) + ruby-lsp-rails (~> 0.2.8) + ruby-lsp-rspec (~> 0.1.8) ruby-magic (~> 0.6) ruby-openai (~> 3.7) ruby-progressbar (~> 1.10) diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 20f82500a02..e45fd508a5b 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -211,19 +211,6 @@ export default { return this.getNoteableData.current_user.can_create_note; }, }, - watch: { - 'idState.moreActionsShown': { - handler(val) { - const el = this.$el.closest('.vue-recycle-scroller__item-view'); - - if (el) { - // We can't add a style with Vue because of the way the virtual - // scroller library renders the diff files - el.style.zIndex = val ? '1' : null; - } - }, - }, - }, methods: { ...mapActions('diffs', [ 'toggleFileDiscussions', diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index 928f81daf92..b29755545f2 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -67,8 +67,6 @@ export default { originalInput: '', users: [], selectedTokens: [], - hasBeenFocused: false, - hideDropdownWithNoItems: true, }; }, computed: { @@ -124,7 +122,6 @@ export default { }, methods: { handleTextInput(inputQuery) { - this.hideDropdownWithNoItems = false; this.originalInput = inputQuery; this.query = inputQuery.trim(); this.loading = true; @@ -161,18 +158,10 @@ export default { handleInput() { this.$emit('input', this.selectedTokens); }, - handleBlur() { - this.hideDropdownWithNoItems = false; - }, handleFocus() { - // The modal auto-focuses on the input when opened. - // This prevents the dropdown from opening when the modal opens. - if (this.hasBeenFocused) { - this.loading = true; - this.retrieveUsers(); - } - - this.hasBeenFocused = true; + // Search for users when focused on the input + this.loading = true; + this.retrieveUsers(); }, handleTokenRemove(value) { if (this.selectedTokens.length) { @@ -208,11 +197,9 @@ export default { :dropdown-items="users" :loading="loading" :allow-user-defined-tokens="emailIsValid" - :hide-dropdown-with-no-items="hideDropdownWithNoItems" :placeholder="placeholderText" :aria-labelledby="ariaLabelledby" :text-input-attrs="textInputAttrs" - @blur="handleBlur" @text-input="handleTextInput" @input="handleInput" @focus="handleFocus" diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index 264dbff525b..4ec57676b79 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -204,7 +204,8 @@ export default { return this.blobInfo.storedExternally && this.blobInfo.externalStorage === LFS_STORAGE; }, isBlameEnabled() { - return this.glFeatures.blobBlameInfo && this.blobInfo.language === 'json'; // This feature is currently scoped to JSON files + // Blame information within the blob viewer is not yet supported in our fallback (HAML) viewers + return this.glFeatures.blobBlameInfo && !this.useFallback; }, }, watch: { @@ -295,7 +296,14 @@ export default { }, handleToggleBlame() { this.switchViewer(SIMPLE_BLOB_VIEWER); - this.showBlame = !this.showBlame; + + if (this.$route?.query?.plain === '0') { + // If the user is not viewing plain code and clicks the blame button, we always want to show blame info + // For instance, when viewing the rendered version of a Markdown file + this.showBlame = true; + } else { + this.showBlame = !this.showBlame; + } const blame = this.showBlame === true ? '1' : '0'; if (this.$route?.query?.blame === blame) return; diff --git a/app/assets/javascripts/search/store/mutations.js b/app/assets/javascripts/search/store/mutations.js index b248681f053..7627b2e0e08 100644 --- a/app/assets/javascripts/search/store/mutations.js +++ b/app/assets/javascripts/search/store/mutations.js @@ -6,7 +6,7 @@ export default { }, [types.RECEIVE_GROUPS_SUCCESS](state, data) { state.fetchingGroups = false; - state.groups = data; + state.groups = [...data]; }, [types.RECEIVE_GROUPS_ERROR](state) { state.fetchingGroups = false; @@ -17,7 +17,7 @@ export default { }, [types.RECEIVE_PROJECTS_SUCCESS](state, data) { state.fetchingProjects = false; - state.projects = data; + state.projects = [...data]; }, [types.RECEIVE_PROJECTS_ERROR](state) { state.fetchingProjects = false; diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue index d9f824b6e18..5bee757856f 100644 --- a/app/assets/javascripts/search/topbar/components/app.vue +++ b/app/assets/javascripts/search/topbar/components/app.vue @@ -16,15 +16,9 @@ export default { i18n: { searchPlaceholder: s__(`GlobalSearch|Search for projects, issues, etc.`), searchLabel: s__(`GlobalSearch|What are you searching for?`), - documentFetchErrorMessage: s__( - 'GlobalSearch|There was an error fetching the "Syntax Options" document.', - ), - searchFieldLabel: s__('GlobalSearch|What are you searching for?'), syntaxOptionsLabel: s__('GlobalSearch|Syntax options'), groupFieldLabel: s__('GlobalSearch|Group'), projectFieldLabel: s__('GlobalSearch|Project'), - searchButtonLabel: s__('GlobalSearch|Search'), - closeButtonLabel: s__('GlobalSearch|Close'), }, components: { GlButton, @@ -124,17 +118,20 @@ export default { @submit="applyQuery" /> </div> - <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-3"> - <label class="gl-display-block gl-mb-1 gl-md-pb-2">{{ + <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-3 gl-min-w-20"> + <label id="groupfilterDropdown" class="gl-display-block gl-mb-1 gl-md-pb-2">{{ $options.i18n.groupFieldLabel }}</label> - <group-filter :initial-data="groupInitialJson" /> + <group-filter label-id="groupfilterDropdown" :group-initial-json="groupInitialJson" /> </div> - <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-ml-3"> - <label class="gl-display-block gl-mb-1 gl-md-pb-2">{{ + <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-ml-3 gl-min-w-20"> + <label id="projectfilterDropdown" class="gl-display-block gl-mb-1 gl-md-pb-2">{{ $options.i18n.projectFieldLabel }}</label> - <project-filter :initial-data="projectInitialJson" /> + <project-filter + label-id="projectfilterDropdown" + :project-initial-json="projectInitialJson" + /> </div> </div> </div> diff --git a/app/assets/javascripts/search/topbar/components/group_filter.vue b/app/assets/javascripts/search/topbar/components/group_filter.vue index a177eb28991..7f13def8a0f 100644 --- a/app/assets/javascripts/search/topbar/components/group_filter.vue +++ b/app/assets/javascripts/search/topbar/components/group_filter.vue @@ -12,27 +12,46 @@ export default { SearchableDropdown, }, props: { - initialData: { + groupInitialJson: { type: Object, required: false, default: () => ({}), }, + labelId: { + type: String, + required: false, + default: 'labelId', + }, + }, + data() { + return { + search: '', + }; }, computed: { ...mapState(['query', 'groups', 'fetchingGroups']), ...mapGetters(['frequentGroups', 'currentScope']), selectedGroup() { - return isEmpty(this.initialData) ? ANY_OPTION : this.initialData; + return isEmpty(this.groupInitialJson) ? ANY_OPTION : this.groupInitialJson; + }, + }, + watch: { + search() { + this.debounceSearch(); }, }, created() { // This tracks groups searched via the top nav search bar - if (this.query.nav_source === 'navbar' && this.initialData?.id) { - this.setFrequentGroup(this.initialData); + if (this.query.nav_source === 'navbar' && this.groupInitialJson?.id) { + this.setFrequentGroup(this.groupInitialJson); } }, methods: { ...mapActions(['fetchGroups', 'setFrequentGroup', 'loadFrequentGroups']), + firstLoad() { + this.loadFrequentGroups(); + this.fetchGroups(); + }, handleGroupChange(group) { // If group.id is null we are clearing the filter and don't need to store that in LS. if (group.id) { @@ -58,13 +77,13 @@ export default { data-testid="group-filter" :header-text="$options.GROUP_DATA.headerText" :name="$options.GROUP_DATA.name" - :full-name="$options.GROUP_DATA.fullName" :loading="fetchingGroups" :selected-item="selectedGroup" :items="groups" :frequent-items="frequentGroups" - @first-open="loadFrequentGroups" - @search="fetchGroups" + :search-handler="fetchGroups" + :label-id="labelId" + @first-open="firstLoad" @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 index c8190b4002d..ecd118a07ac 100644 --- a/app/assets/javascripts/search/topbar/components/project_filter.vue +++ b/app/assets/javascripts/search/topbar/components/project_filter.vue @@ -1,4 +1,5 @@ <script> +import { isEmpty } from 'lodash'; // eslint-disable-next-line no-restricted-imports import { mapState, mapActions, mapGetters } from 'vuex'; import { visitUrl, setUrlParams } from '~/lib/utils/url_utility'; @@ -11,27 +12,46 @@ export default { SearchableDropdown, }, props: { - initialData: { + projectInitialJson: { type: Object, required: false, default: () => null, }, + labelId: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + search: '', + }; }, computed: { ...mapState(['query', 'projects', 'fetchingProjects']), ...mapGetters(['frequentProjects', 'currentScope']), selectedProject() { - return this.initialData ? this.initialData : ANY_OPTION; + return isEmpty(this.projectInitialJson) ? ANY_OPTION : this.projectInitialJson; + }, + }, + watch: { + search() { + this.debounceSearch(); }, }, created() { // This tracks projects searched via the top nav search bar - if (this.query.nav_source === 'navbar' && this.initialData?.id) { - this.setFrequentProject(this.initialData); + if (this.query.nav_source === 'navbar' && this.projectInitialJson?.id) { + this.setFrequentProject(this.projectInitialJson); } }, methods: { ...mapActions(['fetchProjects', 'setFrequentProject', 'loadFrequentProjects']), + firstLoad() { + this.loadFrequentProjects(); + this.fetchProjects(); + }, handleProjectChange(project) { // If project.id is null we are clearing the filter and don't need to store that in LS. if (project.id) { @@ -58,13 +78,13 @@ export default { data-testid="project-filter" :header-text="$options.PROJECT_DATA.headerText" :name="$options.PROJECT_DATA.name" - :full-name="$options.PROJECT_DATA.fullName" :loading="fetchingProjects" :selected-item="selectedProject" :items="projects" :frequent-items="frequentProjects" - @first-open="loadFrequentProjects" - @search="fetchProjects" + :search-handler="fetchProjects" + :label-id="labelId" + @first-open="firstLoad" @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 index ff639d538b3..f4d9de636d4 100644 --- a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue +++ b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue @@ -1,38 +1,31 @@ <script> -import { - GlDropdown, - GlDropdownItem, - GlDropdownSectionHeader, - GlSearchBoxByType, - GlLoadingIcon, - GlIcon, - GlButton, - GlSkeletonLoader, - GlTooltipDirective, -} from '@gitlab/ui'; -import { __ } from '~/locale'; +import { GlCollapsibleListbox, GlAvatar } from '@gitlab/ui'; +import { debounce } from 'lodash'; +import SafeHtml from '~/vue_shared/directives/safe_html'; +import highlight from '~/lib/utils/highlight'; +import { truncateNamespace } from '~/lib/utils/text_utility'; +import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import { __, s__, n__ } from '~/locale'; import { ANY_OPTION } from '../constants'; -import SearchableDropdownItem from './searchable_dropdown_item.vue'; export default { - i18n: { - clearLabel: __('Clear'), - frequentlySearched: __('Frequently searched'), - }, name: 'SearchableDropdown', components: { - GlDropdown, - GlDropdownItem, - GlDropdownSectionHeader, - GlSearchBoxByType, - GlLoadingIcon, - GlIcon, - GlButton, - GlSkeletonLoader, - SearchableDropdownItem, + GlAvatar, + GlCollapsibleListbox, }, directives: { - GlTooltip: GlTooltipDirective, + SafeHtml, + }, + i18n: { + frequentlySearched: __('Frequently searched'), + availableGroups: s__('GlobalSearch|All available groups'), + nothingFound: s__('GlobalSearch|Nothing found…'), + reset: s__('GlobalSearch|Reset'), + itemsFound(count) { + return n__('%d item found', '%d items found', count); + }, }, props: { headerText: { @@ -45,11 +38,6 @@ export default { required: false, default: 'name', }, - fullName: { - type: String, - required: false, - default: 'name', - }, loading: { type: Boolean, required: false, @@ -69,127 +57,167 @@ export default { required: false, default: () => [], }, + searchHandler: { + type: Function, + required: true, + }, + labelId: { + type: String, + required: false, + default: 'labelId', + }, }, data() { return { searchText: '', hasBeenOpened: false, + showableItems: [], + searchInProgress: false, }; }, - computed: { - showFrequentItems() { - return !this.searchText && this.frequentItems.length > 0; + computed: {}, + watch: { + items() { + if (this.searchText === '') { + this.showableItems = this.defaultItems(); + } }, }, + created() { + this.showableItems = this.defaultItems(); + }, methods: { - isSelected(selected) { - return selected.id === this.selectedItem.id; + defaultItems() { + const frequentItems = this.convertItemsFormat([...this.frequentItems]); + const nonFrequentItems = this.convertItemsFormat([ + ...this.uniqueItems(this.items, this.frequentItems), + ]); + + return [ + { + text: '', + options: [ + { + value: ANY_OPTION.name, + text: ANY_OPTION.name, + ...ANY_OPTION, + }, + ], + }, + { + text: this.$options.i18n.frequentlySearched, + options: frequentItems, + }, + { + text: this.$options.i18n.availableGroups, + options: nonFrequentItems, + }, + ].filter((group) => { + return group.options.length > 0; + }); + }, + search(search) { + this.searchText = search; + this.searchInProgress = true; + + if (search !== '') { + debounce(() => { + this.searchHandler(this.searchText); + this.showableItems = this.convertItemsFormat([...this.items]); + }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS)(); + + return; + } + + this.showableItems = this.defaultItems(); }, openDropdown() { if (!this.hasBeenOpened) { this.hasBeenOpened = true; this.$emit('first-open'); } - - this.$emit('search', this.searchText); }, resetDropdown() { this.$emit('change', ANY_OPTION); }, - updateDropdown(item) { - this.$emit('change', item); + convertItemsFormat(items) { + return items.map((item) => ({ value: item.id, text: item.full_name, ...item })); + }, + truncatedNamespace(item) { + const itemDuplicat = { ...item }; + const namespaceWithFallback = itemDuplicat.name_with_namespace + ? itemDuplicat.name_with_namespace + : itemDuplicat.full_name; + + return truncateNamespace(namespaceWithFallback); + }, + highlightedItemName(item) { + return highlight(item.name, item.searchText); + }, + onSelectGroup(selected) { + if (selected === ANY_OPTION.name) { + this.$emit('change', ANY_OPTION); + return; + } + + const flatShowableItems = [...this.frequentItems, ...this.items]; + const newSelectedItem = flatShowableItems.find((item) => item.id === selected); + this.$emit('change', newSelectedItem); + }, + uniqueItems(allItems, frequentItems) { + return allItems.filter((item) => { + const itemNotIdentical = frequentItems.some((fitem) => fitem.id === item.id); + return Boolean(!itemNotIdentical); + }); }, }, ANY_OPTION, + AVATAR_SHAPE_OPTION_RECT, }; </script> <template> - <gl-dropdown - class="gl-w-full" - menu-class="global-search-dropdown-menu" - toggle-class="gl-text-truncate" + <gl-collapsible-listbox + :items="showableItems" :header-text="headerText" - :right="true" - @show="openDropdown" - @shown="$refs.searchBox.focusInput()" + :toggle-text="selectedItem[name]" + :no-results-text="$options.i18n.nothingFound" + :selected="selectedItem.id" + :searching="loading" + :reset-button-label="$options.i18n.reset" + :toggle-aria-labelled-by="labelId" + searchable + block + @shown="openDropdown" + @search="search" + @select="onSelectGroup" + @reset="resetDropdown" > - <template #button-content> - <span class="dropdown-toggle-text gl-flex-grow-1 gl-text-truncate"> - {{ selectedItem[name] }} - </span> - <gl-loading-icon v-if="loading" size="sm" inline class="gl-mr-3" /> - <gl-button - v-if="!isSelected($options.ANY_OPTION)" - v-gl-tooltip - name="clear" - category="tertiary" - :title="$options.i18n.clearLabel" - :aria-label="$options.i18n.clearLabel" - class="gl-p-0! gl-mr-2" - @keydown.enter.stop="resetDropdown" - @click.stop="resetDropdown" - > - <gl-icon name="clear" /> - </gl-button> - <gl-icon name="chevron-down" /> + <template #search-summary-sr-only> + {{ $options.i18n.itemsFound(showableItems.length) }} + </template> + <template #list-item="{ item }"> + <div class="gl-display-flex gl-align-items-center"> + <gl-avatar + :src="item.avatar_url" + :entity-id="item.id" + :entity-name="item.name" + :shape="$options.AVATAR_SHAPE_OPTION_RECT" + :size="32" + class="gl-mr-3" + aria-hidden="true" + /> + <div class="gl-display-flex gl-flex-direction-column"> + <span + v-safe-html="highlightedItemName(item)" + class="gl-font-weight-bold gl-white-space-nowrap" + data-testid="item-title" + ></span> + <span class="gl-font-sm gl-text-gray-700" data-testid="item-namespace"> + {{ truncatedNamespace(item) }}</span + > + </div> + </div> </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="openDropdown" - /> - <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 - :is-checked="isSelected($options.ANY_OPTION)" - is-check-centered - @click="updateDropdown($options.ANY_OPTION)" - > - <span data-testid="item-title">{{ $options.ANY_OPTION.name }}</span> - </gl-dropdown-item> - </div> - <div - v-if="showFrequentItems" - class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2 gl-mb-2" - > - <gl-dropdown-section-header>{{ - $options.i18n.frequentlySearched - }}</gl-dropdown-section-header> - <searchable-dropdown-item - v-for="item in frequentItems" - :key="item.id" - :item="item" - :selected-item="selectedItem" - :search-text="searchText" - :name="name" - :full-name="fullName" - data-testid="frequent-items" - @change="updateDropdown" - /> - </div> - <div v-if="!loading"> - <searchable-dropdown-item - v-for="item in items" - :key="item.id" - :item="item" - :selected-item="selectedItem" - :search-text="searchText" - :name="name" - :full-name="fullName" - data-testid="searchable-items" - @change="updateDropdown" - /> - </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> + </gl-collapsible-listbox> </template> diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue deleted file mode 100644 index c1e33df3c42..00000000000 --- a/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue +++ /dev/null @@ -1,78 +0,0 @@ -<script> -import { GlDropdownItem, GlAvatar } from '@gitlab/ui'; -import SafeHtml from '~/vue_shared/directives/safe_html'; -import highlight from '~/lib/utils/highlight'; -import { truncateNamespace } from '~/lib/utils/text_utility'; -import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; - -export default { - name: 'SearchableDropdownItem', - components: { - GlDropdownItem, - GlAvatar, - }, - directives: { - SafeHtml, - }, - props: { - item: { - type: Object, - required: true, - }, - selectedItem: { - type: Object, - required: true, - }, - searchText: { - type: String, - required: false, - default: '', - }, - name: { - type: String, - required: true, - }, - fullName: { - type: String, - required: true, - }, - }, - computed: { - isSelected() { - return this.item.id === this.selectedItem.id; - }, - truncatedNamespace() { - return truncateNamespace(this.item[this.fullName]); - }, - highlightedItemName() { - return highlight(this.item[this.name], this.searchText); - }, - }, - AVATAR_SHAPE_OPTION_RECT, -}; -</script> - -<template> - <gl-dropdown-item - is-check-item - :is-checked="isSelected" - is-check-centered - @click="$emit('change', item)" - > - <div class="gl-display-flex gl-align-items-center"> - <gl-avatar - :src="item.avatar_url" - :entity-id="item.id" - :entity-name="item[name]" - :shape="$options.AVATAR_SHAPE_OPTION_RECT" - :size="32" - /> - <div class="gl-display-flex gl-flex-direction-column"> - <span v-safe-html="highlightedItemName" data-testid="item-title"></span> - <span class="gl-font-sm gl-text-gray-700" data-testid="item-namespace">{{ - truncatedNamespace - }}</span> - </div> - </div> - </gl-dropdown-item> -</template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql index a5f3f348cfc..c497224cde3 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql +++ b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql @@ -14,6 +14,7 @@ query getBlameData($fullPath: ID!, $filePath: String!, $fromLine: Int, $toLine: span commit { id + authorName titleHtml message authoredDate diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ef87fedf538..b8b79192d3f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -397,6 +397,7 @@ class ProjectsController < Projects::ApplicationController if can?(current_user, :read_code, @project) return render 'projects/no_repo' unless @project.repository_exists? + return render 'projects/missing_default_branch', status: :service_unavailable if @ref == '' render 'projects/empty' if @project.empty_repo? else @@ -553,6 +554,9 @@ class ProjectsController < Projects::ApplicationController # Override get_id from ExtractsPath in this case is just the root of the default branch. def get_id project.repository.root_ref + rescue Gitlab::Git::CommandError + # Empty string is intentional and prevent the @ref reload + '' end def build_canonical_path(project) diff --git a/app/views/projects/missing_default_branch.html.haml b/app/views/projects/missing_default_branch.html.haml new file mode 100644 index 00000000000..66a466d8890 --- /dev/null +++ b/app/views/projects/missing_default_branch.html.haml @@ -0,0 +1,10 @@ +- @skip_current_level_breadcrumb = true + += render Pajamas::AlertComponent.new(alert_options: { class: 'gl-my-5' }, + variant: :danger, + dismissible: false, + title: s_('ProjectPage|Unable to load default branch')) do |c| + - c.with_body do + = s_('ProjectPage|The default branch was not able to be found. Please contact your administrator.') + += render 'home_panel' diff --git a/config/events/perform_completion_worker.yml b/config/events/perform_completion_worker.yml new file mode 100644 index 00000000000..39fbb88bc24 --- /dev/null +++ b/config/events/perform_completion_worker.yml @@ -0,0 +1,21 @@ +--- +description: When a CompletionWorker gets executed to perform an AI request. +category: Llm::CompletionWorker +action: perform_completion_worker +label_description: AI Action that gets performed +property_description: Request ID to link to other events of the same AI request. +value_description: +extra_properties: + client: + type: string +identifiers: + - user +product_section: data-science +product_stage: modelops +product_group: 'group::ai framework' +milestone: '16.7' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137913 +distributions: + - ee +tiers: + - ultimate diff --git a/db/click_house/migrate/20231129062064_create_contributions_table.rb b/db/click_house/migrate/20231129062064_create_contributions_table.rb new file mode 100644 index 00000000000..2467da8bb91 --- /dev/null +++ b/db/click_house/migrate/20231129062064_create_contributions_table.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class CreateContributionsTable < ClickHouse::Migration + def up + execute <<~SQL + CREATE TABLE IF NOT EXISTS contributions + ( + id UInt64 DEFAULT 0, + path String DEFAULT '', + author_id UInt64 DEFAULT 0, + target_type LowCardinality(String) DEFAULT '', + action UInt8 DEFAULT 0, + created_at Date DEFAULT toDate(now64()), + updated_at DateTime64(6, 'UTC') DEFAULT now64() + ) + ENGINE = ReplacingMergeTree + ORDER BY (path, created_at, author_id, id) + PARTITION BY toYear(created_at); + SQL + end + + def down + execute <<~SQL + DROP TABLE IF EXISTS contributions + SQL + end +end diff --git a/db/click_house/migrate/20231129062151_create_contributions_mv.rb b/db/click_house/migrate/20231129062151_create_contributions_mv.rb new file mode 100644 index 00000000000..f6f5054c55c --- /dev/null +++ b/db/click_house/migrate/20231129062151_create_contributions_mv.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class CreateContributionsMv < ClickHouse::Migration + def up + execute <<~SQL + CREATE MATERIALIZED VIEW IF NOT EXISTS contributions_mv + TO contributions + AS + SELECT + id, + argMax(path, events.updated_at) as path, + argMax(author_id, events.updated_at) as author_id, + argMax(target_type, events.updated_at) as target_type, + argMax(action, events.updated_at) as action, + argMax(date(created_at), events.updated_at) as created_at, + max(events.updated_at) as updated_at + FROM events + WHERE (("events"."action" IN (5, 6) AND "events"."target_type" = '') + OR ("events"."action" IN (1, 3, 7, 12) + AND "events"."target_type" IN ('MergeRequest', 'Issue', 'WorkItem'))) + GROUP BY id + SQL + end + + def down + execute <<~SQL + DROP VIEW IF EXISTS contributions_mv + SQL + end +end diff --git a/db/docs/feature_gates.yml b/db/docs/feature_gates.yml index 10060ad38ba..701417e064b 100644 --- a/db/docs/feature_gates.yml +++ b/db/docs/feature_gates.yml @@ -1,6 +1,7 @@ --- table_name: feature_gates classes: +- Feature::BypassLoadBalancer::FlipperGate - Feature::FlipperGate - Flipper::Adapters::ActiveRecord::Gate feature_categories: diff --git a/db/docs/features.yml b/db/docs/features.yml index 9866eff2a3f..b09b666b6d3 100644 --- a/db/docs/features.yml +++ b/db/docs/features.yml @@ -1,6 +1,7 @@ --- table_name: features classes: +- Feature::BypassLoadBalancer::FlipperFeature - Feature::FlipperFeature - Flipper::Adapters::ActiveRecord::Feature feature_categories: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index dd05b368fa1..704bf203f59 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -317,6 +317,11 @@ msgid_plural "%d issues successfully imported with the label" msgstr[0] "" msgstr[1] "" +msgid "%d item found" +msgid_plural "%d items found" +msgstr[0] "" +msgstr[1] "" + msgid "%d job" msgid_plural "%d jobs" msgstr[0] "" @@ -22373,10 +22378,10 @@ msgstr "" msgid "GlobalSearch|Aggregations load error." msgstr "" -msgid "GlobalSearch|Archived" +msgid "GlobalSearch|All available groups" msgstr "" -msgid "GlobalSearch|Close" +msgid "GlobalSearch|Archived" msgstr "" msgid "GlobalSearch|Command palette" @@ -22436,6 +22441,9 @@ msgstr "" msgid "GlobalSearch|No labels found" msgstr "" +msgid "GlobalSearch|Nothing found…" +msgstr "" + msgid "GlobalSearch|Only first %{max_shown} of not indexed projects is shown" msgstr "" @@ -22457,13 +22465,13 @@ msgstr "" msgid "GlobalSearch|Recent merge requests" msgstr "" -msgid "GlobalSearch|Result count is over limit." +msgid "GlobalSearch|Reset" msgstr "" -msgid "GlobalSearch|Results updated. %{count} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit." +msgid "GlobalSearch|Result count is over limit." msgstr "" -msgid "GlobalSearch|Search" +msgid "GlobalSearch|Results updated. %{count} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit." msgstr "" msgid "GlobalSearch|Search for projects, issues, etc." @@ -22493,9 +22501,6 @@ msgstr "" msgid "GlobalSearch|There was an error fetching search autocomplete suggestions." msgstr "" -msgid "GlobalSearch|There was an error fetching the \"Syntax Options\" document." -msgstr "" - msgid "GlobalSearch|Type %{kbdOpen}/%{kbdClose} to search" msgstr "" @@ -29521,97 +29526,97 @@ msgstr "" msgid "MemberInviteEmail|Invitation to join the %{project_or_group} %{project_or_group_name}" msgstr "" -msgid "MemberRoles|Actions" +msgid "MemberRole|%{requirement} has to be enabled in order to enable %{permission}." msgstr "" -msgid "MemberRoles|Add new role" +msgid "MemberRole|Actions" msgstr "" -msgid "MemberRoles|Are you sure you want to delete this role?" +msgid "MemberRole|Add new role" msgstr "" -msgid "MemberRoles|Base role" +msgid "MemberRole|Are you sure you want to delete this role?" msgstr "" -msgid "MemberRoles|Base role to use as template" +msgid "MemberRole|Base role" msgstr "" -msgid "MemberRoles|Could not fetch available permissions: %{message}" +msgid "MemberRole|Base role to use as template" msgstr "" -msgid "MemberRoles|Create new role" +msgid "MemberRole|Could not fetch available permissions: %{message}" msgstr "" -msgid "MemberRoles|Custom roles" +msgid "MemberRole|Create new role" msgstr "" -msgid "MemberRoles|Custom roles based on %{accessLevel}" +msgid "MemberRole|Custom permissions:" msgstr "" -msgid "MemberRoles|Delete role" +msgid "MemberRole|Custom roles" msgstr "" -msgid "MemberRoles|Description" +msgid "MemberRole|Custom roles based on %{accessLevel}" msgstr "" -msgid "MemberRoles|Enter a short name." +msgid "MemberRole|Delete role" msgstr "" -msgid "MemberRoles|Failed to create role." +msgid "MemberRole|Description" msgstr "" -msgid "MemberRoles|Failed to delete the role." +msgid "MemberRole|Enter a short name." msgstr "" -msgid "MemberRoles|Failed to fetch roles." +msgid "MemberRole|Failed to create role." msgstr "" -msgid "MemberRoles|ID" +msgid "MemberRole|Failed to delete the role." msgstr "" -msgid "MemberRoles|Incident manager" +msgid "MemberRole|Failed to fetch roles." msgstr "" -msgid "MemberRoles|Make sure the group is in the Ultimate tier." +msgid "MemberRole|ID" msgstr "" -msgid "MemberRoles|Name" +msgid "MemberRole|Incident manager" msgstr "" -msgid "MemberRoles|No custom roles for this group" +msgid "MemberRole|Make sure the group is in the Ultimate tier." msgstr "" -msgid "MemberRoles|Permissions" +msgid "MemberRole|Name" msgstr "" -msgid "MemberRoles|Role name" +msgid "MemberRole|No custom roles for this group" msgstr "" -msgid "MemberRoles|Role successfully created." +msgid "MemberRole|Permissions" msgstr "" -msgid "MemberRoles|Role successfully deleted." +msgid "MemberRole|Role name" msgstr "" -msgid "MemberRoles|Select a standard role to add permissions." +msgid "MemberRole|Role successfully created." msgstr "" -msgid "MemberRoles|Standard roles" +msgid "MemberRole|Role successfully deleted." msgstr "" -msgid "MemberRoles|To add a new role select 'Add new role'." +msgid "MemberRole|Select a standard role to add permissions." msgstr "" -msgid "MemberRoles|To add a new role select a group and then 'Add new role'." +msgid "MemberRole|Standard roles" msgstr "" -msgid "MemberRoles|To delete the custom role make sure no group member has this custom role" +msgid "MemberRole|To add a new role select 'Add new role'." msgstr "" -msgid "MemberRole|%{requirement} has to be enabled in order to enable %{permission}." +msgid "MemberRole|To add a new role select a group and then 'Add new role'." msgstr "" -msgid "MemberRole|Custom permissions:" +msgid "MemberRole|To delete the custom role make sure no group member has this custom role" msgstr "" msgid "MemberRole|can't be changed" @@ -37468,6 +37473,12 @@ msgstr "" msgid "ProjectPage|Project settings" msgstr "" +msgid "ProjectPage|The default branch was not able to be found. Please contact your administrator." +msgstr "" + +msgid "ProjectPage|Unable to load default branch" +msgstr "" + msgid "ProjectQualitySummary|An error occurred while trying to fetch project quality statistics" msgstr "" @@ -53436,6 +53447,9 @@ msgstr "" msgid "Visual Studio Code (SSH)" msgstr "" +msgid "VsdContributorCount|the ClickHouse data store is not available for this group" +msgstr "" + msgid "Vulnerabilities" msgstr "" diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 017bb6a46a6..7cd0188fe7b 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -362,6 +362,25 @@ RSpec.describe ProjectsController, feature_category: :groups_and_projects do end end + context 'when project default branch is corrupted' do + let_it_be(:corrupted_project) { create(:project, :small_repo, :public) } + + before do + sign_in(user) + + expect_next_instance_of(Repository) do |repository| + expect(repository).to receive(:root_ref).and_raise(Gitlab::Git::CommandError, 'get default branch').twice + end + end + + it 'renders the missing default branch view' do + get :show, params: { namespace_id: corrupted_project.namespace, id: corrupted_project } + + expect(response).to render_template('projects/missing_default_branch') + expect(response).to have_gitlab_http_status(:service_unavailable) + end + end + context "rendering default project view" do let_it_be(:public_project) { create(:project, :public, :repository) } diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index b68dc9557be..2089c9df145 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -42,9 +42,6 @@ RSpec.describe 'File blob', :js, feature_category: :source_code_management do expect(page).to have_css(".js-syntax-highlight") expect(page).to have_content("require 'fileutils'") - # does not show a viewer switcher - expect(page).not_to have_selector('.js-blob-viewer-switcher') - # shows an enabled copy button expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') @@ -299,9 +296,6 @@ RSpec.describe 'File blob', :js, feature_category: :source_code_management do # shows text expect(page).to have_content('size 1575078') - # does not show a viewer switcher - expect(page).not_to have_selector('.js-blob-viewer-switcher') - # shows an enabled copy button expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') @@ -430,9 +424,6 @@ RSpec.describe 'File blob', :js, feature_category: :source_code_management do # shows text expect(page).to have_content('size 1575078') - # does not show a viewer switcher - expect(page).not_to have_selector('.js-blob-viewer-switcher') - # shows an enabled copy button expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb index 976324a5032..0cb2cc3f42a 100644 --- a/spec/features/search/user_searches_for_code_spec.rb +++ b/spec/features/search/user_searches_for_code_spec.rb @@ -40,7 +40,7 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat wait_for_requests page.within('[data-testid="project-filter"]') do - click_on(project.name) + select_listbox_item(project.name) end end @@ -107,6 +107,7 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat visit(project_tree_path(project, ref_name)) submit_search('gitlab-grack') + wait_for_requests select_search_scope('Code') end diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb index 9451e337db1..c10562497e2 100644 --- a/spec/features/search/user_searches_for_issues_spec.rb +++ b/spec/features/search/user_searches_for_issues_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe 'User searches for issues', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do + include ListboxHelpers let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, namespace: user.namespace) } @@ -92,7 +93,7 @@ RSpec.describe 'User searches for issues', :js, :clean_gitlab_redis_rate_limitin wait_for_requests page.within('[data-testid="project-filter"]') do - click_on(project.name) + select_listbox_item project.name end search_for_issue(issue1.title) diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb index d7b52d9e07a..6fa8524ee46 100644 --- a/spec/features/search/user_searches_for_merge_requests_spec.rb +++ b/spec/features/search/user_searches_for_merge_requests_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe 'User searches for merge requests', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do + include ListboxHelpers let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, namespace: user.namespace) } let_it_be(:merge_request1) { create(:merge_request, title: 'Merge Request Foo', source_project: project, target_project: project, created_at: 1.hour.ago) } @@ -60,7 +61,7 @@ RSpec.describe 'User searches for merge requests', :js, :clean_gitlab_redis_rate wait_for_requests page.within('[data-testid="project-filter"]') do - click_on(project.name) + select_listbox_item project.name end search_for_mr(merge_request1.title) diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb index 7ca7958f61b..edd9ea2264b 100644 --- a/spec/features/search/user_searches_for_milestones_spec.rb +++ b/spec/features/search/user_searches_for_milestones_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'User searches for milestones', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do + include ListboxHelpers let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, namespace: user.namespace) } let_it_be(:milestone1) { create(:milestone, title: 'Foo', project: project) } @@ -37,7 +38,7 @@ RSpec.describe 'User searches for milestones', :js, :clean_gitlab_redis_rate_lim wait_for_requests page.within('[data-testid="project-filter"]') do - click_on(project.name) + select_listbox_item project.name end fill_in('dashboard_search', with: milestone1.title) diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb index 65f262075f9..622a19e12ed 100644 --- a/spec/features/search/user_searches_for_wiki_pages_spec.rb +++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'User searches for wiki pages', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do + include ListboxHelpers let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) } let_it_be(:wiki_page) do @@ -29,7 +30,7 @@ RSpec.describe 'User searches for wiki pages', :js, :clean_gitlab_redis_rate_lim wait_for_requests page.within('[data-testid="project-filter"]') do - click_on(project.name) + select_listbox_item project.name end fill_in('dashboard_search', with: search_term) diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb index 5e553cb0869..b95421fab59 100644 --- a/spec/features/search/user_uses_search_filters_spec.rb +++ b/spec/features/search/user_uses_search_filters_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe 'User uses search filters', :js, feature_category: :global_search do + include ListboxHelpers let(:group) { create(:group) } let!(:group_project) { create(:project, group: group) } let(:project) { create(:project, namespace: user.namespace) } @@ -23,7 +24,7 @@ RSpec.describe 'User uses search filters', :js, feature_category: :global_search wait_for_requests page.within('[data-testid="group-filter"]') do - click_on(group.name) + select_listbox_item group.name end expect(find('[data-testid="group-filter"]')).to have_content(group.name) @@ -33,7 +34,7 @@ RSpec.describe 'User uses search filters', :js, feature_category: :global_search wait_for_requests page.within('[data-testid="project-filter"]') do - click_on(group_project.name) + select_listbox_item group_project.name end expect(find('[data-testid="project-filter"]')).to have_content(group_project.name) @@ -46,12 +47,17 @@ RSpec.describe 'User uses search filters', :js, feature_category: :global_search describe 'clear filter button' do it 'removes Group and Project filters' do - find('[data-testid="group-filter"] [data-testid="clear-icon"]').click + page.within '[data-testid="group-filter"]' do + toggle_listbox + wait_for_requests - wait_for_requests + find('[data-testid="listbox-reset-button"]').click - expect(page).to have_current_path(search_path, ignore_query: true) do |uri| - uri.normalized_query(:sorted) == "scope=blobs&search=test" + wait_for_requests + + expect(page).to have_current_path(search_path, ignore_query: true) do |uri| + uri.normalized_query(:sorted) == "scope=blobs&search=test" + end end end end @@ -67,7 +73,7 @@ RSpec.describe 'User uses search filters', :js, feature_category: :global_search wait_for_requests page.within('[data-testid="project-filter"]') do - click_on(project.name) + select_listbox_item project.name end expect(find('[data-testid="project-filter"]')).to have_content(project.name) @@ -82,11 +88,17 @@ RSpec.describe 'User uses search filters', :js, feature_category: :global_search describe 'clear filter button' do it 'removes Project filters' do - find('[data-testid="project-filter"] [data-testid="clear-icon"]').click - wait_for_requests + page.within '[data-testid="project-filter"]' do + toggle_listbox + wait_for_requests + + find('[data-testid="listbox-reset-button"]').click + + wait_for_requests - expect(page).to have_current_path(search_path, ignore_query: true) do |uri| - uri.normalized_query(:sorted) == "scope=blobs&search=test" + expect(page).to have_current_path(search_path, ignore_query: true) do |uri| + uri.normalized_query(:sorted) == "scope=blobs&search=test" + end end end end diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js index 1cda9853ccd..5e36cfe915a 100644 --- a/spec/frontend/invite_members/components/members_token_select_spec.js +++ b/spec/frontend/invite_members/components/members_token_select_spec.js @@ -89,23 +89,11 @@ describe('MembersTokenSelect', () => { wrapper = createComponent(); }); - describe('when input is focused for the first time (modal auto-focus)', () => { - it('does not call the API', async () => { - findTokenSelector().vm.$emit('focus'); - - await waitForPromises(); - - expect(UserApi.getUsers).not.toHaveBeenCalled(); - }); - }); - describe('when input is manually focused', () => { it('calls the API and sets dropdown items as request result', async () => { const tokenSelector = findTokenSelector(); tokenSelector.vm.$emit('focus'); - tokenSelector.vm.$emit('blur'); - tokenSelector.vm.$emit('focus'); await waitForPromises(); diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js index e0d2984893b..cd5bc08faf0 100644 --- a/spec/frontend/repository/components/blob_content_viewer_spec.js +++ b/spec/frontend/repository/components/blob_content_viewer_spec.js @@ -75,6 +75,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute createMergeRequestIn = userPermissionsMock.createMergeRequestIn, isBinary, inject = {}, + blobBlameInfo = true, } = mockData; const blobInfo = { @@ -138,7 +139,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute ...inject, glFeatures: { highlightJsWorker: false, - blobBlameInfo: true, + blobBlameInfo, }, }, }), @@ -185,7 +186,7 @@ describe('Blob content viewer component', () => { expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(false); expect(findBlobHeader().props('blob')).toEqual(simpleViewerMock); expect(findBlobHeader().props('showForkSuggestion')).toEqual(false); - expect(findBlobHeader().props('showBlameToggle')).toEqual(false); + expect(findBlobHeader().props('showBlameToggle')).toEqual(true); expect(findBlobHeader().props('projectPath')).toEqual(propsMock.projectPath); expect(findBlobHeader().props('projectId')).toEqual(projectMock.id); expect(mockRouterPush).not.toHaveBeenCalled(); @@ -197,15 +198,15 @@ describe('Blob content viewer component', () => { await nextTick(); }; - it('renders a blame toggle for JSON files', async () => { - await createComponent({ blob: { ...simpleViewerMock, language: 'json' } }); + it('renders a blame toggle', async () => { + await createComponent({ blob: simpleViewerMock }); expect(findBlobHeader().props('showBlameToggle')).toEqual(true); }); it('adds blame param to the URL and passes `showBlame` to the SourceViewer', async () => { loadViewer.mockReturnValueOnce(SourceViewerNew); - await createComponent({ blob: { ...simpleViewerMock, language: 'json' } }); + await createComponent({ blob: simpleViewerMock }); await triggerBlame(); @@ -217,6 +218,25 @@ describe('Blob content viewer component', () => { expect(mockRouterPush).toHaveBeenCalledWith({ query: { blame: '0' } }); expect(findSourceViewerNew().props('showBlame')).toBe(false); }); + + describe('blobBlameInfo feature flag disabled', () => { + it('does not render a blame toggle', async () => { + await createComponent({ blob: simpleViewerMock, blobBlameInfo: false }); + + expect(findBlobHeader().props('showBlameToggle')).toEqual(false); + }); + }); + + describe('when viewing rich content', () => { + it('always shows the blame when clicking on the blame button', async () => { + loadViewer.mockReturnValueOnce(SourceViewerNew); + const query = { plain: '0', blame: '1' }; + await createComponent({ blob: simpleViewerMock }, shallowMount, { query }); + await triggerBlame(); + + expect(findSourceViewerNew().props('showBlame')).toBe(true); + }); + }); }); it('creates an alert when the BlobHeader component emits an error', async () => { @@ -260,6 +280,7 @@ describe('Blob content viewer component', () => { expect(mockAxios.history.get).toHaveLength(1); expect(mockAxios.history.get[0].url).toBe(legacyViewerUrl); + expect(findBlobHeader().props('showBlameToggle')).toEqual(false); }); it('loads a legacy viewer when a viewer component is not available', async () => { diff --git a/spec/frontend/search/store/mutations_spec.js b/spec/frontend/search/store/mutations_spec.js index a517932b0eb..3462d4a326b 100644 --- a/spec/frontend/search/store/mutations_spec.js +++ b/spec/frontend/search/store/mutations_spec.js @@ -31,7 +31,7 @@ describe('Global Search Store Mutations', () => { mutations[types.RECEIVE_GROUPS_SUCCESS](state, MOCK_GROUPS); expect(state.fetchingGroups).toBe(false); - expect(state.groups).toBe(MOCK_GROUPS); + expect(state.groups).toStrictEqual(MOCK_GROUPS); }); }); @@ -57,7 +57,7 @@ describe('Global Search Store Mutations', () => { mutations[types.RECEIVE_PROJECTS_SUCCESS](state, MOCK_PROJECTS); expect(state.fetchingProjects).toBe(false); - expect(state.projects).toBe(MOCK_PROJECTS); + expect(state.projects).toStrictEqual(MOCK_PROJECTS); }); }); diff --git a/spec/frontend/search/topbar/components/group_filter_spec.js b/spec/frontend/search/topbar/components/group_filter_spec.js index fa8036a7f97..b360c7134cd 100644 --- a/spec/frontend/search/topbar/components/group_filter_spec.js +++ b/spec/frontend/search/topbar/components/group_filter_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { cloneDeep } from 'lodash'; import Vue from 'vue'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; @@ -27,6 +28,7 @@ describe('GroupFilter', () => { const defaultProps = { initialData: null, + searchHandler: jest.fn(), }; const createComponent = (initialState, props) => { @@ -68,19 +70,6 @@ describe('GroupFilter', () => { createComponent(); }); - describe('when @search is emitted', () => { - const search = 'test'; - - beforeEach(() => { - findSearchableDropdown().vm.$emit('search', search); - }); - - it('calls fetchGroups with the search paramter', () => { - expect(actionSpies.fetchGroups).toHaveBeenCalledTimes(1); - expect(actionSpies.fetchGroups).toHaveBeenCalledWith(expect.any(Object), search); - }); - }); - describe('when @change is emitted with Any', () => { beforeEach(() => { findSearchableDropdown().vm.$emit('change', ANY_OPTION); @@ -148,11 +137,12 @@ describe('GroupFilter', () => { describe('when initialData is set', () => { beforeEach(() => { - createComponent({}, { initialData: MOCK_GROUP }); + createComponent({}, { groupInitialJson: { ...MOCK_GROUP } }); }); it('sets selectedGroup to ANY_OPTION', () => { - expect(wrapper.vm.selectedGroup).toBe(MOCK_GROUP); + // cloneDeep to fix Property or method `nodeType` is not defined bug + expect(cloneDeep(wrapper.vm.selectedGroup)).toStrictEqual(MOCK_GROUP); }); }); }); @@ -169,7 +159,12 @@ describe('GroupFilter', () => { initialData ? 'has' : 'does not have' } an initial group`, () => { beforeEach(() => { - createComponent({ query: { ...MOCK_QUERY, nav_source: navSource } }, { initialData }); + createComponent( + { + query: { ...MOCK_QUERY, nav_source: navSource }, + }, + { groupInitialJson: { ...initialData } }, + ); }); it(`${callMethod ? 'does' : 'does not'} call setFrequentGroup`, () => { diff --git a/spec/frontend/search/topbar/components/project_filter_spec.js b/spec/frontend/search/topbar/components/project_filter_spec.js index e7808370098..9aeb2362279 100644 --- a/spec/frontend/search/topbar/components/project_filter_spec.js +++ b/spec/frontend/search/topbar/components/project_filter_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { cloneDeep } from 'lodash'; import Vue from 'vue'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; @@ -27,6 +28,8 @@ describe('ProjectFilter', () => { const defaultProps = { initialData: null, + projectInitialJson: MOCK_PROJECT, + searchHandler: jest.fn(), }; const createComponent = (initialState, props) => { @@ -68,18 +71,6 @@ describe('ProjectFilter', () => { createComponent(); }); - describe('when @search is emitted', () => { - const search = 'test'; - - beforeEach(() => { - findSearchableDropdown().vm.$emit('search', search); - }); - - it('calls fetchProjects with the search paramter', () => { - expect(actionSpies.fetchProjects).toHaveBeenCalledWith(expect.any(Object), search); - }); - }); - describe('when @change is emitted', () => { describe('with Any', () => { beforeEach(() => { @@ -139,17 +130,17 @@ describe('ProjectFilter', () => { describe('selectedProject', () => { describe('when initialData is null', () => { beforeEach(() => { - createComponent(); + createComponent({}, { projectInitialJson: ANY_OPTION }); }); it('sets selectedProject to ANY_OPTION', () => { - expect(wrapper.vm.selectedProject).toBe(ANY_OPTION); + expect(cloneDeep(wrapper.vm.selectedProject)).toStrictEqual(ANY_OPTION); }); }); describe('when initialData is set', () => { beforeEach(() => { - createComponent({}, { initialData: MOCK_PROJECT }); + createComponent({ projectInitialJson: MOCK_PROJECT }, {}); }); it('sets selectedProject to the initialData', () => { @@ -170,7 +161,12 @@ describe('ProjectFilter', () => { initialData ? 'has' : 'does not have' } an initial project`, () => { beforeEach(() => { - createComponent({ query: { ...MOCK_QUERY, nav_source: navSource } }, { initialData }); + createComponent( + { + query: { ...MOCK_QUERY, nav_source: navSource }, + }, + { projectInitialJson: { ...initialData } }, + ); }); it(`${callMethod ? 'does' : 'does not'} call setFrequentProject`, () => { diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js deleted file mode 100644 index c911fe53d40..00000000000 --- a/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js +++ /dev/null @@ -1,93 +0,0 @@ -import { GlDropdownItem, GlAvatar } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { MOCK_GROUPS } from 'jest/search/mock_data'; -import { truncateNamespace } from '~/lib/utils/text_utility'; -import SearchableDropdownItem from '~/search/topbar/components/searchable_dropdown_item.vue'; -import { GROUP_DATA } from '~/search/topbar/constants'; - -describe('Global Search Searchable Dropdown Item', () => { - let wrapper; - - const defaultProps = { - item: MOCK_GROUPS[0], - selectedItem: MOCK_GROUPS[0], - name: GROUP_DATA.name, - fullName: GROUP_DATA.fullName, - }; - - const createComponent = (props) => { - wrapper = shallowMountExtended(SearchableDropdownItem, { - propsData: { - ...defaultProps, - ...props, - }, - }); - }; - - const findGlDropdownItem = () => wrapper.findComponent(GlDropdownItem); - const findGlAvatar = () => wrapper.findComponent(GlAvatar); - const findDropdownTitle = () => wrapper.findByTestId('item-title'); - const findDropdownSubtitle = () => wrapper.findByTestId('item-namespace'); - - describe('template', () => { - describe('always', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders GlDropdownItem', () => { - expect(findGlDropdownItem().exists()).toBe(true); - }); - - it('renders GlAvatar', () => { - expect(findGlAvatar().exists()).toBe(true); - }); - - it('renders Dropdown Title correctly', () => { - const titleEl = findDropdownTitle(); - - expect(titleEl.exists()).toBe(true); - expect(titleEl.text()).toBe(MOCK_GROUPS[0][GROUP_DATA.name]); - }); - - it('renders Dropdown Subtitle correctly', () => { - const subtitleEl = findDropdownSubtitle(); - - expect(subtitleEl.exists()).toBe(true); - expect(subtitleEl.text()).toBe(truncateNamespace(MOCK_GROUPS[0][GROUP_DATA.fullName])); - }); - }); - - describe('when item === selectedItem', () => { - beforeEach(() => { - createComponent({ item: MOCK_GROUPS[0], selectedItem: MOCK_GROUPS[0] }); - }); - - it('marks the dropdown as checked', () => { - expect(findGlDropdownItem().attributes('ischecked')).toBe('true'); - }); - }); - - describe('when item !== selectedItem', () => { - beforeEach(() => { - createComponent({ item: MOCK_GROUPS[0], selectedItem: MOCK_GROUPS[1] }); - }); - - it('marks the dropdown as not checked', () => { - expect(findGlDropdownItem().attributes('ischecked')).toBeUndefined(); - }); - }); - }); - - describe('actions', () => { - beforeEach(() => { - createComponent(); - }); - - it('clicking the dropdown item $emits change with the item', () => { - findGlDropdownItem().vm.$emit('click'); - - expect(wrapper.emitted('change')[0]).toEqual([MOCK_GROUPS[0]]); - }); - }); -}); diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js index 5acaa1c1900..1d4ccbf66a6 100644 --- a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js +++ b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js @@ -1,12 +1,14 @@ -import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; +import { GlCollapsibleListbox } from '@gitlab/ui'; +import { cloneDeep } from 'lodash'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import { MOCK_GROUPS, MOCK_GROUP, MOCK_QUERY } from 'jest/search/mock_data'; +import waitForPromises from 'helpers/wait_for_promises'; +import { MOCK_GROUPS, MOCK_QUERY } from 'jest/search/mock_data'; import SearchableDropdown from '~/search/topbar/components/searchable_dropdown.vue'; import { ANY_OPTION, GROUP_DATA } from '~/search/topbar/constants'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; Vue.use(Vuex); @@ -20,9 +22,11 @@ describe('Global Search Searchable Dropdown', () => { loading: false, selectedItem: ANY_OPTION, items: [], + frequentItems: [{ ...MOCK_GROUPS[0] }], + searchHandler: jest.fn(), }; - const createComponent = (initialState, props, mountFn = shallowMount) => { + const createComponent = (initialState, props) => { const store = new Vuex.Store({ state: { query: MOCK_QUERY, @@ -30,26 +34,16 @@ describe('Global Search Searchable Dropdown', () => { }, }); - wrapper = extendedWrapper( - mountFn(SearchableDropdown, { - store, - propsData: { - ...defaultProps, - ...props, - }, - }), - ); + wrapper = shallowMount(SearchableDropdown, { + store, + propsData: { + ...defaultProps, + ...props, + }, + }); }; - const findGlDropdown = () => wrapper.findComponent(GlDropdown); - const findGlDropdownSearch = () => findGlDropdown().findComponent(GlSearchBoxByType); - const findDropdownText = () => findGlDropdown().find('.dropdown-toggle-text'); - const findSearchableDropdownItems = () => wrapper.findAllByTestId('searchable-items'); - const findFrequentDropdownItems = () => wrapper.findAllByTestId('frequent-items'); - const findAnyDropdownItem = () => findGlDropdown().findComponent(GlDropdownItem); - const findFirstSearchableDropdownItem = () => findSearchableDropdownItems().at(0); - const findFirstFrequentDropdownItem = () => findFrequentDropdownItems().at(0); - const findLoader = () => wrapper.findComponent(GlSkeletonLoader); + const findGlDropdown = () => wrapper.findComponent(GlCollapsibleListbox); describe('template', () => { beforeEach(() => { @@ -60,161 +54,64 @@ describe('Global Search Searchable Dropdown', () => { expect(findGlDropdown().exists()).toBe(true); }); - describe('findGlDropdownSearch', () => { - it('renders always', () => { - expect(findGlDropdownSearch().exists()).toBe(true); - }); - - it('has debounce prop', () => { - expect(findGlDropdownSearch().attributes('debounce')).toBe('500'); - }); - - describe('onSearch', () => { - const search = 'test search'; - - beforeEach(() => { - findGlDropdownSearch().vm.$emit('input', search); - }); + const propItems = [ + { text: '', options: [{ value: ANY_OPTION.name, text: ANY_OPTION.name, ...ANY_OPTION }] }, + { + text: 'Frequently searched', + options: [{ value: MOCK_GROUPS[0].id, text: MOCK_GROUPS[0].full_name, ...MOCK_GROUPS[0] }], + }, + { + text: 'All available groups', + options: [{ value: MOCK_GROUPS[1].id, text: MOCK_GROUPS[1].full_name, ...MOCK_GROUPS[1] }], + }, + ]; - it('$emits @search when input event is fired from GlSearchBoxByType', () => { - expect(wrapper.emitted('search')[0]).toEqual([search]); - }); - }); + beforeEach(() => { + createComponent({}, { items: MOCK_GROUPS }); }); - describe('Searchable Dropdown Items', () => { - describe('when loading is false', () => { - beforeEach(() => { - createComponent({}, { items: MOCK_GROUPS }); - }); - - it('does not render loader', () => { - expect(findLoader().exists()).toBe(false); - }); - - it('renders the Any Dropdown', () => { - expect(findAnyDropdownItem().exists()).toBe(true); - }); - - it('renders searchable dropdown item for each item', () => { - expect(findSearchableDropdownItems()).toHaveLength(MOCK_GROUPS.length); - }); - }); - - describe('when loading is true', () => { - beforeEach(() => { - createComponent({}, { loading: true, items: MOCK_GROUPS }); - }); - - it('does render loader', () => { - expect(findLoader().exists()).toBe(true); - }); - - it('renders the Any Dropdown', () => { - expect(findAnyDropdownItem().exists()).toBe(true); - }); - - it('does not render searchable dropdown items', () => { - expect(findSearchableDropdownItems()).toHaveLength(0); - }); - }); + it('contains correct set of items', () => { + expect(findGlDropdown().props('items')).toStrictEqual(propItems); }); - describe.each` - searchText | frequentItems | length - ${''} | ${[]} | ${0} - ${''} | ${MOCK_GROUPS} | ${MOCK_GROUPS.length} - ${'test'} | ${[]} | ${0} - ${'test'} | ${MOCK_GROUPS} | ${0} - `('Frequent Dropdown Items', ({ searchText, frequentItems, length }) => { - describe(`when search is ${searchText} and frequentItems length is ${frequentItems.length}`, () => { - beforeEach(() => { - createComponent({}, { frequentItems }); - findGlDropdownSearch().vm.$emit('input', searchText); - }); - - it(`should${length ? '' : ' not'} render frequent dropdown items`, () => { - expect(findFrequentDropdownItems()).toHaveLength(length); - }); - }); + it('renders searchable prop', () => { + expect(findGlDropdown().props('searchable')).toBe(true); }); - describe('Dropdown Text', () => { - describe('when selectedItem is any', () => { - beforeEach(() => { - createComponent({}, {}, mount); - }); - - it('sets dropdown text to Any', () => { - expect(findDropdownText().text()).toBe(ANY_OPTION.name); - }); + describe('events', () => { + it('emits select', () => { + findGlDropdown().vm.$emit('select', 1); + expect(cloneDeep(wrapper.emitted('change')[0][0])).toStrictEqual(MOCK_GROUPS[0]); }); - describe('selectedItem is set', () => { - beforeEach(() => { - createComponent({}, { selectedItem: MOCK_GROUP }, mount); - }); + it('emits reset', () => { + findGlDropdown().vm.$emit('reset'); + expect(cloneDeep(wrapper.emitted('change')[0][0])).toStrictEqual(ANY_OPTION); + }); - it('sets dropdown text to the selectedItem name', () => { - expect(findDropdownText().text()).toBe(MOCK_GROUP[GROUP_DATA.name]); - }); + it('emits first-open', () => { + findGlDropdown().vm.$emit('shown'); + expect(wrapper.emitted('first-open')).toHaveLength(1); + findGlDropdown().vm.$emit('shown'); + expect(wrapper.emitted('first-open')).toHaveLength(1); }); }); }); - describe('actions', () => { - beforeEach(() => { - createComponent({}, { items: MOCK_GROUPS, frequentItems: MOCK_GROUPS }); - }); - - it('clicking "Any" dropdown item $emits @change with ANY_OPTION', () => { - findAnyDropdownItem().vm.$emit('click'); + describe('when @search is emitted', () => { + const search = 'test'; - expect(wrapper.emitted('change')[0]).toEqual([ANY_OPTION]); - }); - - it('on searchable item @change, the wrapper $emits change with the item', () => { - findFirstSearchableDropdownItem().vm.$emit('change', MOCK_GROUPS[0]); - - expect(wrapper.emitted('change')[0]).toEqual([MOCK_GROUPS[0]]); - }); - - it('on frequent item @change, the wrapper $emits change with the item', () => { - findFirstFrequentDropdownItem().vm.$emit('change', MOCK_GROUPS[0]); + beforeEach(async () => { + createComponent(); + findGlDropdown().vm.$emit('search', search); - expect(wrapper.emitted('change')[0]).toEqual([MOCK_GROUPS[0]]); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + await waitForPromises(); }); - describe('opening the dropdown', () => { - beforeEach(() => { - findGlDropdown().vm.$emit('show'); - }); - - it('$emits @search and @first-open on the first open', () => { - expect(wrapper.emitted('search')[0]).toStrictEqual(['']); - expect(wrapper.emitted('first-open')[0]).toStrictEqual([]); - }); - - describe('when the dropdown has been opened', () => { - it('$emits @search with the searchText', async () => { - const searchText = 'foo'; - - findGlDropdownSearch().vm.$emit('input', searchText); - await nextTick(); - - expect(wrapper.emitted('search')[1]).toStrictEqual([searchText]); - expect(wrapper.emitted('first-open')).toHaveLength(1); - }); - - it('does not emit @first-open again', async () => { - expect(wrapper.emitted('first-open')).toHaveLength(1); - - findGlDropdownSearch().vm.$emit('input'); - await nextTick(); - - expect(wrapper.emitted('first-open')).toHaveLength(1); - }); - }); + it('calls fetchGroups with the search paramter', () => { + expect(defaultProps.searchHandler).toHaveBeenCalledTimes(1); + expect(defaultProps.searchHandler).toHaveBeenCalledWith(search); }); }); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/mock_data.js index cfff3a15b77..c98f945fc54 100644 --- a/spec/frontend/vue_shared/components/source_viewer/mock_data.js +++ b/spec/frontend/vue_shared/components/source_viewer/mock_data.js @@ -79,6 +79,7 @@ export const BLAME_DATA_QUERY_RESPONSE_MOCK = { titleHtml: 'Upload New File', message: 'Upload New File', authoredDate: '2022-10-31T10:38:30+00:00', + authorName: 'Peter', authorGravatar: 'path/to/gravatar', webPath: '/commit/1234', author: {}, diff --git a/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue b/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue index e319b199cb0..d1874d2f514 100644 --- a/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue +++ b/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue @@ -32,7 +32,7 @@ left: !useTransform && direction !== 'vertical' ? `${view.position}px` : null, } : null" class="vue-recycle-scroller__item-view" - :class="{ hover: hoverKey === view.nr.key }" + :class="{ hover: hoverKey === view.nr.key, 'will-change-transform': useTransform }" @mouseenter="hoverKey = view.nr.key" @mouseleave="hoverKey = null" > @@ -670,6 +670,9 @@ export default { position: absolute; top: 0; left: 0; +} + +.will-change-transform { will-change: transform; } |