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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/rails/shared.gitlab-ci.yml2
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_approved.vue21
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue29
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_expired.vue12
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_joined.vue12
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_left.vue12
-rw-r--r--app/assets/javascripts/contribution_events/components/resource_parent_link.vue4
-rw-r--r--app/assets/javascripts/contribution_events/components/target_link.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue6
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar.vue6
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue6
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue41
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue17
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue13
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue42
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue95
-rw-r--r--app/assets/javascripts/work_items/graphql/cache_utils.js40
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/work_item_note.fragment.graphql6
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql12
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql5
-rw-r--r--app/assets/javascripts/work_items/notes/award_utils.js67
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/models/organizations/organization.rb3
-rw-r--r--app/models/organizations/organization_user.rb8
-rw-r--r--app/models/user.rb3
-rw-r--r--app/views/shared/_new_merge_request_checkbox.html.haml3
-rw-r--r--config/feature_flags/development/auto_merge_labels_mr_widget.yml8
-rw-r--r--data/whats_new/202306220001_16_1.yml98
-rw-r--r--db/docs/organization_users.yml10
-rw-r--r--db/migrate/20230608113106_create_organization_users.rb22
-rw-r--r--db/migrate/20230615104902_add_user_id_foreign_key_to_organization_users.rb15
-rw-r--r--db/migrate/20230620104217_add_organization_id_foreign_key_to_organization_users.rb15
-rw-r--r--db/schema_migrations/202306081131061
-rw-r--r--db/schema_migrations/202306151049021
-rw-r--r--db/schema_migrations/202306201042171
-rw-r--r--db/structure.sql32
-rw-r--r--doc/api/runners.md21
-rw-r--r--doc/development/database/adding_database_indexes.md3
-rw-r--r--doc/development/fe_guide/vue.md9
-rw-r--r--doc/development/sec/gemnasium_analyzer_data.md33
-rw-r--r--doc/user/application_security/dependency_list/index.md2
-rw-r--r--doc/user/application_security/dependency_scanning/analyzers.md127
-rw-r--r--doc/user/application_security/dependency_scanning/index.md55
-rw-r--r--doc/user/project/repository/code_suggestions.md2
-rw-r--r--locale/gitlab.pot21
-rw-r--r--spec/factories/organizations/organization_users.rb8
-rw-r--r--spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb141
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb157
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb55
-rw-r--r--spec/frontend/admin/topics/components/topic_select_spec.js4
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js2
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js1
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js1
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js4
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js26
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js79
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js21
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js21
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js21
-rw-r--r--spec/frontend/contribution_events/components/contribution_events_spec.js8
-rw-r--r--spec/frontend/contribution_events/components/resource_parent_link_spec.js46
-rw-r--r--spec/frontend/contribution_events/components/target_link_spec.js43
-rw-r--r--spec/frontend/contribution_events/utils.js12
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js4
-rw-r--r--spec/frontend/drawio/drawio_editor_spec.js1
-rw-r--r--spec/frontend/editor/source_editor_extension_base_spec.js1
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js7
-rw-r--r--spec/frontend/editor/source_editor_yaml_ext_spec.js4
-rw-r--r--spec/frontend/header_search/init_spec.js2
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js1
-rw-r--r--spec/frontend/jira_connect/branches/components/project_dropdown_spec.js1
-rw-r--r--spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js1
-rw-r--r--spec/frontend/lib/utils/downloader_spec.js4
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js1
-rw-r--r--spec/frontend/pipeline_wizard/components/commit_spec.js8
-rw-r--r--spec/frontend/pipelines/graph_shared/links_inner_spec.js1
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/index_spec.js1
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js1
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js2
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js12
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js15
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js15
-rw-r--r--spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js32
-rw-r--r--spec/frontend/tracking/tracking_spec.js1
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js2
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js27
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js147
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js19
-rw-r--r--spec/frontend/work_items/mock_data.js54
-rw-r--r--spec/frontend/work_items/notes/award_utils_spec.js109
-rw-r--r--spec/models/organizations/organization_spec.rb2
-rw-r--r--spec/models/organizations/organization_user_spec.rb10
-rw-r--r--spec/models/user_spec.rb9
-rw-r--r--spec/support/formatters/json_formatter.rb3
99 files changed, 1257 insertions, 901 deletions
diff --git a/.gitlab/ci/rails/shared.gitlab-ci.yml b/.gitlab/ci/rails/shared.gitlab-ci.yml
index 6ea6d8e523c..a7552dabff3 100644
--- a/.gitlab/ci/rails/shared.gitlab-ci.yml
+++ b/.gitlab/ci/rails/shared.gitlab-ci.yml
@@ -79,7 +79,7 @@ include:
- echo -e "\e[0Ksection_start:`date +%s`:report_results_section[collapsed=true]\r\e[0KReport results"
- |
if [ "$CREATE_RAILS_TEST_FAILURE_ISSUES" == "true" ]; then
- bundle exec relate-failure-issue --input-files "rspec/rspec-*.json" --system-log-files "log" --project "gitlab-org-sandbox/rails-test-failures" --token "${RAILS_TEST_FAILURES_PROJECT_TOKEN}";
+ bundle exec relate-failure-issue --input-files "rspec/rspec-*.json" --system-log-files "log" --project "gitlab-org/gitlab" --token "${TEST_FAILURES_PROJECT_TOKEN}";
fi
- echo -e "\e[0Ksection_end:`date +%s`:report_results_section\r\e[0K"
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_approved.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_approved.vue
index a7787ae84bc..9f166e594b8 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_approved.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_approved.vue
@@ -1,8 +1,5 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
-import TargetLink from '../target_link.vue';
-import ResourceParentLink from '../resource_parent_link.vue';
import ContributionEventBase from './contribution_event_base.vue';
export default {
@@ -12,7 +9,7 @@ export default {
'ContributionEvent|Approved merge request %{targetLink} in %{resourceParentLink}.',
),
},
- components: { ContributionEventBase, GlSprintf, TargetLink, ResourceParentLink },
+ components: { ContributionEventBase },
props: {
/**
* Expected format
@@ -52,14 +49,10 @@ export default {
</script>
<template>
- <contribution-event-base :event="event" icon-name="approval-solid" icon-class="gl-text-green-500">
- <gl-sprintf :message="$options.i18n.message">
- <template #targetLink>
- <target-link :event="event" />
- </template>
- <template #resourceParentLink>
- <resource-parent-link :event="event" />
- </template>
- </gl-sprintf>
- </contribution-event-base>
+ <contribution-event-base
+ :event="event"
+ :message="$options.i18n.message"
+ icon-name="approval-solid"
+ icon-class="gl-text-green-500"
+ />
</template>
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue
index 93ac94a6f4f..e3d3360cd0c 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue
@@ -1,9 +1,19 @@
<script>
-import { GlAvatarLabeled, GlAvatarLink, GlIcon } from '@gitlab/ui';
+import { GlAvatarLabeled, GlAvatarLink, GlIcon, GlSprintf } from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import TargetLink from '../target_link.vue';
+import ResourceParentLink from '../resource_parent_link.vue';
export default {
- components: { GlAvatarLabeled, GlAvatarLink, GlIcon, TimeAgoTooltip },
+ components: {
+ GlAvatarLabeled,
+ GlAvatarLink,
+ GlIcon,
+ GlSprintf,
+ TimeAgoTooltip,
+ TargetLink,
+ ResourceParentLink,
+ },
props: {
event: {
type: Object,
@@ -13,6 +23,11 @@ export default {
type: String,
required: true,
},
+ message: {
+ type: String,
+ required: false,
+ default: '',
+ },
iconClass: {
type: String,
required: false,
@@ -44,7 +59,15 @@ export default {
<div class="gl-pl-8 gl-mt-2" data-testid="event-body">
<div class="gl-text-secondary">
<gl-icon :class="iconClass" :name="iconName" />
- <slot></slot>
+ <gl-sprintf v-if="message" :message="message">
+ <template #targetLink>
+ <target-link :event="event" />
+ </template>
+ <template #resourceParentLink>
+ <resource-parent-link :event="event" />
+ </template>
+ </gl-sprintf>
+ <slot v-else></slot>
</div>
<div v-if="$scopedSlots['additional-info']" class="gl-mt-2">
<slot name="additional-info"></slot>
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_expired.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_expired.vue
index 7e0c81ce8c2..8daccd66aeb 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_expired.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_expired.vue
@@ -1,7 +1,5 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
-import ResourceParentLink from '../resource_parent_link.vue';
import ContributionEventBase from './contribution_event_base.vue';
export default {
@@ -11,7 +9,7 @@ export default {
'ContributionEvent|Removed due to membership expiration from %{resourceParentLink}.',
),
},
- components: { ContributionEventBase, ResourceParentLink, GlSprintf },
+ components: { ContributionEventBase },
props: {
/**
* Expected format
@@ -44,11 +42,5 @@ export default {
</script>
<template>
- <contribution-event-base :event="event" icon-name="expire">
- <gl-sprintf :message="$options.i18n.message">
- <template #resourceParentLink>
- <resource-parent-link :event="event" />
- </template>
- </gl-sprintf>
- </contribution-event-base>
+ <contribution-event-base :event="event" :message="$options.i18n.message" icon-name="expire" />
</template>
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_joined.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_joined.vue
index 9ecf0f88772..1b60582e7e1 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_joined.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_joined.vue
@@ -1,7 +1,5 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
-import ResourceParentLink from '../resource_parent_link.vue';
import ContributionEventBase from './contribution_event_base.vue';
export default {
@@ -9,7 +7,7 @@ export default {
i18n: {
message: s__('ContributionEvent|Joined project %{resourceParentLink}.'),
},
- components: { ContributionEventBase, ResourceParentLink, GlSprintf },
+ components: { ContributionEventBase },
props: {
/**
* Expected format
@@ -42,11 +40,5 @@ export default {
</script>
<template>
- <contribution-event-base :event="event" icon-name="users">
- <gl-sprintf :message="$options.i18n.message">
- <template #resourceParentLink>
- <resource-parent-link :event="event" />
- </template>
- </gl-sprintf>
- </contribution-event-base>
+ <contribution-event-base :event="event" :message="$options.i18n.message" icon-name="users" />
</template>
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_left.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_left.vue
index 2fd9383c05e..701126b4a74 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_left.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_left.vue
@@ -1,7 +1,5 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
-import ResourceParentLink from '../resource_parent_link.vue';
import ContributionEventBase from './contribution_event_base.vue';
export default {
@@ -9,7 +7,7 @@ export default {
i18n: {
message: s__('ContributionEvent|Left project %{resourceParentLink}.'),
},
- components: { ContributionEventBase, ResourceParentLink, GlSprintf },
+ components: { ContributionEventBase },
props: {
/**
* Expected format
@@ -42,11 +40,5 @@ export default {
</script>
<template>
- <contribution-event-base :event="event" icon-name="leave">
- <gl-sprintf :message="$options.i18n.message">
- <template #resourceParentLink>
- <resource-parent-link :event="event" />
- </template>
- </gl-sprintf>
- </contribution-event-base>
+ <contribution-event-base :event="event" :message="$options.i18n.message" icon-name="leave" />
</template>
diff --git a/app/assets/javascripts/contribution_events/components/resource_parent_link.vue b/app/assets/javascripts/contribution_events/components/resource_parent_link.vue
index 5add9d788bb..dd7b20ac794 100644
--- a/app/assets/javascripts/contribution_events/components/resource_parent_link.vue
+++ b/app/assets/javascripts/contribution_events/components/resource_parent_link.vue
@@ -18,5 +18,7 @@ export default {
</script>
<template>
- <gl-link :href="resourceParent.web_url">{{ resourceParent.full_name }}</gl-link>
+ <gl-link v-if="resourceParent" :href="resourceParent.web_url">{{
+ resourceParent.full_name
+ }}</gl-link>
</template>
diff --git a/app/assets/javascripts/contribution_events/components/target_link.vue b/app/assets/javascripts/contribution_events/components/target_link.vue
index a661121b2fb..6559d6c7272 100644
--- a/app/assets/javascripts/contribution_events/components/target_link.vue
+++ b/app/assets/javascripts/contribution_events/components/target_link.vue
@@ -27,5 +27,5 @@ export default {
</script>
<template>
- <gl-link v-bind="targetLinkAttributes">{{ targetLinkText }}</gl-link>
+ <gl-link v-if="target" v-bind="targetLinkAttributes">{{ targetLinkText }}</gl-link>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue b/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue
index 9d2836e9dfa..6058ed3a1cd 100644
--- a/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue
@@ -1,5 +1,6 @@
<script>
import { getCssClassDimensions } from '~/lib/utils/css_utils';
+import Tracking from '~/tracking';
import { SUPER_SIDEBAR_PEEK_OPEN_DELAY, SUPER_SIDEBAR_PEEK_CLOSE_DELAY } from '../constants';
export const STATE_CLOSED = 'closed';
@@ -9,6 +10,7 @@ export const STATE_WILL_CLOSE = 'will-close';
export default {
name: 'SidebarPeek',
+ mixins: [Tracking.mixin()],
created() {
// Nothing needs to observe these properties, so they are not reactive.
this.state = null;
@@ -88,6 +90,10 @@ export default {
open() {
if (this.changeState(STATE_OPEN)) {
this.clearTimers();
+ this.track('nav_peek', {
+ label: 'nav_hover',
+ property: 'nav_sidebar',
+ });
}
},
close() {
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
index 6b1efc4217c..c194401ce95 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
@@ -3,6 +3,7 @@ import { GlButton } from '@gitlab/ui';
import { Mousetrap } from '~/lib/mousetrap';
import { keysFor, TOGGLE_SUPER_SIDEBAR } from '~/behaviors/shortcuts/keybindings';
import { __ } from '~/locale';
+import Tracking from '~/tracking';
import { sidebarState } from '../constants';
import { isCollapsed, toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager';
import UserBar from './user_bar.vue';
@@ -26,6 +27,7 @@ export default {
TrialStatusPopover: () =>
import('ee_component/contextual_sidebar/components/trial_status_popover.vue'),
},
+ mixins: [Tracking.mixin()],
i18n: {
skipToMainContent: __('Skip to main content'),
},
@@ -68,6 +70,10 @@ export default {
},
methods: {
toggleSidebar() {
+ this.track(isCollapsed() ? 'nav_show' : 'nav_hide', {
+ label: 'nav_toggle_keyboard_shortcut',
+ property: 'nav_sidebar',
+ });
toggleSuperSidebarCollapsed(!isCollapsed(), true);
},
collapseSidebar() {
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
index 4fff5cf832e..87762a62c0f 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
+import Tracking from '~/tracking';
import { JS_TOGGLE_COLLAPSE_CLASS, JS_TOGGLE_EXPAND_CLASS, sidebarState } from '../constants';
import { toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager';
@@ -11,6 +12,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [Tracking.mixin()],
props: {
tooltipContainer: {
type: String,
@@ -52,6 +54,10 @@ export default {
},
methods: {
toggle() {
+ this.track(this.isCollapsed ? 'nav_show' : 'nav_hide', {
+ label: 'nav_toggle',
+ property: 'nav_sidebar',
+ });
toggleSuperSidebarCollapsed(!this.isCollapsed, true);
this.focusOtherToggle();
},
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js
index 2687ea5ccf8..feb7e274b07 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js
@@ -1,6 +1,7 @@
import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils';
import { debounce } from 'lodash';
import { setCookie, getCookie } from '~/lib/utils/common_utils';
+import Tracking from '~/tracking';
import { sidebarState } from './constants';
export const SIDEBAR_COLLAPSED_CLASS = 'page-with-super-sidebar-collapsed';
@@ -50,7 +51,15 @@ export const bindSuperSidebarCollapsedEvents = (forceDesktopExpandedSidebar = fa
const widthChanged = previousWindowWidth !== newWindowWidth;
if (widthChanged) {
+ const collapsedBeforeResize = sidebarState.isCollapsed;
initSuperSidebarCollapsedState(forceDesktopExpandedSidebar);
+ const collapsedAfterResize = sidebarState.isCollapsed;
+ if (!collapsedBeforeResize && collapsedAfterResize) {
+ Tracking.event(undefined, 'nav_hide', {
+ label: 'browser_resize',
+ property: 'nav_sidebar',
+ });
+ }
}
previousWindowWidth = newWindowWidth;
}, 100);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 083ce4f87af..4738238de42 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -27,7 +27,6 @@ import readyToMergeSubscription from '~/vue_merge_request_widget/queries/states/
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import {
AUTO_MERGE_STRATEGIES,
- WARNING,
MT_MERGE_STRATEGY,
PIPELINE_FAILED_STATE,
STATE_MACHINE,
@@ -42,7 +41,6 @@ import CommitMessageDropdown from './commit_message_dropdown.vue';
import SquashBeforeMerge from './squash_before_merge.vue';
import MergeFailedPipelineConfirmationDialog from './merge_failed_pipeline_confirmation_dialog.vue';
-const PIPELINE_RUNNING_STATE = 'running';
const PIPELINE_PENDING_STATE = 'pending';
const PIPELINE_SUCCESS_STATE = 'success';
@@ -133,8 +131,6 @@ export default {
GlFormCheckbox,
GlSkeletonLoader,
MergeFailedPipelineConfirmationDialog,
- MergeTrainHelperIcon: () =>
- import('ee_component/vue_merge_request_widget/components/merge_train_helper_icon.vue'),
MergeImmediatelyConfirmationDialog: () =>
import(
'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue'
@@ -246,30 +242,11 @@ export default {
return PIPELINE_SUCCESS_STATE;
},
- iconClass() {
- if (this.shouldRenderMergeTrainHelperIcon && !this.mr.preventMerge) {
- return PIPELINE_RUNNING_STATE;
- }
-
- if (
- this.status === PIPELINE_FAILED_STATE ||
- !this.commitMessage.length ||
- !this.isMergeAllowed ||
- this.mr.preventMerge
- ) {
- return WARNING;
- }
-
- return PIPELINE_SUCCESS_STATE;
- },
mergeButtonText() {
if (this.isMergingImmediately) {
return __('Merge in progress');
}
- if (this.isAutoMergeAvailable && !this.autoMergeLabelsEnabled) {
- return this.autoMergeTextLegacy;
- }
- if (this.isAutoMergeAvailable && this.autoMergeLabelsEnabled) {
+ if (this.isAutoMergeAvailable) {
return this.autoMergeText;
}
@@ -279,9 +256,6 @@ export default {
return __('Merge');
},
- autoMergeLabelsEnabled() {
- return window.gon?.features?.autoMergeLabelsMrWidget;
- },
showAutoMergeHelperText() {
return (
!(this.status === PIPELINE_FAILED_STATE || this.isPipelineFailed) &&
@@ -708,18 +682,19 @@ export default {
@cancel="isPipelineFailedModalVisibleNormalMerge = false"
/>
</gl-button-group>
- <merge-train-helper-icon
- v-if="shouldRenderMergeTrainHelperIcon && !autoMergeLabelsEnabled"
- class="gl-mx-3"
- />
- <template v-if="showAutoMergeHelperText && autoMergeLabelsEnabled">
+ <template v-if="showAutoMergeHelperText">
<div
class="gl-ml-4 gl-text-gray-500 gl-font-sm"
data-qa-selector="auto_merge_helper_text"
+ data-testid="auto-merge-helper-text"
>
{{ autoMergeHelperText }}
</div>
- <help-popover class="gl-ml-2" :options="autoMergeHelpPopoverOptions">
+ <help-popover
+ class="gl-ml-2"
+ :options="autoMergeHelpPopoverOptions"
+ data-testid="auto-merge-helper-text-icon"
+ >
<gl-sprintf :message="autoMergePopoverSettings.bodyText">
<template #link="{ content }">
<gl-link
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index 10a54c73273..de801d922d3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -31,10 +31,6 @@ export default {
pipelineMustSucceedConflictText() {
return PIPELINE_MUST_SUCCEED_CONFLICT_TEXT;
},
- autoMergeTextLegacy() {
- // MWPS is currently the only auto merge strategy available in CE
- return __('Merge when pipeline succeeds');
- },
autoMergeText() {
return __('Set to auto-merge');
},
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
index 3a3929fba9b..3e24a35ea39 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
@@ -12,8 +12,22 @@ export default {
SafeHtml,
},
mixins: [ViewerMixin],
+ data() {
+ return {
+ isLoading: true,
+ };
+ },
mounted() {
- handleBlobRichViewer(this.$refs.content, this.type);
+ window.requestIdleCallback(async () => {
+ /**
+ * Rendering Markdown usually takes long due to the amount of HTML being parsed.
+ * This ensures that content is loaded only when the browser goes into idle.
+ * More details here: https://gitlab.com/gitlab-org/gitlab/-/issues/331448
+ * */
+ this.isLoading = false;
+ await this.$nextTick();
+ handleBlobRichViewer(this.$refs.content, this.type);
+ });
},
safeHtmlConfig: {
ADD_TAGS: ['gl-emoji', 'copy-code'],
@@ -22,6 +36,7 @@ export default {
</script>
<template>
<markdown-field-view
+ v-if="!isLoading"
ref="content"
v-safe-html:[$options.safeHtmlConfig]="richViewer || content"
/>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
index 7ad424868c6..4ec13f3d24a 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -11,6 +11,7 @@ import { getLocationHash } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import EditedAt from '~/issues/show/components/edited.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
import NoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
@@ -18,10 +19,12 @@ import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutati
import updateWorkItemNoteMutation from '../../graphql/notes/update_work_item_note.mutation.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import WorkItemCommentForm from './work_item_comment_form.vue';
+import WorkItemNoteAwardsList from './work_item_note_awards_list.vue';
export default {
name: 'WorkItemNoteThread',
components: {
+ WorkItemNoteAwardsList,
TimelineEntryItem,
NoteBody,
NoteHeader,
@@ -31,7 +34,7 @@ export default {
WorkItemCommentForm,
EditedAt,
},
- mixins: [Tracking.mixin()],
+ mixins: [Tracking.mixin(), glFeatureFlagsMixin()],
inject: ['fullPath'],
props: {
workItemId: {
@@ -323,6 +326,8 @@ export default {
<div class="gl-display-inline-flex">
<note-actions
:show-award-emoji="hasAwardEmojiPermission"
+ :work-item-iid="workItemIid"
+ :note="note"
:note-url="noteUrl"
:show-reply="showReply"
:show-edit="hasAdminPermission"
@@ -355,6 +360,12 @@ export default {
:updated-by-path="lastEditedBy.webPath"
:class="isFirstNote ? 'gl-pl-3' : 'gl-pl-8'"
/>
+ <work-item-note-awards-list
+ v-if="glFeatures.workItemsMvc2"
+ :note="note"
+ :work-item-iid="workItemIid"
+ :is-modal="isModal"
+ />
</div>
</div>
</timeline-entry-item>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
index b32a8c78c93..bc5dfe37280 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
@@ -7,11 +7,11 @@ import {
GlDisclosureDropdownItem,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
-import { __, s__, sprintf } from '~/locale';
+import { __, sprintf } from '~/locale';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import addAwardEmojiMutation from '../../graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
+import { getMutation, optimisticAwardUpdate } from '../../notes/award_utils';
export default {
name: 'WorkItemNoteActions',
@@ -37,7 +37,16 @@ export default {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
+ inject: ['fullPath'],
props: {
+ workItemIid: {
+ type: String,
+ required: true,
+ },
+ note: {
+ type: Object,
+ required: true,
+ },
showReply: {
type: Boolean,
required: true,
@@ -126,24 +135,29 @@ export default {
methods: {
async setAwardEmoji(name) {
+ const { mutation, mutationName, errorMessage } = getMutation({ note: this.note, name });
+
try {
- const {
- data: {
- awardEmojiAdd: { errors = [] },
- },
- } = await this.$apollo.mutate({
- mutation: addAwardEmojiMutation,
+ await this.$apollo.mutate({
+ mutation,
variables: {
- awardableId: this.noteId,
+ awardableId: this.note.id,
name,
},
+ optimisticResponse: {
+ [mutationName]: {
+ errors: [],
+ },
+ },
+ update: optimisticAwardUpdate({
+ note: this.note,
+ name,
+ fullPath: this.fullPath,
+ workItemIid: this.workItemIid,
+ }),
});
-
- if (errors.length > 0) {
- throw new Error(errors[0].message);
- }
} catch (error) {
- this.$emit('error', s__('WorkItem|Failed to award emoji'));
+ this.$emit('error', errorMessage);
Sentry.captureException(error);
}
},
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
new file mode 100644
index 00000000000..3c30c204ab6
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
@@ -0,0 +1,95 @@
+<script>
+import * as Sentry from '@sentry/browser';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import AwardsList from '~/vue_shared/components/awards_list.vue';
+import { getMutation, optimisticAwardUpdate } from '../../notes/award_utils';
+
+export default {
+ components: {
+ AwardsList,
+ },
+ inject: ['fullPath'],
+ props: {
+ workItemIid: {
+ type: String,
+ required: true,
+ },
+ note: {
+ type: Object,
+ required: true,
+ },
+ isModal: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ awardsListBoundary() {
+ return this.isModal ? '.modal-body' : '';
+ },
+ awards() {
+ return this.note.awardEmoji.nodes.map((award) => {
+ return {
+ ...award,
+ user: {
+ ...award.user,
+ id: getIdFromGraphQLId(award.user.id),
+ },
+ };
+ });
+ },
+ hasAwardEmojiPermission() {
+ return this.note.userPermissions.awardEmoji;
+ },
+ currentUserId() {
+ return window.gon.current_user_id;
+ },
+ },
+ methods: {
+ async handleAward(name) {
+ if (!this.hasAwardEmojiPermission) {
+ return;
+ }
+
+ const { mutation, mutationName, errorMessage } = getMutation({ note: this.note, name });
+
+ try {
+ await this.$apollo.mutate({
+ mutation,
+ variables: {
+ awardableId: this.note.id,
+ name,
+ },
+ optimisticResponse: {
+ [mutationName]: {
+ errors: [],
+ },
+ },
+ update: optimisticAwardUpdate({
+ note: this.note,
+ name,
+ fullPath: this.fullPath,
+ workItemIid: this.workItemIid,
+ }),
+ });
+ } catch (error) {
+ this.$emit('error', errorMessage);
+ Sentry.captureException(error);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <awards-list
+ v-if="awards.length"
+ :awards="awards"
+ :can-award-emoji="hasAwardEmojiPermission"
+ :current-user-id="currentUserId"
+ :boundary="awardsListBoundary"
+ class="gl-px-2"
+ @award="handleAward($event)"
+ />
+</template>
diff --git a/app/assets/javascripts/work_items/graphql/cache_utils.js b/app/assets/javascripts/work_items/graphql/cache_utils.js
index 03b45a45c39..14eedf5cdd8 100644
--- a/app/assets/javascripts/work_items/graphql/cache_utils.js
+++ b/app/assets/javascripts/work_items/graphql/cache_utils.js
@@ -87,6 +87,46 @@ export const updateCacheAfterDeletingNote = (currentNotes, subscriptionData) =>
});
};
+function updateNoteAwardEmojiCache(currentNotes, note, callback) {
+ if (!note.awardEmoji) {
+ return currentNotes;
+ }
+ const { awardEmoji } = note;
+
+ return produce(currentNotes, (draftData) => {
+ const notesWidget = getNotesWidgetFromSourceData(draftData);
+
+ if (!notesWidget.discussions) {
+ return;
+ }
+
+ notesWidget.discussions.nodes.forEach((discussion) => {
+ discussion.notes.nodes.forEach((n) => {
+ if (n.id === note.id) {
+ callback(n, awardEmoji);
+ }
+ });
+ });
+
+ updateNotesWidgetDataInDraftData(draftData, notesWidget);
+ });
+}
+
+export const updateCacheAfterAddingAwardEmojiToNote = (currentNotes, note) => {
+ return updateNoteAwardEmojiCache(currentNotes, note, (n, awardEmoji) => {
+ n.awardEmoji.nodes.push(awardEmoji);
+ });
+};
+
+export const updateCacheAfterRemovingAwardEmojiFromNote = (currentNotes, note) => {
+ return updateNoteAwardEmojiCache(currentNotes, note, (n, awardEmoji) => {
+ // eslint-disable-next-line no-param-reassign
+ n.awardEmoji.nodes = n.awardEmoji.nodes.filter((emoji) => {
+ return emoji.name !== awardEmoji.name || emoji.user.id !== awardEmoji.user.id;
+ });
+ });
+};
+
export const addHierarchyChild = (cache, fullPath, iid, workItem) => {
const queryArgs = { query: workItemByIidQuery, variables: { fullPath, iid } };
const sourceData = cache.readQuery(queryArgs);
diff --git a/app/assets/javascripts/work_items/graphql/notes/work_item_note.fragment.graphql b/app/assets/javascripts/work_items/graphql/notes/work_item_note.fragment.graphql
index c8b7d379074..6543e1a52f9 100644
--- a/app/assets/javascripts/work_items/graphql/notes/work_item_note.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/notes/work_item_note.fragment.graphql
@@ -1,4 +1,5 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/work_items/graphql/award_emoji.fragment.graphql"
fragment WorkItemNote on Note {
id
@@ -22,6 +23,11 @@ fragment WorkItemNote on Note {
author {
...User
}
+ awardEmoji {
+ nodes {
+ ...AwardEmojiFragment
+ }
+ }
userPermissions {
adminNote
awardEmoji
diff --git a/app/assets/javascripts/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql b/app/assets/javascripts/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql
index dc51c53428b..bc228c0dd3d 100644
--- a/app/assets/javascripts/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql
@@ -1,17 +1,5 @@
-#import "~/graphql_shared/fragments/user.fragment.graphql"
-
mutation workItemNoteAddAwardEmoji($awardableId: AwardableID!, $name: String!) {
awardEmojiAdd(input: { awardableId: $awardableId, name: $name }) {
- awardEmoji {
- name
- description
- unicode
- emoji
- unicodeVersion
- user {
- ...User
- }
- }
errors
}
}
diff --git a/app/assets/javascripts/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql b/app/assets/javascripts/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql
new file mode 100644
index 00000000000..22942fbb823
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql
@@ -0,0 +1,5 @@
+mutation workItemNoteRemoveAwardEmoji($awardableId: AwardableID!, $name: String!) {
+ awardEmojiRemove(input: { awardableId: $awardableId, name: $name }) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/work_items/notes/award_utils.js b/app/assets/javascripts/work_items/notes/award_utils.js
new file mode 100644
index 00000000000..5351a22d593
--- /dev/null
+++ b/app/assets/javascripts/work_items/notes/award_utils.js
@@ -0,0 +1,67 @@
+import { __ } from '~/locale';
+import { TYPENAME_USER } from '~/graphql_shared/constants';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import {
+ updateCacheAfterAddingAwardEmojiToNote,
+ updateCacheAfterRemovingAwardEmojiFromNote,
+} from '~/work_items/graphql/cache_utils';
+import workItemNotesByIidQuery from '../graphql/notes/work_item_notes_by_iid.query.graphql';
+import addAwardEmojiMutation from '../graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
+import removeAwardEmojiMutation from '../graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
+
+function awardedByCurrentUser(note) {
+ return (note.awardEmoji?.nodes ?? [])
+ .filter((award) => {
+ return getIdFromGraphQLId(award.user.id) === window.gon.current_user_id;
+ })
+ .map((award) => award.name);
+}
+
+export function getMutation({ note, name }) {
+ if (awardedByCurrentUser(note).includes(name)) {
+ return {
+ mutation: removeAwardEmojiMutation,
+ mutationName: 'awardEmojiRemove',
+ errorMessage: __('Failed to remove emoji. Please try again'),
+ };
+ }
+ return {
+ mutation: addAwardEmojiMutation,
+ mutationName: 'awardEmojiAdd',
+ errorMessage: __('Failed to add emoji. Please try again'),
+ };
+}
+
+export function optimisticAwardUpdate({ note, name, fullPath, workItemIid }) {
+ const { mutation } = getMutation({ note, name });
+
+ const currentUserId = window.gon.current_user_id;
+
+ return (store) => {
+ store.updateQuery(
+ {
+ query: workItemNotesByIidQuery,
+ variables: { fullPath, iid: workItemIid },
+ },
+ (sourceData) => {
+ const updatedNote = {
+ id: note.id,
+ awardEmoji: {
+ __typename: 'AwardEmoji',
+ name,
+ user: {
+ __typename: 'UserCore',
+ id: convertToGraphQLId(TYPENAME_USER, currentUserId),
+ name: null,
+ },
+ },
+ };
+
+ if (mutation === removeAwardEmojiMutation) {
+ return updateCacheAfterRemovingAwardEmojiFromNote(sourceData, updatedNote);
+ }
+ return updateCacheAfterAddingAwardEmojiToNote(sourceData, updatedNote);
+ },
+ );
+ };
+}
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 60f619a8d20..4d9cbe889fb 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -46,7 +46,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:mr_experience_survey, project)
push_frontend_feature_flag(:saved_replies, current_user)
push_frontend_feature_flag(:code_quality_inline_drawer, project)
- push_frontend_feature_flag(:auto_merge_labels_mr_widget, project)
push_force_frontend_feature_flag(:summarize_my_code_review, summarize_my_code_review_enabled?)
push_frontend_feature_flag(:mr_activity_filters, current_user)
push_frontend_feature_flag(:review_apps_redeploy_mr_widget, project)
diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb
index 22b73a85d5e..8aeca2eb137 100644
--- a/app/models/organizations/organization.rb
+++ b/app/models/organizations/organization.rb
@@ -13,6 +13,9 @@ module Organizations
has_one :settings, class_name: "OrganizationSetting"
+ has_many :organization_users, inverse_of: :organization
+ has_many :users, through: :organization_users, inverse_of: :organizations
+
validates :name,
presence: true,
length: { maximum: 255 }
diff --git a/app/models/organizations/organization_user.rb b/app/models/organizations/organization_user.rb
new file mode 100644
index 00000000000..5aa1133b017
--- /dev/null
+++ b/app/models/organizations/organization_user.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Organizations
+ class OrganizationUser < ApplicationRecord
+ belongs_to :organization, inverse_of: :organization_users, optional: false
+ belongs_to :user, inverse_of: :organization_users, optional: false
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 57904f95372..4560eb1f9c7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -256,6 +256,9 @@ class User < ApplicationRecord
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
+ has_many :organization_users, class_name: 'Organizations::OrganizationUser', inverse_of: :user
+ has_many :organizations, through: :organization_users, class_name: 'Organizations::Organization', inverse_of: :users
+
has_many :metrics_users_starred_dashboards, class_name: 'Metrics::UsersStarredDashboard', inverse_of: :user
has_one :status, class_name: 'UserStatus'
diff --git a/app/views/shared/_new_merge_request_checkbox.html.haml b/app/views/shared/_new_merge_request_checkbox.html.haml
index fb3dfba2691..b84efd2d577 100644
--- a/app/views/shared/_new_merge_request_checkbox.html.haml
+++ b/app/views/shared/_new_merge_request_checkbox.html.haml
@@ -2,7 +2,8 @@
- nonce = SecureRandom.hex
= render Pajamas::CheckboxTagComponent.new(name: 'create_merge_request',
checked: true,
- checkbox_options: { class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" }) do |c|
+ checkbox_options: { class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" },
+ label_options: { for: "create_merge_request-#{nonce}" }) do |c|
- c.with_label do
- translation_variables = { new_merge_request: "<strong>#{_('new merge request')}</strong>" }
- translation = _('Start a %{new_merge_request} with these changes') % translation_variables
diff --git a/config/feature_flags/development/auto_merge_labels_mr_widget.yml b/config/feature_flags/development/auto_merge_labels_mr_widget.yml
deleted file mode 100644
index ea689058ee2..00000000000
--- a/config/feature_flags/development/auto_merge_labels_mr_widget.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: auto_merge_labels_mr_widget
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115436
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/409530
-milestone: '15.11'
-type: development
-group: group::pipeline execution
-default_enabled: true
diff --git a/data/whats_new/202306220001_16_1.yml b/data/whats_new/202306220001_16_1.yml
new file mode 100644
index 00000000000..cd14713143a
--- /dev/null
+++ b/data/whats_new/202306220001_16_1.yml
@@ -0,0 +1,98 @@
+- name: All new navigation experience
+ description: | # Do not modify this line, instead modify the lines below.
+ GitLab 16.1 features an all-new navigation experience! We've defaulted this experience to on for all users. To get started, go to your avatar in the top right of the UI and turn on the **New navigation** toggle.
+
+ The new navigation was designed to solve three key areas of feedback: navigating GitLab can be overwhelming, it can be hard to pick up where you left off, and you can't customize the navigation.
+
+ The new navigation includes a streamlined and improved left sidebar, where you can:
+
+ * Pin 📌 frequently accessed items.
+ * Completely hide the sidebar and "peek" it back into view.
+ * Easily switch contexts, search, and view subsets of data with the new **Your Work** and **Explore** options.
+ * Scan more quickly because of fewer top-level menu items.
+
+ We are proud of the new navigation and can't wait to see what you think. Review a [list of what's changed](https://gitlab.com/groups/gitlab-org/-/epics/9044#whats-different) and read our blog posts about the navigation [vision](https://about.gitlab.com/blog/2023/05/01/gitlab-product-navigation/) and [design](https://about.gitlab.com/blog/2023/05/15/overhauling-the-navigation-is-like-building-a-dream-home/).
+
+ Please try the new navigation and let us know about your experience in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/409005). We are already [addressing](https://gitlab.com/gitlab-org/gitlab/-/issues/409005#actions-we-are-taking-from-the-feedback) the feedback and will eventually remove the toggle.
+ stage: Manage
+ self-managed: true
+ gitlab-com: true
+ available_in: [Free, Premium, Ultimate]
+ documentation_link: https://docs.gitlab.com/ee/tutorials/left_sidebar/index.html
+ image_url: https://img.youtube.com/vi/rGTl9_HIpbY/hqdefault.jpg
+ published_at: 2023-06-22
+ release: 16.1
+
+- name: Visualize Kubernetes resources in GitLab
+ description: | # Do not modify this line, instead modify the lines below.
+ How do you check the status of the applications running in your clusters? The pipeline status and environment pages provide insights about the latest deployment runs. However, previous versions of GitLab lacked insights about the state of your deployments. In GitLab 16.1, you can see an overview of the primary resources in your Kubernetes deployments.
+
+ This feature works with every connected Kubernetes cluster. It doesn't matter if you deploy your workloads with the CI/CD integration or GitOps. To further improve the feature for Flux users, support for showing the synchronization status of an environment is proposed in [issue 391581](https://gitlab.com/gitlab-org/gitlab/-/issues/391581). There are many use cases for which a non-human user might need to authenticate. Previously, depending on the desired scope, users could use personal, project, or group access tokens to meet this need. These tokens were not ideal, due to still being either tied to a human (for personal access tokens), or an unnecessarily privileged role (for group and project access tokens).
+ stage: Deploy
+ self-managed: true
+ gitlab-com: true
+ available_in: [Free, Premium, Ultimate]
+ documentation_link: https://docs.gitlab.com/ee/ci/environments/kubernetes_dashboard.html
+ image_url: https://about.gitlab.com/images/16_1/whats-new-k8s-visualization.png
+ published_at: 2023-06-22
+ release: 16.1
+
+- name: Authenticate with service accounts
+ description: | # Do not modify this line, instead modify the lines below.
+ There are many use cases for which a non-human user might need to authenticate. Previously, depending on the desired scope, users could use personal, project, or group access tokens to meet this need. These tokens were not ideal, due to still being either tied to a human (for personal access tokens), or an unnecessarily privileged role (for group and project access tokens).
+
+ Service accounts are not tied to a human user, and are more granular in scope. Service account creation and management is API-only. Support for a UI option is proposed in [issue 9965](https://gitlab.com/groups/gitlab-org/-/epics/9965).
+ stage: Manage
+ self-managed: true
+ gitlab-com: true
+ available_in: [Premium, Ultimate]
+ documentation_link: https://docs.gitlab.com/ee/api/groups.html#service-accounts
+ image_url: https://img.youtube.com/vi/oZvjg0SCsqY/hqdefault.jpg
+ published_at: 2023-06-22
+ release: 16.1
+
+- name: GitLab Dedicated is now generally available
+ description: | # Do not modify this line, instead modify the lines below.
+ GitLab Dedicated is a fully managed, single-tenant SaaS deployment of our comprehensive DevSecOps platform designed to address the needs of customers with stringent compliance requirements.
+
+ Customers in highly-regulated industries are unable to adopt multi-tenant SaaS offerings due to strict compliance requirements like data isolation. With GitLab Dedicated, organizations can access all of the benefits of the DevSecOps platform – including faster releases, better security, and more productive developers – while satisfying compliance requirements such as data residency, isolation, and private networking.
+
+ [Learn more](https://about.gitlab.com/dedicated/) about GitLab Dedicated today.
+ stage: Platforms
+ self-managed: true
+ gitlab-com: false
+ available_in: [Ultimate]
+ documentation_link: https://docs.gitlab.com/ee/subscriptions/gitlab_dedicated/
+ image_url: https://about.gitlab.com/images/16_1/gitlab-dedicated.png
+ published_at: 2023-06-22
+ release: 16.1
+
+- name: Manage job artifacts though the Artifacts page
+ description: | # Do not modify this line, instead modify the lines below.
+ Previously, if you wanted to view or manage job artifacts, you had to go to each job's detail page, or use the API. Now, you can view and manage job artifacts through the **Artifacts** page accessed at **Build > Artifacts**.
+
+ Users with at least the Maintainer role can use this new interface to delete artifacts too. You can delete individual artifacts, or bulk delete up to 100 artifacts at a time through either manual selection or checking the **Select all** option at the top of the page.
+
+ Please use the survey at the top of the Artifacts page to share any feedback you have about this new functionality. To view additional UI features under consideration, you can check out the [Build Artifacts page enhancements epic](https://gitlab.com/groups/gitlab-org/-/epics/8311).
+ stage: Verify
+ self-managed: true
+ gitlab-com: true
+ available_in: [Free, Premium, Ultimate]
+ documentation_link: https://docs.gitlab.com/ee/ci/jobs/job_artifacts.html#view-all-job-artifacts-in-a-project
+ image_url: https://about.gitlab.com/images/16_1/artifacts-bulk-delete.png
+ published_at: 2023-06-22
+ release: 16.1
+
+- name: Improved CI/CD variables list view
+ description: | # Do not modify this line, instead modify the lines below.
+ CI/CD variables are a key part of all pipelines and can be defined in multiple places, including in the project and group settings. To prepare for making bigger improvements that will help users intuitively navigate between variables at different hierarchy, we are starting out with improving the usability and layout of the variable list.
+
+ In GitLab 16.1, you will see the first iteration of these improvements. We have merged the "Type" and "Options" columns into a new **Attributes** column, which better represents these related attributes. We appreciate your feedback on how we can continue to improve the CI/CD variable experience, you are welcome to comment in our [variables improvement epic](https://gitlab.com/groups/gitlab-org/-/epics/10506).
+ stage: Verify
+ self-managed: true
+ gitlab-com: true
+ available_in: [Free, Premium, Ultimate]
+ documentation_link: https://docs.gitlab.com/ee/ci/variables/#define-a-cicd-variable-in-the-ui
+ image_url: https://about.gitlab.com/images/16_1/layout_changes.png
+ published_at: 2023-06-22
+ release: 16.1
diff --git a/db/docs/organization_users.yml b/db/docs/organization_users.yml
new file mode 100644
index 00000000000..6a1ccb7210b
--- /dev/null
+++ b/db/docs/organization_users.yml
@@ -0,0 +1,10 @@
+---
+table_name: organization_users
+classes:
+- Organizations::OrganizationUser
+feature_categories:
+- cell
+description: User who has joined an organization
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123804
+milestone: '16.2'
+gitlab_schema: gitlab_main_clusterwide
diff --git a/db/migrate/20230608113106_create_organization_users.rb b/db/migrate/20230608113106_create_organization_users.rb
new file mode 100644
index 00000000000..305765bc9df
--- /dev/null
+++ b/db/migrate/20230608113106_create_organization_users.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class CreateOrganizationUsers < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def up
+ create_table :organization_users do |t|
+ t.bigint :organization_id,
+ null: false
+ t.bigint :user_id,
+ null: false,
+ index: true
+ t.timestamps_with_timezone null: false
+ t.index 'organization_id, user_id',
+ name: 'index_organization_users_on_organization_id_and_user_id', unique: true
+ end
+ end
+
+ def down
+ drop_table :organization_users
+ end
+end
diff --git a/db/migrate/20230615104902_add_user_id_foreign_key_to_organization_users.rb b/db/migrate/20230615104902_add_user_id_foreign_key_to_organization_users.rb
new file mode 100644
index 00000000000..0f8d18ee384
--- /dev/null
+++ b/db/migrate/20230615104902_add_user_id_foreign_key_to_organization_users.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddUserIdForeignKeyToOrganizationUsers < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :organization_users, :users, column: :user_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :organization_users, column: :user_id
+ end
+ end
+end
diff --git a/db/migrate/20230620104217_add_organization_id_foreign_key_to_organization_users.rb b/db/migrate/20230620104217_add_organization_id_foreign_key_to_organization_users.rb
new file mode 100644
index 00000000000..0a9757d2e14
--- /dev/null
+++ b/db/migrate/20230620104217_add_organization_id_foreign_key_to_organization_users.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddOrganizationIdForeignKeyToOrganizationUsers < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :organization_users, :organizations, column: :organization_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :organization_users, column: :organization_id
+ end
+ end
+end
diff --git a/db/schema_migrations/20230608113106 b/db/schema_migrations/20230608113106
new file mode 100644
index 00000000000..82a65911642
--- /dev/null
+++ b/db/schema_migrations/20230608113106
@@ -0,0 +1 @@
+137039d31d0e8b4abce8260f067783334538787c3f22c638d5c65c8b7016aad7 \ No newline at end of file
diff --git a/db/schema_migrations/20230615104902 b/db/schema_migrations/20230615104902
new file mode 100644
index 00000000000..9d52b635ff0
--- /dev/null
+++ b/db/schema_migrations/20230615104902
@@ -0,0 +1 @@
+f96102edb32531bda4828af85709f7510f730617aea6c80859da697595c96fcc \ No newline at end of file
diff --git a/db/schema_migrations/20230620104217 b/db/schema_migrations/20230620104217
new file mode 100644
index 00000000000..f07e758bdbd
--- /dev/null
+++ b/db/schema_migrations/20230620104217
@@ -0,0 +1 @@
+cd2692308d23e12450ff96bf7a0cde9866b8d920babc1187e9e4b50fa5b2d43a \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 98bd746f945..8e201d47796 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -19243,6 +19243,23 @@ CREATE TABLE organization_settings (
settings jsonb DEFAULT '{}'::jsonb NOT NULL
);
+CREATE TABLE organization_users (
+ id bigint NOT NULL,
+ organization_id bigint NOT NULL,
+ user_id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL
+);
+
+CREATE SEQUENCE organization_users_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE organization_users_id_seq OWNED BY organization_users.id;
+
CREATE TABLE organizations (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -25539,6 +25556,8 @@ ALTER TABLE ONLY operations_strategies_user_lists ALTER COLUMN id SET DEFAULT ne
ALTER TABLE ONLY operations_user_lists ALTER COLUMN id SET DEFAULT nextval('operations_user_lists_id_seq'::regclass);
+ALTER TABLE ONLY organization_users ALTER COLUMN id SET DEFAULT nextval('organization_users_id_seq'::regclass);
+
ALTER TABLE ONLY organizations ALTER COLUMN id SET DEFAULT nextval('organizations_id_seq'::regclass);
ALTER TABLE ONLY p_ci_builds ALTER COLUMN id SET DEFAULT nextval('ci_builds_id_seq'::regclass);
@@ -27799,6 +27818,9 @@ ALTER TABLE ONLY operations_user_lists
ALTER TABLE ONLY organization_settings
ADD CONSTRAINT organization_settings_pkey PRIMARY KEY (organization_id);
+ALTER TABLE ONLY organization_users
+ ADD CONSTRAINT organization_users_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY organizations
ADD CONSTRAINT organizations_pkey PRIMARY KEY (id);
@@ -32043,6 +32065,10 @@ CREATE UNIQUE INDEX index_ops_feature_flags_issues_on_feature_flag_id_and_issue_
CREATE UNIQUE INDEX index_ops_strategies_user_lists_on_strategy_id_and_user_list_id ON operations_strategies_user_lists USING btree (strategy_id, user_list_id);
+CREATE UNIQUE INDEX index_organization_users_on_organization_id_and_user_id ON organization_users USING btree (organization_id, user_id);
+
+CREATE INDEX index_organization_users_on_user_id ON organization_users USING btree (user_id);
+
CREATE UNIQUE INDEX index_organizations_on_unique_name_per_group ON customer_relations_organizations USING btree (group_id, lower(name), id);
CREATE UNIQUE INDEX index_p_ci_job_annotations_on_partition_id_job_id_name ON ONLY p_ci_job_annotations USING btree (partition_id, job_id, name);
@@ -35640,6 +35666,9 @@ ALTER TABLE ONLY import_export_uploads
ALTER TABLE ONLY push_rules
ADD CONSTRAINT fk_83b29894de FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY organization_users
+ ADD CONSTRAINT fk_8471abad75 FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY merge_request_diffs
ADD CONSTRAINT fk_8483f3258f FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
@@ -35682,6 +35711,9 @@ ALTER TABLE ONLY protected_branch_merge_access_levels
ALTER TABLE ONLY bulk_import_exports
ADD CONSTRAINT fk_8c6f33cebe FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+ALTER TABLE ONLY organization_users
+ ADD CONSTRAINT fk_8d9b20725d FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY releases
ADD CONSTRAINT fk_8e4456f90f FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL;
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 574bce82793..525cfaaff3e 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -776,13 +776,12 @@ Example response:
}
```
-## Reset instance's runner registration token (deprecated)
+## Reset instance's runner registration token
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30942) in GitLab 14.3.
-> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104691) in GitLab 15.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30942) in GitLab 14.3.
WARNING:
-This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/383341) in GitLab 15.7 and is planned for removal in 17.0. This change is a breaking change.
+Runner registration tokens, and support for certain configuration arguments, were [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/380872) in GitLab 15.6 and will be removed in GitLab 17.0. After GitLab 17.0, you will no longer be able to reset runner registration tokens and the `reset_registration_token` endpoint will not function.
Reset the runner registration token for the GitLab instance.
@@ -795,13 +794,12 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/runners/reset_registration_token"
```
-## Reset project's runner registration token (deprecated)
+## Reset project's runner registration token
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30942) in GitLab 14.3.
-> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104691) in GitLab 15.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30942) in GitLab 14.3.
WARNING:
-This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/383341) in GitLab 15.7 and is planned for removal in 17.0. This change is a breaking change.
+Runner registration tokens, and support for certain configuration arguments, were [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/380872) in GitLab 15.6 and will be removed in GitLab 17.0. After GitLab 17.0, you will no longer be able to reset runner registration tokens and the `reset_registration_token` endpoint will not function.
Reset the runner registration token for a project.
@@ -814,13 +812,12 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/9/runners/reset_registration_token"
```
-## Reset group's runner registration token (deprecated)
+## Reset group's runner registration token
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30942) in GitLab 14.3.
-> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104691) in GitLab 15.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30942) in GitLab 14.3.
WARNING:
-This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/383341) in GitLab 15.7 and is planned for removal in 17.0. This change is a breaking change.
+Runner registration tokens, and support for certain configuration arguments, were [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/380872) in GitLab 15.6 and will be removed in GitLab 17.0. After GitLab 17.0, you will no longer be able to reset runner registration tokens and the `reset_registration_token` endpoint will not function.
Reset the runner registration token for a group.
diff --git a/doc/development/database/adding_database_indexes.md b/doc/development/database/adding_database_indexes.md
index 23a12413975..a41f4bef147 100644
--- a/doc/development/database/adding_database_indexes.md
+++ b/doc/development/database/adding_database_indexes.md
@@ -471,7 +471,8 @@ This migration enters the index name and definition into the `postgres_async_ind
table. The process that runs on weekends pulls indexes from this table and attempt
to remove them.
-You must test the database index changes locally before creating a merge request.
+You must [test the database index changes locally](#verify-indexes-removed-asynchronously) before creating a merge request.
+Include the output of the test in the merge request description.
### Verify the MR was deployed and the index no longer exists in production
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 1a43084245e..8230f38ad8e 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -353,10 +353,11 @@ return new Vue({
#### Accessing feature flags
-Use the [`provide` and `inject`](https://v2.vuejs.org/v2/api/#provide-inject) mechanisms
-in Vue to make feature flags available to any descendant components in a Vue
-application. The `glFeatures` object is already provided in `commons/vue.js`, so
-only the mixin is required to use the flags:
+After pushing a feature flag to the [frontend](../feature_flags/index.md#frontend),
+use the [`provide` and `inject`](https://v2.vuejs.org/v2/api/#provide-inject)
+mechanisms in Vue to make feature flags available to any descendant components
+in a Vue application. The `glFeatures` object is already provided in
+`commons/vue.js`, so only the mixin is required to use the flags:
```javascript
// An arbitrary descendant component
diff --git a/doc/development/sec/gemnasium_analyzer_data.md b/doc/development/sec/gemnasium_analyzer_data.md
new file mode 100644
index 00000000000..2da787a277a
--- /dev/null
+++ b/doc/development/sec/gemnasium_analyzer_data.md
@@ -0,0 +1,33 @@
+---
+stage: Secure
+group: Composition Analysis
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Gemnasium analyzer data
+
+The following table lists the data available for the Gemnasium analyzer.
+
+| Property \ Tool | Gemnasium |
+|:----------------------------------------------|:-----------------------:|
+| Severity | **{check-circle}** Yes |
+| Title | **{check-circle}** Yes |
+| File | **{check-circle}** Yes |
+| Start line | **{dotted-circle}** No |
+| End line | **{dotted-circle}** No |
+| External ID (for example, CVE) | **{check-circle}** Yes |
+| URLs | **{check-circle}** Yes |
+| Internal doc/explanation | **{check-circle}** Yes |
+| Solution | **{check-circle}** Yes |
+| Confidence | **{dotted-circle}** No |
+| Affected item (for example, class or package) | **{check-circle}** Yes |
+| Source code extract | **{dotted-circle}** No |
+| Internal ID | **{check-circle}** Yes |
+| Date | **{check-circle}** Yes |
+| Credits | **{check-circle}** Yes |
+
+- **{check-circle}** Yes => we have that data
+- **{dotted-circle}** No => we don't have that data, or it would need to develop specific or inefficient/unreliable logic to obtain it.
+
+The values provided by these tools are heterogeneous, so they are sometimes normalized into common
+values (for example, `severity`, `confidence`, etc).
diff --git a/doc/user/application_security/dependency_list/index.md b/doc/user/application_security/dependency_list/index.md
index afed5b3b0ca..cba87164f12 100644
--- a/doc/user/application_security/dependency_list/index.md
+++ b/doc/user/application_security/dependency_list/index.md
@@ -89,4 +89,4 @@ You can download your project's list of dependencies and their details in JSON f
### Using the API
-You can download your project's list of dependencies [using the API](../../../api/dependencies.md#list-project-dependencies). Note this only provides the dependencies identified by the Gemnasium family of analyzers and [not any other of the GitLab dependency analyzers](../dependency_scanning/analyzers.md).
+You can download your project's list of dependencies [using the API](../../../api/dependencies.md#list-project-dependencies). Note this only provides the dependencies identified by the [Gemnasium family of analyzers](../dependency_scanning/index.md#dependency-analyzers) and not any other of the GitLab dependency analyzers.
diff --git a/doc/user/application_security/dependency_scanning/analyzers.md b/doc/user/application_security/dependency_scanning/analyzers.md
index 4feac0cb5e6..0d7d495ba49 100644
--- a/doc/user/application_security/dependency_scanning/analyzers.md
+++ b/doc/user/application_security/dependency_scanning/analyzers.md
@@ -1,124 +1,11 @@
---
-type: reference, howto
-stage: Secure
-group: Composition Analysis
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+redirect_to: 'index.md'
+remove_date: '2023-09-22'
---
-# Dependency Scanning Analyzers **(ULTIMATE)**
+This document was moved to [another location](index.md).
-Dependency Scanning supports the following official analyzers:
-
-- [`gemnasium`](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium)
-- [`gemnasium-maven`](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven)
-- [`gemnasium-python`](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python)
-
-The analyzers are published as Docker images, which Dependency Scanning uses
-to launch dedicated containers for each analysis.
-
-Dependency Scanning is pre-configured with a set of **default images** that are
-maintained by GitLab, but users can also integrate their own **custom images**.
-
-## Official default analyzers
-
-Any custom change to the official analyzers can be achieved by using a
-[CI/CD variable in your `.gitlab-ci.yml`](index.md#customizing-the-dependency-scanning-settings).
-
-### Using a custom Docker mirror
-
-You can switch to a custom Docker registry that provides the official analyzer
-images under a different prefix. For instance, the following instructs Dependency
-Scanning to pull `my-docker-registry/gl-images/gemnasium`
-instead of `registry.gitlab.com/security-products/gemnasium`.
-In `.gitlab-ci.yml` define:
-
-```yaml
-include:
- template: Security/Dependency-Scanning.gitlab-ci.yml
-
-variables:
- SECURE_ANALYZERS_PREFIX: my-docker-registry/gl-images
-```
-
-This configuration requires that your custom registry provides images for all
-the official analyzers.
-
-### Disable specific analyzers
-
-You can select the official analyzers you don't want to run. Here's how to disable
-the `gemnasium` analyzer.
-In `.gitlab-ci.yml` define:
-
-```yaml
-include:
- template: Security/Dependency-Scanning.gitlab-ci.yml
-
-variables:
- DS_EXCLUDED_ANALYZERS: "gemnasium"
-```
-
-### Disabling default analyzers
-
-Setting `DS_EXCLUDED_ANALYZERS` to a list of the official analyzers disables them.
-In `.gitlab-ci.yml` define:
-
-```yaml
-include:
- template: Security/Dependency-Scanning.gitlab-ci.yml
-
-variables:
- DS_EXCLUDED_ANALYZERS: "gemnasium, gemnasium-maven, gemnasium-python"
-```
-
-This is used when one totally relies on [custom analyzers](#custom-analyzers).
-
-## Custom analyzers
-
-You can provide your own analyzers by
-defining CI jobs in your CI configuration. For consistency, you should suffix your custom Dependency
-Scanning jobs with `-dependency_scanning`. Here's how to add a scanning job that's based on the
-Docker image `my-docker-registry/analyzers/nuget` and generates a Dependency Scanning report
-`gl-dependency-scanning-report.json` when `/analyzer run` is executed. Define the following in
-`.gitlab-ci.yml`:
-
-```yaml
-nuget-dependency_scanning:
- image:
- name: "my-docker-registry/analyzers/nuget"
- script:
- - /analyzer run
- artifacts:
- reports:
- dependency_scanning: gl-dependency-scanning-report.json
-```
-
-The [Security Scanner Integration](../../../development/integrations/secure.md) documentation explains how to integrate custom security scanners into GitLab.
-
-## Analyzers data
-
-The following table lists the data available for the Gemnasium analyzer.
-
-| Property \ Tool | Gemnasium |
-|---------------------------------------|:------------------:|
-| Severity | ✓ |
-| Title | ✓ |
-| File | ✓ |
-| Start line | 𐄂 |
-| End line | 𐄂 |
-| External ID (for example, CVE) | ✓ |
-| URLs | ✓ |
-| Internal doc/explanation | ✓ |
-| Solution | ✓ |
-| Confidence | 𐄂 |
-| Affected item (for example, class or package) | ✓ |
-| Source code extract | 𐄂 |
-| Internal ID | ✓ |
-| Date | ✓ |
-| Credits | ✓ |
-
-- ✓ => we have that data
-- ⚠ => we have that data, but it's partially reliable, or we need to extract that data from unstructured content
-- 𐄂 => we don't have that data, or it would need to develop specific or inefficient/unreliable logic to obtain it.
-
-The values provided by these tools are heterogeneous, so they are sometimes
-normalized into common values (for example, `severity`, `confidence`, etc).
+<!-- This redirect file can be deleted after <2023-09-22>. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html --> \ No newline at end of file
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index db4f4a7e51c..1179985d43b 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -63,21 +63,6 @@ If you need it, explain why by filling out [the survey](https://docs.google.com/
## Supported languages and package managers
-Dependency Scanning automatically detects the languages used in the repository. All analyzers
-matching the detected languages are run. There is usually no need to customize the selection of
-analyzers. We recommend not specifying the analyzers so you automatically use the full selection
-for best coverage, avoiding the need to make adjustments when there are deprecations or removals.
-However, you can override the selection using the variable `DS_EXCLUDED_ANALYZERS`.
-
-The language detection relies on CI job [`rules`](../../../ci/yaml/index.md#rules) and searches a
-maximum of two directory levels from the repository's root. For example, the
-`gemnasium-dependency_scanning` job is enabled if a repository contains either `Gemfile`,
-`api/Gemfile`, or `api/client/Gemfile`, but not if the only supported dependency file is `api/v1/client/Gemfile`.
-
-For Java and Python, when a supported dependency file is detected, Dependency Scanning attempts to build the project and execute some Java or Python commands to get the list of dependencies. For all other projects, the lock file is parsed to obtain the list of dependencies without needing to build the project first.
-
-When a supported dependency file is detected, all dependencies, including transitive dependencies are analyzed. There is no limit to the depth of nested or transitive dependencies that are analyzed.
-
The following languages and dependency managers are supported:
<style>
@@ -303,6 +288,40 @@ table.supported-languages ul {
</ol>
<!-- markdownlint-enable MD044 -->
+## Dependency detection
+
+Dependency Scanning automatically detects the languages used in the repository. All analyzers
+matching the detected languages are run. There is usually no need to customize the selection of
+analyzers. We recommend not specifying the analyzers so you automatically use the full selection for
+best coverage, avoiding the need to make adjustments when there are deprecations or removals.
+However, you can override the selection using the variable `DS_EXCLUDED_ANALYZERS`.
+
+The language detection relies on CI job [`rules`](../../../ci/yaml/index.md#rules) and searches a
+maximum of two directory levels from the repository's root. For example, the
+`gemnasium-dependency_scanning` job is enabled if a repository contains either `Gemfile`,
+`api/Gemfile`, or `api/client/Gemfile`, but not if the only supported dependency file is
+`api/v1/client/Gemfile`.
+
+For Java and Python, when a supported dependency file is detected, Dependency Scanning attempts to
+build the project and execute some Java or Python commands to get the list of dependencies. For all
+other projects, the lock file is parsed to obtain the list of dependencies without needing to build
+the project first.
+
+When a supported dependency file is detected, all dependencies, including transitive dependencies
+are analyzed. There is no limit to the depth of nested or transitive dependencies that are analyzed.
+
+### Dependency analyzers
+
+Dependency Scanning supports the following official analyzers:
+
+- [`gemnasium`](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium)
+- [`gemnasium-maven`](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven)
+- [`gemnasium-python`](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python)
+
+The analyzers are published as Docker images, which Dependency Scanning uses
+to launch dedicated containers for each analysis. You can also integrate a custom
+[security scanner](../../../development/integrations/secure.md).
+
### How analyzers obtain dependency information
GitLab analyzers obtain dependency information using one of the following two methods:
@@ -611,12 +630,12 @@ The following variables allow configuration of global dependency scanning settin
| CI/CD variables | Description |
| ----------------------------|------------ |
| `ADDITIONAL_CA_CERT_BUNDLE` | Bundle of CA certs to trust. The bundle of certificates provided here is also used by other tools during the scanning process, such as `git`, `yarn`, or `npm`. See [Using a custom SSL CA certificate authority](#using-a-custom-ssl-ca-certificate-authority) for more details. |
-| `DS_EXCLUDED_ANALYZERS` | Specify the analyzers (by name) to exclude from Dependency Scanning. For more information, see [Dependency Scanning Analyzers](analyzers.md). |
+| `DS_EXCLUDED_ANALYZERS` | Specify the analyzers (by name) to exclude from Dependency Scanning. For more information, see [Dependency Scanning Analyzers](#dependency-analyzers). |
| `DS_EXCLUDED_PATHS` | Exclude files and directories from the scan based on the paths. A comma-separated list of patterns. Patterns can be globs (see [`doublestar.Match`](https://pkg.go.dev/github.com/bmatcuk/doublestar/v4@v4.0.2#Match) for supported patterns), or file or folder paths (for example, `doc,spec`). Parent directories also match patterns. Default: `"spec, test, tests, tmp"`. |
| `DS_IMAGE_SUFFIX` | Suffix added to the image name. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354796) in GitLab 14.10.) Automatically set to `"-fips"` when FIPS mode is enabled. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/357922) in GitLab 15.0.) |
| `DS_MAX_DEPTH` | Defines how many directory levels deep that the analyzer should search for supported files to scan. A value of `-1` scans all directories regardless of depth. Default: `2`. |
-| `SECURE_ANALYZERS_PREFIX` | Override the name of the Docker registry providing the official default images (proxy). Read more about [customizing analyzers](analyzers.md). |
-| `SECURE_LOG_LEVEL` | Set the minimum logging level. Messages of this logging level or higher are output. From highest to lowest severity, the logging levels are: `fatal`, `error`, `warn`, `info`, `debug`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10880) in GitLab 13.1. Default: `info`. |
+| `SECURE_ANALYZERS_PREFIX` | Override the name of the Docker registry providing the official default images (proxy). |
+| `SECURE_LOG_LEVEL` | Set the minimum logging level. Messages of this logging level or higher are output. From highest to lowest severity, the logging levels are: `fatal`, `error`, `warn`, `info` (default), `debug`. |
#### Configuring specific analyzers used by dependency scanning
diff --git a/doc/user/project/repository/code_suggestions.md b/doc/user/project/repository/code_suggestions.md
index 33ef10c9c3d..f3b10db8a64 100644
--- a/doc/user/project/repository/code_suggestions.md
+++ b/doc/user/project/repository/code_suggestions.md
@@ -77,7 +77,6 @@ Prerequisites:
- Code Suggestions must be [enabled for the top-level group](../../group/manage.md#enable-code-suggestions).
- Code Suggestions must be [enabled for your user account](#enable-code-suggestions-for-an-individual-user).
-- You must be a GitLab team member.
Code Suggestions work automatically in the GitLab WebIDE if the prerequisites are met.
@@ -182,7 +181,6 @@ Prerequisites:
- Code Suggestions must be [enabled for the top-level group](../../group/manage.md#enable-code-suggestions).
- Code Suggestions must be [enabled for your user account](#enable-code-suggestions-for-an-individual-user).
-- You are a GitLab team member.
You should check that AI assisted code suggestions in the
[GitLab WebIDE](../../project/web_ide/index.md) are enabled:
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ee98ce52a0c..1b80e836115 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -18672,6 +18672,9 @@ msgstr ""
msgid "Failed to add a resource link"
msgstr ""
+msgid "Failed to add emoji. Please try again"
+msgstr ""
+
msgid "Failed to apply commands."
msgstr ""
@@ -18854,6 +18857,9 @@ msgstr ""
msgid "Failed to remove a to-do item for the design."
msgstr ""
+msgid "Failed to remove emoji. Please try again"
+msgstr ""
+
msgid "Failed to remove mirror."
msgstr ""
@@ -44088,9 +44094,6 @@ msgstr ""
msgid "Start merge train"
msgstr ""
-msgid "Start merge train when pipeline succeeds"
-msgstr ""
-
msgid "Start merge train..."
msgstr ""
@@ -51938,9 +51941,6 @@ msgstr ""
msgid "WorkItem|Existing task"
msgstr ""
-msgid "WorkItem|Failed to award emoji"
-msgstr ""
-
msgid "WorkItem|Health status"
msgstr ""
@@ -54806,9 +54806,6 @@ msgstr ""
msgid "mrWidget|%{rules} invalid rules have been approved automatically"
msgstr ""
-msgid "mrWidget|A merge train is a queued list of merge requests waiting to be merged into the target branch. The changes in each merge request are combined with the changes in earlier merge requests and tested before merge."
-msgstr ""
-
msgid "mrWidget|A new merge train has started and this merge request is the first of the queue."
msgstr ""
@@ -54930,9 +54927,6 @@ msgstr ""
msgid "mrWidget|Merged by"
msgstr ""
-msgid "mrWidget|More information"
-msgstr ""
-
msgid "mrWidget|Please restore it or use a different %{type} branch."
msgstr ""
@@ -55005,9 +54999,6 @@ msgstr ""
msgid "mrWidget|To change this default message, edit the template for squash commit messages. %{linkStart}Learn more%{linkEnd}."
msgstr ""
-msgid "mrWidget|What is a merge train?"
-msgstr ""
-
msgid "mrWidget|Your merge request is almost ready!"
msgstr ""
diff --git a/spec/factories/organizations/organization_users.rb b/spec/factories/organizations/organization_users.rb
new file mode 100644
index 00000000000..761f260ccb3
--- /dev/null
+++ b/spec/factories/organizations/organization_users.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :organization_user, class: 'Organizations::OrganizationUser' do
+ user
+ organization
+ end
+end
diff --git a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
index 19b5ad0fa84..a4c03dc4e73 100644
--- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
@@ -13,22 +13,6 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
context 'project does not have CI enabled' do
it 'allows MR to be merged' do
- stub_feature_flags(auto_merge_labels_mr_widget: false)
-
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- page.within('.mr-state-widget') do
- expect(page).to have_button 'Merge'
- end
- end
- end
-
- context 'project does not have CI enabled and auto_merge_labels_mr_widget on' do
- it 'allows MR to be merged' do
- stub_feature_flags(auto_merge_labels_mr_widget: true)
-
visit project_merge_request_path(project, merge_request)
wait_for_requests
@@ -51,79 +35,6 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
context 'when merge requests can only be merged if the pipeline succeeds' do
before do
project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
-
- stub_feature_flags(auto_merge_labels_mr_widget: false)
- end
-
- context 'when CI is running' do
- let(:status) { :running }
-
- it 'does not allow to merge immediately' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- expect(page).to have_button 'Merge when pipeline succeeds'
- expect(page).not_to have_button '.js-merge-moment'
- end
- end
-
- context 'when CI failed' do
- let(:status) { :failed }
-
- it 'does not allow MR to be merged' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- expect(page).not_to have_button('Merge', exact: true)
- expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.')
- end
- end
-
- context 'when CI canceled' do
- let(:status) { :canceled }
-
- it 'does not allow MR to be merged' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- expect(page).not_to have_button('Merge', exact: true)
- expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.')
- end
- end
-
- context 'when CI succeeded' do
- let(:status) { :success }
-
- it 'allows MR to be merged' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- expect(page).to have_button('Merge', exact: true)
- end
- end
-
- context 'when CI skipped' do
- let(:status) { :skipped }
-
- it 'does not allow MR to be merged' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- expect(page).not_to have_button('Merge', exact: true)
- end
- end
- end
-
- context 'when merge requests can only be merged if the pipeline succeeds with auto_merge_labels_mr_widget on' do
- before do
- project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
-
- stub_feature_flags(auto_merge_labels_mr_widget: true)
end
context 'when CI is running' do
@@ -193,58 +104,6 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
context 'when merge requests can be merged when the build failed' do
before do
project.update_attribute(:only_allow_merge_if_pipeline_succeeds, false)
-
- stub_feature_flags(auto_merge_labels_mr_widget: false)
- end
-
- context 'when CI is running' do
- let(:status) { :running }
-
- it 'allows MR to be merged immediately' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- expect(page).to have_button 'Merge when pipeline succeeds'
-
- page.find('.js-merge-moment').click
- expect(page).to have_content 'Merge immediately'
- end
- end
-
- context 'when CI failed' do
- let(:status) { :failed }
-
- it 'allows MR to be merged' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
- page.within('.mr-state-widget') do
- expect(page).to have_button 'Merge'
- end
- end
- end
-
- context 'when CI succeeded' do
- let(:status) { :success }
-
- it 'allows MR to be merged' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- page.within('.mr-state-widget') do
- expect(page).to have_button 'Merge'
- end
- end
- end
- end
-
- context 'when merge requests can be merged when the build failed with auto_merge_labels_mr_widget on' do
- before do
- project.update_attribute(:only_allow_merge_if_pipeline_succeeds, false)
-
- stub_feature_flags(auto_merge_labels_mr_widget: true)
end
context 'when CI is running' do
diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
index e42e4735ee2..9a8384bfc9f 100644
--- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
@@ -26,83 +26,6 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js, featur
context 'when there is active pipeline for merge request' do
before do
create(:ci_build, pipeline: pipeline)
- stub_feature_flags(auto_merge_labels_mr_widget: false)
-
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- end
-
- describe 'enabling Merge when pipeline succeeds' do
- shared_examples 'Merge when pipeline succeeds activator' do
- it 'activates the Merge when pipeline succeeds feature', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410105' do
- click_button "Merge when pipeline succeeds"
-
- expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds"
- expect(page).to have_content "Source branch will not be deleted"
- expect(page).to have_selector ".js-cancel-auto-merge"
- visit project_merge_request_path(project, merge_request) # Needed to refresh the page
- expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i
- end
- end
-
- context "when enabled immediately" do
- it_behaves_like 'Merge when pipeline succeeds activator'
- end
-
- context 'when enabled after pipeline status changed', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/258667' do
- before do
- pipeline.run!
-
- # We depend on merge request widget being reloaded
- # so we have to wait for asynchronous call to reload it
- # and have_content expectation handles that.
- #
- expect(page).to have_content "Pipeline ##{pipeline.id} running"
- end
-
- it_behaves_like 'Merge when pipeline succeeds activator'
- end
-
- context 'when enabled after it was previously canceled' do
- before do
- click_button "Merge when pipeline succeeds"
-
- wait_for_requests
-
- click_button "Cancel auto-merge"
-
- wait_for_requests
-
- expect(page).to have_content 'Merge when pipeline succeeds'
- end
-
- it_behaves_like 'Merge when pipeline succeeds activator'
- end
-
- context 'when it was enabled and then canceled' do
- let(:merge_request) do
- create(:merge_request_with_diffs,
- :merge_when_pipeline_succeeds,
- source_project: project,
- title: 'Bug NS-04',
- author: user,
- merge_user: user)
- end
-
- before do
- merge_request.merge_params['force_remove_source_branch'] = '0'
- merge_request.save!
- click_button "Cancel auto-merge"
- end
-
- it_behaves_like 'Merge when pipeline succeeds activator'
- end
- end
- end
-
- context 'when there is active pipeline for merge request with auto_merge_labels_mr_widget on' do
- before do
- create(:ci_build, pipeline: pipeline)
stub_feature_flags(auto_merge_labels_mr_widget: true)
sign_in(user)
@@ -166,86 +89,6 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js, featur
context 'when merge when pipeline succeeds is enabled' do
let(:merge_request) do
create(:merge_request_with_diffs, :simple, :merge_when_pipeline_succeeds,
- source_project: project,
- author: user,
- merge_user: user,
- title: 'MepMep')
- end
-
- let!(:build) do
- create(:ci_build, pipeline: pipeline)
- end
-
- before do
- stub_feature_flags(auto_merge_labels_mr_widget: false)
- sign_in user
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'allows to cancel the automatic merge', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/412416' do
- click_button "Cancel auto-merge"
-
- expect(page).to have_button "Merge when pipeline succeeds"
-
- refresh
-
- expect(page).to have_content "canceled the automatic merge"
- end
-
- context 'when pipeline succeeds' do
- before do
- build.success
- refresh
- end
-
- it 'merges merge request', :sidekiq_might_not_need_inline do
- expect(page).to have_content 'Changes merged'
- expect(merge_request.reload).to be_merged
- end
- end
-
- context 'view merge request with MWPS enabled but automatically merge fails' do
- before do
- merge_request.update!(
- merge_user: merge_request.author,
- merge_error: 'Something went wrong'
- )
- refresh
- end
-
- it 'shows information about the merge error' do
- # Wait for the `ci_status` and `merge_check` requests
- wait_for_requests
-
- page.within('.mr-state-widget') do
- expect(page).to have_content('Something went wrong. Try again.')
- end
- end
- end
-
- context 'view merge request with MWPS enabled but automatically merge fails' do
- before do
- merge_request.update!(
- merge_user: merge_request.author,
- merge_error: 'Something went wrong.'
- )
- refresh
- end
-
- it 'shows information about the merge error' do
- # Wait for the `ci_status` and `merge_check` requests
- wait_for_requests
-
- page.within('.mr-state-widget') do
- expect(page).to have_content('Something went wrong. Try again.')
- end
- end
- end
- end
-
- context 'when merge when pipeline succeeds is enabled and auto_merge_labels_mr_widget on' do
- let(:merge_request) do
- create(:merge_request_with_diffs, :simple, :merge_when_pipeline_succeeds,
source_project: project,
author: user,
merge_user: user,
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index fba25b41b83..4ea36286768 100644
--- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -58,8 +58,6 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
end
before do
- stub_feature_flags(auto_merge_labels_mr_widget: false)
-
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
@@ -146,53 +144,8 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
end
end
- context 'when a user merges a merge request in the parent project', :sidekiq_might_not_need_inline do
- before do
- click_link 'Overview'
- click_button 'Merge when pipeline succeeds'
-
- wait_for_requests
- end
-
- context 'when detached merge request pipeline is pending' do
- it 'waits the head pipeline' do
- expect(page).to have_content('to be merged automatically when the pipeline succeeds')
- expect(page).to have_button('Cancel auto-merge')
- end
- end
-
- context 'when detached merge request pipeline succeeds' do
- before do
- detached_merge_request_pipeline.reload.succeed!
-
- wait_for_requests
- end
-
- it 'merges the merge request' do
- expect(page).to have_content('Merged by')
- expect(page).to have_button('Revert')
- end
- end
-
- context 'when branch pipeline succeeds' do
- before do
- click_link 'Overview'
- push_pipeline.reload.succeed!
-
- wait_for_requests
- end
-
- it 'waits the head pipeline' do
- expect(page).to have_content('to be merged automatically when the pipeline succeeds')
- expect(page).to have_button('Cancel auto-merge')
- end
- end
- end
-
- context 'when a user created a merge request in the parent project with auto_merge_labels_mr_widget on' do
+ context 'when a user created a merge request in the parent project' do
before do
- stub_feature_flags(auto_merge_labels_mr_widget: true)
-
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
@@ -408,10 +361,10 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
project.update!(only_allow_merge_if_pipeline_succeeds: true)
end
- it 'shows MWPS button' do
+ it 'shows Set to auto-merge button' do
visit project_merge_request_path(project, merge_request)
- expect(page).to have_button('Merge when pipeline succeeds')
+ expect(page).to have_button('Set to auto-merge')
end
end
end
@@ -421,7 +374,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
before do
click_link("Overview")
- click_button 'Merge when pipeline succeeds'
+ click_button 'Set to auto-merge'
wait_for_requests
end
diff --git a/spec/frontend/admin/topics/components/topic_select_spec.js b/spec/frontend/admin/topics/components/topic_select_spec.js
index 113a0e3d404..5b7e6365606 100644
--- a/spec/frontend/admin/topics/components/topic_select_spec.js
+++ b/spec/frontend/admin/topics/components/topic_select_spec.js
@@ -58,10 +58,6 @@ describe('TopicSelect', () => {
});
}
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it('mounts', () => {
createComponent();
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
index 521bbf06b02..de4db23bae2 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
@@ -239,8 +239,6 @@ describe('Batch comments store actions', () => {
params = { note: { id: 1 }, noteText: 'test' };
});
- afterEach(() => jest.clearAllMocks());
-
it('commits RECEIVE_DRAFT_UPDATE_SUCCESS with returned data', () => {
return actions.updateDraft(context, { ...params, callback() {} }).then(() => {
expect(commit).toHaveBeenCalledWith('RECEIVE_DRAFT_UPDATE_SUCCESS', { id: 1 });
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index 9ab20fc2cd7..1bdc54723ce 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -61,7 +61,6 @@ describe('Blob Editing', () => {
});
afterEach(() => {
mock.restore();
- jest.clearAllMocks();
unuseMock.mockClear();
useMock.mockClear();
resetHTMLFixture();
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index a925f752f5e..47e6a305447 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -111,7 +111,6 @@ describe('Board card component', () => {
afterEach(() => {
store = null;
- jest.clearAllMocks();
});
it('renders issue title', () => {
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index b1e14be8ceb..affe1260c66 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -90,10 +90,6 @@ describe('BoardSettingsSidebar', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findRemoveButton = () => wrapper.findComponent(GlButton);
- afterEach(() => {
- jest.restoreAllMocks();
- });
-
it('finds a MountingPortal component', () => {
createComponent();
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js
index fb27e160897..5bce0ca3746 100644
--- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js
@@ -1,20 +1,17 @@
-import events from 'test_fixtures/controller/users/activity.json';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContributionEventApproved from '~/contribution_events/components/contribution_event/contribution_event_approved.vue';
import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
-import TargetLink from '~/contribution_events/components/target_link.vue';
-import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
import { eventApproved } from '../../utils';
const defaultPropsData = {
- event: eventApproved(events),
+ event: eventApproved(),
};
describe('ContributionEventApproved', () => {
let wrapper;
const createComponent = () => {
- wrapper = mountExtended(ContributionEventApproved, {
+ wrapper = shallowMountExtended(ContributionEventApproved, {
propsData: defaultPropsData,
});
};
@@ -28,22 +25,7 @@ describe('ContributionEventApproved', () => {
event: defaultPropsData.event,
iconName: 'approval-solid',
iconClass: 'gl-text-green-500',
+ message: ContributionEventApproved.i18n.message,
});
});
-
- it('renders message', () => {
- expect(wrapper.findByTestId('event-body').text()).toBe(
- `Approved merge request ${defaultPropsData.event.target.reference_link_text} in ${defaultPropsData.event.resource_parent.full_name}.`,
- );
- });
-
- it('renders target link', () => {
- expect(wrapper.findComponent(TargetLink).props('event')).toEqual(defaultPropsData.event);
- });
-
- it('renders resource parent link', () => {
- expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual(
- defaultPropsData.event,
- );
- });
});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
index 8c951e20bed..310966243d1 100644
--- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
@@ -1,23 +1,27 @@
import { GlAvatarLabeled, GlAvatarLink, GlIcon } from '@gitlab/ui';
-import events from 'test_fixtures/controller/users/activity.json';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-
-const [event] = events;
+import TargetLink from '~/contribution_events/components/target_link.vue';
+import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
+import { eventApproved } from '../../utils';
describe('ContributionEventBase', () => {
let wrapper;
const defaultPropsData = {
- event,
+ event: eventApproved(),
iconName: 'approval-solid',
iconClass: 'gl-text-green-500',
+ message: 'Approved merge request %{targetLink} in %{resourceParentLink}.',
};
- const createComponent = () => {
- wrapper = shallowMountExtended(ContributionEventBase, {
- propsData: defaultPropsData,
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(ContributionEventBase, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
scopedSlots: {
default: '<div data-testid="default-slot"></div>',
'additional-info': '<div data-testid="additional-info-slot"></div>',
@@ -25,38 +29,75 @@ describe('ContributionEventBase', () => {
});
};
- beforeEach(() => {
+ it('renders avatar', () => {
createComponent();
- });
- it('renders avatar', () => {
const avatarLink = wrapper.findComponent(GlAvatarLink);
+ const avatarLabeled = avatarLink.findComponent(GlAvatarLabeled);
- expect(avatarLink.attributes('href')).toBe(event.author.web_url);
- expect(avatarLink.findComponent(GlAvatarLabeled).attributes()).toMatchObject({
- label: event.author.name,
- sublabel: `@${event.author.username}`,
- src: event.author.avatar_url,
+ expect(avatarLink.attributes('href')).toBe(defaultPropsData.event.author.web_url);
+ expect(avatarLabeled.attributes()).toMatchObject({
+ src: defaultPropsData.event.author.avatar_url,
size: '32',
});
+ expect(avatarLabeled.props()).toMatchObject({
+ label: defaultPropsData.event.author.name,
+ subLabel: `@${defaultPropsData.event.author.username}`,
+ });
});
it('renders time ago tooltip', () => {
- expect(wrapper.findComponent(TimeAgoTooltip).props('time')).toBe(event.created_at);
+ createComponent();
+
+ expect(wrapper.findComponent(TimeAgoTooltip).props('time')).toBe(
+ defaultPropsData.event.created_at,
+ );
});
it('renders icon', () => {
+ createComponent();
+
const icon = wrapper.findComponent(GlIcon);
expect(icon.props('name')).toBe(defaultPropsData.iconName);
expect(icon.classes()).toContain(defaultPropsData.iconClass);
});
- it('renders `default` slot', () => {
- expect(wrapper.findByTestId('default-slot').exists()).toBe(true);
+ describe('when `message` prop is passed', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders message', () => {
+ expect(wrapper.findByTestId('event-body').text()).toBe(
+ `Approved merge request ${defaultPropsData.event.target.reference_link_text} in ${defaultPropsData.event.resource_parent.full_name}.`,
+ );
+ });
+
+ it('renders target link', () => {
+ expect(wrapper.findComponent(TargetLink).props('event')).toEqual(defaultPropsData.event);
+ });
+
+ it('renders resource parent link', () => {
+ expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual(
+ defaultPropsData.event,
+ );
+ });
+ });
+
+ describe('when `message` prop is not passed', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { message: '' } });
+ });
+
+ it('renders `default` slot', () => {
+ expect(wrapper.findByTestId('default-slot').exists()).toBe(true);
+ });
});
it('renders `additional-info` slot', () => {
+ createComponent();
+
expect(wrapper.findByTestId('additional-info-slot').exists()).toBe(true);
});
});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js
index 90cb7161952..c58fca1ad12 100644
--- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js
@@ -1,19 +1,17 @@
-import events from 'test_fixtures/controller/users/activity.json';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContributionEventExpired from '~/contribution_events/components/contribution_event/contribution_event_expired.vue';
import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
-import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
import { eventExpired } from '../../utils';
const defaultPropsData = {
- event: eventExpired(events),
+ event: eventExpired(),
};
describe('ContributionEventExpired', () => {
let wrapper;
const createComponent = () => {
- wrapper = mountExtended(ContributionEventExpired, {
+ wrapper = shallowMountExtended(ContributionEventExpired, {
propsData: defaultPropsData,
});
};
@@ -26,18 +24,7 @@ describe('ContributionEventExpired', () => {
expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
event: defaultPropsData.event,
iconName: 'expire',
+ message: ContributionEventExpired.i18n.message,
});
});
-
- it('renders message', () => {
- expect(wrapper.findByTestId('event-body').text()).toBe(
- `Removed due to membership expiration from ${defaultPropsData.event.resource_parent.full_name}.`,
- );
- });
-
- it('renders resource parent link', () => {
- expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual(
- defaultPropsData.event,
- );
- });
});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js
index 511972e35dd..56688e2ef27 100644
--- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js
@@ -1,19 +1,17 @@
-import events from 'test_fixtures/controller/users/activity.json';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContributionEventJoined from '~/contribution_events/components/contribution_event/contribution_event_joined.vue';
import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
-import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
import { eventJoined } from '../../utils';
const defaultPropsData = {
- event: eventJoined(events),
+ event: eventJoined(),
};
describe('ContributionEventJoined', () => {
let wrapper;
const createComponent = () => {
- wrapper = mountExtended(ContributionEventJoined, {
+ wrapper = shallowMountExtended(ContributionEventJoined, {
propsData: defaultPropsData,
});
};
@@ -26,18 +24,7 @@ describe('ContributionEventJoined', () => {
expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
event: defaultPropsData.event,
iconName: 'users',
+ message: ContributionEventJoined.i18n.message,
});
});
-
- it('renders message', () => {
- expect(wrapper.findByTestId('event-body').text()).toBe(
- `Joined project ${defaultPropsData.event.resource_parent.full_name}.`,
- );
- });
-
- it('renders resource parent link', () => {
- expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual(
- defaultPropsData.event,
- );
- });
});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js
index 2e82addcda2..58cb8714d03 100644
--- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js
@@ -1,19 +1,17 @@
-import events from 'test_fixtures/controller/users/activity.json';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContributionEventLeft from '~/contribution_events/components/contribution_event/contribution_event_left.vue';
import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
-import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
import { eventLeft } from '../../utils';
const defaultPropsData = {
- event: eventLeft(events),
+ event: eventLeft(),
};
describe('ContributionEventLeft', () => {
let wrapper;
const createComponent = () => {
- wrapper = mountExtended(ContributionEventLeft, {
+ wrapper = shallowMountExtended(ContributionEventLeft, {
propsData: defaultPropsData,
});
};
@@ -26,18 +24,7 @@ describe('ContributionEventLeft', () => {
expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
event: defaultPropsData.event,
iconName: 'leave',
+ message: ContributionEventLeft.i18n.message,
});
});
-
- it('renders message', () => {
- expect(wrapper.findByTestId('event-body').text()).toBe(
- `Left project ${defaultPropsData.event.resource_parent.full_name}.`,
- );
- });
-
- it('renders resource parent link', () => {
- expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual(
- defaultPropsData.event,
- );
- });
});
diff --git a/spec/frontend/contribution_events/components/contribution_events_spec.js b/spec/frontend/contribution_events/components/contribution_events_spec.js
index dcea9bacb78..064799d4a82 100644
--- a/spec/frontend/contribution_events/components/contribution_events_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_events_spec.js
@@ -20,10 +20,10 @@ describe('ContributionEvents', () => {
it.each`
expectedComponent | expectedEvent
- ${ContributionEventApproved} | ${eventApproved(events)}
- ${ContributionEventExpired} | ${eventExpired(events)}
- ${ContributionEventJoined} | ${eventJoined(events)}
- ${ContributionEventLeft} | ${eventLeft(events)}
+ ${ContributionEventApproved} | ${eventApproved()}
+ ${ContributionEventExpired} | ${eventExpired()}
+ ${ContributionEventJoined} | ${eventJoined()}
+ ${ContributionEventLeft} | ${eventLeft()}
`(
'renders `$expectedComponent.name` component and passes expected event',
({ expectedComponent, expectedEvent }) => {
diff --git a/spec/frontend/contribution_events/components/resource_parent_link_spec.js b/spec/frontend/contribution_events/components/resource_parent_link_spec.js
index 8d586db2a30..815a1b751cf 100644
--- a/spec/frontend/contribution_events/components/resource_parent_link_spec.js
+++ b/spec/frontend/contribution_events/components/resource_parent_link_spec.js
@@ -1,30 +1,52 @@
import { GlLink } from '@gitlab/ui';
-import events from 'test_fixtures/controller/users/activity.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { EVENT_TYPE_APPROVED } from '~/contribution_events/constants';
import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
-
-const eventApproved = events.find((event) => event.action === EVENT_TYPE_APPROVED);
+import { EVENT_TYPE_PRIVATE } from '~/contribution_events/constants';
+import { eventApproved } from '../utils';
describe('ResourceParentLink', () => {
let wrapper;
- const createComponent = () => {
+ const defaultPropsData = {
+ event: eventApproved(),
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
wrapper = shallowMountExtended(ResourceParentLink, {
propsData: {
- event: eventApproved,
+ ...defaultPropsData,
+ ...propsData,
},
});
};
- beforeEach(() => {
- createComponent();
+ describe('when resource parent is defined', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders link', () => {
+ const link = wrapper.findComponent(GlLink);
+ const { web_url, full_name } = defaultPropsData.event.resource_parent;
+
+ expect(link.attributes('href')).toBe(web_url);
+ expect(link.text()).toBe(full_name);
+ });
});
- it('renders link', () => {
- const link = wrapper.findComponent(GlLink);
+ describe('when resource parent is not defined', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ event: {
+ type: EVENT_TYPE_PRIVATE,
+ },
+ },
+ });
+ });
- expect(link.attributes('href')).toBe(eventApproved.resource_parent.web_url);
- expect(link.text()).toBe(eventApproved.resource_parent.full_name);
+ it('renders nothing', () => {
+ expect(wrapper.html()).toBe('');
+ });
});
});
diff --git a/spec/frontend/contribution_events/components/target_link_spec.js b/spec/frontend/contribution_events/components/target_link_spec.js
index 7944375487b..b71d6eff432 100644
--- a/spec/frontend/contribution_events/components/target_link_spec.js
+++ b/spec/frontend/contribution_events/components/target_link_spec.js
@@ -1,33 +1,48 @@
import { GlLink } from '@gitlab/ui';
-import events from 'test_fixtures/controller/users/activity.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { EVENT_TYPE_APPROVED } from '~/contribution_events/constants';
import TargetLink from '~/contribution_events/components/target_link.vue';
-
-const eventApproved = events.find((event) => event.action === EVENT_TYPE_APPROVED);
+import { eventApproved, eventJoined } from '../utils';
describe('TargetLink', () => {
let wrapper;
- const createComponent = () => {
+ const defaultPropsData = {
+ event: eventApproved(),
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
wrapper = shallowMountExtended(TargetLink, {
propsData: {
- event: eventApproved,
+ ...defaultPropsData,
+ ...propsData,
},
});
};
- beforeEach(() => {
- createComponent();
+ describe('when target is defined', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders link', () => {
+ const link = wrapper.findComponent(GlLink);
+ const { web_url: webUrl, title, reference_link_text } = defaultPropsData.event.target;
+
+ expect(link.attributes()).toMatchObject({
+ href: webUrl,
+ title,
+ });
+ expect(link.text()).toBe(reference_link_text);
+ });
});
- it('renders link', () => {
- const link = wrapper.findComponent(GlLink);
+ describe('when target is not defined', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { event: eventJoined() } });
+ });
- expect(link.attributes()).toMatchObject({
- href: eventApproved.target.web_url,
- title: eventApproved.target.title,
+ it('renders nothing', () => {
+ expect(wrapper.html()).toBe('');
});
- expect(link.text()).toBe(eventApproved.target.reference_link_text);
});
});
diff --git a/spec/frontend/contribution_events/utils.js b/spec/frontend/contribution_events/utils.js
index 0887a178e5c..736479fce04 100644
--- a/spec/frontend/contribution_events/utils.js
+++ b/spec/frontend/contribution_events/utils.js
@@ -1,3 +1,4 @@
+import events from 'test_fixtures/controller/users/activity.json';
import {
EVENT_TYPE_APPROVED,
EVENT_TYPE_EXPIRED,
@@ -5,11 +6,12 @@ import {
EVENT_TYPE_LEFT,
} from '~/contribution_events/constants';
-export const eventApproved = (events) =>
- events.find((event) => event.action === EVENT_TYPE_APPROVED);
+const findEventByAction = (action) => events.find((event) => event.action === action);
-export const eventExpired = (events) => events.find((event) => event.action === EVENT_TYPE_EXPIRED);
+export const eventApproved = () => findEventByAction(EVENT_TYPE_APPROVED);
-export const eventJoined = (events) => events.find((event) => event.action === EVENT_TYPE_JOINED);
+export const eventExpired = () => findEventByAction(EVENT_TYPE_EXPIRED);
-export const eventLeft = (events) => events.find((event) => event.action === EVENT_TYPE_LEFT);
+export const eventJoined = () => findEventByAction(EVENT_TYPE_JOINED);
+
+export const eventLeft = () => findEventByAction(EVENT_TYPE_LEFT);
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index fdcea6d88c0..e64dec14461 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -220,10 +220,6 @@ describe('Design management design presentation component', () => {
);
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it('sets overlay position correctly when overlay is smaller than viewport', () => {
jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200);
jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200);
diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js
index 698535d8937..2262e5fdd83 100644
--- a/spec/frontend/design_management/components/design_todo_button_spec.js
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -50,10 +50,6 @@ describe('Design management design todo button', () => {
createComponent();
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it('renders TodoButton component', () => {
expect(wrapper.findComponent(TodoButton).exists()).toBe(true);
});
diff --git a/spec/frontend/drawio/drawio_editor_spec.js b/spec/frontend/drawio/drawio_editor_spec.js
index 4d93908b757..5a77b9d4689 100644
--- a/spec/frontend/drawio/drawio_editor_spec.js
+++ b/spec/frontend/drawio/drawio_editor_spec.js
@@ -66,7 +66,6 @@ describe('drawio/drawio_editor', () => {
});
afterEach(() => {
- jest.clearAllMocks();
findDrawioIframe()?.remove();
});
diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js
index 70bc1dee0ee..c820d6ac63d 100644
--- a/spec/frontend/editor/source_editor_extension_base_spec.js
+++ b/spec/frontend/editor/source_editor_extension_base_spec.js
@@ -56,7 +56,6 @@ describe('The basis for an Source Editor extension', () => {
});
afterEach(() => {
- jest.clearAllMocks();
resetHTMLFixture();
});
diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
index 512b298bbbd..d9e1a22d60d 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -182,10 +182,6 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
instance.togglePreview();
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it('does not do anything if there is no model', () => {
instance.setModel(null);
@@ -199,9 +195,6 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
mockAxios.onPost().reply(HTTP_STATUS_OK, { body: responseData });
await togglePreview();
});
- afterEach(() => {
- jest.clearAllMocks();
- });
it('removes the registered buttons from the toolbar', () => {
expect(instance.toolbar.removeItems).not.toHaveBeenCalled();
diff --git a/spec/frontend/editor/source_editor_yaml_ext_spec.js b/spec/frontend/editor/source_editor_yaml_ext_spec.js
index 14ec7f8b93f..4b1ed0fbb42 100644
--- a/spec/frontend/editor/source_editor_yaml_ext_spec.js
+++ b/spec/frontend/editor/source_editor_yaml_ext_spec.js
@@ -368,10 +368,6 @@ abc: def
let highlightLinesSpy;
let removeHighlightsSpy;
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it.each`
highlightPathOnSetup | path | keepOnNotFound | expectHighlightLinesToBeCalled | withLines | expectRemoveHighlightsToBeCalled | storedHighlightPath
${null} | ${undefined} | ${false} | ${false} | ${undefined} | ${true} | ${null}
diff --git a/spec/frontend/header_search/init_spec.js b/spec/frontend/header_search/init_spec.js
index baf3c6f08b2..459ca33ee66 100644
--- a/spec/frontend/header_search/init_spec.js
+++ b/spec/frontend/header_search/init_spec.js
@@ -5,7 +5,6 @@ import initHeaderSearch, { eventHandler, cleanEventListeners } from '~/header_se
describe('Header Search EventListener', () => {
beforeEach(() => {
jest.resetModules();
- jest.restoreAllMocks();
setHTMLFixture(`
<div class="js-header-content">
<div class="header-search-form" id="js-header-search" data-autocomplete-path="/search/autocomplete" data-issues-path="/dashboard/issues" data-mr-path="/dashboard/merge_requests" data-search-context="{}" data-search-path="/search">
@@ -16,7 +15,6 @@ describe('Header Search EventListener', () => {
afterEach(() => {
resetHTMLFixture();
- jest.clearAllMocks();
});
it('attached event listener', () => {
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 6747ec97050..aa99b1cacef 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -158,7 +158,6 @@ describe('RepoEditor', () => {
});
afterEach(() => {
- jest.clearAllMocks();
// create a new model each time, otherwise tests conflict with each other
// because of same model being used in multiple tests
monacoEditor.getModels().forEach((model) => model.dispose());
diff --git a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
index 0a887efee4b..f4f4936a134 100644
--- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
@@ -137,7 +137,6 @@ describe('ProjectDropdown', () => {
describe('when searching branches', () => {
it('triggers a refetch', async () => {
createComponent({ mountFn: mount });
- jest.clearAllMocks();
const mockSearchTerm = 'gitl';
await findDropdown().vm.$emit('search', mockSearchTerm);
diff --git a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
index a3bc8e861b2..cf2dacb50d8 100644
--- a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
@@ -104,7 +104,6 @@ describe('SourceBranchDropdown', () => {
it('triggers a refetch', async () => {
createComponent({ mountFn: mount, props: { selectedProject: mockSelectedProject } });
await waitForPromises();
- jest.clearAllMocks();
const mockSearchTerm = 'mai';
await findListbox().vm.$emit('search', mockSearchTerm);
diff --git a/spec/frontend/lib/utils/downloader_spec.js b/spec/frontend/lib/utils/downloader_spec.js
index c14cba3a62b..a95b46d1440 100644
--- a/spec/frontend/lib/utils/downloader_spec.js
+++ b/spec/frontend/lib/utils/downloader_spec.js
@@ -8,10 +8,6 @@ describe('Downloader', () => {
jest.spyOn(document, 'createElement').mockImplementation(() => a);
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
describe('when inline file content is provided', () => {
const fileData = 'inline content';
const fileName = 'test.csv';
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index b6a2b318ec3..bef8ed8e659 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -74,7 +74,6 @@ describe('Discussion navigation mixin', () => {
});
afterEach(() => {
- jest.clearAllMocks();
resetHTMLFixture();
});
diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js
index 7095525e948..bb9a4b85e0e 100644
--- a/spec/frontend/pipeline_wizard/components/commit_spec.js
+++ b/spec/frontend/pipeline_wizard/components/commit_spec.js
@@ -141,10 +141,6 @@ describe('Pipeline Wizard - Commit Page', () => {
it('emits a done event', () => {
expect(wrapper.emitted().done.length).toBe(1);
});
-
- afterEach(() => {
- jest.clearAllMocks();
- });
});
describe('failed commit', () => {
@@ -167,10 +163,6 @@ describe('Pipeline Wizard - Commit Page', () => {
it('will not emit a done event', () => {
expect(wrapper.emitted().done?.length).toBeUndefined();
});
-
- afterEach(() => {
- jest.clearAllMocks();
- });
});
});
diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
index 50f754393fe..b4ffd2658fe 100644
--- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
@@ -80,7 +80,6 @@ describe('Links Inner component', () => {
};
afterEach(() => {
- jest.restoreAllMocks();
resetHTMLFixture();
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
index 077995ab6e4..76d45692a63 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
@@ -91,7 +91,6 @@ describe('View branch rules', () => {
expect(findBranchName().text()).toBe(I18N.allBranches);
expect(findBranchTitle().text()).toBe(I18N.targetBranch);
- jest.restoreAllMocks();
});
it('renders the correct branch title', () => {
diff --git a/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
index 472a89e9b21..4385db43a4a 100644
--- a/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
@@ -23,7 +23,6 @@ describe('Todo Button', () => {
afterEach(() => {
dispatchEventSpy = null;
- jest.clearAllMocks();
});
it('renders GlButton', () => {
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index f2003aee96e..9c12088216b 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -25,8 +25,6 @@ describe('Sidebar mediator', () => {
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
-
- jest.clearAllMocks();
});
it('assigns yourself', () => {
diff --git a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
index 047dc9a6599..abd9c1dc44d 100644
--- a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
+++ b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
@@ -9,6 +9,7 @@ import SidebarPeek, {
STATE_OPEN,
STATE_WILL_CLOSE,
} from '~/super_sidebar/components/sidebar_peek_behavior.vue';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
// These are measured at runtime in the browser, but statically defined here
// since Jest does not do layout/styling.
@@ -32,6 +33,7 @@ jest.mock('~/lib/utils/css_utils', () => ({
describe('SidebarPeek component', () => {
let wrapper;
+ let trackingSpy = null;
const createComponent = () => {
wrapper = mount(SidebarPeek);
@@ -54,6 +56,11 @@ describe('SidebarPeek component', () => {
beforeEach(() => {
createComponent();
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
});
it('begins in the closed state', () => {
@@ -87,6 +94,11 @@ describe('SidebarPeek component', () => {
jest.advanceTimersByTime(1);
expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_OPEN]);
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_peek', {
+ label: 'nav_hover',
+ property: 'nav_sidebar',
+ });
});
it('cancels transition will-open -> open if mouse out of peek region', () => {
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index b76c637caf4..0c785109b5e 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -19,6 +19,7 @@ import {
isCollapsed,
} from '~/super_sidebar/super_sidebar_collapsed_state_manager';
import { stubComponent } from 'helpers/stub_component';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { sidebarData as mockSidebarData } from '../mock_data';
const initialSidebarState = { ...sidebarState };
@@ -49,6 +50,7 @@ describe('SuperSidebar component', () => {
const findTrialStatusWidget = () => wrapper.findByTestId(trialStatusWidgetStubTestId);
const findTrialStatusPopover = () => wrapper.findByTestId(trialStatusPopoverStubTestId);
const findSidebarMenu = () => wrapper.findComponent(SidebarMenu);
+ let trackingSpy = null;
const createWrapper = ({
provide = {},
@@ -77,6 +79,11 @@ describe('SuperSidebar component', () => {
beforeEach(() => {
Object.assign(sidebarState, initialSidebarState);
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
});
describe('default', () => {
@@ -143,12 +150,20 @@ describe('SuperSidebar component', () => {
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(1);
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(true, true);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', {
+ label: 'nav_toggle_keyboard_shortcut',
+ property: 'nav_sidebar',
+ });
isCollapsed.mockReturnValue(true);
Mousetrap.trigger('mod+\\');
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(2);
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_show', {
+ label: 'nav_toggle_keyboard_shortcut',
+ property: 'nav_sidebar',
+ });
jest.spyOn(Mousetrap, 'unbind');
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
index 8bb20186e16..23b735c2773 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
@@ -7,6 +7,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { JS_TOGGLE_COLLAPSE_CLASS, JS_TOGGLE_EXPAND_CLASS } from '~/super_sidebar/constants';
import SuperSidebarToggle from '~/super_sidebar/components/super_sidebar_toggle.vue';
import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager.js', () => ({
toggleSuperSidebarCollapsed: jest.fn(),
@@ -61,7 +62,7 @@ describe('SuperSidebarToggle component', () => {
});
});
- describe('toolip', () => {
+ describe('tooltip', () => {
it('displays collapse when expanded', () => {
createWrapper();
expect(getTooltip().title).toBe(__('Hide sidebar'));
@@ -74,15 +75,19 @@ describe('SuperSidebarToggle component', () => {
});
describe('toggle', () => {
+ let trackingSpy = null;
+
beforeEach(() => {
setHTMLFixture(`
<button class="${JS_TOGGLE_COLLAPSE_CLASS}">Hide</button>
<button class="${JS_TOGGLE_EXPAND_CLASS}">Show</button>
`);
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
afterEach(() => {
resetHTMLFixture();
+ unmockTracking();
});
it('collapses the sidebar and focuses the other toggle', async () => {
@@ -93,6 +98,10 @@ describe('SuperSidebarToggle component', () => {
expect(document.activeElement).toEqual(
document.querySelector(`.${JS_TOGGLE_COLLAPSE_CLASS}`),
);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', {
+ label: 'nav_toggle',
+ property: 'nav_sidebar',
+ });
});
it('expands the sidebar and focuses the other toggle', async () => {
@@ -101,6 +110,10 @@ describe('SuperSidebarToggle component', () => {
await nextTick();
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true);
expect(document.activeElement).toEqual(document.querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`));
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_show', {
+ label: 'nav_toggle',
+ property: 'nav_sidebar',
+ });
});
});
});
diff --git a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
index 771d1f07fea..9388d837186 100644
--- a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
+++ b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
@@ -11,8 +11,10 @@ import {
findPage,
bindSuperSidebarCollapsedEvents,
} from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
const { xl, sm } = breakpoints;
+let trackingSpy = null;
jest.mock('~/lib/utils/common_utils', () => ({
getCookie: jest.fn(),
@@ -27,6 +29,15 @@ const pageHasCollapsedClass = (hasClass) => {
}
};
+const tracksCollapse = (shouldTrack) => {
+ if (shouldTrack) {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', {
+ label: 'browser_resize',
+ property: 'nav_sidebar',
+ });
+ }
+};
+
describe('Super Sidebar Collapsed State Manager', () => {
beforeEach(() => {
setHTMLFixture(`
@@ -34,10 +45,12 @@ describe('Super Sidebar Collapsed State Manager', () => {
<aside class="super-sidebar"></aside>
</div>
`);
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
});
afterEach(() => {
resetHTMLFixture();
+ unmockTracking();
});
describe('toggleSuperSidebarCollapsed', () => {
@@ -109,14 +122,20 @@ describe('Super Sidebar Collapsed State Manager', () => {
});
it.each`
- initialWindowWidth | updatedWindowWidth | hasClassBeforeResize | hasClassAfterResize
- ${xl} | ${sm} | ${false} | ${true}
- ${sm} | ${xl} | ${true} | ${false}
- ${xl} | ${xl} | ${false} | ${false}
- ${sm} | ${sm} | ${true} | ${true}
+ initialWindowWidth | updatedWindowWidth | hasClassBeforeResize | hasClassAfterResize | sendsTrackingEvent
+ ${xl} | ${sm} | ${false} | ${true} | ${true}
+ ${sm} | ${xl} | ${true} | ${false} | ${false}
+ ${xl} | ${xl} | ${false} | ${false} | ${false}
+ ${sm} | ${sm} | ${true} | ${true} | ${false}
`(
'when changing width from $initialWindowWidth to $updatedWindowWidth expect page to have collapsed class before resize to be $hasClassBeforeResize and after resize to be $hasClassAfterResize',
- ({ initialWindowWidth, updatedWindowWidth, hasClassBeforeResize, hasClassAfterResize }) => {
+ ({
+ initialWindowWidth,
+ updatedWindowWidth,
+ hasClassBeforeResize,
+ hasClassAfterResize,
+ sendsTrackingEvent,
+ }) => {
getCookie.mockReturnValue(undefined);
window.innerWidth = initialWindowWidth;
initSuperSidebarCollapsedState();
@@ -129,6 +148,7 @@ describe('Super Sidebar Collapsed State Manager', () => {
window.dispatchEvent(new Event('resize'));
pageHasCollapsedClass(hasClassAfterResize);
+ tracksCollapse(sendsTrackingEvent);
},
);
});
diff --git a/spec/frontend/tracking/tracking_spec.js b/spec/frontend/tracking/tracking_spec.js
index c23790bb589..55ce8039399 100644
--- a/spec/frontend/tracking/tracking_spec.js
+++ b/spec/frontend/tracking/tracking_spec.js
@@ -59,7 +59,6 @@ describe('Tracking', () => {
window.doNotTrack = undefined;
navigator.doNotTrack = undefined;
navigator.msDoNotTrack = undefined;
- jest.clearAllMocks();
});
it('tracks to snowplow (our current tracking system)', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 07fc0be9e51..123ff7ded63 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -113,11 +113,6 @@ const createComponent = (customConfig = {}, createState = true) => {
GlSprintf,
},
apolloProvider: createMockApollo([[readyToMergeQuery, readyToMergeResponseSpy]]),
- provide: {
- glFeatures: {
- autoMergeLabelsMrWidget: false,
- },
- },
});
};
@@ -144,6 +139,7 @@ const findDeleteSourceBranchCheckbox = () =>
const triggerApprovalUpdated = () => eventHub.$emit('ApprovalUpdated');
const triggerEditCommitInput = () =>
wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+const findMergeHelperText = () => wrapper.find('[data-testid="auto-merge-helper-text"]');
describe('ReadyToMerge', () => {
beforeEach(() => {
@@ -185,26 +181,6 @@ describe('ReadyToMerge', () => {
expect(wrapper.vm.status).toEqual('failed');
});
});
-
- describe('status icon', () => {
- it('defaults to tick icon', () => {
- createComponent({ mr: { mergeable: true } });
-
- expect(wrapper.vm.iconClass).toEqual('success');
- });
-
- it('shows tick for success status', () => {
- createComponent({ mr: { pipeline: { status: 'SUCCESS' }, mergeable: true } });
-
- expect(wrapper.vm.iconClass).toEqual('success');
- });
-
- it('shows tick for pending status', () => {
- createComponent({ mr: { pipeline: { active: true }, mergeable: true } });
-
- expect(wrapper.vm.iconClass).toEqual('success');
- });
- });
});
describe('merge button text', () => {
@@ -214,18 +190,11 @@ describe('ReadyToMerge', () => {
expect(findMergeButton().text()).toBe('Merge');
});
- it('should return "Merge when pipeline succeeds" when the MWPS auto merge strategy is available', () => {
- createComponent({
- mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY },
- });
-
- expect(findMergeButton().text()).toBe('Merge when pipeline succeeds');
- });
-
- it('should return Merge when pipeline succeeds', () => {
+ it('should return Set to auto-merge in the button and Merge when pipeline succeeds in the helper text', () => {
createComponent({ mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY } });
- expect(findMergeButton().text()).toBe('Merge when pipeline succeeds');
+ expect(findMergeButton().text()).toBe('Set to auto-merge');
+ expect(findMergeHelperText().text()).toBe('Merge when pipeline succeeds');
});
});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
index 6acd1f51a86..1f3029435ee 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { handleBlobRichViewer } from '~/blob/viewer';
import RichViewer from '~/vue_shared/components/blob_viewers/rich_viewer.vue';
@@ -21,16 +22,24 @@ describe('Blob Rich Viewer component', () => {
}
beforeEach(() => {
+ const execImmediately = (callback) => callback();
+ jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
+
createComponent();
});
+ it('listens to requestIdleCallback', () => {
+ expect(window.requestIdleCallback).toHaveBeenCalled();
+ });
+
it('renders the passed content without transformations', () => {
expect(wrapper.html()).toContain(content);
});
- it('renders the richViewer if one is present', () => {
+ it('renders the richViewer if one is present', async () => {
const richViewer = '<div class="js-pdf-viewer"></div>';
createComponent('pdf', richViewer);
+ await nextTick();
expect(wrapper.html()).toContain(richViewer);
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
index 919abc26e05..1154c930e5d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
@@ -40,8 +40,6 @@ describe('Chunk component', () => {
describe('rendering', () => {
it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => {
- jest.clearAllMocks();
-
expect(window.requestIdleCallback).not.toHaveBeenCalled();
expect(findContent().text()).toBe(CHUNK_1.highlightedContent);
});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
index 18d7b2397c9..e4180b2d178 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -5,8 +5,6 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective } from 'helpers/vue_mock_directive';
import { stubComponent } from 'helpers/stub_component';
-import EmojiPicker from '~/emoji/components/picker.vue';
-import waitForPromises from 'helpers/wait_for_promises';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
import WorkItemNoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
@@ -36,11 +34,6 @@ describe('Work Item Note Actions', () => {
},
});
- const EmojiPickerStub = {
- props: EmojiPicker.props,
- template: '<div></div>',
- };
-
const createComponent = ({
showReply = true,
showEdit = true,
@@ -57,6 +50,8 @@ describe('Work Item Note Actions', () => {
propsData: {
showReply,
showEdit,
+ workItemIid: '1',
+ note: {},
noteId,
showAwardEmoji,
showAssignUnassign,
@@ -68,12 +63,12 @@ describe('Work Item Note Actions', () => {
projectName,
},
provide: {
+ fullPath: 'gitlab-org',
glFeatures: {
workItemsMvc2: true,
},
},
stubs: {
- EmojiPicker: EmojiPickerStub,
GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
methods: { close: showSpy },
}),
@@ -136,22 +131,6 @@ describe('Work Item Note Actions', () => {
expect(findEmojiButton().exists()).toBe(false);
});
-
- it('commits mutation on click', async () => {
- const awardName = 'carrot';
-
- createComponent();
-
- findEmojiButton().vm.$emit('click', awardName);
-
- await waitForPromises();
-
- expect(findEmojiButton().emitted('errors')).toEqual(undefined);
- expect(addEmojiMutationResolver).toHaveBeenCalledWith({
- awardableId: noteId,
- name: awardName,
- });
- });
});
describe('delete note', () => {
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
new file mode 100644
index 00000000000..d425f1e50dc
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
@@ -0,0 +1,147 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import mockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { __ } from '~/locale';
+import AwardsList from '~/vue_shared/components/awards_list.vue';
+import WorkItemNoteAwardsList from '~/work_items/components/notes/work_item_note_awards_list.vue';
+import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
+import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
+import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
+import {
+ mockWorkItemNotesResponseWithComments,
+ mockAwardEmojiThumbsUp,
+} from 'jest/work_items/mock_data';
+import { EMOJI_THUMBSUP, EMOJI_THUMBSDOWN } from '~/work_items/constants';
+
+Vue.use(VueApollo);
+
+describe('Work Item Note Awards List', () => {
+ let wrapper;
+ const workItem = mockWorkItemNotesResponseWithComments.data.workspace.workItems.nodes[0];
+ const firstNote = workItem.widgets.find((w) => w.type === 'NOTES').discussions.nodes[0].notes
+ .nodes[0];
+ const fullPath = 'test-project-path';
+ const workItemIid = workItem.iid;
+ const currentUserId = getIdFromGraphQLId(mockAwardEmojiThumbsUp.user.id);
+
+ const addAwardEmojiMutationSuccessHandler = jest.fn().mockResolvedValue({
+ data: {
+ awardEmojiAdd: {
+ errors: [],
+ },
+ },
+ });
+ const removeAwardEmojiMutationSuccessHandler = jest.fn().mockResolvedValue({
+ data: {
+ awardEmojiRemove: {
+ errors: [],
+ },
+ },
+ });
+
+ const findAwardsList = () => wrapper.findComponent(AwardsList);
+
+ const createComponent = ({
+ note = firstNote,
+ addAwardEmojiMutationHandler = addAwardEmojiMutationSuccessHandler,
+ removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler,
+ } = {}) => {
+ const apolloProvider = mockApollo([
+ [addAwardEmojiMutation, addAwardEmojiMutationHandler],
+ [removeAwardEmojiMutation, removeAwardEmojiMutationHandler],
+ ]);
+
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: workItemNotesByIidQuery,
+ variables: { fullPath, iid: workItemIid },
+ ...mockWorkItemNotesResponseWithComments,
+ });
+
+ wrapper = shallowMount(WorkItemNoteAwardsList, {
+ provide: {
+ fullPath,
+ },
+ propsData: {
+ workItemIid,
+ note,
+ isModal: false,
+ },
+ apolloProvider,
+ });
+ };
+
+ beforeEach(() => {
+ window.gon.current_user_id = currentUserId;
+ });
+
+ describe('when not editing', () => {
+ it.each([true, false])('passes emoji permission to awards-list', (hasAwardEmojiPermission) => {
+ const note = {
+ ...firstNote,
+ userPermissions: {
+ ...firstNote.userPermissions,
+ awardEmoji: hasAwardEmojiPermission,
+ },
+ };
+ createComponent({ note });
+
+ expect(findAwardsList().props('canAwardEmoji')).toBe(hasAwardEmojiPermission);
+ });
+
+ it('adds award if not already awarded', async () => {
+ createComponent();
+ await waitForPromises();
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
+
+ expect(addAwardEmojiMutationSuccessHandler).toHaveBeenCalledWith({
+ awardableId: firstNote.id,
+ name: EMOJI_THUMBSUP,
+ });
+ });
+
+ it('emits error if awarding emoji fails', async () => {
+ createComponent({
+ addAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no'),
+ });
+ await waitForPromises();
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[__('Failed to add emoji. Please try again')]]);
+ });
+
+ it('removes award if already awarded', async () => {
+ const removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler;
+
+ createComponent({ removeAwardEmojiMutationHandler });
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN);
+
+ await waitForPromises();
+
+ expect(removeAwardEmojiMutationHandler).toHaveBeenCalledWith({
+ awardableId: firstNote.id,
+ name: EMOJI_THUMBSDOWN,
+ });
+ });
+
+ it('restores award if remove fails', async () => {
+ createComponent({
+ removeAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no'),
+ });
+ await waitForPromises();
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN);
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[__('Failed to remove emoji. Please try again')]]);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js
index 8dbd2818fc5..09299f1733c 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { updateDraft, clearDraft } from '~/lib/utils/autosave';
import EditedAt from '~/issues/show/components/edited.vue';
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
+import WorkItemNoteAwardsList from '~/work_items/components/notes/work_item_note_awards_list.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
@@ -76,6 +77,7 @@ describe('Work Item Note', () => {
const errorHandler = jest.fn().mockRejectedValue('Oops');
+ const findAwardsList = () => wrapper.findComponent(WorkItemNoteAwardsList);
const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
const findNoteHeader = () => wrapper.findComponent(NoteHeader);
const findNoteBody = () => wrapper.findComponent(NoteBody);
@@ -92,10 +94,14 @@ describe('Work Item Note', () => {
updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler,
assignees = mockAssignees,
workItemByIidResponseHandler = workItemResponseHandler,
+ workItemsMvc2 = false,
} = {}) => {
wrapper = shallowMount(WorkItemNote, {
provide: {
fullPath: 'test-project-path',
+ glFeatures: {
+ workItemsMvc2,
+ },
},
propsData: {
workItemId,
@@ -404,5 +410,18 @@ describe('Work Item Note', () => {
});
});
});
+
+ it('does not show awards when feature flag disabled', () => {
+ createComponent();
+
+ expect(findAwardsList().exists()).toBe(false);
+ });
+
+ it('passes note props to awards list', () => {
+ createComponent({ note: mockWorkItemCommentNote, workItemsMvc2: true });
+
+ expect(findAwardsList().props('note')).toBe(mockWorkItemCommentNote);
+ expect(findAwardsList().props('workItemIid')).toBe('1');
+ });
});
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index c9c73d29903..41f3adb6703 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -1982,6 +1982,9 @@ export const mockWorkItemNotesResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2093,6 +2096,9 @@ export const mockWorkItemNotesByIidResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2146,6 +2152,9 @@ export const mockWorkItemNotesByIidResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2200,6 +2209,9 @@ export const mockWorkItemNotesByIidResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2312,6 +2324,9 @@ export const mockMoreWorkItemNotesResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2365,6 +2380,9 @@ export const mockMoreWorkItemNotesResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2416,6 +2434,9 @@ export const mockMoreWorkItemNotesResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2483,6 +2504,9 @@ export const createWorkItemNoteResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2534,6 +2558,9 @@ export const mockWorkItemCommentNote = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [mockAwardEmojiThumbsDown],
+ },
};
export const mockWorkItemCommentNoteByContributor = {
@@ -2633,6 +2660,9 @@ export const mockWorkItemNotesResponseWithComments = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ awardEmoji: {
+ nodes: [mockAwardEmojiThumbsDown],
+ },
__typename: 'Note',
},
{
@@ -2673,6 +2703,9 @@ export const mockWorkItemNotesResponseWithComments = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2721,6 +2754,9 @@ export const mockWorkItemNotesResponseWithComments = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2797,6 +2833,9 @@ export const workItemNotesCreateSubscriptionResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2824,6 +2863,9 @@ export const workItemNotesCreateSubscriptionResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
},
@@ -2869,6 +2911,9 @@ export const workItemNotesUpdateSubscriptionResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
},
@@ -3028,6 +3073,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
},
__typename: 'SystemNoteMetadata',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -3091,6 +3139,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
},
__typename: 'SystemNoteMetadata',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -3154,6 +3205,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
},
__typename: 'SystemNoteMetadata',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
diff --git a/spec/frontend/work_items/notes/award_utils_spec.js b/spec/frontend/work_items/notes/award_utils_spec.js
new file mode 100644
index 00000000000..8ae32ce5f40
--- /dev/null
+++ b/spec/frontend/work_items/notes/award_utils_spec.js
@@ -0,0 +1,109 @@
+import { getMutation, optimisticAwardUpdate } from '~/work_items/notes/award_utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import mockApollo from 'helpers/mock_apollo_helper';
+import { __ } from '~/locale';
+import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
+import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
+import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
+import {
+ mockWorkItemNotesResponseWithComments,
+ mockAwardEmojiThumbsUp,
+ mockAwardEmojiThumbsDown,
+} from '../mock_data';
+
+function getWorkItem(data) {
+ return data.workspace.workItems.nodes[0];
+}
+function getFirstNote(workItem) {
+ return workItem.widgets.find((w) => w.type === 'NOTES').discussions.nodes[0].notes.nodes[0];
+}
+
+describe('Work item note award utils', () => {
+ const workItem = getWorkItem(mockWorkItemNotesResponseWithComments.data);
+ const firstNote = getFirstNote(workItem);
+ const fullPath = 'test-project-path';
+ const workItemIid = workItem.iid;
+ const currentUserId = getIdFromGraphQLId(mockAwardEmojiThumbsDown.user.id);
+
+ beforeEach(() => {
+ window.gon = { current_user_id: currentUserId };
+ });
+
+ describe('getMutation', () => {
+ it('returns remove mutation when user has already awarded award', () => {
+ const note = firstNote;
+ const { name } = mockAwardEmojiThumbsDown;
+
+ expect(getMutation({ note, name })).toEqual({
+ mutation: removeAwardEmojiMutation,
+ mutationName: 'awardEmojiRemove',
+ errorMessage: __('Failed to remove emoji. Please try again'),
+ });
+ });
+
+ it('returns remove mutation when user has not already awarded award', () => {
+ const note = {};
+ const { name } = mockAwardEmojiThumbsUp;
+
+ expect(getMutation({ note, name })).toEqual({
+ mutation: addAwardEmojiMutation,
+ mutationName: 'awardEmojiAdd',
+ errorMessage: __('Failed to add emoji. Please try again'),
+ });
+ });
+ });
+
+ describe('optimisticAwardUpdate', () => {
+ let apolloProvider;
+ beforeEach(() => {
+ apolloProvider = mockApollo();
+
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: workItemNotesByIidQuery,
+ variables: { fullPath, iid: workItemIid },
+ ...mockWorkItemNotesResponseWithComments,
+ });
+ });
+
+ it('adds new emoji to cache', () => {
+ const note = firstNote;
+ const { name } = mockAwardEmojiThumbsUp;
+
+ const updateFn = optimisticAwardUpdate({ note, name, fullPath, workItemIid });
+
+ updateFn(apolloProvider.clients.defaultClient.cache);
+
+ const updatedResult = apolloProvider.clients.defaultClient.readQuery({
+ query: workItemNotesByIidQuery,
+ variables: { fullPath, iid: workItemIid },
+ });
+
+ const updatedWorkItem = getWorkItem(updatedResult);
+ const updatedNote = getFirstNote(updatedWorkItem);
+
+ expect(updatedNote.awardEmoji.nodes).toEqual([
+ mockAwardEmojiThumbsDown,
+ mockAwardEmojiThumbsUp,
+ ]);
+ });
+
+ it('removes existing emoji from cache', () => {
+ const note = firstNote;
+ const { name } = mockAwardEmojiThumbsDown;
+
+ const updateFn = optimisticAwardUpdate({ note, name, fullPath, workItemIid });
+
+ updateFn(apolloProvider.clients.defaultClient.cache);
+
+ const updatedResult = apolloProvider.clients.defaultClient.readQuery({
+ query: workItemNotesByIidQuery,
+ variables: { fullPath, iid: workItemIid },
+ });
+
+ const updatedWorkItem = getWorkItem(updatedResult);
+ const updatedNote = getFirstNote(updatedWorkItem);
+
+ expect(updatedNote.awardEmoji.nodes).toEqual([]);
+ });
+ });
+});
diff --git a/spec/models/organizations/organization_spec.rb b/spec/models/organizations/organization_spec.rb
index a3c6b9edf0d..f462fbefccc 100644
--- a/spec/models/organizations/organization_spec.rb
+++ b/spec/models/organizations/organization_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe Organizations::Organization, type: :model, feature_category: :cel
describe 'associations' do
it { is_expected.to have_many :namespaces }
it { is_expected.to have_many :groups }
+ it { is_expected.to have_many(:users).through(:organization_users).inverse_of(:organizations) }
+ it { is_expected.to have_many(:organization_users).inverse_of(:organization) }
end
describe 'validations' do
diff --git a/spec/models/organizations/organization_user_spec.rb b/spec/models/organizations/organization_user_spec.rb
new file mode 100644
index 00000000000..392ffa1b5be
--- /dev/null
+++ b/spec/models/organizations/organization_user_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Organizations::OrganizationUser, type: :model, feature_category: :cell do
+ describe 'associations' do
+ it { is_expected.to belong_to(:organization).inverse_of(:organization_users).required }
+ it { is_expected.to belong_to(:user).inverse_of(:organization_users).required }
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 9709684f44b..1a7d86328e7 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -186,6 +186,15 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to have_many(:merge_request_assignment_events).class_name('ResourceEvents::MergeRequestAssignmentEvent') }
it do
+ is_expected.to have_many(:organization_users).class_name('Organizations::OrganizationUser').inverse_of(:user)
+ end
+
+ it do
+ is_expected.to have_many(:organizations)
+ .through(:organization_users).class_name('Organizations::Organization').inverse_of(:users)
+ end
+
+ it do
is_expected.to have_many(:alert_assignees).class_name('::AlertManagement::AlertAssignee').inverse_of(:assignee)
end
diff --git a/spec/support/formatters/json_formatter.rb b/spec/support/formatters/json_formatter.rb
index 10af5445b7a..e9d65af710a 100644
--- a/spec/support/formatters/json_formatter.rb
+++ b/spec/support/formatters/json_formatter.rb
@@ -74,7 +74,8 @@ module Support
product_group: example.metadata[:product_group],
feature_category: example.metadata[:feature_category],
ci_job_url: ENV['CI_JOB_URL'],
- retry_attempts: example.metadata[:retry_attempts]
+ retry_attempts: example.metadata[:retry_attempts],
+ level: example.metadata[:level]
}
end