diff --git a/app/assets/javascripts/merge_request_dashboard/queries/merge_request.fragment.graphql b/app/assets/javascripts/merge_request_dashboard/queries/merge_request.fragment.graphql
index 077ae82594f..cdb50fe9900 100644
--- a/app/assets/javascripts/merge_request_dashboard/queries/merge_request.fragment.graphql
+++ b/app/assets/javascripts/merge_request_dashboard/queries/merge_request.fragment.graphql
@@ -38,7 +38,7 @@ fragment MergeRequestDashboardFragment on MergeRequest {
...CiIcon
}
}
- userDiscussionsCount
+ userNotesCount
createdAt
updatedAt
...MergeRequestApprovalFragment
diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js
index f5b405ab6b4..5c546056e60 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -13,7 +13,7 @@ const sidebarInitState = () => {
const el = document.getElementById('js-search-sidebar');
if (!el) return {};
- const { navigationJson, searchType, searchLevel, groupInitialJson, projectInitialJson } =
+ const { navigationJson, searchType, searchLevel, groupInitialJson, projectInitialJson, ref } =
el.dataset;
const navigationJsonParsed = JSON.parse(navigationJson);
@@ -26,6 +26,7 @@ const sidebarInitState = () => {
searchLevel,
groupInitialJsonParsed,
projectInitialJsonParsed,
+ ref,
};
};
@@ -49,6 +50,7 @@ export const initSearchApp = () => {
searchLevel,
groupInitialJsonParsed: groupInitialJson,
projectInitialJsonParsed: projectInitialJson,
+ ref,
} = sidebarInitState() || {};
const { defaultBranchName } = topBarInitState() || {};
@@ -61,6 +63,7 @@ export const initSearchApp = () => {
groupInitialJson,
projectInitialJson,
defaultBranchName,
+ repositoryRef: ref,
});
initTopbar(store);
diff --git a/app/assets/javascripts/search/results/components/app.vue b/app/assets/javascripts/search/results/components/app.vue
index 73a6c8f0b95..c87874e3152 100644
--- a/app/assets/javascripts/search/results/components/app.vue
+++ b/app/assets/javascripts/search/results/components/app.vue
@@ -1,33 +1,99 @@
-
+
+
+ {{ $options.i18n.blobDataFetchError }}
+
+
+
diff --git a/app/assets/javascripts/search/results/components/blob_body.vue b/app/assets/javascripts/search/results/components/blob_body.vue
index c2dcfe98082..3cfa63cb4ae 100644
--- a/app/assets/javascripts/search/results/components/blob_body.vue
+++ b/app/assets/javascripts/search/results/components/blob_body.vue
@@ -51,7 +51,7 @@ export default {
diff --git a/app/assets/javascripts/search/results/components/blob_chunks.vue b/app/assets/javascripts/search/results/components/blob_chunks.vue
index 67638194da5..218675b2957 100644
--- a/app/assets/javascripts/search/results/components/blob_chunks.vue
+++ b/app/assets/javascripts/search/results/components/blob_chunks.vue
@@ -1,6 +1,7 @@
+
+
+
+
+ {{ resultsTotal }}
+ {{ query.search }}
+
+
+
+ {{ resultsTotal }}
+ {{ query.search }}{{
+ groupInitialJson.full_name
+ }}
+
+
+
+ {{ resultsTotal }}
+ {{ query.search }}
+
+
+
+ {{
+ projectInitialJson.name_with_namespace
+ }}
+
+
+
diff --git a/app/assets/javascripts/search/results/components/zoekt_blob_results.vue b/app/assets/javascripts/search/results/components/zoekt_blob_results.vue
index 82aebe2511a..a4e18a31532 100644
--- a/app/assets/javascripts/search/results/components/zoekt_blob_results.vue
+++ b/app/assets/javascripts/search/results/components/zoekt_blob_results.vue
@@ -1,90 +1,55 @@
-
-
-
+
+
+
-
+
{
getters.currentScope === SCOPE_BLOB &&
gon.features.zoektMultimatchFrontend
) {
- const newUrl = setUrlParams({ ...state.query, page: null }, window.location.href, false, true);
+ const newUrl = setUrlParams({ ...state.query }, window.location.href, false, true);
updateHistory({ state: state.query, url: newUrl, replace: true });
}
};
diff --git a/app/assets/javascripts/search/store/state.js b/app/assets/javascripts/search/store/state.js
index 279ba467bba..7dc4bc8a8bc 100644
--- a/app/assets/javascripts/search/store/state.js
+++ b/app/assets/javascripts/search/store/state.js
@@ -9,6 +9,7 @@ const createState = ({
groupInitialJson,
projectInitialJson,
defaultBranchName,
+ repositoryRef,
}) => ({
urlQuery: cloneDeep(query),
query,
@@ -33,6 +34,7 @@ const createState = ({
groupInitialJson,
projectInitialJson,
defaultBranchName,
+ repositoryRef,
});
export default createState;
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index fc486cada6b..92a933fcc63 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -23,6 +23,7 @@ import {
updateCacheAfterDeletingNote,
} from '~/work_items/graphql/cache_utils';
import { getLocationHash } from '~/lib/utils/url_utility';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { collapseSystemNotes } from '~/work_items/notes/collapse_utils';
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
import WorkItemHistoryOnlyFilterNote from '~/work_items/components/notes/work_item_history_only_filter_note.vue';
@@ -284,6 +285,18 @@ export default {
reportAbuse(isOpen, reply = {}) {
this.$emit('openReportAbuse', reply);
},
+ noteId(note) {
+ return getIdFromGraphQLId(note.id);
+ },
+ isHashTargeted(discussion) {
+ return (
+ discussion.notes.nodes.length &&
+ discussion.notes.nodes.some((note) => this.targetNoteHash === `note_${this.noteId(note)}`)
+ );
+ },
+ isDiscussionExpandedOnLoad(discussion) {
+ return !this.isDiscussionResolved(discussion) || this.isHashTargeted(discussion);
+ },
isDiscussionResolved(discussion) {
return discussion.notes.nodes[0]?.discussion?.resolved;
},
@@ -390,7 +403,7 @@ export default {
:can-set-work-item-metadata="canSetWorkItemMetadata"
:is-discussion-locked="isDiscussionLocked"
:is-work-item-confidential="isWorkItemConfidential"
- :is-expanded-on-load="!isDiscussionResolved(discussion)"
+ :is-expanded-on-load="isDiscussionExpandedOnLoad(discussion)"
@deleteNote="showDeleteNoteModal($event, discussion)"
@reportAbuse="reportAbuse(true, $event)"
@error="$emit('error', $event)"
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index c764a8edfa4..8e6bdbc9b5d 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -10,6 +10,7 @@
overflow-y: auto;
transition: box-shadow ease-in-out 0.15s;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
width: calc(100% - 6px);
margin: 2px 3px;
@@ -304,6 +305,7 @@
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-dark .ProseMirror {
.suggestion-added-input,
.suggestion-deleted {
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index 242e96a2fa2..bf697155a42 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -23,6 +23,7 @@
.board-swimlanes-headers {
background-color: $white;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
background-color: var(--gray-10);
}
diff --git a/app/assets/stylesheets/page_bundles/branches.scss b/app/assets/stylesheets/page_bundles/branches.scss
index 973ba1afb17..cc87e324671 100644
--- a/app/assets/stylesheets/page_bundles/branches.scss
+++ b/app/assets/stylesheets/page_bundles/branches.scss
@@ -43,6 +43,7 @@
.branches-list .branch-item:not(:last-of-type) {
border-bottom: 1px solid $border-color;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
border-bottom-color: $gray-800;
}
diff --git a/app/assets/stylesheets/page_bundles/incidents.scss b/app/assets/stylesheets/page_bundles/incidents.scss
index 6eb343ecf89..e0523e491a1 100644
--- a/app/assets/stylesheets/page_bundles/incidents.scss
+++ b/app/assets/stylesheets/page_bundles/incidents.scss
@@ -10,6 +10,7 @@
grid-template-columns: auto minmax(0, 1fr) #{$gl-spacing-scale-7};
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-dark .timeline-event-icon {
background-color: $gray-950;
color: $gl-text-secondary;
@@ -46,6 +47,7 @@
height: calc(100% + #{$gl-spacing-scale-5});
top: -#{$gl-spacing-scale-5};
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
border-color: var(--gray-50);
}
@@ -88,6 +90,7 @@
width: calc(100% - $gl-spacing-scale-8 - $gl-spacing-scale-5);
bottom: $gl-spacing-scale-3;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
border-color: var(--gray-50);
}
diff --git a/app/assets/stylesheets/page_bundles/login.scss b/app/assets/stylesheets/page_bundles/login.scss
index f274000a865..e83ea3c7d9d 100644
--- a/app/assets/stylesheets/page_bundles/login.scss
+++ b/app/assets/stylesheets/page_bundles/login.scss
@@ -4,12 +4,12 @@
margin: 0;
padding: 0;
height: 100%;
-
+
body {
&.with-system-header {
padding-top: $system-header-height;
}
-
+
&.with-system-footer {
.footer-container {
padding-bottom: $system-footer-height;
@@ -40,6 +40,7 @@
.sm-bg-gray {
background-color: $gray-10;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
background-color: var(--gray-100);
}
diff --git a/app/assets/stylesheets/page_bundles/merge_request.scss b/app/assets/stylesheets/page_bundles/merge_request.scss
index c938d9e485e..a20b212ecc6 100644
--- a/app/assets/stylesheets/page_bundles/merge_request.scss
+++ b/app/assets/stylesheets/page_bundles/merge_request.scss
@@ -162,6 +162,7 @@ $comparison-empty-state-height: 62px;
border-bottom: 1px solid var(--gl-border-color-default);
background-color: $white;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
background-color: var(--gray-10);
}
diff --git a/app/assets/stylesheets/page_bundles/notes/_system_notes_v2.scss b/app/assets/stylesheets/page_bundles/notes/_system_notes_v2.scss
index cbb0bf628da..fa1d8f083d8 100644
--- a/app/assets/stylesheets/page_bundles/notes/_system_notes_v2.scss
+++ b/app/assets/stylesheets/page_bundles/notes/_system_notes_v2.scss
@@ -96,9 +96,3 @@
margin-left: $gl-spacing-scale-5;
}
}
-
-// Timeline avatars
-
-.timeline-avatar {
- @apply gl-bg-default;
-}
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index a2cd4470cba..08d4bac4829 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -230,6 +230,7 @@
.ci-job-item-failed {
background-color: var(--red-50, $red-50);
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
background-color: var(--gray-200, $gray-200);
}
@@ -309,6 +310,7 @@
// stylelint-disable-next-line gitlab/no-gl-class
.gl-badge.badge-muted {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
@apply gl-bg-gray-100;
}
diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss
index a441b92da29..b78e059b95a 100644
--- a/app/assets/stylesheets/page_bundles/pipelines.scss
+++ b/app/assets/stylesheets/page_bundles/pipelines.scss
@@ -60,6 +60,7 @@
}
.dark-mode-override {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
background-color: $white;
}
diff --git a/app/assets/stylesheets/page_bundles/profiles/preferences.scss b/app/assets/stylesheets/page_bundles/profiles/preferences.scss
index ecd4cd04b69..15f18157e62 100644
--- a/app/assets/stylesheets/page_bundles/profiles/preferences.scss
+++ b/app/assets/stylesheets/page_bundles/profiles/preferences.scss
@@ -48,6 +48,7 @@
&.ui-neutral {
background-color: $gray-50;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
background-color: $gray-950;
border: solid 1px $border-color;
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index 4a18009fff1..8a5abd2afaf 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -287,6 +287,7 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem;
width: 100%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, $white 100%);
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
background: linear-gradient(180deg, rgba(31, 30, 36, 0.00) 0%, $gray-950 100%);
}
@@ -296,6 +297,7 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem;
pointer-events: auto;
background-color: $white;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
background-color: $gray-950;
}
@@ -306,6 +308,7 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem;
flex: 1;
border-top: 1px solid $gray-50;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
border-top: 1px solid $gray-900;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 70dfc7139f2..fc52e0b2d2a 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -84,6 +84,7 @@
margin: 2px;
padding-left: calc(#{$gl-spacing-scale-5} - 2px);
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
width: calc(100% - 6px);
margin: 2px 3px;
@@ -313,6 +314,7 @@ table {
@apply gl-border-b;
@apply gl-border-b-subtle;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
@apply gl-bg-strong;
border-bottom-color: var(--gl-background-color-default);
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 1265a9baa05..d051db96f31 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -35,6 +35,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
.main-notes-list::before {
background: var(--gray-50, $gray-50);
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark .modal-body & {
background: var(--gray-100, $gray-100);
}
@@ -43,10 +44,12 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
.timeline-entry:not(.draft-note):last-child::before {
background: var(--white);
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
background: var(--gray-10);
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark .modal-body & {
background: var(--gray-50, $gray-50);
}
@@ -98,6 +101,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
border-radius: $gl-border-radius-base;
padding: $gl-padding-4 $gl-padding-8;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
@apply gl-bg-strong;
border-color: var(--gl-background-color-default);
@@ -136,6 +140,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
border-top-right-radius: $gl-border-radius-base;
padding: $gl-padding-4 $gl-padding-8;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
@apply gl-bg-strong;
border-bottom-color: var(--gl-background-color-default);
@@ -197,6 +202,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
@apply gl-border-b;
@apply gl-border-b-subtle;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
@apply gl-bg-strong;
border-bottom-color: var(--gl-background-color-default);
@@ -585,6 +591,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
.discussion-notes .timeline-entry:first-of-type > .timeline-entry-inner {
@apply gl-bg-default;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
@apply gl-bg-strong;
border-bottom-color: var(--gl-background-color-default);
@@ -766,6 +773,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
padding: $gl-padding-8 !important;
@include gl-border;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
@apply gl-bg-strong;
@apply gl-border-b;
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 56743f1beaa..9dc540dd71d 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -57,6 +57,7 @@
&.development {
background-color: $perf-bar-development;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
background-color: $red-950;
}
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 45cdf73c757..314b9c0bfbf 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,3 +1,3 @@
.gl-w-full.gl-grow
- = render partial: 'search/results_status'
+ = render partial: 'search/results_status' unless should_show_zoekt_results?(@scope, @search_type)
= render partial: 'search/results_list'
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index b28988e2e83..e0b67e73c22 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -18,6 +18,6 @@
#js-search-topbar{ data: { "default-branch-name": @project&.default_branch } }
.results.lg:gl-flex.gl-mt-0
- #js-search-sidebar{ data: { navigation_json: search_navigation_json, search_type: search_service.search_type, search_level: search_service.level, group_initial_json: group_attributes.to_json, project_initial_json: project_attributes.to_json, } }
+ #js-search-sidebar{ data: { navigation_json: search_navigation_json, search_type: search_service.search_type, search_level: search_service.level, group_initial_json: group_attributes.to_json, project_initial_json: project_attributes.to_json, ref: @project.present? ? repository_ref(@project) : nil } }
- if @search_term
= render 'search/results'
diff --git a/config/events/20211215022206_default_slugged_stream_name_click_dropdown.yml b/config/events/20211215022206_default_slugged_stream_name_click_dropdown.yml
index 271b4f9c80b..e6b77c08311 100644
--- a/config/events/20211215022206_default_slugged_stream_name_click_dropdown.yml
+++ b/config/events/20211215022206_default_slugged_stream_name_click_dropdown.yml
@@ -1,4 +1,4 @@
-description: "Selec stream from dropdown"
+description: "Select stream from dropdown"
category: default
action: click_dropdown
extra_properties:
diff --git a/config/events/create_user.yml b/config/events/create_user.yml
new file mode 100644
index 00000000000..5421b50a7a3
--- /dev/null
+++ b/config/events/create_user.yml
@@ -0,0 +1,18 @@
+---
+description: A user is created
+action: create_user
+identifiers:
+- user
+additional_properties:
+ label:
+ description: "'signup' or 'invited'"
+product_group: acquisition
+milestone: '15.8'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108508
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/config/feature_flags/gitlab_com_derisk/ci_conditionals_reduce_gitaly_calls.yml b/config/feature_flags/gitlab_com_derisk/ci_conditionals_reduce_gitaly_calls.yml
new file mode 100644
index 00000000000..0b2c9c310fe
--- /dev/null
+++ b/config/feature_flags/gitlab_com_derisk/ci_conditionals_reduce_gitaly_calls.yml
@@ -0,0 +1,9 @@
+---
+name: ci_conditionals_reduce_gitaly_calls
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/472223
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162741
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/479151
+milestone: '17.4'
+group: group::security policies
+type: gitlab_com_derisk
+default_enabled: false
diff --git a/config/gitlab_loose_foreign_keys.yml b/config/gitlab_loose_foreign_keys.yml
index 4843a1b7f58..d5a18935425 100644
--- a/config/gitlab_loose_foreign_keys.yml
+++ b/config/gitlab_loose_foreign_keys.yml
@@ -470,6 +470,19 @@ vulnerability_export_parts:
- table: organizations
column: organization_id
on_delete: async_delete
+vulnerability_exports:
+ - table: organizations
+ column: organization_id
+ on_delete: async_delete
+ - table: namespaces
+ column: group_id
+ on_delete: async_delete
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+ - table: users
+ column: author_id
+ on_delete: async_delete
vulnerability_feedback:
- table: ci_pipelines
column: pipeline_id
diff --git a/db/migrate/20240808175619_add_composite_index_to_subscription_add_on_purchases.rb b/db/migrate/20240808175619_add_composite_index_to_subscription_add_on_purchases.rb
new file mode 100644
index 00000000000..8f5b79c28d4
--- /dev/null
+++ b/db/migrate/20240808175619_add_composite_index_to_subscription_add_on_purchases.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddCompositeIndexToSubscriptionAddOnPurchases < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+ milestone '17.4'
+
+ INDEX_NAME = 'index_subscription_add_on_purchases_on_namespace_id_add_on_id'
+
+ def up
+ add_concurrent_index :subscription_add_on_purchases,
+ [:namespace_id, :subscription_add_on_id],
+ name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index :subscription_add_on_purchases,
+ [:namespace_id, :subscription_add_on_id],
+ name: INDEX_NAME
+ end
+end
diff --git a/db/migrate/20240808182052_remove_namespace_id_index_from_subscription_add_on_purchases.rb b/db/migrate/20240808182052_remove_namespace_id_index_from_subscription_add_on_purchases.rb
new file mode 100644
index 00000000000..64e32e6cbc1
--- /dev/null
+++ b/db/migrate/20240808182052_remove_namespace_id_index_from_subscription_add_on_purchases.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class RemoveNamespaceIdIndexFromSubscriptionAddOnPurchases < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+ milestone '17.4'
+
+ INDEX_NAME = 'index_subscription_add_on_purchases_on_namespace_id'
+
+ def up
+ remove_concurrent_index :subscription_add_on_purchases, :namespace_id, name: INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :subscription_add_on_purchases, :namespace_id, name: INDEX_NAME
+ end
+end
diff --git a/db/migrate/20240808182231_remove_subscription_add_on_id_index_from_subscription_add_on_purchases.rb b/db/migrate/20240808182231_remove_subscription_add_on_id_index_from_subscription_add_on_purchases.rb
new file mode 100644
index 00000000000..1c8084270c6
--- /dev/null
+++ b/db/migrate/20240808182231_remove_subscription_add_on_id_index_from_subscription_add_on_purchases.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class RemoveSubscriptionAddOnIdIndexFromSubscriptionAddOnPurchases < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+ milestone '17.4'
+
+ INDEX_NAME = 'index_subscription_add_on_purchases_on_subscription_add_on_id'
+
+ def up
+ remove_concurrent_index :subscription_add_on_purchases, :subscription_add_on_id,
+ name: INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :subscription_add_on_purchases, :subscription_add_on_id,
+ name: INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20240827115134_remove_organizations_vulnerability_exports_organization_id_fk.rb b/db/post_migrate/20240827115134_remove_organizations_vulnerability_exports_organization_id_fk.rb
new file mode 100644
index 00000000000..f889e7c6879
--- /dev/null
+++ b/db/post_migrate/20240827115134_remove_organizations_vulnerability_exports_organization_id_fk.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class RemoveOrganizationsVulnerabilityExportsOrganizationIdFk < Gitlab::Database::Migration[2.2]
+ milestone '17.4'
+ disable_ddl_transaction!
+
+ FOREIGN_KEY_NAME = "fk_90e75ccdf8"
+
+ def up
+ with_lock_retries do
+ remove_foreign_key_if_exists(:vulnerability_exports, :organizations,
+ name: FOREIGN_KEY_NAME, reverse_lock_order: true)
+ end
+ end
+
+ def down
+ add_concurrent_foreign_key(:vulnerability_exports, :organizations,
+ name: FOREIGN_KEY_NAME, column: :organization_id,
+ target_column: :id, on_delete: :cascade)
+ end
+end
diff --git a/db/post_migrate/20240827115156_remove_namespaces_vulnerability_exports_group_id_fk.rb b/db/post_migrate/20240827115156_remove_namespaces_vulnerability_exports_group_id_fk.rb
new file mode 100644
index 00000000000..39351f0e6bc
--- /dev/null
+++ b/db/post_migrate/20240827115156_remove_namespaces_vulnerability_exports_group_id_fk.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class RemoveNamespacesVulnerabilityExportsGroupIdFk < Gitlab::Database::Migration[2.2]
+ milestone '17.4'
+ disable_ddl_transaction!
+
+ FOREIGN_KEY_NAME = "fk_c3d3cb5d0f"
+
+ def up
+ with_lock_retries do
+ remove_foreign_key_if_exists(:vulnerability_exports, :namespaces,
+ name: FOREIGN_KEY_NAME, reverse_lock_order: true)
+ end
+ end
+
+ def down
+ add_concurrent_foreign_key(:vulnerability_exports, :namespaces,
+ name: FOREIGN_KEY_NAME, column: :group_id,
+ target_column: :id, on_delete: :cascade)
+ end
+end
diff --git a/db/post_migrate/20240827115218_remove_users_vulnerability_exports_author_id_fk.rb b/db/post_migrate/20240827115218_remove_users_vulnerability_exports_author_id_fk.rb
new file mode 100644
index 00000000000..b2e9831a12f
--- /dev/null
+++ b/db/post_migrate/20240827115218_remove_users_vulnerability_exports_author_id_fk.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class RemoveUsersVulnerabilityExportsAuthorIdFk < Gitlab::Database::Migration[2.2]
+ milestone '17.4'
+ disable_ddl_transaction!
+
+ FOREIGN_KEY_NAME = "fk_rails_1019162882"
+
+ def up
+ with_lock_retries do
+ remove_foreign_key_if_exists(:vulnerability_exports, :users,
+ name: FOREIGN_KEY_NAME, reverse_lock_order: true)
+ end
+ end
+
+ def down
+ add_concurrent_foreign_key(:vulnerability_exports, :users,
+ name: FOREIGN_KEY_NAME, column: :author_id,
+ target_column: :id, on_delete: :cascade)
+ end
+end
diff --git a/db/post_migrate/20240827115239_remove_projects_vulnerability_exports_project_id_fk.rb b/db/post_migrate/20240827115239_remove_projects_vulnerability_exports_project_id_fk.rb
new file mode 100644
index 00000000000..0a0aee867bb
--- /dev/null
+++ b/db/post_migrate/20240827115239_remove_projects_vulnerability_exports_project_id_fk.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class RemoveProjectsVulnerabilityExportsProjectIdFk < Gitlab::Database::Migration[2.2]
+ milestone '17.4'
+ disable_ddl_transaction!
+
+ FOREIGN_KEY_NAME = "fk_rails_9aff2c3b45"
+
+ def up
+ with_lock_retries do
+ remove_foreign_key_if_exists(:vulnerability_exports, :projects,
+ name: FOREIGN_KEY_NAME, reverse_lock_order: true)
+ end
+ end
+
+ def down
+ add_concurrent_foreign_key(:vulnerability_exports, :projects,
+ name: FOREIGN_KEY_NAME, column: :project_id,
+ target_column: :id, on_delete: :cascade)
+ end
+end
diff --git a/db/schema_migrations/20240808175619 b/db/schema_migrations/20240808175619
new file mode 100644
index 00000000000..adc0561f764
--- /dev/null
+++ b/db/schema_migrations/20240808175619
@@ -0,0 +1 @@
+b19bdab41864682ccef9b4cbde289e3c4bd5a924286bd8282df5559686ac414b
\ No newline at end of file
diff --git a/db/schema_migrations/20240808182052 b/db/schema_migrations/20240808182052
new file mode 100644
index 00000000000..d530b43113f
--- /dev/null
+++ b/db/schema_migrations/20240808182052
@@ -0,0 +1 @@
+ad1958f4ade8d13c64d19894bd0115a357d7322443fb95acbcfc0c582611afc5
\ No newline at end of file
diff --git a/db/schema_migrations/20240808182231 b/db/schema_migrations/20240808182231
new file mode 100644
index 00000000000..bd795aa54cd
--- /dev/null
+++ b/db/schema_migrations/20240808182231
@@ -0,0 +1 @@
+4bebe3172ceb8fcb39dbbf722bc8eb841eb73ca6e409979f325acc54e6672cf4
\ No newline at end of file
diff --git a/db/schema_migrations/20240827115134 b/db/schema_migrations/20240827115134
new file mode 100644
index 00000000000..82aeb5fd34a
--- /dev/null
+++ b/db/schema_migrations/20240827115134
@@ -0,0 +1 @@
+42f405277c2e1b5c4ea935ddcfe20d550a135bf9bbfc5ef6174e531b3b04e540
\ No newline at end of file
diff --git a/db/schema_migrations/20240827115156 b/db/schema_migrations/20240827115156
new file mode 100644
index 00000000000..171c66b1658
--- /dev/null
+++ b/db/schema_migrations/20240827115156
@@ -0,0 +1 @@
+df9e7c4f161cdc7bb359d9cfa36bf3117c28d950481f69be90c80ffdf1f3c96d
\ No newline at end of file
diff --git a/db/schema_migrations/20240827115218 b/db/schema_migrations/20240827115218
new file mode 100644
index 00000000000..bdfc6152866
--- /dev/null
+++ b/db/schema_migrations/20240827115218
@@ -0,0 +1 @@
+4ca6cc8ccecbdec8dae4af9af5099e782a69a3b2e92e806e04c7fad8e3a26e70
\ No newline at end of file
diff --git a/db/schema_migrations/20240827115239 b/db/schema_migrations/20240827115239
new file mode 100644
index 00000000000..49ef4326b69
--- /dev/null
+++ b/db/schema_migrations/20240827115239
@@ -0,0 +1 @@
+02b4b6ab225fc477382c66434dba8ffda9deede17f610361f7d34ab5c135622e
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index b1794194905..33d3b118020 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -30202,9 +30202,7 @@ CREATE UNIQUE INDEX index_status_page_published_incidents_on_issue_id ON status_
CREATE INDEX index_status_page_settings_on_project_id ON status_page_settings USING btree (project_id);
-CREATE INDEX index_subscription_add_on_purchases_on_namespace_id ON subscription_add_on_purchases USING btree (namespace_id);
-
-CREATE INDEX index_subscription_add_on_purchases_on_subscription_add_on_id ON subscription_add_on_purchases USING btree (subscription_add_on_id);
+CREATE INDEX index_subscription_add_on_purchases_on_namespace_id_add_on_id ON subscription_add_on_purchases USING btree (namespace_id, subscription_add_on_id);
CREATE UNIQUE INDEX index_subscription_add_ons_on_name ON subscription_add_ons USING btree (name);
@@ -33792,9 +33790,6 @@ ALTER TABLE ONLY audit_events_streaming_group_namespace_filters
ALTER TABLE ONLY compliance_requirements
ADD CONSTRAINT fk_8f5fb77fc7 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
-ALTER TABLE ONLY vulnerability_exports
- ADD CONSTRAINT fk_90e75ccdf8 FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY todos
ADD CONSTRAINT fk_91d1f47b13 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
@@ -34125,9 +34120,6 @@ ALTER TABLE ONLY issues
ALTER TABLE ONLY user_group_callouts
ADD CONSTRAINT fk_c366e12ec3 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-ALTER TABLE ONLY vulnerability_exports
- ADD CONSTRAINT fk_c3d3cb5d0f FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY timelogs
ADD CONSTRAINT fk_c49c83dd77 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
@@ -34665,9 +34657,6 @@ ALTER TABLE ONLY issue_email_participants
ALTER TABLE ONLY merge_request_context_commits
ADD CONSTRAINT fk_rails_0fe0039f60 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
-ALTER TABLE ONLY vulnerability_exports
- ADD CONSTRAINT fk_rails_1019162882 FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY prometheus_alert_events
ADD CONSTRAINT fk_rails_106f901176 FOREIGN KEY (prometheus_alert_id) REFERENCES prometheus_alerts(id) ON DELETE CASCADE;
@@ -35709,9 +35698,6 @@ ALTER TABLE ONLY pages_deployments
ALTER TABLE ONLY dast_pre_scan_verification_steps
ADD CONSTRAINT fk_rails_9990fc2adf FOREIGN KEY (dast_pre_scan_verification_id) REFERENCES dast_pre_scan_verifications(id) ON DELETE CASCADE;
-ALTER TABLE ONLY vulnerability_exports
- ADD CONSTRAINT fk_rails_9aff2c3b45 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY users_ops_dashboard_projects
ADD CONSTRAINT fk_rails_9b4ebf005b FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
diff --git a/doc/development/ai_features/duo_chat.md b/doc/development/ai_features/duo_chat.md
index f61208ebbac..d3ba717feb4 100644
--- a/doc/development/ai_features/duo_chat.md
+++ b/doc/development/ai_features/duo_chat.md
@@ -30,9 +30,9 @@ We aim to employ the Chat for all use cases and workflows that can benefit from
- Among the latter are tasks where the **AI may not get it right the first time but** where **users can easily course correct** by telling the AI more precisely what they need. For instance, "Explain this code" is a common question that most of the time would result in a satisfying answer, but sometimes the user may have additional questions.
- **Tasks that benefit from the history of a conversation**, so neither the user nor the AI need to repeat themselves.
-The chat aims to be context aware and ultimately have access to all the resources in GitLab that the user has access to. Initially, this context was limited to the content of individual issues and epics, as well as GitLab documentation. Since then additional contexts have been added, such as code selection and code files. Currently, work is underway contributing vulnerability context and pipeline job context, so that users can ask questions about these contexts.
+Chat aims to be context aware and ultimately have access to all the resources in GitLab that the user has access to. Initially, this context was limited to the content of individual issues and epics, as well as GitLab documentation. Since then additional contexts have been added, such as code selection and code files. Currently, work is underway contributing vulnerability context and pipeline job context, so that users can ask questions about these contexts.
-To scale the context awareness and hence to scale creation, ideation, and learning use cases across the entire DevSecOps domain, the Duo Chat team welcomes contributions to the chat platform from other GitLab teams and the wider community. They are the experts for the use cases and workflows to accelerate.
+To scale the context awareness and hence to scale creation, ideation, and learning use cases across the entire DevSecOps domain, the Duo Chat team welcomes contributions to the Chat platform from other GitLab teams and the wider community. They are the experts for the use cases and workflows to accelerate.
### Which use cases are better implemented as stand-alone AI features?
@@ -51,7 +51,7 @@ message writing workflow.
Using Chat for commit message writing would probably take longer than writing the message oneself. The user would have to switch to the Chat window, type the request and then copy the result into the commit message field.
-That said, it does not mean that Chat can't write commit messages, nor that it would be prevented from doing so. If Chat has the commit context (which may be added at some point for reasons other than commit message writing), the user can certainly ask to do anything with this commit content, including writing a commit message. But users are certainly unlikely to do that with Chat as they would only loose time. Note: the resulting commit messages may be different if created from chat with a prompt written by the user vs. a static prompt behind a purpose-built commit message creation.
+That said, it does not mean that Chat can't write commit messages, nor that it would be prevented from doing so. If Chat has the commit context (which may be added at some point for reasons other than commit message writing), the user can certainly ask to do anything with this commit content, including writing a commit message. But users are certainly unlikely to do that with Chat as they would only loose time. Note: the resulting commit messages may be different if created from Chat with a prompt written by the user vs. a static prompt behind a purpose-built commit message creation.
## Set up GitLab Duo Chat
@@ -81,7 +81,7 @@ you find a solution.
| There is no Chat button in the GitLab UI. | Make sure your user is a part of a group with Premium or Ultimate license and enabled Chat. |
| Chat replies with "Forbidden by auth provider" error. | Backend can't access LLMs. Make sure your [AI Gateway](index.md#required-install-ai-gateway) is set up correctly. |
| Requests take too long to appear in UI | Consider restarting Sidekiq by running `gdk restart rails-background-jobs`. If that doesn't work, try `gdk kill` and then `gdk start`. Alternatively, you can bypass Sidekiq entirely. To do that temporary alter `Llm::CompletionWorker.perform_async` statements with `Llm::CompletionWorker.perform_inline` |
-| There is no chat button in GitLab UI when GDK is running on non-SaaS mode | You do not have cloud connector access token record or seat assigned. To create cloud connector access record, in rails console put following code: `CloudConnector::Access.new(data: { available_services: [{ name: "duo_chat", serviceStartTime: ":date_in_the_future" }] }).save`. |
+| There is no Chat button in GitLab UI when GDK is running on non-SaaS mode | You do not have cloud connector access token record or seat assigned. To create cloud connector access record, in rails console put following code: `CloudConnector::Access.new(data: { available_services: [{ name: "duo_chat", serviceStartTime: ":date_in_the_future" }] }).save`. |
Please, see also the section on [error codes](#interpreting-gitlab-duo-chat-error-codes) where you can read about codes
that Chat sends to assist troubleshooting.
@@ -266,7 +266,7 @@ It's not available in Production environment.
project will be created during request.
1. Restart GDK.
-1. Ask any question to chat.
+1. Ask any question to Chat.
1. Observe project in the LangSmith [page](https://smith.langchain.com/) > Projects > \[Project name\]. 'Runs' tab should contain
your last requests.
diff --git a/doc/development/ai_features/glossary.md b/doc/development/ai_features/glossary.md
index 69d388f31a3..d81a21fc484 100644
--- a/doc/development/ai_features/glossary.md
+++ b/doc/development/ai_features/glossary.md
@@ -49,7 +49,7 @@ to AI that you think could benefit from being in this list, add it!
`embeddings` database. The embeddings search is done in Postgres using the
`vector` extension. The vertex embeddings database is updated based on the
latest version of GitLab documentation on a daily basis by running `Llm::Embedding::GitlabDocumentation::CreateEmbeddingsRecordsWorker` as a cronjob.
-- **Fine Tuning**: Altering an existing model using a supervised learning process that utilizes a dataset of labeled examples to update the weights of the LLM, improving its output for specific tasks such as code completion or chat.
+- **Fine Tuning**: Altering an existing model using a supervised learning process that utilizes a dataset of labeled examples to update the weights of the LLM, improving its output for specific tasks such as code completion or Chat.
**Foundational Model**: A general purpose LLM trained using a generic objective, typically next token prediction. These models are capable and flexible, and can be adjusted to solved many domain-specific tasks (through finetuning or prompt engineering). This means that these general purpose models are ideal to serve as the foundation of many downstream models. Examples of foundational models are: GPT-4o, Claude 3.5 Sonnet.
- **Frozen Model**: A LLM which cannot be fine-tuned (also Frozen LLM).
- **GitLab Duo**: AI-assisted features across the GitLab DevSecOps platform. These features aim to help increase velocity and solve key pain points across the software development lifecycle. See also the [GitLab Duo](../../user/ai_features.md) features page.
diff --git a/doc/development/ai_features/index.md b/doc/development/ai_features/index.md
index 53557573081..353a8beddf3 100644
--- a/doc/development/ai_features/index.md
+++ b/doc/development/ai_features/index.md
@@ -303,7 +303,7 @@ subscription aiCompletionResponse(
}
```
-The [subscription for chat](duo_chat.md#graphql-subscription) behaves differently.
+The [subscription for Chat](duo_chat.md#graphql-subscription) behaves differently.
To not have many concurrent subscriptions, you should also only subscribe to the subscription once the mutation is sent by using [`skip()`](https://apollo.vuejs.org/guide-option/subscriptions.html#skipping-the-subscription).
diff --git a/doc/development/code_suggestions/index.md b/doc/development/code_suggestions/index.md
index 94d3b5f8c91..b0f26a699dc 100644
--- a/doc/development/code_suggestions/index.md
+++ b/doc/development/code_suggestions/index.md
@@ -27,7 +27,7 @@ This should enable everyone to see locally any change in an IDE being sent to th
1. If you'd like to test that Code Suggestions is working from inside the VS Code Extension, then follow the [steps to set up a personal access token](https://gitlab.com/gitlab-org/gitlab-vscode-extension/#setup) with your GDK inside the new window of VS Code that pops up when you run the "Run and Debug" command.
- Once you complete the steps below, to test you are hitting your local `/code_suggestions/completions` endpoint and not production, follow these steps:
1. Inside the new window, in the built in terminal select the "Output" tab then "GitLab Language Server" from the drop down menu on the right.
- 1. Open a new file inside of this VS Code window and begin typing to see code suggestions in action.
+ 1. Open a new file inside of this VS Code window and begin typing to see Code Suggestions in action.
1. You will see completion request URLs being fetched that match the Git remote URL for your GDK.
1. Main Application (GDK):
@@ -35,7 +35,7 @@ This should enable everyone to see locally any change in an IDE being sent to th
1. Enable Feature Flag ```ai_duo_code_suggestions_switch```:
1. In your terminal, go to your `gitlab-development-kit` > `gitlab` directory.
1. Run `gdk rails console` or `bundle exec rails c` to start a Rails console.
- 1. [Enable the Feature Flag](../../administration/feature_flags.md#enable-or-disable-the-feature) for the code suggestions tokens API by calling `Feature.enable(:ai_duo_code_suggestions_switch)` from the console.
+ 1. [Enable the Feature Flag](../../administration/feature_flags.md#enable-or-disable-the-feature) for the Code Suggestions tokens API by calling `Feature.enable(:ai_duo_code_suggestions_switch)` from the console.
1. [Setup AI Gateway](../ai_features/index.md#required-install-ai-gateway).
1. Run your GDK server with `gdk start` if it's not already running.
diff --git a/doc/development/event_store.md b/doc/development/event_store.md
index 5067f0a5cb6..69852174834 100644
--- a/doc/development/event_store.md
+++ b/doc/development/event_store.md
@@ -127,9 +127,32 @@ back into Sidekiq to be retried.
## Define an event
-An `Event` object represents a domain event that occurred in a bounded context.
-Notify other bounded contexts about something
-that happened by publishing events, so that they can react to it.
+An `Event` object represents a domain event that occurred in a [bounded context](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/bounded_contexts.yml).
+Producers can notify other bounded contexts about something that happened by publishing events, so that they can react to it. An event should be named `Event`, where the `action` is in past tense, e.g. `ReviewerAddedEvent` instead of `AddReviewerEvent`. The `domain_object` may be elided when it is obvious based on the bounded context, e.g. `MergeRequest::ApprovedEvent` instead of `MergeRequest::MergeRequestApprovedEvent`.
+
+### Guidance for good events
+
+Events are a public interface, just like an API or a UI. Collaborate with your
+product and design counterparts to ensure new events will address the needs of
+subscribers. Whenever possible, new events should strive to meet the following
+principles:
+
+- **Semantic**: Events should describe what occurred within the bounded context, _not_ the intended
+ action for subscribers.
+- **Specific**: Events should be narrowly defined without being overly precise. This minimizes the
+ amount of event filtering that subscribers have to perform, as well as the number of unique events
+ to which they need to subscribe. Consider using properties to communicate additional information.
+- **Scoped**: Events should be scoped to their bounded context. Avoid publishing events about domain objects that are not contained by your bounded context.
+
+#### Examples
+
+| Principle | Good | Bad |
+| --- | --- | --- |
+| Semantic | `MergeRequest::ApprovedEvent` | `MergeRequest::NotifyAuthorEvent` |
+| Specific | `MergeRequest::ReviewerAddedEvent` | • `MergeRequest::ChangedEvent`
• `MergeRequest::CodeownerAddedAsReviewerEvent` |
+| Scoped | `MergeRequest::CreatedEvent` | `Project::MergeRequestCreatedEvent` |
+
+### Creating the event schema
Define new event classes under `app/events//` with a name representing something that happened in the past:
diff --git a/doc/editor_extensions/visual_studio/index.md b/doc/editor_extensions/visual_studio/index.md
index 91d62382390..15e27fbc8c6 100644
--- a/doc/editor_extensions/visual_studio/index.md
+++ b/doc/editor_extensions/visual_studio/index.md
@@ -64,7 +64,7 @@ This extension provides these custom commands:
| Command name | Default keyboard shortcut | Feature |
|--------------------------------|---------------------------|---------|
-| `GitLab.ToggleCodeSuggestions` | not applicable | Enable or disable automated code suggestions. |
+| `GitLab.ToggleCodeSuggestions` | not applicable | Enable or disable automated Code Suggestions. |
You can access the extension's custom commands with keyboard shortcuts, which you can customize:
diff --git a/doc/user/gitlab_duo/index.md b/doc/user/gitlab_duo/index.md
index ae1c2bbe458..91dff8e38ce 100644
--- a/doc/user/gitlab_duo/index.md
+++ b/doc/user/gitlab_duo/index.md
@@ -52,7 +52,7 @@ DETAILS:
- [Watch overview](https://youtu.be/ds7SG1wgcVM)
- [View documentation](../project/repository/code_suggestions/index.md).
-### Code explanation
+### Code Explanation
DETAILS:
**Tier: GitLab.com and Self-managed:** For a limited time, Premium or Ultimate. In the future, Premium with GitLab Duo Pro or Ultimate with [GitLab Duo Pro or Enterprise](../../subscriptions/subscription-add-ons.md). **GitLab Dedicated:** GitLab Duo Pro or Enterprise.
@@ -121,7 +121,7 @@ DETAILS:
- [Watch overview](https://www.youtube.com/watch?v=MMVFvGrmMzw&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW)
- [View documentation](../application_security/vulnerabilities/index.md#explaining-a-vulnerability).
-### AI Impact dashboard
+### AI Impact Dashboard
DETAILS:
**Tier:** For a limited time, Ultimate. In the future, Ultimate with [GitLab Duo Enterprise](../../subscriptions/subscription-add-ons.md).
@@ -173,7 +173,7 @@ DETAILS:
## Experimental features
-### Issue description generation
+### Issue Description Generation
DETAILS:
**Tier:** For a limited time, Ultimate. In the future, Ultimate with [GitLab Duo Enterprise](../../subscriptions/subscription-add-ons.md).
@@ -184,7 +184,7 @@ DETAILS:
- LLM: Anthropic [Claude Instant 1.2](https://docs.anthropic.com/en/docs/about-claude/models#legacy-models)
- [View documentation](experiments.md#populate-an-issue-with-issue-description-generation).
-### Code review summary
+### Code Review Summary
DETAILS:
**Tier:** For a limited time, Ultimate. In the future, Ultimate with [GitLab Duo Enterprise](../../subscriptions/subscription-add-ons.md).
diff --git a/doc/user/gitlab_duo_chat/best_practices.md b/doc/user/gitlab_duo_chat/best_practices.md
index 6e9e4ba57d0..ff0a31601b4 100644
--- a/doc/user/gitlab_duo_chat/best_practices.md
+++ b/doc/user/gitlab_duo_chat/best_practices.md
@@ -85,7 +85,7 @@ Explain labels in GitLab. Provide an example for efficient usage with issue boar
## Reset when needed
-Use `/reset` if chat gets stuck on a wrong track. Start fresh.
+Use `/reset` if Chat gets stuck on a wrong track. Start fresh.
## Refine slash command prompts
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 140b4b6c948..4a2cdb1ba6d 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -330,7 +330,7 @@ Project permissions for [merge requests](project/merge_requests/index.md):
|--------------------------------------------------------------------------------------------------------------|:-----:|:--------:|:---------:|:----------:|:-----:|-------|
| [View](project/merge_requests/index.md#view-merge-requests) a merge request | ✓ | ✓ | ✓ | ✓ | ✓ | On self-managed GitLab instances, users with the Guest role are able to perform this action only on public and internal projects (not on private projects). [External users](../administration/external_users.md) must be given explicit access (at least the **Reporter** role) even if the project is internal. Users with the Guest role on GitLab.com are only able to perform this action on public projects because internal visibility is not available. |
| [Create](project/merge_requests/creating_merge_requests.md) a merge request | | | ✓ | ✓ | ✓ | In projects that accept contributions from external members, users can create, edit, and close their own merge requests. For **private** projects, this excludes the Guest role as those users [cannot clone private projects](public_access.md#private-projects-and-groups). For **internal** projects, includes users with read-only access to the project, as [they can clone internal projects](public_access.md#internal-projects-and-groups). |
-| Update a merge request including assign, review, code suggestions, approve, labels, lock and resolve threads | | | ✓ | ✓ | ✓ | For information on eligible approvers for merge requests, see [Eligible approvers](project/merge_requests/approvals/rules.md#eligible-approvers). |
+| Update a merge request including assign, review, Code Suggestions, approve, labels, lock and resolve threads | | | ✓ | ✓ | ✓ | For information on eligible approvers for merge requests, see [Eligible approvers](project/merge_requests/approvals/rules.md#eligible-approvers). |
| Manage [merge request settings](project/merge_requests/approvals/settings.md) | | | | ✓ | ✓ | |
| Manage [merge request approval rules](project/merge_requests/approvals/rules.md) | | | | ✓ | ✓ | |
| Delete merge request | | | | | ✓ | |
diff --git a/doc/user/project/merge_requests/duo_in_merge_requests.md b/doc/user/project/merge_requests/duo_in_merge_requests.md
index fce70a6d295..fc7d01421d4 100644
--- a/doc/user/project/merge_requests/duo_in_merge_requests.md
+++ b/doc/user/project/merge_requests/duo_in_merge_requests.md
@@ -47,7 +47,7 @@ DETAILS:
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10466) in GitLab 16.0 as an [experiment](../../../policy/experiment-beta-support.md#experiment).
-When you've completed your review of a merge request and are ready to [submit your review](reviews/index.md#submit-a-review), generate a GitLab Duo Code review summary:
+When you've completed your review of a merge request and are ready to [submit your review](reviews/index.md#submit-a-review), generate a GitLab Duo Code Review Summary:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Code > Merge requests** and find the merge request you want to review.
diff --git a/doc/user/project/repository/code_suggestions/index.md b/doc/user/project/repository/code_suggestions/index.md
index b2dd37369f7..03bfd5a7a06 100644
--- a/doc/user/project/repository/code_suggestions/index.md
+++ b/doc/user/project/repository/code_suggestions/index.md
@@ -144,7 +144,7 @@ Prerequisites:
what you want to build. Code Generation treats your code comments like chat. Your code comments
update the `user_instruction`, and then improve the next results you receive.
-As you work, GitLab Duo provides code suggestions that use your other open files
+As you work, GitLab Duo provides Code Suggestions that use your other open files
(within [truncation limits](#truncation-of-file-content))
as extra context.
diff --git a/lib/gitlab/ci/build/context/base.rb b/lib/gitlab/ci/build/context/base.rb
index 81f96e822f4..7f74b7e3a3e 100644
--- a/lib/gitlab/ci/build/context/base.rb
+++ b/lib/gitlab/ci/build/context/base.rb
@@ -32,12 +32,16 @@ module Gitlab
end
def top_level_worktree_paths
+ return pipeline.top_level_worktree_paths if reduce_gitaly_calls?
+
strong_memoize(:top_level_worktree_paths) do
project.repository.tree(sha).blobs.map(&:path)
end
end
def all_worktree_paths
+ return pipeline.all_worktree_paths if reduce_gitaly_calls?
+
strong_memoize(:all_worktree_paths) do
project.repository.ls_files(sha)
end
@@ -56,6 +60,10 @@ module Gitlab
protected: pipeline.protected_ref?
}
end
+
+ def reduce_gitaly_calls?
+ Feature.enabled?(:ci_conditionals_reduce_gitaly_calls, project)
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5439eadfc78..9dbeca292ce 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -24828,7 +24828,7 @@ msgstr ""
msgid "GlobalSearch|Commits"
msgstr ""
-msgid "GlobalSearch|Could not load search results. Please refresh the page to try again."
+msgid "GlobalSearch|Could not load search results. Refresh the page to try again."
msgstr ""
msgid "GlobalSearch|Explore"
@@ -25020,6 +25020,21 @@ msgstr ""
msgid "GlobalSearch|Show more"
msgstr ""
+msgid "GlobalSearch|Showing 1 code result for %{term}"
+msgid_plural "GlobalSearch|Showing %{resultsTotal} code results for %{term}"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "GlobalSearch|Showing 1 code result for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}"
+msgid_plural "GlobalSearch|Showing %{resultsTotal} code results for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "GlobalSearch|Showing 1 code result for %{term} in group %{groupNameLink}"
+msgid_plural "GlobalSearch|Showing %{resultsTotal} code results for %{term} in group %{groupNameLink}"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "GlobalSearch|Showing top %{maxItems}"
msgstr ""
@@ -25059,6 +25074,12 @@ msgstr ""
msgid "GlobalSearch|Users"
msgstr ""
+msgid "GlobalSearch|View blame"
+msgstr ""
+
+msgid "GlobalSearch|View line in repository"
+msgstr ""
+
msgid "GlobalSearch|View syntax options."
msgstr ""
@@ -59391,9 +59412,6 @@ msgstr ""
msgid "View File Metadata"
msgstr ""
-msgid "View Line in repository"
-msgstr ""
-
msgid "View Stage: %{title}"
msgstr ""
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index be7035509d3..54c63e11d13 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -36,7 +36,8 @@ RSpec.describe 'Database schema', feature_category: :database do
search_namespace_index_assignments: [%w[search_index_id index_type]],
slack_integrations_scopes: [%w[slack_api_scope_id]],
snippets: %w[organization_id], # this index is added in an async manner, hence it needs to be ignored in the first phase.
- users: [%w[accepted_term_id]]
+ users: [%w[accepted_term_id]],
+ subscription_add_on_purchases: [["subscription_add_on_id"]] # index handled via composite index with namespace_id
}.with_indifferent_access.freeze
# If splitting FK and table removal into two MRs as suggested in the docs, use this constant in the initial FK removal MR.
diff --git a/spec/frontend/badges/components/badge_list_spec.js b/spec/frontend/badges/components/badge_list_spec.js
index 612e9bdc41f..ee9c3da38ac 100644
--- a/spec/frontend/badges/components/badge_list_spec.js
+++ b/spec/frontend/badges/components/badge_list_spec.js
@@ -8,6 +8,7 @@ import createState from '~/badges/store/state';
import mutations from '~/badges/store/mutations';
import actions from '~/badges/store/actions';
import BadgeList from '~/badges/components/badge_list.vue';
+import Badge from '~/badges/components/badge.vue';
import { createDummyBadge } from '../dummy_badge';
Vue.use(Vuex);
@@ -27,6 +28,7 @@ describe('BadgeList component', () => {
const findButtons = () => wrapper.findByTestId('badge-actions').findAllComponents(GlButton);
const findEditButton = () => wrapper.findByTestId('edit-badge-button');
const findDeleteButton = () => wrapper.findByTestId('delete-badge');
+ const findBadge = () => wrapper.findComponent(Badge);
const createComponent = (customState) => {
mockedActions = Object.fromEntries(Object.keys(actions).map((name) => [name, jest.fn()]));
@@ -100,10 +102,7 @@ describe('BadgeList component', () => {
});
it('renders the badge', () => {
- const badgeImage = wrapper.find('.project-badge');
-
- expect(badgeImage.exists()).toBe(true);
- expect(badgeImage.attributes('src')).toBe(badges[0].renderedImageUrl);
+ expect(findBadge().props('imageUrl')).toBe(badges[0].renderedImageUrl);
});
it('renders the badge name', () => {
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 3cf87fe5d23..4187ecd4942 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -6,6 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
@@ -48,6 +49,7 @@ describe('Board card component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon');
const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon);
+ const findUserAvatar = () => wrapper.findComponent(UserAvatarLink);
const mockApollo = createMockApollo();
@@ -250,9 +252,6 @@ describe('Board card component', () => {
item: {
...wrapper.props('item'),
assignees: [user],
- updateData(newData) {
- Object.assign(this, newData);
- },
},
},
});
@@ -274,25 +273,25 @@ describe('Board card component', () => {
expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
});
- it('renders the avatar using avatarUrl property', async () => {
- wrapper.props('item').updateData({
- ...wrapper.props('item'),
- assignees: [
- {
- id: '1',
- name: 'test',
- state: 'active',
- username: 'test_name',
- avatarUrl: 'test_image_from_avatar_url',
+ it('renders the avatar using avatarUrl property', () => {
+ createWrapper({
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ assignees: [
+ {
+ id: '1',
+ name: 'test',
+ state: 'active',
+ username: 'test_name',
+ avatarUrl: 'test_image_from_avatar_url',
+ },
+ ],
},
- ],
+ },
});
- await nextTick();
-
- expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
- 'test_image_from_avatar_url?width=48',
- );
+ expect(findUserAvatar().props('imgSrc')).toBe('test_image_from_avatar_url');
});
});
@@ -317,10 +316,7 @@ describe('Board card component', () => {
});
it('displays defaults avatar if users avatar is null', () => {
- expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
- expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
- 'default_avatar?width=48',
- );
+ expect(findUserAvatar().props('imgSrc')).toBe('default_avatar');
});
});
});
diff --git a/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js b/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js
index ea47edb6842..5211d6a4c82 100644
--- a/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js
@@ -7,7 +7,7 @@ import PipelinesCiTemplates from '~/ci/pipelines_page/components/empty_state/pip
describe('Pipelines Empty State', () => {
let wrapper;
- const findIllustration = () => wrapper.find('img');
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findButton = () => wrapper.find('a');
const pipelinesCiTemplates = () => wrapper.findComponent(PipelinesCiTemplates);
@@ -46,7 +46,7 @@ describe('Pipelines Empty State', () => {
});
it('should render empty state SVG', () => {
- expect(findIllustration().attributes('src')).toBe('foo.svg');
+ expect(findEmptyState().props('svgPath')).toBe('foo.svg');
});
it('should render empty state header', () => {
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index fa16af92701..5aa65732382 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils';
import { GlFormCheckbox } from '@gitlab/ui';
import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper';
import Component from '~/diffs/components/commit_item.vue';
@@ -21,15 +22,15 @@ describe('diffs/components/commit_item', () => {
const timeago = getTimeago();
const { commit } = getDiffWithCommit;
- const getTitleElement = () => wrapper.find('.commit-row-message.item-title');
- const getDescElement = () => wrapper.find('pre.commit-row-description');
- const getDescExpandElement = () => wrapper.find('.commit-content .js-toggle-button');
- const getShaElement = () => wrapper.find('[data-testid="commit-sha-group"]');
- const getAvatarElement = () => wrapper.find('.user-avatar-link');
- const getCommitterElement = () => wrapper.find('.committer');
- const getCommitActionsElement = () => wrapper.find('.commit-actions');
- const getCommitPipelineStatus = () => wrapper.findComponent(CommitPipelineStatus);
- const getCommitCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findTitleElement = () => wrapper.find('.commit-row-message.item-title');
+ const findDescElement = () => wrapper.find('pre.commit-row-description');
+ const findDescExpandElement = () => wrapper.find('.commit-content .js-toggle-button');
+ const findShaElement = () => wrapper.find('[data-testid="commit-sha-group"]');
+ const findUserAvatar = () => wrapper.findComponent(UserAvatarLink);
+ const findCommitterElement = () => wrapper.find('.committer');
+ const findCommitActionsElement = () => wrapper.find('.commit-actions');
+ const findCommitPipelineStatus = () => wrapper.findComponent(CommitPipelineStatus);
+ const findCommitCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const mountComponent = (propsData) => {
wrapper = mount(Component, {
@@ -49,15 +50,15 @@ describe('diffs/components/commit_item', () => {
});
it('renders commit title', () => {
- const titleElement = getTitleElement();
+ const titleElement = findTitleElement();
expect(titleElement.attributes('href')).toBe(commit.commit_url);
expect(titleElement.text()).toBe(commit.title_html);
});
it('renders commit description', () => {
- const descElement = getDescElement();
- const descExpandElement = getDescExpandElement();
+ const descElement = findDescElement();
+ const descExpandElement = findDescExpandElement();
const expected = commit.description_html.replace(/
/g, '');
@@ -66,7 +67,7 @@ describe('diffs/components/commit_item', () => {
});
it('renders commit sha', () => {
- const shaElement = getShaElement();
+ const shaElement = findShaElement();
const labelElement = shaElement.find('[data-testid="commit-sha-short-id"]');
const buttonElement = shaElement.find('button.input-group-text');
@@ -75,17 +76,16 @@ describe('diffs/components/commit_item', () => {
});
it('renders author avatar', () => {
- const avatarElement = getAvatarElement();
- const imgElement = avatarElement.find('img');
-
- expect(avatarElement.attributes('href')).toBe(commit.author.web_url);
- expect(imgElement.classes()).toContain('gl-avatar-s32');
- expect(imgElement.attributes('alt')).toBe(commit.author.name);
- expect(imgElement.attributes('src')).toBe(commit.author.avatar_url);
+ expect(findUserAvatar().props()).toMatchObject({
+ linkHref: commit.author.web_url,
+ imgSrc: commit.author.avatar_url,
+ imgAlt: commit.author.name,
+ imgSize: 32,
+ });
});
it('renders committer text', () => {
- const committerElement = getCommitterElement();
+ const committerElement = findCommitterElement();
const nameElement = committerElement.find('a');
const expectTimeText = timeago.format(commit.authored_date);
@@ -105,8 +105,8 @@ describe('diffs/components/commit_item', () => {
});
it('hides description', () => {
- const descElement = getDescElement();
- const descExpandElement = getDescExpandElement();
+ const descElement = findDescElement();
+ const descExpandElement = findDescExpandElement();
expect(descElement.exists()).toBe(false);
expect(descExpandElement.exists()).toBe(false);
@@ -127,16 +127,16 @@ describe('diffs/components/commit_item', () => {
});
it('renders author avatar', () => {
- const avatarElement = getAvatarElement();
- const imgElement = avatarElement.find('img');
-
- expect(avatarElement.attributes('href')).toBe(`mailto:${TEST_AUTHOR_EMAIL}`);
- expect(imgElement.attributes('alt')).toBe(TEST_AUTHOR_NAME);
- expect(imgElement.attributes('src')).toBe(TEST_AUTHOR_GRAVATAR);
+ expect(findUserAvatar().props()).toMatchObject({
+ linkHref: `mailto:${TEST_AUTHOR_EMAIL}`,
+ imgSrc: TEST_AUTHOR_GRAVATAR,
+ imgAlt: TEST_AUTHOR_NAME,
+ imgSize: 32,
+ });
});
it('renders committer text', () => {
- const committerElement = getCommitterElement();
+ const committerElement = findCommitterElement();
const nameElement = committerElement.find('a');
expect(nameElement.attributes('href')).toBe(`mailto:${TEST_AUTHOR_EMAIL}`);
@@ -152,7 +152,7 @@ describe('diffs/components/commit_item', () => {
});
it('renders signature html', () => {
- const actionsElement = getCommitActionsElement();
+ const actionsElement = findCommitActionsElement();
const signatureElement = actionsElement.find('.signature-badge');
expect(signatureElement.html()).toBe(TEST_SIGNATURE_HTML);
@@ -167,7 +167,7 @@ describe('diffs/components/commit_item', () => {
});
it('renders pipeline status', () => {
- expect(getCommitPipelineStatus().exists()).toBe(true);
+ expect(findCommitPipelineStatus().exists()).toBe(true);
});
});
@@ -180,12 +180,12 @@ describe('diffs/components/commit_item', () => {
});
it('renders checkbox', () => {
- expect(getCommitCheckbox().exists()).toBe(true);
+ expect(findCommitCheckbox().exists()).toBe(true);
});
it('emits "handleCheckboxChange" event on change', () => {
expect(wrapper.emitted('handleCheckboxChange')).toBeUndefined();
- getCommitCheckbox().vm.$emit('change');
+ findCommitCheckbox().vm.$emit('change');
expect(wrapper.emitted('handleCheckboxChange')[0]).toEqual([true]);
});
diff --git a/spec/frontend/environments/commit_spec.js b/spec/frontend/environments/commit_spec.js
index 1930acd4022..a32ac9f4eee 100644
--- a/spec/frontend/environments/commit_spec.js
+++ b/spec/frontend/environments/commit_spec.js
@@ -1,3 +1,4 @@
+import { GlAvatar } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import Commit from '~/environments/components/commit.vue';
import { resolvedEnvironment } from './graphql/mock_data';
@@ -6,6 +7,8 @@ describe('~/environments/components/commit.vue', () => {
let commit;
let wrapper;
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+
beforeEach(() => {
commit = resolvedEnvironment.lastDeployment.commit;
});
@@ -33,8 +36,7 @@ describe('~/environments/components/commit.vue', () => {
});
it('displays the user avatar', () => {
- const avatar = wrapper.findByRole('img', { name: 'avatar' });
- expect(avatar.attributes('src')).toBe(commit.author.avatarUrl);
+ expect(findAvatar().props('src')).toBe(commit.author.avatarUrl);
});
it('links the commit title to the commit', () => {
@@ -59,8 +61,7 @@ describe('~/environments/components/commit.vue', () => {
});
it('displays the user avatar', () => {
- const avatar = wrapper.findByRole('img', { name: 'avatar' });
- expect(avatar.attributes('src')).toBe(commit.authorGravatarUrl);
+ expect(findAvatar().props('src')).toBe(commit.authorGravatarUrl);
});
it('displays the commit title', () => {
@@ -82,8 +83,7 @@ describe('~/environments/components/commit.vue', () => {
});
it('displays the user avatar', () => {
- const avatar = wrapper.findByRole('img', { name: 'avatar' });
- expect(avatar.attributes('src')).toBe(commit.author.avatarUrl);
+ expect(findAvatar().props('src')).toBe(commit.author.avatarUrl);
});
it('links the commit title to the commit', () => {
diff --git a/spec/frontend/members/components/avatars/group_avatar_spec.js b/spec/frontend/members/components/avatars/group_avatar_spec.js
index 1463aa5ae59..64365f20e61 100644
--- a/spec/frontend/members/components/avatars/group_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/group_avatar_spec.js
@@ -40,7 +40,7 @@ describe('MemberList', () => {
it("renders group's avatar", () => {
createComponent();
- expect(wrapper.find('img').attributes('src')).toBe(group.avatarUrl);
+ expect(wrapper.findComponent(GlAvatarLabeled).attributes('src')).toBe(group.avatarUrl);
});
describe('when group is private', () => {
diff --git a/spec/frontend/members/components/avatars/invite_avatar_spec.js b/spec/frontend/members/components/avatars/invite_avatar_spec.js
index 84878fb9be2..9b26b570caf 100644
--- a/spec/frontend/members/components/avatars/invite_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/invite_avatar_spec.js
@@ -1,11 +1,14 @@
import { getByText as getByTextHelper } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
+import { GlAvatarLabeled } from '@gitlab/ui';
import InviteAvatar from '~/members/components/avatars/invite_avatar.vue';
import { invite as member } from '../../mock_data';
describe('MemberList', () => {
let wrapper;
+ const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
+
const { invite } = member;
const createComponent = (propsData = {}) => {
@@ -29,6 +32,6 @@ describe('MemberList', () => {
});
it('renders avatar', () => {
- expect(wrapper.find('img').attributes('src')).toBe(invite.avatarUrl);
+ expect(findAvatarLabeled().attributes('src')).toBe(invite.avatarUrl);
});
});
diff --git a/spec/frontend/members/components/avatars/user_avatar_spec.js b/spec/frontend/members/components/avatars/user_avatar_spec.js
index 84ab80d3ccf..6ab945bbbe0 100644
--- a/spec/frontend/members/components/avatars/user_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/user_avatar_spec.js
@@ -1,4 +1,4 @@
-import { GlAvatarLink, GlBadge } from '@gitlab/ui';
+import { GlAvatarLink, GlAvatarLabeled, GlBadge } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import UserAvatar from '~/members/components/avatars/user_avatar.vue';
import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
@@ -8,6 +8,8 @@ import { member as memberMock, member2faEnabled, orphanedMember } from '../../mo
describe('UserAvatar', () => {
let wrapper;
+ const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
+
const { user } = memberMock;
const createComponent = (propsData = {}, provide = {}) => {
@@ -55,7 +57,7 @@ describe('UserAvatar', () => {
it("renders user's avatar", () => {
createComponent();
- expect(wrapper.find('img').attributes('src')).toBe(
+ expect(findAvatarLabeled().attributes('src')).toBe(
'https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80&d=identicon&width=96',
);
});
diff --git a/spec/frontend/merge_request_dashboard/components/merge_request_spec.js b/spec/frontend/merge_request_dashboard/components/merge_request_spec.js
index 5c3858ce37c..38ed15c0cc8 100644
--- a/spec/frontend/merge_request_dashboard/components/merge_request_spec.js
+++ b/spec/frontend/merge_request_dashboard/components/merge_request_spec.js
@@ -58,7 +58,7 @@ describe('Merge request dashboard merge request component', () => {
},
],
},
- userDiscussionsCount: 5,
+ userNotesCount: 5,
createdAt: '2024-04-22T10:13:09Z',
updatedAt: '2024-04-19T14:34:42Z',
diffStatsSummary: {
diff --git a/spec/frontend/merge_request_dashboard/mock_data.js b/spec/frontend/merge_request_dashboard/mock_data.js
index 6d4a924bf16..6fbea21b174 100644
--- a/spec/frontend/merge_request_dashboard/mock_data.js
+++ b/spec/frontend/merge_request_dashboard/mock_data.js
@@ -25,7 +25,7 @@ export function createMockMergeRequest(mergeRequest = {}) {
nodes: [],
},
headPipeline: null,
- userDiscussionsCount: 0,
+ userNotesCount: 0,
createdAt: '',
updatedAt: '',
approved: false,
diff --git a/spec/frontend/profile/components/user_achievements_spec.js b/spec/frontend/profile/components/user_achievements_spec.js
index f1858b110e1..518b0b1506e 100644
--- a/spec/frontend/profile/components/user_achievements_spec.js
+++ b/spec/frontend/profile/components/user_achievements_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlAvatar } from '@gitlab/ui';
import getUserAchievementsEmptyResponse from 'test_fixtures/graphql/get_user_achievements_empty_response.json';
import getUserAchievementsLongResponse from 'test_fixtures/graphql/get_user_achievements_long_response.json';
import getUserAchievementsResponse from 'test_fixtures/graphql/get_user_achievements_with_avatar_and_description_response.json';
@@ -24,7 +24,8 @@ describe('UserAchievements', () => {
let wrapper;
const getUserAchievementsQueryHandler = jest.fn().mockResolvedValue(getUserAchievementsResponse);
- const achievement = () => wrapper.findByTestId('user-achievement');
+ const findUserAchievement = () => wrapper.findByTestId('user-achievement');
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
const createComponent = ({ queryHandler = getUserAchievementsQueryHandler } = {}) => {
const fakeApollo = createMockApollo([[getUserAchievements, queryHandler]]);
@@ -61,7 +62,7 @@ describe('UserAchievements', () => {
await waitForPromises();
- expect(achievement().findComponent(GlBadge).text()).toBe('2x');
+ expect(findUserAchievement().findComponent(GlBadge).text()).toBe('2x');
});
it('renders correctly if the achievement is from a private namespace', async () => {
@@ -74,8 +75,8 @@ describe('UserAchievements', () => {
const userAchievement =
getUserAchievementsPrivateGroupResponse.data.user.userAchievements.nodes[0];
- expect(achievement().text()).toContain(userAchievement.achievement.name);
- expect(achievement().text()).toContain(
+ expect(findUserAchievement().text()).toContain(userAchievement.achievement.name);
+ expect(findUserAchievement().text()).toContain(
`Awarded ${getTimeago().format(
userAchievement.createdAt,
timeagoLanguageCode,
@@ -88,15 +89,13 @@ describe('UserAchievements', () => {
await waitForPromises();
- expect(achievement().text()).toContain(userAchievement1.achievement.name);
- expect(achievement().text()).toContain(
+ expect(findUserAchievement().text()).toContain(userAchievement1.achievement.name);
+ expect(findUserAchievement().text()).toContain(
`Awarded ${getTimeago().format(userAchievement1.createdAt, timeagoLanguageCode)} by`,
);
- expect(achievement().text()).toContain(userAchievement1.achievement.namespace.fullPath);
- expect(achievement().text()).toContain(userAchievement1.achievement.description);
- expect(achievement().find('img').attributes('src')).toBe(
- userAchievement1.achievement.avatarUrl,
- );
+ expect(findUserAchievement().text()).toContain(userAchievement1.achievement.namespace.fullPath);
+ expect(findUserAchievement().text()).toContain(userAchievement1.achievement.description);
+ expect(findAvatar().props('src')).toBe(userAchievement1.achievement.avatarUrl);
});
it('renders a placeholder when no avatar is present', async () => {
@@ -107,7 +106,7 @@ describe('UserAchievements', () => {
await waitForPromises();
- expect(achievement().find('img').attributes('src')).toBe(PLACEHOLDER_URL);
+ expect(findAvatar().props('src')).toBe(PLACEHOLDER_URL);
});
it('does not render a description when none is present', async () => {
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index 12e3807c9fa..0f854ef4f11 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -6,6 +6,7 @@ import originalOneReleaseQueryResponse from 'test_fixtures/graphql/releases/grap
import { convertOneReleaseGraphQLResponse } from '~/releases/util';
import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants';
import { trimText } from 'helpers/text_helper';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
// TODO: Encapsulate date helpers https://gitlab.com/gitlab-org/gitlab/-/issues/320883
@@ -38,6 +39,7 @@ describe('Release block footer', () => {
const tagInfoSection = () => wrapper.find('.js-tag-info');
const tagInfoSectionLink = () => tagInfoSection().findComponent(GlLink);
const authorDateInfoSection = () => wrapper.find('.js-author-date-info');
+ const findUserAvatar = () => wrapper.findComponent(UserAvatarLink);
describe.each`
sortFlag | expectedInfoString
@@ -88,10 +90,10 @@ describe('Release block footer', () => {
});
if (authorFlag) {
it("renders the author's avatar image", () => {
- const avatarImg = authorDateInfoSection().find('img');
+ const avatarImg = findUserAvatar();
expect(avatarImg.exists()).toBe(true);
- expect(avatarImg.attributes('src')).toBe(release.author.avatarUrl);
+ expect(avatarImg.props('imgSrc')).toBe(release.author.avatarUrl);
});
it("renders a link to the author's profile", () => {
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index dc3d2c1266a..6a74b5029a5 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -906,6 +906,19 @@ export const defaultProvide = {
},
};
+export const mockGetBlobSearchQueryEmpty = {
+ data: {
+ blobSearch: {
+ fileCount: 0,
+ files: [],
+ matchCount: 0,
+ perPage: 0,
+ searchLevel: 'PROJECT',
+ searchType: 'ZOEKT',
+ },
+ },
+};
+
export const mockGetBlobSearchQuery = {
data: {
blobSearch: {
diff --git a/spec/frontend/search/results/components/app_spec.js b/spec/frontend/search/results/components/app_spec.js
index 413c1126042..dc21d53fc67 100644
--- a/spec/frontend/search/results/components/app_spec.js
+++ b/spec/frontend/search/results/components/app_spec.js
@@ -2,65 +2,124 @@ import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
+import { GlAlert } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getBlobSearchQuery from '~/search/graphql/blob_search_zoekt.query.graphql';
import GlobalSearchResultsApp from '~/search/results/components/app.vue';
import ZoektBlobResults from '~/search/results/components/zoekt_blob_results.vue';
-import { MOCK_QUERY, mockGetBlobSearchQuery } from '../../mock_data';
+import StatusBar from '~/search/results/components/status_bar.vue';
+import { MOCK_QUERY, mockGetBlobSearchQuery, mockGetBlobSearchQueryEmpty } from '../../mock_data';
Vue.use(Vuex);
Vue.use(VueApollo);
+jest.mock('~/alert');
+
describe('GlobalSearchResultsApp', () => {
let wrapper;
- let apolloMock;
const getterSpies = {
currentScope: jest.fn(() => 'blobs'),
};
const blobSearchHandler = jest.fn().mockResolvedValue(mockGetBlobSearchQuery);
+ const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
+ const mockQueryEmpty = jest.fn().mockReturnValue(mockGetBlobSearchQueryEmpty);
+ const mockQueryError = jest.fn().mockRejectedValue(new Error('Network error'));
+
+ const createComponent = ({
+ initialState = { query: { scope: 'blobs' }, searchType: 'zoekt' },
+ queryHandler = blobSearchHandler,
+ } = {}) => {
+ const requestHandlers = [[getBlobSearchQuery, queryHandler]];
+ const apolloProvider = createMockApollo(requestHandlers);
- const createComponent = (initialState = {}) => {
const store = new Vuex.Store({
state: {
- urlQuery: MOCK_QUERY,
+ query: MOCK_QUERY,
...initialState,
},
getters: getterSpies,
});
- apolloMock = createMockApollo([[getBlobSearchQuery, blobSearchHandler]]);
wrapper = shallowMountExtended(GlobalSearchResultsApp, {
- apolloProvider: apolloMock,
+ apolloProvider,
store,
});
};
- afterEach(() => {
- apolloMock = null;
+ const findZoektBlobResults = () => wrapper.findComponent(ZoektBlobResults);
+ const findStatusBar = () => wrapper.findComponent(StatusBar);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ describe('when loading results', () => {
+ beforeEach(async () => {
+ createComponent({
+ initialState: { query: { scope: 'blobs' }, searchType: 'zoekt' },
+ queryHandler: mockQueryLoading,
+ });
+ jest.advanceTimersByTime(500);
+ await waitForPromises();
+ });
+
+ it('renders loading icon', () => {
+ expect(findZoektBlobResults().props('isLoading')).toBe(true);
+ });
});
- const findZoektBlobResults = () => wrapper.findComponent(ZoektBlobResults);
+ describe('when component has load error', () => {
+ beforeEach(async () => {
+ createComponent({
+ initialState: { query: { scope: 'blobs' }, searchType: 'zoekt' },
+ queryHandler: mockQueryError,
+ });
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+ });
- describe('component', () => {
+ it('renders alert', () => {
+ expect(findAlert().text()).toBe(
+ 'Could not load search results. Refresh the page to try again.',
+ );
+ expect(findZoektBlobResults().exists()).toBe(false);
+ });
+ });
+
+ describe('when component has no results', () => {
+ beforeEach(async () => {
+ createComponent({
+ initialState: { query: { scope: 'blobs' }, searchType: 'zoekt' },
+ queryHandler: mockQueryEmpty,
+ });
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+ });
+
+ it(`renders component properly`, async () => {
+ await waitForPromises();
+ expect(findZoektBlobResults().props('hasResults')).toBe(false);
+ });
+ });
+
+ describe('when we have results', () => {
describe.each`
scope | searchType | isRendered
${'blobs'} | ${'zoekt'} | ${true}
${'issues'} | ${'zoekt'} | ${false}
${'blobs'} | ${'advanced'} | ${false}
${'issues'} | ${'basic'} | ${false}
- `('template', ({ scope, searchType, isRendered }) => {
+ `(`has scope: $scope, searchType: $searchType`, ({ scope, searchType, isRendered }) => {
beforeEach(async () => {
getterSpies.currentScope = jest.fn(() => scope);
- createComponent({ query: { scope }, searchType });
+ createComponent({ initialState: { query: { scope }, searchType } });
jest.advanceTimersByTime(500);
await waitForPromises();
});
- it(`renders component based on conditions`, () => {
+ it(`correctly renders components`, () => {
expect(findZoektBlobResults().exists()).toBe(isRendered);
+ expect(findStatusBar().exists()).toBe(isRendered);
});
});
});
diff --git a/spec/frontend/search/results/components/blob_chunks_spec.js b/spec/frontend/search/results/components/blob_chunks_spec.js
index 0bd5f63d767..d563206d109 100644
--- a/spec/frontend/search/results/components/blob_chunks_spec.js
+++ b/spec/frontend/search/results/components/blob_chunks_spec.js
@@ -70,7 +70,7 @@ describe('BlobChunks', () => {
expect(findGlLink().at(0).findComponent(GlIcon).props('name')).toBe('git');
expect(findGlLink().at(1).attributes('href')).toBe('https://gitlab.com/file/test.js#L1');
- expect(findGlLink().at(1).attributes('title')).toBe('View Line in repository');
+ expect(findGlLink().at(1).attributes('title')).toBe('View line in repository');
expect(findGlLink().at(1).text()).toBe('1');
});
});
diff --git a/spec/frontend/search/results/components/blob_footer_spec.js b/spec/frontend/search/results/components/blob_footer_spec.js
index d494b434e29..fa671c6b5c9 100644
--- a/spec/frontend/search/results/components/blob_footer_spec.js
+++ b/spec/frontend/search/results/components/blob_footer_spec.js
@@ -51,11 +51,6 @@ describe('BlobFooter', () => {
describe('component with too many results', () => {
beforeEach(() => {
createComponent({
- // matchCountTotal: 100,
- // matchCount: 100,
- // filePath: 'test/file.js',
- // projectPath: 'Testjs/Test',
- // fileLink: 'https://gitlab.com/test/file.js',
file: {
...mockDataForBlobBody,
chunks: [
diff --git a/spec/frontend/search/results/components/status_bar_spec.js b/spec/frontend/search/results/components/status_bar_spec.js
new file mode 100644
index 00000000000..bd3af9e3e46
--- /dev/null
+++ b/spec/frontend/search/results/components/status_bar_spec.js
@@ -0,0 +1,228 @@
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
+import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import GlobalSearchStatusBar from '~/search/results/components/status_bar.vue';
+import { MOCK_QUERY } from '../../mock_data';
+
+Vue.use(Vuex);
+
+describe('GlobalSearchStatusBar', () => {
+ let wrapper;
+
+ const defaultProps = {
+ blobSearch: {
+ perPage: 20,
+ fileCount: 1074,
+ matchCount: 3000,
+ },
+ hasError: false,
+ hasResults: true,
+ isLoading: false,
+ };
+
+ const defaultState = {
+ query: {
+ ...MOCK_QUERY,
+ group_id: null,
+ project_id: null,
+ search: 'test',
+ },
+ projectInitialJson: {},
+ groupInitialJson: {},
+ repositoryRef: 'main',
+ };
+
+ const groupInitialJson = {
+ id: 1,
+ name: 'group-name',
+ full_name: 'Group Full Name',
+ };
+
+ const projectInitialJson = {
+ id: 1,
+ name: 'project-name',
+ name_with_namespace: 'Project with Namespace',
+ };
+
+ const createComponent = ({ propsData = {}, initialState = {} } = {}) => {
+ const store = new Vuex.Store({
+ state: {
+ ...defaultState,
+ ...initialState,
+ },
+ });
+
+ wrapper = shallowMountExtended(GlobalSearchStatusBar, {
+ propsData: {
+ ...defaultProps,
+ ...propsData,
+ },
+ store,
+ stubs: {
+ GlSprintf,
+ GlLink,
+ },
+ });
+ };
+
+ describe('simple status message', () => {
+ describe('multiple results', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the status bar', () => {
+ expect(wrapper.text()).toContain('Showing 3000 code results for test');
+ });
+ });
+
+ describe('one result status message', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ blobSearch: {
+ perPage: 20,
+ fileCount: 1,
+ matchCount: 1,
+ },
+ },
+ });
+ });
+
+ it('renders the status bar', () => {
+ expect(wrapper.text()).toContain('Showing 1 code result for test');
+ });
+ });
+ });
+ describe('group status message', () => {
+ describe('multiple results', () => {
+ beforeEach(() => {
+ createComponent({
+ initialState: {
+ query: {
+ ...MOCK_QUERY,
+ group_id: 1,
+ project_id: null,
+ search: 'test',
+ },
+ groupInitialJson,
+ },
+ });
+ });
+
+ it('renders the status bar', () => {
+ expect(wrapper.text()).toContain(
+ 'Showing 3000 code results for test in group Group Full Name',
+ );
+ });
+ });
+ describe('single result', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ blobSearch: {
+ perPage: 20,
+ fileCount: 1,
+ matchCount: 1,
+ },
+ },
+ initialState: {
+ query: {
+ ...MOCK_QUERY,
+ group_id: 1,
+ project_id: null,
+ search: 'test',
+ },
+ groupInitialJson,
+ },
+ });
+ });
+
+ it('renders the status bar', () => {
+ expect(wrapper.text()).toContain('Showing 1 code result for test in group Group Full Name');
+ });
+ });
+ });
+
+ describe('project status message', () => {
+ describe('multiple results', () => {
+ beforeEach(() => {
+ createComponent({
+ initialState: {
+ query: {
+ ...MOCK_QUERY,
+ group_id: null,
+ project_id: 1,
+ search: 'test',
+ },
+ projectInitialJson,
+ },
+ });
+ });
+
+ it('renders the status bar', () => {
+ expect(wrapper.text()).toContain(
+ 'Showing 3000 code results for test in of Project with Namespace',
+ );
+ });
+ });
+ describe('single result', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ blobSearch: {
+ perPage: 20,
+ fileCount: 1,
+ matchCount: 1,
+ },
+ },
+ initialState: {
+ query: {
+ ...MOCK_QUERY,
+ group_id: null,
+ project_id: 1,
+ search: 'test',
+ },
+ projectInitialJson,
+ },
+ });
+ });
+
+ it('renders the status bar', () => {
+ expect(wrapper.text()).toContain(
+ 'Showing 1 code result for test in of Project with Namespace',
+ );
+ });
+ });
+ });
+
+ describe('when there are no results', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ hasResults: false,
+ },
+ });
+ });
+
+ it('does not render the status bar', () => {
+ expect(wrapper.text()).toBe('');
+ });
+ });
+
+ describe('when loading', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ isLoading: true,
+ },
+ });
+ });
+
+ it('does not render the status bar', () => {
+ expect(wrapper.text()).toBe('');
+ });
+ });
+});
diff --git a/spec/frontend/search/results/components/zoekt_blob_results_spec.js b/spec/frontend/search/results/components/zoekt_blob_results_spec.js
index 9ad36971c40..812e5af3d16 100644
--- a/spec/frontend/search/results/components/zoekt_blob_results_spec.js
+++ b/spec/frontend/search/results/components/zoekt_blob_results_spec.js
@@ -1,21 +1,16 @@
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
-import VueApollo from 'vue-apollo';
import { GlLoadingIcon, GlCard } from '@gitlab/ui';
-import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import getBlobSearchQuery from '~/search/graphql/blob_search_zoekt.query.graphql';
import ZoektBlobResults from '~/search/results/components/zoekt_blob_results.vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/alert';
import EmptyResult from '~/search/results/components/result_empty.vue';
import { MOCK_QUERY, mockGetBlobSearchQuery } from '../../mock_data';
jest.mock('~/alert');
-Vue.use(VueApollo);
Vue.use(Vuex);
describe('ZoektBlobResults', () => {
@@ -25,28 +20,22 @@ describe('ZoektBlobResults', () => {
currentScope: jest.fn(() => 'blobs'),
};
- const blobSearchHandler = jest.fn().mockResolvedValue(mockGetBlobSearchQuery);
- const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
- const mockQueryEmpty = jest.fn().mockReturnValue({});
- const mockQueryError = jest.fn().mockRejectedValue(new Error('Network error'));
-
- const createComponent = ({
- initialState = { query: { scope: 'blobs' }, searchType: 'zoekt' },
- queryHandler = blobSearchHandler,
- } = {}) => {
- const requestHandlers = [[getBlobSearchQuery, queryHandler]];
- const apolloProvider = createMockApollo(requestHandlers);
+ const defaultState = { ...MOCK_QUERY, query: { scope: 'blobs' }, searchType: 'zoekt' };
+ const defaultProps = { hasResults: true, isLoading: false, blobSearch: {} };
+ const createComponent = ({ initialState = {}, propsData = {} } = {}) => {
const store = new Vuex.Store({
state: {
- query: MOCK_QUERY,
+ ...defaultState,
...initialState,
},
getters: getterSpies,
});
- // apolloMock = createMockApollo([[getBlobSearchQuery, blobSearchHandler]]);
wrapper = shallowMountExtended(ZoektBlobResults, {
- apolloProvider,
+ propsData: {
+ ...defaultProps,
+ ...propsData,
+ },
store,
stubs: {
GlCard,
@@ -60,7 +49,7 @@ describe('ZoektBlobResults', () => {
describe('when loading results', () => {
beforeEach(async () => {
createComponent({
- queryHandler: mockQueryLoading,
+ propsData: { isLoading: true },
});
jest.advanceTimersByTime(500);
await waitForPromises();
@@ -73,7 +62,11 @@ describe('ZoektBlobResults', () => {
describe('when component loads normally', () => {
beforeEach(async () => {
- createComponent();
+ createComponent({
+ propsData: {
+ blobSearch: mockGetBlobSearchQuery.data.blobSearch,
+ },
+ });
jest.advanceTimersByTime(500);
await waitForPromises();
});
@@ -87,7 +80,7 @@ describe('ZoektBlobResults', () => {
describe('when component has no results', () => {
beforeEach(async () => {
createComponent({
- queryHandler: mockQueryEmpty,
+ propsData: { hasResults: false },
});
jest.advanceTimersByTime(500);
await waitForPromises();
@@ -98,20 +91,4 @@ describe('ZoektBlobResults', () => {
expect(findEmptyResult().exists()).toBe(true);
});
});
-
- describe('when component has load error', () => {
- beforeEach(async () => {
- createComponent({ queryHandler: mockQueryError });
- jest.runOnlyPendingTimers();
- await nextTick();
- });
-
- it('calls createAlert', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: 'Could not load search results. Please refresh the page to try again.',
- captureError: true,
- error: expect.any(Error),
- });
- });
- });
});
diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
index 6e2dba8d56a..e0376c80926 100644
--- a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
@@ -16,7 +16,7 @@ import {
import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
const mockWorkItemNotesWidgetResponseWithComments =
- mockWorkItemNotesResponseWithComments.data.workspace.workItem.widgets.find(
+ mockWorkItemNotesResponseWithComments().data.workspace.workItem.widgets.find(
(widget) => widget.type === WIDGET_TYPE_NOTES,
);
diff --git a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
index 521c64459bc..db73a407be9 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
@@ -19,7 +19,7 @@ Vue.use(VueApollo);
describe('Work Item Note Awards List', () => {
let wrapper;
- const { workItem } = mockWorkItemNotesResponseWithComments.data.workspace;
+ const { workItem } = mockWorkItemNotesResponseWithComments().data.workspace;
const firstNote = workItem.widgets.find((w) => w.type === 'NOTES').discussions.nodes[0].notes
.nodes[0];
const fullPath = 'test-project-path';
@@ -57,7 +57,7 @@ describe('Work Item Note Awards List', () => {
apolloProvider.clients.defaultClient.writeQuery({
query,
variables: { fullPath, iid: workItemIid },
- ...mockWorkItemNotesResponseWithComments,
+ ...mockWorkItemNotesResponseWithComments(),
});
wrapper = shallowMount(WorkItemNoteAwardsList, {
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js
index b583829191a..1c376a5c30e 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -2,6 +2,8 @@ import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { setHTMLFixture } from 'helpers/fixtures';
import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
@@ -42,7 +44,7 @@ const mockMoreNotesWidgetResponse =
);
const mockWorkItemNotesWidgetResponseWithComments =
- mockWorkItemNotesResponseWithComments.data.workspace.workItem.widgets.find(
+ mockWorkItemNotesResponseWithComments().data.workspace.workItem.widgets.find(
(widget) => widget.type === WIDGET_TYPE_NOTES,
);
@@ -71,7 +73,7 @@ describe('WorkItemNotes component', () => {
const workItemMoreNotesQueryHandler = jest.fn().mockResolvedValue(mockMoreWorkItemNotesResponse);
const workItemNotesWithCommentsQueryHandler = jest
.fn()
- .mockResolvedValue(mockWorkItemNotesResponseWithComments);
+ .mockResolvedValue(mockWorkItemNotesResponseWithComments());
const deleteWorkItemNoteMutationSuccessHandler = jest.fn().mockResolvedValue({
data: { destroyNote: { note: null, __typename: 'DestroyNote' } },
});
@@ -122,6 +124,7 @@ describe('WorkItemNotes component', () => {
};
beforeEach(() => {
+ setHTMLFixture('');
createComponent();
});
@@ -381,6 +384,41 @@ describe('WorkItemNotes component', () => {
});
});
+ describe('discussions expanded status', () => {
+ it('should be expanded when the discussion is not resolved', async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ await waitForPromises();
+ expect(findAllWorkItemCommentNotes().at(0).props('isExpandedOnLoad')).toBe(true);
+ });
+
+ it('should be collapsed when the discussion is resolved', async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: jest
+ .fn()
+ .mockResolvedValue(mockWorkItemNotesResponseWithComments(true)),
+ });
+
+ await waitForPromises();
+ expect(findAllWorkItemCommentNotes().at(0).props('isExpandedOnLoad')).toBe(false);
+ });
+
+ it('should be expanded when the notes are resolved but the target note hash has note id', async () => {
+ setWindowLocation('#note_174');
+
+ createComponent({
+ defaultWorkItemNotesQueryHandler: jest
+ .fn()
+ .mockResolvedValue(mockWorkItemNotesResponseWithComments(true)),
+ });
+
+ await waitForPromises();
+ await nextTick();
+ expect(findAllWorkItemCommentNotes().at(0).props('isExpandedOnLoad')).toBe(true);
+ });
+ });
+
describe('when group context', () => {
it('should pass the correct `autoCompleteDataSources` to group work item comment note', async () => {
const groupWorkItemNotes = {
@@ -388,7 +426,7 @@ describe('WorkItemNotes component', () => {
workspace: {
id: 'gid://gitlab/Group/24',
workItem: {
- ...mockWorkItemNotesResponseWithComments.data.workspace.workItem,
+ ...mockWorkItemNotesResponseWithComments().data.workspace.workItem,
namespace: {
id: 'gid://gitlab/Group/24',
__typename: 'Namespace',
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 86a3598fde2..d8bdab12f0e 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -3372,216 +3372,218 @@ export const mockWorkItemCommentByMaintainer = {
maxAccessLevelOfAuthor: 'Maintainer',
};
-export const mockWorkItemNotesResponseWithComments = {
- data: {
- workspace: {
- id: 'gid://gitlab/Project/6',
- workItem: {
- id: 'gid://gitlab/WorkItem/600',
- iid: '60',
- namespace: {
- id: 'gid://gitlab/Namespaces::ProjectNamespace/34',
- __typename: 'Namespace',
- },
- widgets: [
- {
- __typename: 'WorkItemWidgetIteration',
+export const mockWorkItemNotesResponseWithComments = (resolved = false) => {
+ return {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/6',
+ workItem: {
+ id: 'gid://gitlab/WorkItem/600',
+ iid: '60',
+ namespace: {
+ id: 'gid://gitlab/Namespaces::ProjectNamespace/34',
+ __typename: 'Namespace',
},
- {
- __typename: 'WorkItemWidgetWeight',
- },
- {
- __typename: 'WorkItemWidgetAssignees',
- },
- {
- __typename: 'WorkItemWidgetLabels',
- },
- {
- __typename: 'WorkItemWidgetDescription',
- },
- {
- __typename: 'WorkItemWidgetHierarchy',
- },
- {
- __typename: 'WorkItemWidgetStartAndDueDate',
- },
- {
- __typename: 'WorkItemWidgetMilestone',
- },
- {
- type: 'NOTES',
- discussions: {
- pageInfo: {
- hasNextPage: false,
- hasPreviousPage: false,
- startCursor: null,
- endCursor: null,
- __typename: 'PageInfo',
- },
- nodes: [
- {
- id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
- notes: {
- nodes: [
- {
- id: 'gid://gitlab/DiscussionNote/174',
- body: 'Separate thread',
- bodyHtml: 'Separate thread
',
- system: false,
- internal: false,
- systemNoteIconName: null,
- createdAt: '2023-01-12T07:47:40Z',
- lastEditedAt: null,
- url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
- lastEditedBy: null,
- maxAccessLevelOfAuthor: 'Owner',
- authorIsContributor: false,
- discussion: {
- id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
- resolved: false,
- resolvable: true,
- resolvedBy: null,
- __typename: 'Discussion',
- },
- author: {
- id: 'gid://gitlab/User/1',
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://127.0.0.1:3000/root',
- webPath: '/root',
- __typename: 'UserCore',
- },
- systemNoteMetadata: null,
- userPermissions: {
- adminNote: true,
- awardEmoji: true,
- readNote: true,
- createNote: true,
- resolveNote: true,
- repositionNote: true,
- __typename: 'NotePermissions',
- },
- awardEmoji: {
- nodes: [mockAwardEmojiThumbsDown],
- },
- __typename: 'Note',
- },
- {
- id: 'gid://gitlab/DiscussionNote/235',
- body: 'Thread comment',
- bodyHtml: 'Thread comment
',
- system: false,
- internal: false,
- systemNoteIconName: null,
- createdAt: '2023-01-18T09:09:54Z',
- lastEditedAt: null,
- url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
- lastEditedBy: null,
- maxAccessLevelOfAuthor: 'Owner',
- authorIsContributor: false,
- discussion: {
- id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
- resolved: false,
- resolvable: true,
- resolvedBy: null,
- __typename: 'Discussion',
- },
- author: {
- id: 'gid://gitlab/User/1',
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://127.0.0.1:3000/root',
- webPath: '/root',
- __typename: 'UserCore',
- },
- systemNoteMetadata: null,
- userPermissions: {
- adminNote: true,
- awardEmoji: true,
- readNote: true,
- createNote: true,
- resolveNote: true,
- repositionNote: true,
- __typename: 'NotePermissions',
- },
- awardEmoji: {
- nodes: [],
- },
- __typename: 'Note',
- },
- ],
- __typename: 'NoteConnection',
- },
- __typename: 'Discussion',
- },
- {
- id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
- notes: {
- nodes: [
- {
- id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
- body: 'Main thread 2',
- bodyHtml: 'Main thread 2
',
- systemNoteIconName: 'weight',
- createdAt: '2022-11-25T07:16:20Z',
- lastEditedAt: null,
- url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
- lastEditedBy: null,
- system: false,
- internal: false,
- maxAccessLevelOfAuthor: 'Owner',
- authorIsContributor: false,
- discussion: {
- id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
- resolved: false,
- resolvable: true,
- resolvedBy: null,
- __typename: 'Discussion',
- },
- userPermissions: {
- adminNote: false,
- awardEmoji: true,
- readNote: true,
- createNote: true,
- resolveNote: true,
- repositionNote: true,
- __typename: 'NotePermissions',
- },
- systemNoteMetadata: null,
- author: {
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- id: 'gid://gitlab/User/1',
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://127.0.0.1:3000/root',
- webPath: '/root',
- __typename: 'UserCore',
- },
- awardEmoji: {
- nodes: [],
- },
- __typename: 'Note',
- },
- ],
- __typename: 'NoteConnection',
- },
- __typename: 'Discussion',
- },
- ],
- __typename: 'DiscussionConnection',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetIteration',
},
- __typename: 'WorkItemWidgetNotes',
- },
- ],
- __typename: 'WorkItem',
+ {
+ __typename: 'WorkItemWidgetWeight',
+ },
+ {
+ __typename: 'WorkItemWidgetAssignees',
+ },
+ {
+ __typename: 'WorkItemWidgetLabels',
+ },
+ {
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ {
+ __typename: 'WorkItemWidgetStartAndDueDate',
+ },
+ {
+ __typename: 'WorkItemWidgetMilestone',
+ },
+ {
+ type: 'NOTES',
+ discussions: {
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: null,
+ __typename: 'PageInfo',
+ },
+ nodes: [
+ {
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiscussionNote/174',
+ body: 'Separate thread',
+ bodyHtml: 'Separate thread
',
+ system: false,
+ internal: false,
+ systemNoteIconName: null,
+ createdAt: '2023-01-12T07:47:40Z',
+ lastEditedAt: null,
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
+ lastEditedBy: null,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
+ resolved,
+ resolvable: true,
+ resolvedBy: null,
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ webPath: '/root',
+ __typename: 'UserCore',
+ },
+ systemNoteMetadata: null,
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ awardEmoji: {
+ nodes: [mockAwardEmojiThumbsDown],
+ },
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/235',
+ body: 'Thread comment',
+ bodyHtml: 'Thread comment
',
+ system: false,
+ internal: false,
+ systemNoteIconName: null,
+ createdAt: '2023-01-18T09:09:54Z',
+ lastEditedAt: null,
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
+ lastEditedBy: null,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
+ resolved,
+ resolvable: true,
+ resolvedBy: null,
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ webPath: '/root',
+ __typename: 'UserCore',
+ },
+ systemNoteMetadata: null,
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ awardEmoji: {
+ nodes: [],
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
+ body: 'Main thread 2',
+ bodyHtml: 'Main thread 2
',
+ systemNoteIconName: 'weight',
+ createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
+ lastEditedBy: null,
+ system: false,
+ internal: false,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
+ resolved,
+ resolvable: true,
+ resolvedBy: null,
+ __typename: 'Discussion',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ systemNoteMetadata: null,
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ webPath: '/root',
+ __typename: 'UserCore',
+ },
+ awardEmoji: {
+ nodes: [],
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ ],
+ __typename: 'DiscussionConnection',
+ },
+ __typename: 'WorkItemWidgetNotes',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
},
},
- },
+ };
};
export const workItemNotesCreateSubscriptionResponse = {
diff --git a/spec/frontend/work_items/notes/award_utils_spec.js b/spec/frontend/work_items/notes/award_utils_spec.js
index 0c2ba8099d5..be67df83a7c 100644
--- a/spec/frontend/work_items/notes/award_utils_spec.js
+++ b/spec/frontend/work_items/notes/award_utils_spec.js
@@ -18,7 +18,7 @@ function getFirstNote(workItem) {
}
describe('Work item note award utils', () => {
- const workItem = getWorkItem(mockWorkItemNotesResponseWithComments.data);
+ const workItem = getWorkItem(mockWorkItemNotesResponseWithComments().data);
const firstNote = getFirstNote(workItem);
const fullPath = 'test-project-path';
const workItemIid = workItem.iid;
@@ -60,7 +60,7 @@ describe('Work item note award utils', () => {
apolloProvider.clients.defaultClient.writeQuery({
query: workItemNotesByIidQuery,
variables: { fullPath, iid: workItemIid },
- ...mockWorkItemNotesResponseWithComments,
+ ...mockWorkItemNotesResponseWithComments(),
});
});
diff --git a/spec/graphql/mutations/todos/create_spec.rb b/spec/graphql/mutations/todos/create_spec.rb
index d9a31b0ecaa..3b1b5244c7e 100644
--- a/spec/graphql/mutations/todos/create_spec.rb
+++ b/spec/graphql/mutations/todos/create_spec.rb
@@ -14,8 +14,14 @@ RSpec.describe Mutations::Todos::Create do
target = create(:milestone)
input = { target_id: global_id_of(target).to_s }
mutation = graphql_mutation(described_class, input)
+ headers = { "Referer" => "foobar", "User-Agent" => "user-agent" }
+ request = instance_double(ActionDispatch::Request, headers: headers, env: nil)
- response = GitlabSchema.execute(mutation.query, context: query_context, variables: mutation.variables).to_h
+ response = GitlabSchema.execute(
+ mutation.query,
+ context: query_context(request: request),
+ variables: mutation.variables
+ ).to_h
expect(response).to include(
'errors' => contain_exactly(
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index d05f59476ab..b8faec68437 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -1189,8 +1189,12 @@ RSpec.describe SearchHelper, feature_category: :global_search do
end
describe '#should_show_zoekt_results?' do
+ before do
+ allow(self).to receive(:current_user).and_return(nil)
+ end
+
it 'returns false for any scope and search type' do
- expect(should_show_zoekt_results?(:any_scope, :any_type)).to be false
+ expect(should_show_zoekt_results?(:some_scope, :some_type)).to be false
end
end
end
diff --git a/spec/lib/gitlab/ci/build/context/global_spec.rb b/spec/lib/gitlab/ci/build/context/global_spec.rb
index cf511cf1560..7bd9e877a87 100644
--- a/spec/lib/gitlab/ci/build/context/global_spec.rb
+++ b/spec/lib/gitlab/ci/build/context/global_spec.rb
@@ -37,4 +37,50 @@ RSpec.describe Gitlab::Ci::Build::Context::Global, feature_category: :pipeline_c
it_behaves_like 'variables collection'
end
+
+ describe '#top_level_worktree_paths' do
+ subject(:top_level_worktree_paths) { context.top_level_worktree_paths }
+
+ it 'delegates to pipeline' do
+ expect(pipeline).to receive(:top_level_worktree_paths)
+
+ top_level_worktree_paths
+ end
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(ci_conditionals_reduce_gitaly_calls: false)
+ end
+
+ it 'accesses repository' do
+ expect(pipeline).not_to receive(:top_level_worktree_paths)
+ expect(context.project.repository).to receive(:tree).and_return(instance_double('Tree', blobs: []))
+
+ top_level_worktree_paths
+ end
+ end
+ end
+
+ describe '#all_worktree_paths' do
+ subject(:all_worktree_paths) { context.all_worktree_paths }
+
+ it 'delegates to pipeline' do
+ expect(pipeline).to receive(:all_worktree_paths)
+
+ all_worktree_paths
+ end
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(ci_conditionals_reduce_gitaly_calls: false)
+ end
+
+ it 'accesses repository' do
+ expect(context.project.repository).to receive(:ls_files).with(instance_of(String))
+ expect(pipeline).not_to receive(:all_worktree_paths)
+
+ all_worktree_paths
+ end
+ end
+ end
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 3b159815adb..8032c78c755 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -169,9 +169,9 @@ module GraphqlHelpers
end
# create a valid query context object
- def query_context(user: current_user)
+ def query_context(user: current_user, request: {})
query = GraphQL::Query.new(empty_schema, document: nil, context: {}, variables: {})
- GraphQL::Query::Context.new(query: query, values: { current_user: user })
+ GraphQL::Query::Context.new(query: query, values: { current_user: user, request: request })
end
# rubocop:enable Metrics/ParameterLists
diff --git a/tooling/stylelint/gitlab_no_gl_class.plugin.js b/tooling/stylelint/gitlab_no_gl_class.plugin.js
index 4f8cde8d26e..067555e29c3 100644
--- a/tooling/stylelint/gitlab_no_gl_class.plugin.js
+++ b/tooling/stylelint/gitlab_no_gl_class.plugin.js
@@ -25,7 +25,7 @@ const ruleFunction = (primary) => {
if (!validOptions) return;
- root.walkRules(/\.gl-(?!dark)/, (ruleNode) => {
+ root.walkRules(/\.gl-/, (ruleNode) => {
report({
result,
ruleName,