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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-12-05 00:14:01 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-12-05 00:14:01 +0300
commitf4056ff4474daf3da66ceaf4473306b0c4652897 (patch)
treeba6ca64ede0a7ec8d2c6971c7f3f5b3d8ab5f81d
parent795b6eb292706d577c13556a3583897f082dda6e (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.checksum10
-rw-r--r--Gemfile.lock26
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue13
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue19
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue12
-rw-r--r--app/assets/javascripts/search/store/mutations.js4
-rw-r--r--app/assets/javascripts/search/topbar/components/app.vue21
-rw-r--r--app/assets/javascripts/search/topbar/components/group_filter.vue33
-rw-r--r--app/assets/javascripts/search/topbar/components/project_filter.vue34
-rw-r--r--app/assets/javascripts/search/topbar/components/searchable_dropdown.vue278
-rw-r--r--app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue78
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql1
-rw-r--r--app/controllers/projects_controller.rb4
-rw-r--r--app/views/projects/missing_default_branch.html.haml10
-rw-r--r--config/events/perform_completion_worker.yml21
-rw-r--r--db/click_house/migrate/20231129062064_create_contributions_table.rb27
-rw-r--r--db/click_house/migrate/20231129062151_create_contributions_mv.rb30
-rw-r--r--db/docs/feature_gates.yml1
-rw-r--r--db/docs/features.yml1
-rw-r--r--locale/gitlab.pot92
-rw-r--r--spec/controllers/projects_controller_spec.rb19
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb9
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb3
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb3
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb3
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb3
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb3
-rw-r--r--spec/features/search/user_uses_search_filters_spec.rb34
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js12
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js31
-rw-r--r--spec/frontend/search/store/mutations_spec.js4
-rw-r--r--spec/frontend/search/topbar/components/group_filter_spec.js27
-rw-r--r--spec/frontend/search/topbar/components/project_filter_spec.js28
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js93
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_spec.js219
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/mock_data.js1
-rw-r--r--vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue5
38 files changed, 566 insertions, 652 deletions
diff --git a/Gemfile b/Gemfile
index 44894d6a62c..d7a422924da 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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;
}