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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-21 18:10:23 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-21 18:10:23 +0300
commit75196424b189d70aff3d88a95117adb33c2b1263 (patch)
treeed55cbd4e0974bf7062ad0d794eaec32dfe5dfdb
parenteac99f198f2834788c38108bcee3a2c567fba9e0 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue72
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue47
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue25
-rw-r--r--app/assets/javascripts/design_management/components/toolbar/index.vue3
-rw-r--r--app/assets/javascripts/design_management/constants.js5
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/destroy_note.mutation.graphql8
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue9
-rw-r--r--app/assets/javascripts/design_management/utils/error_messages.js8
-rw-r--r--app/assets/javascripts/header.js2
-rw-r--r--app/assets/javascripts/nav/components/new_nav_toggle.vue21
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue22
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue199
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_name_group.vue77
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js3
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss70
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss11
-rw-r--r--app/controllers/confirmations_controller.rb8
-rw-r--r--app/helpers/sidebars_helper.rb22
-rw-r--r--app/helpers/users_helper.rb8
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--config/feature_flags/development/remove_job_token_on_completion.yml8
-rw-r--r--db/structure.sql22
-rw-r--r--doc/administration/geo/replication/geo_validation_tests.md3
-rw-r--r--doc/api/draft_notes.md21
-rw-r--r--doc/architecture/blueprints/runner_tokens/index.md4
-rw-r--r--doc/development/contributing/design.md2
-rw-r--r--doc/development/fe_guide/accessibility.md2
-rw-r--r--doc/development/fe_guide/frontend_faq.md2
-rw-r--r--doc/development/fe_guide/source_editor.md4
-rw-r--r--doc/development/fe_guide/style/html.md2
-rw-r--r--doc/development/gitpod_internals.md2
-rw-r--r--doc/development/i18n/proofreader.md2
-rw-r--r--doc/development/integrations/index.md2
-rw-r--r--doc/development/merge_request_concepts/index.md2
-rw-r--r--doc/development/service_ping/implement.md2
-rw-r--r--doc/development/service_ping/metrics_dictionary.md4
-rw-r--r--doc/development/snowplow/event_dictionary_guide.md4
-rw-r--r--doc/development/windows.md2
-rw-r--r--doc/operations/img/copy-group-id.pngbin22172 -> 0 bytes
-rw-r--r--doc/operations/img/create-gitlab-application.pngbin162624 -> 0 bytes
-rw-r--r--doc/operations/img/error_tracking_setting_dsn_v14_4.pngbin39249 -> 0 bytes
-rw-r--r--doc/operations/img/error_tracking_setting_v14_3.pngbin27537 -> 0 bytes
-rw-r--r--doc/operations/img/listing_groups.pngbin15444 -> 0 bytes
-rw-r--r--doc/subscriptions/index.md2
-rw-r--r--doc/user/group/contribution_analytics/img/group_stats_cal.pngbin2029 -> 0 bytes
-rw-r--r--doc/user/group/contribution_analytics/img/group_stats_table.pngbin22691 -> 0 bytes
-rw-r--r--doc/user/group/epics/manage_epics.md2
-rw-r--r--doc/user/project/import/img/bitbucket_import_select_project_v12_3.pngbin31980 -> 0 bytes
-rw-r--r--doc/user/project/import/img/fogbugz_import_finished.pngbin17744 -> 0 bytes
-rw-r--r--doc/user/project/import/img/manifest_status_v13_3.pngbin31313 -> 0 bytes
-rw-r--r--doc/user/project/issues/design_management.md13
-rw-r--r--doc/user/project/merge_requests/img/remove_source_branch_status.pngbin32586 -> 0 bytes
-rw-r--r--locale/gitlab.pot19
-rw-r--r--qa/lib/gitlab/page/group/settings/usage_quotas.rb3
-rw-r--r--spec/features/nav/new_nav_toggle_spec.rb2
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap2
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js142
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js46
-rw-r--r--spec/frontend/design_management/components/design_sidebar_spec.js4
-rw-r--r--spec/frontend/nav/components/new_nav_toggle_spec.js178
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/list_spec.js8
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js221
-rw-r--r--spec/frontend/super_sidebar/components/user_name_group_spec.js100
-rw-r--r--spec/frontend/super_sidebar/mock_data.js30
-rw-r--r--spec/helpers/sidebars_helper_spec.rb26
-rw-r--r--spec/helpers/users_helper_spec.rb10
-rw-r--r--spec/models/ci/build_spec.rb6
68 files changed, 1333 insertions, 197 deletions
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index 3091c6703b4..7083c5cd0b7 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -1,16 +1,20 @@
<script>
import { GlButton, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
+import * as Sentry from '@sentry/browser';
import { createAlert } from '~/flash';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import { updateGlobalTodoCount } from '~/sidebar/utils';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
import { isLoggedIn } from '~/lib/utils/common_utils';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
+import { TYPENAME_NOTE, TYPENAME_DISCUSSION } from '~/graphql_shared/constants';
+import { ACTIVE_DISCUSSION_SOURCE_TYPES, DELETE_NOTE_ERROR_MSG } from '../../constants';
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
+import destroyNoteMutation from '../../graphql/mutations/destroy_note.mutation.graphql';
import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
import allVersionsMixin from '../../mixins/all_versions';
@@ -23,6 +27,13 @@ import DesignReplyForm from './design_reply_form.vue';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
export default {
+ i18n: {
+ deleteNote: {
+ confirmationText: __('Are you sure you want to delete this comment?'),
+ primaryModalBtnText: __('Delete comment'),
+ errorText: DELETE_NOTE_ERROR_MSG,
+ },
+ },
components: {
ApolloMutation,
DesignNote,
@@ -100,6 +111,7 @@ export default {
discussionComment: '',
isFormRendered: false,
activeDiscussion: {},
+ noteToDelete: null,
isResolving: false,
shouldChangeResolvedStatus: false,
areRepliesCollapsed: this.discussion.resolved,
@@ -219,13 +231,65 @@ export default {
const { source } = activeDiscussion;
return ALLOWED_ACTIVE_DISCUSSION_SOURCES.includes(source) && this.isDiscussionActive;
},
+ async showDeleteNoteConfirmationModal(note) {
+ const isLast = note?.discussion?.notes?.nodes.length === 1;
+ this.noteToDelete = { ...note, isLast };
+
+ const confirmed = await confirmAction(this.$options.i18n.deleteNote.confirmationText, {
+ primaryBtnVariant: 'danger',
+ primaryBtnText: this.$options.i18n.deleteNote.primaryModalBtnText,
+ });
+
+ if (confirmed) {
+ await this.deleteNote();
+ }
+ },
+ async deleteNote() {
+ const { id, discussion, isLast } = this.noteToDelete;
+ try {
+ await this.$apollo.mutate({
+ mutation: destroyNoteMutation,
+ variables: {
+ input: {
+ id,
+ },
+ },
+ update: (cache, { data }) => {
+ const { errors } = data.destroyNote;
+
+ if (errors?.length) {
+ this.$emit('delete-note-error', errors[0]);
+ }
+
+ const objectToIdentify = isLast
+ ? { __typename: TYPENAME_DISCUSSION, id: discussion?.id }
+ : { __typename: TYPENAME_NOTE, id };
+
+ cache.modify({
+ id: cache.identify(objectToIdentify),
+ fields: (_, { DELETE }) => DELETE,
+ });
+ },
+ optimisticResponse: {
+ destroyNote: {
+ note: null,
+ errors: [],
+ __typename: 'DestroyNotePayload',
+ },
+ },
+ });
+ } catch (error) {
+ this.$emit('delete-note-error', this.$options.i18n.deleteNote.errorText);
+ Sentry.captureException(error);
+ }
+ },
},
createNoteMutation,
};
</script>
<template>
- <div class="design-discussion-wrapper">
+ <div class="design-discussion-wrapper" @click="$emit('update-active-discussion')">
<design-note-pin :is-resolved="discussion.resolved" :label="discussion.index" />
<ul
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
@@ -237,6 +301,7 @@ export default {
:is-resolving="isResolving"
:noteable-id="noteableId"
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
+ @delete-note="showDeleteNoteConfirmationModal($event)"
@error="$emit('update-note-error', $event)"
>
<template v-if="isLoggedIn && discussion.resolvable" #resolve-discussion>
@@ -280,6 +345,7 @@ export default {
:is-resolving="isResolving"
:noteable-id="noteableId"
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
+ @delete-note="showDeleteNoteConfirmationModal($event)"
@error="$emit('update-note-error', $event)"
/>
<li
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index af4bf7eb14d..c1207ad527e 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -1,5 +1,13 @@
<script>
-import { GlAvatar, GlAvatarLink, GlButton, GlLink, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlAvatar,
+ GlAvatarLink,
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlLink,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -14,6 +22,8 @@ import DesignReplyForm from './design_reply_form.vue';
export default {
i18n: {
editCommentLabel: __('Edit comment'),
+ moreActionsLabel: __('More actions'),
+ deleteCommentText: __('Delete comment'),
},
components: {
ApolloMutation,
@@ -21,6 +31,8 @@ export default {
GlAvatar,
GlAvatarLink,
GlButton,
+ GlDropdown,
+ GlDropdownItem,
GlLink,
TimeAgoTooltip,
TimelineEntryItem,
@@ -48,6 +60,7 @@ export default {
return {
noteText: this.note.body,
isEditing: false,
+ isError: true,
};
},
computed: {
@@ -70,7 +83,13 @@ export default {
};
},
isEditButtonVisible() {
- return !this.isEditing && this.note.userPermissions.adminNote;
+ return !this.isEditing && this.adminPermissions;
+ },
+ isMoreActionsButtonVisible() {
+ return !this.isEditing && this.adminPermissions;
+ },
+ adminPermissions() {
+ return this.note.userPermissions.adminNote;
},
},
methods: {
@@ -132,6 +151,30 @@ export default {
size="small"
@click="isEditing = true"
/>
+ <gl-dropdown
+ v-if="isMoreActionsButtonVisible"
+ v-gl-tooltip.hover
+ class="gl-display-none gl-sm-display-inline-flex! gl-ml-3"
+ icon="ellipsis_v"
+ category="tertiary"
+ data-qa-selector="design_discussion_actions_ellipsis_dropdown"
+ data-testid="more-actions-dropdown"
+ :text="$options.i18n.moreActionsLabel"
+ text-sr-only
+ :title="$options.i18n.moreActionsLabel"
+ :aria-label="$options.i18n.moreActionsLabel"
+ no-caret
+ left
+ >
+ <gl-dropdown-item
+ variant="danger"
+ data-qa-selector="delete_design_note_button"
+ data-testid="delete-note-button"
+ @click="$emit('delete-note', note)"
+ >
+ {{ $options.i18n.deleteCommentText }}
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
</div>
<template v-if="!isEditing">
diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue
index 24cc93f5eaf..c34d5cea0c2 100644
--- a/app/assets/javascripts/design_management/components/design_sidebar.vue
+++ b/app/assets/javascripts/design_management/components/design_sidebar.vue
@@ -57,7 +57,6 @@ export default {
},
data() {
return {
- isResolvedDiscussionsExpanded: this.resolvedDiscussionsExpanded,
discussionWithOpenForm: '',
isLoggedIn: isLoggedIn(),
};
@@ -87,13 +86,13 @@ export default {
unresolvedDiscussions() {
return this.discussions.filter((discussion) => !discussion.resolved);
},
- },
- watch: {
- resolvedDiscussionsExpanded(resolvedDiscussionsExpanded) {
- this.isResolvedDiscussionsExpanded = resolvedDiscussionsExpanded;
- },
- isResolvedDiscussionsExpanded() {
- this.$emit('toggleResolvedComments');
+ isResolvedDiscussionsExpanded: {
+ get() {
+ return this.resolvedDiscussionsExpanded;
+ },
+ set(isExpanded) {
+ this.$emit('toggleResolvedComments', isExpanded);
+ },
},
},
mounted() {
@@ -129,7 +128,7 @@ export default {
</script>
<template>
- <div class="image-notes gl-pt-0" @click="handleSidebarClick">
+ <div class="image-notes gl-pt-0" @click.self="handleSidebarClick">
<div
class="gl-py-4 gl-mb-4 gl-display-flex gl-justify-content-space-between gl-align-items-center gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
>
@@ -179,8 +178,9 @@ export default {
data-testid="unresolved-discussion"
@create-note-error="$emit('onDesignDiscussionError', $event)"
@update-note-error="$emit('updateNoteError', $event)"
+ @delete-note-error="$emit('deleteNoteError', $event)"
@resolve-discussion-error="$emit('resolveDiscussionError', $event)"
- @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
+ @update-active-discussion="updateActiveDiscussion(discussion.notes[0].id)"
@open-form="updateDiscussionWithOpenForm"
/>
<gl-accordion v-if="hasResolvedDiscussions" :header-level="3" class="gl-mb-5">
@@ -202,9 +202,10 @@ export default {
:discussion-with-open-form="discussionWithOpenForm"
data-testid="resolved-discussion"
@error="$emit('onDesignDiscussionError', $event)"
- @updateNoteError="$emit('updateNoteError', $event)"
+ @update-note-error="$emit('updateNoteError', $event)"
+ @delete-note-error="$emit('deleteNoteError', $event)"
@open-form="updateDiscussionWithOpenForm"
- @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
+ @update-active-discussion="updateActiveDiscussion(discussion.notes[0].id)"
/>
</gl-accordion-item>
</gl-accordion>
diff --git a/app/assets/javascripts/design_management/components/toolbar/index.vue b/app/assets/javascripts/design_management/components/toolbar/index.vue
index 6d571365306..cd76b6c1885 100644
--- a/app/assets/javascripts/design_management/components/toolbar/index.vue
+++ b/app/assets/javascripts/design_management/components/toolbar/index.vue
@@ -60,7 +60,8 @@ export default {
},
image: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
isLoading: {
type: Boolean,
diff --git a/app/assets/javascripts/design_management/constants.js b/app/assets/javascripts/design_management/constants.js
index afe621ac3c5..6720245b5f1 100644
--- a/app/assets/javascripts/design_management/constants.js
+++ b/app/assets/javascripts/design_management/constants.js
@@ -1,3 +1,4 @@
+import { __ } from '~/locale';
// WARNING: replace this with something
// more sensical as per https://gitlab.com/gitlab-org/gitlab/issues/118611
export const VALID_DESIGN_FILE_MIMETYPE = {
@@ -14,3 +15,7 @@ export const ACTIVE_DISCUSSION_SOURCE_TYPES = {
export const DESIGN_DETAIL_LAYOUT_CLASSLIST = ['design-detail-layout', 'overflow-hidden', 'm-0'];
export const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
+
+export const DELETE_NOTE_ERROR_MSG = __(
+ 'Something went wrong when deleting a comment. Please try again.',
+);
diff --git a/app/assets/javascripts/design_management/graphql/mutations/destroy_note.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/destroy_note.mutation.graphql
new file mode 100644
index 00000000000..58fb05e2140
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/mutations/destroy_note.mutation.graphql
@@ -0,0 +1,8 @@
+mutation destroyNote($input: DestroyNoteInput!) {
+ destroyNote(input: $input) {
+ errors
+ note {
+ id
+ }
+ }
+}
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index f448e2f9e3d..3a1c8ae43c5 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -41,6 +41,7 @@ import {
DESIGN_VERSION_NOT_EXIST_ERROR,
UPDATE_NOTE_ERROR,
TOGGLE_TODO_ERROR,
+ DELETE_NOTE_ERROR,
designDeletionError,
} from '../../utils/error_messages';
import { trackDesignDetailView, servicePingDesignDetailView } from '../../utils/tracking';
@@ -263,6 +264,9 @@ export default {
onUpdateNoteError(e) {
this.onError(UPDATE_NOTE_ERROR, e);
},
+ onDeleteNoteError(e) {
+ this.onError(DELETE_NOTE_ERROR, e);
+ },
onDesignDiscussionError(e) {
this.onError(ADD_DISCUSSION_COMMENT_ERROR, e);
},
@@ -324,8 +328,8 @@ export default {
const diffNoteGid = noteId ? toDiffNoteGid(noteId) : undefined;
return this.updateActiveDiscussion(diffNoteGid, ACTIVE_DISCUSSION_SOURCE_TYPES.url);
},
- toggleResolvedComments() {
- this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
+ toggleResolvedComments(newValue) {
+ this.resolvedDiscussionsExpanded = newValue;
},
setMaxScale(event) {
this.maxScale = 1 / event;
@@ -397,6 +401,7 @@ export default {
@onDesignDiscussionError="onDesignDiscussionError"
@onCreateImageDiffNoteError="onCreateImageDiffNoteError"
@updateNoteError="onUpdateNoteError"
+ @deleteNoteError="onDeleteNoteError"
@resolveDiscussionError="onResolveDiscussionError"
@toggleResolvedComments="toggleResolvedComments"
@todoError="onTodoError"
diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js
index 42f752efc9e..1ed054abe22 100644
--- a/app/assets/javascripts/design_management/utils/error_messages.js
+++ b/app/assets/javascripts/design_management/utils/error_messages.js
@@ -13,7 +13,13 @@ export const UPDATE_IMAGE_DIFF_NOTE_ERROR = s__(
'DesignManagement|Could not update discussion. Please try again.',
);
-export const UPDATE_NOTE_ERROR = s__('DesignManagement|Could not update note. Please try again.');
+export const UPDATE_NOTE_ERROR = s__(
+ 'DesignManagement|Could not update comment. Please try again.',
+);
+
+export const DELETE_NOTE_ERROR = s__(
+ 'DesignManagement|Could not delete comment. Please try again.',
+);
export const UPLOAD_DESIGN_ERROR = s__(
'DesignManagement|Error uploading a new design. Please try again.',
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 6c9354b663f..9cb96283689 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -28,7 +28,7 @@ export default function initTodoToggle() {
});
}
-function initStatusTriggers() {
+export function initStatusTriggers() {
const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
if (setStatusModalTriggerEl) {
diff --git a/app/assets/javascripts/nav/components/new_nav_toggle.vue b/app/assets/javascripts/nav/components/new_nav_toggle.vue
index 37e29cc7f04..eb0b6c02583 100644
--- a/app/assets/javascripts/nav/components/new_nav_toggle.vue
+++ b/app/assets/javascripts/nav/components/new_nav_toggle.vue
@@ -1,5 +1,5 @@
<script>
-import { GlBadge, GlToggle } from '@gitlab/ui';
+import { GlBadge, GlToggle, GlDisclosureDropdownItem } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { createAlert } from '~/flash';
import { s__ } from '~/locale';
@@ -18,6 +18,7 @@ export default {
components: {
GlBadge,
GlToggle,
+ GlDisclosureDropdownItem,
},
props: {
enabled: {
@@ -28,6 +29,11 @@ export default {
type: String,
required: true,
},
+ newNavigation: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -61,7 +67,18 @@ export default {
</script>
<template>
- <li>
+ <gl-disclosure-dropdown-item v-if="newNavigation" @action="toggleNav">
+ <div class="gl-new-dropdown-item-content">
+ <div
+ class="gl-new-dropdown-item-text-wrapper gl-display-flex! gl-justify-content-space-between gl-align-items-center gl-py-2!"
+ >
+ {{ $options.i18n.toggleMenuItemLabel }}
+ <gl-toggle :value="isEnabled" :label="$options.i18n.toggleLabel" label-position="hidden" />
+ </div>
+ </div>
+ </gl-disclosure-dropdown-item>
+
+ <li v-else>
<div
class="gl-px-4 gl-py-2 gl-display-flex gl-justify-content-space-between gl-align-items-center"
>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
index 31c76c95e45..ce58ec74914 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
@@ -44,7 +44,7 @@ export default {
return this.queryVariables;
},
update(data) {
- return data[this.graphqlResource].packages;
+ return data[this.graphqlResource]?.packages ?? {};
},
skip() {
return !this.sort;
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index 58a6f215a9c..04499b2edce 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -1,25 +1,23 @@
<script>
-import { GlAvatar, GlButton, GlDropdown, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
-import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import logo from '../../../../views/shared/_logo.svg';
import { toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager';
import CreateMenu from './create_menu.vue';
import Counter from './counter.vue';
import MergeRequestMenu from './merge_request_menu.vue';
+import UserMenu from './user_menu.vue';
export default {
logo,
components: {
- GlAvatar,
GlButton,
- GlDropdown,
GlIcon,
CreateMenu,
- NewNavToggle,
Counter,
MergeRequestMenu,
+ UserMenu,
},
i18n: {
collapseSidebar: __('Collapse sidebar'),
@@ -32,7 +30,7 @@ export default {
GlTooltip: GlTooltipDirective,
SafeHtml,
},
- inject: ['rootPath', 'toggleNewNavEndpoint'],
+ inject: ['rootPath'],
props: {
sidebarData: {
type: Object,
@@ -64,17 +62,7 @@ export default {
<button class="gl-border-none">
<gl-icon name="search" class="gl-vertical-align-middle" />
</button>
- <gl-dropdown data-testid="user-dropdown" data-qa-selector="user_menu" variant="link" no-caret>
- <template #button-content>
- <gl-avatar
- :entity-name="sidebarData.name"
- :src="sidebarData.avatar_url"
- :size="32"
- data-qa-selector="user_avatar_content"
- />
- </template>
- <new-nav-toggle :endpoint="toggleNewNavEndpoint" enabled />
- </gl-dropdown>
+ <user-menu :data="sidebarData" />
</div>
<div class="gl-display-flex gl-justify-content-space-between gl-px-3 gl-py-2 gl-gap-2">
<counter
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
new file mode 100644
index 00000000000..09cf167c285
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -0,0 +1,199 @@
+<script>
+import {
+ GlAvatar,
+ GlBadge,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+} from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { s__, __, sprintf } from '~/locale';
+import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
+import UserNameGroup from './user_name_group.vue';
+
+export default {
+ feedbackUrl: 'https://gitlab.com/gitlab-org/gitlab/-/issues/new',
+ i18n: {
+ newNavigation: {
+ badgeLabel: s__('NorthstarNavigation|Alpha'),
+ sectionTitle: s__('NorthstarNavigation|Navigation redesign'),
+ },
+ user: {
+ setStatus: s__('SetStatusModal|Set status'),
+ editStatus: s__('SetStatusModal|Edit status'),
+ editProfile: s__('CurrentUser|Edit profile'),
+ preferences: s__('CurrentUser|Preferences'),
+ },
+ provideFeedback: s__('NorthstarNavigation|Provide feedback'),
+ startTrial: s__('CurrentUser|Start an Ultimate trial'),
+ signOut: __('Sign out'),
+ },
+ components: {
+ GlAvatar,
+ GlBadge,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+ NewNavToggle,
+ UserNameGroup,
+ },
+ directives: {
+ SafeHtml,
+ },
+ inject: ['toggleNewNavEndpoint'],
+ props: {
+ data: {
+ required: true,
+ type: Object,
+ },
+ },
+ computed: {
+ toggleText() {
+ return sprintf(__('%{user} user’s menu'), { user: this.data.name });
+ },
+ statusItem() {
+ const { busy, customized } = this.data.status;
+
+ const statusLabel =
+ busy || customized ? this.$options.i18n.user.editStatus : this.$options.i18n.user.setStatus;
+
+ return {
+ text: statusLabel,
+ extraAttrs: {
+ class: 'js-set-status-modal-trigger',
+ },
+ };
+ },
+ trialItem() {
+ return {
+ text: this.$options.i18n.startTrial,
+ href: this.data.trial.url,
+ };
+ },
+ editProfileItem() {
+ return {
+ text: this.$options.i18n.user.editProfile,
+ href: this.data.settings.profile_path,
+ };
+ },
+ preferencesItem() {
+ return {
+ text: this.$options.i18n.user.preferences,
+ href: this.data.settings.profile_preferences_path,
+ };
+ },
+ feedbackItem() {
+ return {
+ text: this.$options.i18n.provideFeedback,
+ href: this.$options.feedbackUrl,
+ extraAttrs: {
+ target: '_blank',
+ },
+ };
+ },
+ signOutGroup() {
+ return {
+ items: [
+ {
+ text: this.$options.i18n.signOut,
+ href: this.data.sign_out_link,
+ extraAttrs: {
+ 'data-method': 'post',
+ class: 'sign-out-link',
+ },
+ },
+ ],
+ };
+ },
+ statusModalData() {
+ const defaultData = {
+ 'data-current-emoji': '',
+ 'data-current-message': '',
+ 'data-default-emoji': 'speech_balloon',
+ };
+
+ if (!this.data.status.customized) {
+ return defaultData;
+ }
+ return {
+ ...defaultData,
+ 'data-current-emoji': this.data.status.emoji,
+ 'data-current-message': this.data.status.message,
+ 'data-current-availability': this.data.status.availability,
+ 'data-current-clear-status-after': this.data.status.clear_after,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-disclosure-dropdown
+ placement="right"
+ data-testid="user-dropdown"
+ data-qa-selector="user_menu"
+ >
+ <template #toggle>
+ <button class="user-bar-item">
+ <span class="gl-sr-only">{{ toggleText }}</span>
+ <gl-avatar
+ :size="24"
+ :entity-name="data.name"
+ :src="data.avatar_url"
+ aria-hidden="true"
+ data-qa-selector="user_avatar_content"
+ />
+ </button>
+ </template>
+
+ <user-name-group :user="data" />
+ <gl-disclosure-dropdown-group bordered>
+ <gl-disclosure-dropdown-item
+ v-if="data.status.can_update"
+ :item="statusItem"
+ data-testid="status-item"
+ />
+
+ <gl-disclosure-dropdown-item
+ v-if="data.trial.has_start_trial"
+ :item="trialItem"
+ data-testid="start-trial-item"
+ >
+ <template #list-item>
+ {{ trialItem.text }}
+ <gl-emoji data-name="rocket" />
+ </template>
+ </gl-disclosure-dropdown-item>
+
+ <gl-disclosure-dropdown-item :item="editProfileItem" data-testid="edit-profile-item" />
+
+ <gl-disclosure-dropdown-item :item="preferencesItem" data-testid="preferences-item" />
+ </gl-disclosure-dropdown-group>
+
+ <gl-disclosure-dropdown-group bordered>
+ <template #group-label>
+ <span class="gl-font-sm">{{ $options.i18n.newNavigation.sectionTitle }}</span>
+ <gl-badge size="sm" variant="info"
+ >{{ $options.i18n.newNavigation.badgeLabel }}
+ </gl-badge>
+ </template>
+ <new-nav-toggle :endpoint="toggleNewNavEndpoint" enabled new-navigation />
+ <gl-disclosure-dropdown-item :item="feedbackItem" data-testid="feedback-item" />
+ </gl-disclosure-dropdown-group>
+
+ <gl-disclosure-dropdown-group
+ v-if="data.can_sign_out"
+ bordered
+ :group="signOutGroup"
+ data-testid="sign-out-group"
+ />
+ </gl-disclosure-dropdown>
+
+ <div
+ v-if="data.status.can_update"
+ class="js-set-status-modal-wrapper"
+ v-bind="statusModalData"
+ ></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/user_name_group.vue b/app/assets/javascripts/super_sidebar/components/user_name_group.vue
new file mode 100644
index 00000000000..2489f462122
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/user_name_group.vue
@@ -0,0 +1,77 @@
+<script>
+import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlTooltip } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ user: {
+ busy: s__('UserProfile|(Busy)'),
+ },
+ },
+ components: {
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+ GlTooltip,
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ user: {
+ required: true,
+ type: Object,
+ },
+ },
+ computed: {
+ menuItem() {
+ const item = {
+ text: this.user.name,
+ };
+ if (this.user.has_link_to_profile) {
+ item.href = this.user.link_to_profile;
+ }
+ return item;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown-group>
+ <gl-disclosure-dropdown-item :item="menuItem">
+ <template #list-item>
+ <span class="gl-display-flex gl-flex-direction-column">
+ <span>
+ <span class="gl-font-weight-bold">
+ {{ user.name }}
+ </span>
+ <span v-if="user.status.busy" class="gl-text-gray-500">{{
+ $options.i18n.user.busy
+ }}</span>
+ </span>
+
+ <span class="gl-text-gray-400">@{{ user.username }}</span>
+
+ <span
+ v-if="user.status.customized"
+ ref="statusTooltipTarget"
+ data-testid="user-menu-status"
+ class="gl-display-flex gl-align-items-center gl-mt-2 gl-font-sm"
+ >
+ <gl-emoji :data-name="user.status.emoji" class="gl-mr-1" />
+ <span v-safe-html="user.status.message" class="gl-text-truncate"></span>
+ <gl-tooltip
+ :target="() => $refs.statusTooltipTarget"
+ boundary="viewport"
+ placement="bottom"
+ >
+ <span v-safe-html="user.status.message"></span>
+ </gl-tooltip>
+ </span>
+ </span>
+ </template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown-group>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index dcad6a47f22..9e0c2aa1693 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { initStatusTriggers } from '../header';
import {
bindSuperSidebarCollapsedEvents,
initSuperSidebarCollapsedState,
@@ -31,3 +32,5 @@ export const initSuperSidebar = () => {
},
});
};
+
+requestIdleCallback(initStatusTriggers);
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
index 6758c8b4014..cb3293dd1c3 100644
--- a/app/assets/stylesheets/framework/super_sidebar.scss
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -1,3 +1,16 @@
+@mixin active-toggle {
+ @include gl-bg-gray-50;
+ mix-blend-mode: multiply;
+
+ .gl-dark & {
+ mix-blend-mode: screen;
+ }
+
+ .notification {
+ border-color: $gray-50;
+ }
+}
+
.super-sidebar {
@include gl-fixed;
@include gl-top-0;
@@ -28,6 +41,30 @@
.tanuki-logo {
@include gl-vertical-align-middle;
}
+
+ .user-bar-item {
+ @include gl-rounded-base;
+ @include gl-p-2;
+ @include gl-bg-transparent;
+ @include gl-border-none;
+
+
+ &:hover,
+ &:focus,
+ &:active {
+ @include active-toggle;
+ }
+
+ &:focus,
+ &:active {
+ @include gl-focus;
+ }
+ }
+
+ .gl-new-dropdown-toggle[aria-expanded='true'],
+ .gl-new-dropdown-custom-toggle[aria-expanded='true'] .user-bar-item {
+ @include active-toggle;
+ }
}
.counter .gl-icon {
@@ -36,13 +73,17 @@
.counter:hover,
.counter:focus,
- .gl-dropdown-custom-toggle:hover .counter,
- .gl-dropdown-custom-toggle:focus .counter,
- .gl-dropdown-custom-toggle[aria-expanded='true'] .counter {
+ .gl-new-dropdown-custom-toggle:hover .counter,
+ .gl-new-dropdown-custom-toggle:focus .counter,
+ .gl-new-dropdown-custom-toggle[aria-expanded='true'] .counter {
background-color: $gray-50;
border-color: transparent;
mix-blend-mode: multiply;
+ .gl-dark & {
+ mix-blend-mode: screen;
+ }
+
.gl-icon {
color: var(--gray-700, $gray-700);
}
@@ -55,7 +96,6 @@
}
.btn-with-notification {
- mix-blend-mode: unset !important; // Our tertiary buttons otherwise use another mix-blend mode, making border-color semi-transparent.
position: relative;
.notification {
@@ -77,6 +117,18 @@
}
}
}
+
+ .gl-new-dropdown-custom-toggle {
+ .btn-with-notification {
+ mix-blend-mode: unset; // Our tertiary buttons otherwise use another mix-blend mode, making border-color semi-transparent.
+ }
+
+ &[aria-expanded='true'] {
+ .btn-with-notification {
+ @include active-toggle;
+ }
+ }
+ }
}
.page-with-super-sidebar {
@@ -117,3 +169,13 @@
.with-performance-bar .super-sidebar {
top: $performance-bar-height;
}
+
+.gl-dark {
+ .super-sidebar {
+ .gl-new-dropdown-custom-toggle {
+ .btn-with-notification.btn-with-notification {
+ mix-blend-mode: unset;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index bb97261a1ca..89a49b2cf86 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -143,6 +143,17 @@ body.gl-dark {
background-color: $gray-200;
}
}
+
+ .gl-new-dropdown-item {
+ &:active,
+ &:hover,
+ &:focus,
+ &:focus:active {
+ .gl-new-dropdown-item-content {
+ @include gl-bg-gray-10;
+ }
+ }
+ }
}
// Some hacks and overrides for things that don't properly support dark mode
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 6dd4d72bbc7..a0ba5b9c8a4 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -25,7 +25,7 @@ class ConfirmationsController < Devise::ConfirmationsController
stored_location_for(resource) || dashboard_projects_path
end
- def after_confirmation_path_for(resource_name, resource)
+ def after_confirmation_path_for(_resource_name, resource)
accept_pending_invitations
# incoming resource can either be a :user or an :email
@@ -34,10 +34,14 @@ class ConfirmationsController < Devise::ConfirmationsController
else
Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}")
flash[:notice] = flash[:notice] + _(" Please sign in.")
- new_session_path(:user, anchor: 'login-pane', invite_email: resource.email)
+ sign_in_path(resource)
end
end
+ def sign_in_path(user)
+ new_session_path(:user, anchor: 'login-pane', invite_email: resource.email)
+ end
+
def check_recaptcha
return unless resource_params[:email].present?
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index a7f56ebf004..16d9f0dde21 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -41,6 +41,28 @@ module SidebarsHelper
name: user.name,
username: user.username,
avatar_url: user.avatar_url,
+ has_link_to_profile: current_user_menu?(:profile),
+ link_to_profile: user_url(user),
+ status: {
+ can_update: can?(current_user, :update_user_status, current_user),
+ busy: user.status&.busy?,
+ customized: user.status&.customized?,
+ availability: user.status&.availability.to_s,
+ emoji: user.status&.emoji,
+ message: user.status&.message_html&.html_safe,
+ clear_after: user.status&.clear_status_at.to_s
+ },
+ trial: {
+ has_start_trial: current_user_menu?(:start_trial),
+ url: trials_link_url
+ },
+ settings: {
+ has_settings: current_user_menu?(:settings),
+ profile_path: profile_path,
+ profile_preferences_path: profile_preferences_path
+ },
+ can_sign_out: current_user_menu?(:sign_out),
+ sign_out_link: destroy_user_session_path,
assigned_open_issues_count: user.assigned_open_issues_count,
todos_pending_count: user.todos_pending_count,
issues_dashboard_path: issues_dashboard_path(assignee_username: user.username),
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 62b9eb2b506..34a378abaa4 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -168,6 +168,10 @@ module UsersHelper
user.public_email.present?
end
+ def trials_link_url
+ 'https://about.gitlab.com/free-trial/'
+ end
+
private
def admin_users_paths
@@ -211,10 +215,6 @@ module UsersHelper
tabs
end
- def trials_link_url
- 'https://about.gitlab.com/free-trial/'
- end
-
def trials_allowed?(user)
false
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index f8b3777841d..0941f6d4cf0 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -756,9 +756,7 @@ module Ci
end
def remove_token!
- if Feature.enabled?(:remove_job_token_on_completion, project)
- update!(token_encrypted: nil)
- end
+ update!(token_encrypted: nil)
end
# acts_as_taggable uses this method create/remove tags with contexts
diff --git a/config/feature_flags/development/remove_job_token_on_completion.yml b/config/feature_flags/development/remove_job_token_on_completion.yml
deleted file mode 100644
index 4ab5ffc27ee..00000000000
--- a/config/feature_flags/development/remove_job_token_on_completion.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: remove_job_token_on_completion
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108021
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/386871
-milestone: '15.8'
-type: development
-group: group::pipeline execution
-default_enabled: false
diff --git a/db/structure.sql b/db/structure.sql
index a732006ceb8..6c15719abf1 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -543,6 +543,13 @@ CREATE TABLE batched_background_migration_job_transition_logs (
)
PARTITION BY RANGE (created_at);
+CREATE TABLE p_ci_runner_machine_builds (
+ partition_id bigint NOT NULL,
+ build_id bigint NOT NULL,
+ runner_machine_id bigint NOT NULL
+)
+PARTITION BY LIST (partition_id);
+
CREATE TABLE incident_management_pending_alert_escalations (
id bigint NOT NULL,
rule_id bigint NOT NULL,
@@ -11214,8 +11221,8 @@ CREATE TABLE appearances (
email_header_and_footer_enabled boolean DEFAULT false NOT NULL,
profile_image_guidelines text,
profile_image_guidelines_html text,
- pwa_short_name text,
pwa_icon text,
+ pwa_short_name text,
pwa_name text,
pwa_description text,
CONSTRAINT appearances_profile_image_guidelines CHECK ((char_length(profile_image_guidelines) <= 4096)),
@@ -11711,13 +11718,12 @@ CREATE TABLE application_settings (
encrypted_telesign_customer_xid_iv bytea,
encrypted_telesign_api_key bytea,
encrypted_telesign_api_key_iv bytea,
- disable_personal_access_tokens boolean DEFAULT false NOT NULL,
max_terraform_state_size_bytes integer DEFAULT 0 NOT NULL,
+ disable_personal_access_tokens boolean DEFAULT false NOT NULL,
bulk_import_enabled boolean DEFAULT false NOT NULL,
allow_runner_registration_token boolean DEFAULT true NOT NULL,
user_defaults_to_private_profile boolean DEFAULT false NOT NULL,
allow_possible_spam boolean DEFAULT false NOT NULL,
- default_syntax_highlighting_theme integer DEFAULT 1 NOT NULL,
encrypted_product_analytics_clickhouse_connection_string bytea,
encrypted_product_analytics_clickhouse_connection_string_iv bytea,
search_max_shard_size_gb integer DEFAULT 50 NOT NULL,
@@ -11726,8 +11732,9 @@ CREATE TABLE application_settings (
deactivation_email_additional_text text,
jira_connect_public_key_storage_enabled boolean DEFAULT false NOT NULL,
git_rate_limit_users_alertlist integer[] DEFAULT '{}'::integer[] NOT NULL,
- allow_deploy_tokens_and_keys_with_external_authn boolean DEFAULT false NOT NULL,
security_policy_global_group_approvers_enabled boolean DEFAULT true NOT NULL,
+ allow_deploy_tokens_and_keys_with_external_authn boolean DEFAULT false NOT NULL,
+ default_syntax_highlighting_theme integer DEFAULT 1 NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
@@ -18971,13 +18978,6 @@ CREATE SEQUENCE operations_user_lists_id_seq
ALTER SEQUENCE operations_user_lists_id_seq OWNED BY operations_user_lists.id;
-CREATE TABLE p_ci_runner_machine_builds (
- partition_id bigint NOT NULL,
- build_id bigint NOT NULL,
- runner_machine_id bigint NOT NULL
-)
-PARTITION BY LIST (partition_id);
-
CREATE TABLE packages_build_infos (
id bigint NOT NULL,
package_id integer NOT NULL,
diff --git a/doc/administration/geo/replication/geo_validation_tests.md b/doc/administration/geo/replication/geo_validation_tests.md
index a12dd8d9d68..cad3a396bfc 100644
--- a/doc/administration/geo/replication/geo_validation_tests.md
+++ b/doc/administration/geo/replication/geo_validation_tests.md
@@ -127,8 +127,7 @@ The following are PostgreSQL upgrade validation tests we performed.
- Description: With PostgreSQL 12 available as an opt-in version in GitLab 13.3, we tested upgrading
existing Geo installations from PostgreSQL 11 to 12. We also re-tested fresh installations of GitLab
with Geo after fixes were made to support PostgreSQL 12. These tests were done using a
- [nightly build](https://packages.gitlab.com/gitlab/nightly-builds/packages/ubuntu/bionic/gitlab-ee_13.3.6+rnightly.169516.d5209202-0_amd64.deb)
- of GitLab 13.4.
+ nightly build of GitLab 13.4.
- Outcome: Tests were successful for Geo deployments with a single database node on the primary and secondary.
We encountered known issues with repmgr and Patroni managed PostgreSQL clusters on the Geo primary. Using
PostgreSQL 12 with a database cluster on the primary is not recommended until the issues are resolved.
diff --git a/doc/api/draft_notes.md b/doc/api/draft_notes.md
index a168c41092c..e0d00517291 100644
--- a/doc/api/draft_notes.md
+++ b/doc/api/draft_notes.md
@@ -94,6 +94,27 @@ GET /projects/:id/merge_requests/:merge_request_iid/draft_notes/:draft_note_id
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/14/merge_requests/11/draft_notes/5"
```
+## Create a draft note
+
+Create a draft note for a given merge request.
+
+```plaintext
+POST /projects/:id/merge_requests/:merge_request_iid/draft_notes
+```
+
+| Attribute | Type | Required | Description |
+| --------------------------- | ----------------- | ----------- | --------------------- |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding).
+| `merge_request_iid` | integer | yes | The IID of a project merge request.
+| `note` | string | yes | The content of a note.
+| `commit_id` | string | no | The SHA of a commit to associate the draft note to.
+| `in_reply_to_discussion_id` | integer | no | The ID of a discussion the draft note replies to.
+| `resolve_discussion` | boolean | no | The associated discussion should be resolved.
+
+```shell
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/14/merge_requests/11/draft_notes?note=note
+```
+
## Delete a draft note
Deletes an existing draft note for a given merge request.
diff --git a/doc/architecture/blueprints/runner_tokens/index.md b/doc/architecture/blueprints/runner_tokens/index.md
index 7f59f8639f8..039c27b64c6 100644
--- a/doc/architecture/blueprints/runner_tokens/index.md
+++ b/doc/architecture/blueprints/runner_tokens/index.md
@@ -370,10 +370,10 @@ scope.
| GitLab Rails app | `%15.8` | Create database migration to add `config` column to `ci_runner_machines` table. |
| GitLab Runner | `%15.9` | Start sending `system_id` value in `POST /jobs/request` request and other follow-up requests that require identifying the unique system. |
| GitLab Rails app | `%15.9` | Create service similar to `StaleGroupRunnersPruneCronWorker` service to clean up `ci_runner_machines` records instead of `ci_runners` records.<br/>Existing service continues to exist but focuses only on legacy runners. |
-| GitLab Rails app | `%15.9` | Enabled `create_runner_machine` [with a flag](../../../administration/feature_flags.md) named `flag_name`. |
+| GitLab Rails app | `%15.9` | Implement the `create_runner_machine` [feature flag](../../../administration/feature_flags.md). |
| GitLab Rails app | `%15.9` | Create `ci_runner_machines` record in `POST /runners/verify` request if the runner token is prefixed with `glrt-`. |
| GitLab Rails app | `%15.9` | Use runner token + `system_id` JSON parameters in `POST /jobs/request` request in the [heartbeat request](https://gitlab.com/gitlab-org/gitlab/blob/c73c96a8ffd515295842d72a3635a8ae873d688c/lib/api/ci/helpers/runner.rb#L14-20) to update the `ci_runner_machines` cache/table. |
-| GitLab Rails app | `%15.9` | Enable runner creation workflow (`create_runner_workflow`) [with a flag](../../../administration/feature_flags.md) named `flag_name`. |
+| GitLab Rails app | `%15.9` | Implement the `create_runner_workflow` [feature flag](../../../administration/feature_flags.md). |
| GitLab Rails app | `%15.9` | Implement `create_{instance|group|project}_runner` permissions. |
| GitLab Rails app | `%15.9` | Rename `ci_runner_machines.machine_xid` column to `system_xid` to be consistent with `system_id` passed in APIs. |
| GitLab Rails app | `%15.10` | Drop `ci_runner_machines.machine_xid` column. |
diff --git a/doc/development/contributing/design.md b/doc/development/contributing/design.md
index aec487ed184..cff184380d0 100644
--- a/doc/development/contributing/design.md
+++ b/doc/development/contributing/design.md
@@ -12,7 +12,7 @@ Follow these guidelines when contributing or reviewing design and user interface
advice and best practices for code review in general.
The basis for most of these guidelines is [Pajamas](https://design.gitlab.com/),
-GitLab design system. We encourage you to [contribute to Pajamas](https://design.gitlab.com/get-started/contribute/)
+GitLab design system. We encourage you to [contribute to Pajamas](https://design.gitlab.com/get-started/contributing/)
with additions and improvements.
## Merge request reviews
diff --git a/doc/development/fe_guide/accessibility.md b/doc/development/fe_guide/accessibility.md
index af45603782f..f273c8133cb 100644
--- a/doc/development/fe_guide/accessibility.md
+++ b/doc/development/fe_guide/accessibility.md
@@ -345,7 +345,7 @@ Keep in mind that:
- When you add `:hover` styles, in most cases you should add `:focus` styles too so that the styling is applied for both mouse **and** keyboard users.
- If you remove an interactive element's `outline`, make sure you maintain visual focus state in another way such as with `box-shadow`.
-See the [Pajamas Keyboard-only page](https://design.gitlab.com/accessibility-audits/keyboard-only/) for more detail.
+See the [Pajamas Keyboard-only page](https://design.gitlab.com/accessibility/keyboard-only) for more detail.
## `tabindex`
diff --git a/doc/development/fe_guide/frontend_faq.md b/doc/development/fe_guide/frontend_faq.md
index ba28eea9265..995730796b4 100644
--- a/doc/development/fe_guide/frontend_faq.md
+++ b/doc/development/fe_guide/frontend_faq.md
@@ -69,7 +69,7 @@ banner on top of the component examples indicates that:
> component.
For example, at the time of writing, this type of warning can be observed for
-[all form components](https://design.gitlab.com/components/form/). It, however,
+all form components, such as the [checkbox](https://design.gitlab.com/components/checkbox). It, however,
doesn't imply that the component should not be used.
GitLab always asks to use `<gl-*>` components whenever a suitable component exists.
diff --git a/doc/development/fe_guide/source_editor.md b/doc/development/fe_guide/source_editor.md
index 5f2e8c1e029..1cea8ccb1ab 100644
--- a/doc/development/fe_guide/source_editor.md
+++ b/doc/development/fe_guide/source_editor.md
@@ -35,7 +35,7 @@ Vue component, but the integration of Source Editor is generally straightforward
const editor = new SourceEditor({
// Editor Options.
// The list of all accepted options can be found at
- // https://microsoft.github.io/monaco-editor/api/enums/monaco.editor.EditorOption.html
+ // https://microsoft.github.io/monaco-editor/docs.html
});
```
@@ -61,7 +61,7 @@ An instance of Source Editor accepts the following configuration options:
## API
The editor uses the same public API as
-[provided by Monaco editor](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneCodeEditor.html)
+[provided by Monaco editor](https://microsoft.github.io/monaco-editor/docs.html)
with additional functions on the instance level:
| Function | Arguments | Description
diff --git a/doc/development/fe_guide/style/html.md b/doc/development/fe_guide/style/html.md
index b1cce29bc61..c92f77e9033 100644
--- a/doc/development/fe_guide/style/html.md
+++ b/doc/development/fe_guide/style/html.md
@@ -58,7 +58,7 @@ Button tags requires a `type` attribute according to the [W3C HTML specification
### Blank target
-Arbitrarily opening links in a new tab is not recommended, so refer to the [Pajamas guidelines on links](https://design.gitlab.com/product-foundations/interaction/#links) when considering adding `target="_blank"` to links.
+Arbitrarily opening links in a new tab is not recommended, so refer to the [Pajamas guidelines on links](https://design.gitlab.com/components/link) when considering adding `target="_blank"` to links.
When using `target="_blank"` with `a` tags, you must also add the `rel="noopener noreferrer"` attribute. This prevents a security vulnerability [documented by JitBit](https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/).
diff --git a/doc/development/gitpod_internals.md b/doc/development/gitpod_internals.md
index a4674df758d..a4b340916dd 100644
--- a/doc/development/gitpod_internals.md
+++ b/doc/development/gitpod_internals.md
@@ -25,6 +25,6 @@ You can find this webhook in [Webhook Settings in `gitlab-org/gitlab`](https://g
If a webhook failed to connect for a long time, then it may have been disabled in the project.
-To re-enable a failing or failed webhook, send a test request in [Webhook Settings](https://gitlab.com/gitlab-org/gitlab/-/hooks). See [Re-enable disabled webhooks page](https://docs.gitlab.com/15.4/ee/user/project/integrations/webhooks.html#re-enable-disabled-webhooks) for more details.
+To re-enable a failing or failed webhook, send a test request in [Webhook Settings](https://gitlab.com/gitlab-org/gitlab/-/hooks). See [Re-enable disabled webhooks page](../user/project/integrations/webhooks.md#re-enable-disabled-webhooks) for more details.
After re-enabling, check the prebuilds' health in a [project's prebuilds](https://gitpod.io/t/gitlab-org/gitlab/prebuilds) and confirm that prebuilds start without any errors.
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index cc7232cd793..1957662317a 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -103,7 +103,7 @@ are very appreciative of the work done by translators and proofreaders!
- Portuguese
- Diogo Trindade - [GitLab](https://gitlab.com/luisdiogo2071317), [Crowdin](https://crowdin.com/profile/ldiogotrindade)
- Portuguese, Brazilian
- - Paulo George Gomes Bezerra - [GitLab](https://gitlab.com/paulobezerra), [Crowdin](https://crowdin.com/profile/paulogomes.rep)
+ - Paulo George Gomes Bezerra - [GitLab](https://gitlab.com/paulobezerra)
- André Gama - [GitLab](https://gitlab.com/andregamma), [Crowdin](https://crowdin.com/profile/ToeOficial)
- Eduardo Addad de Oliveira - [GitLab](https://gitlab.com/eduardoaddad), [Crowdin](https://crowdin.com/profile/eduardoaddad)
- Horberlan Brito - [GitLab](https://gitlab.com/horberlan), [Crowdin](https://crowdin.com/profile/horberlan)
diff --git a/doc/development/integrations/index.md b/doc/development/integrations/index.md
index 9fd8fb7eb61..f8814f96f1b 100644
--- a/doc/development/integrations/index.md
+++ b/doc/development/integrations/index.md
@@ -306,7 +306,7 @@ When developing a new integration, we also recommend you gate the availability b
You can provide help text in the integration form, including links to off-site documentation,
as described above in [Customize the frontend form](#customize-the-frontend-form). Refer to
-our [usability guidelines](https://design.gitlab.com/usability/helping-users/) for help text.
+our [usability guidelines](https://design.gitlab.com/usability/contextual-help) for help text.
For more detailed documentation, provide a page in `doc/user/project/integrations`,
and link it from the [Integrations overview](../../user/project/integrations/index.md).
diff --git a/doc/development/merge_request_concepts/index.md b/doc/development/merge_request_concepts/index.md
index 14d9582ad84..8e9b586d5b0 100644
--- a/doc/development/merge_request_concepts/index.md
+++ b/doc/development/merge_request_concepts/index.md
@@ -34,7 +34,7 @@ This area of the merge request is where all of the options and commit messages a
Reports are widgets within the merge request that report information about changes within the merge request. These widgets provide information to better help the author understand the changes and further improvements to the proposed changes.
-[Design Documentation](https://design.gitlab.com/regions/merge-request-reports/)
+[Design Documentation](https://design.gitlab.com/patterns/merge-request-reports)
![merge request reports](../img/merge_request_reports_v14_7.png)
diff --git a/doc/development/service_ping/implement.md b/doc/development/service_ping/implement.md
index 7a68e929c37..1f2f0cfd40e 100644
--- a/doc/development/service_ping/implement.md
+++ b/doc/development/service_ping/implement.md
@@ -174,7 +174,7 @@ Errors return a value of `-1`.
WARNING:
This functionality estimates a distinct count of a specific ActiveRecord_Relation in a given column,
-which uses the [HyperLogLog](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf) algorithm.
+which uses the [HyperLogLog](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/40671.pdf) algorithm.
As the HyperLogLog algorithm is probabilistic, the **results always include error**.
The highest encountered error rate is 4.9%.
diff --git a/doc/development/service_ping/metrics_dictionary.md b/doc/development/service_ping/metrics_dictionary.md
index 28581f81f94..90ce776af19 100644
--- a/doc/development/service_ping/metrics_dictionary.md
+++ b/doc/development/service_ping/metrics_dictionary.md
@@ -44,9 +44,9 @@ Each metric is defined in a separate YAML file consisting of a number of fields:
| `data_source` | yes | `string`; may be set to a value like `database`, `redis`, `redis_hll`, `prometheus`, `system`, `license`. |
| `data_category` | yes | `string`; [categories](#data-category) of the metric, may be set to `operational`, `optional`, `subscription`, `standard`. The default value is `optional`.|
| `instrumentation_class` | yes | `string`; [the class that implements the metric](metrics_instrumentation.md). |
-| `distribution` | yes | `array`; may be set to one of `ce, ee` or `ee`. The [distribution](https://about.gitlab.com/handbook/marketing/strategic-marketing/tiers/#definitions) where the tracked feature is available. |
+| `distribution` | yes | `array`; may be set to one of `ce, ee` or `ee`. The [distribution](https://about.gitlab.com/handbook/marketing/brand-and-product-marketing/product-and-solution-marketing/tiers/#definitions) where the tracked feature is available. |
| `performance_indicator_type` | no | `array`; may be set to one of [`gmau`, `smau`, `paid_gmau`, `umau` or `customer_health_score`](https://about.gitlab.com/handbook/business-technology/data-team/data-catalog/xmau-analysis/). |
-| `tier` | yes | `array`; may contain one or a combination of `free`, `premium` or `ultimate`. The [tier]( https://about.gitlab.com/handbook/marketing/strategic-marketing/tiers/) where the tracked feature is available. This should be verbose and contain all tiers where a metric is available. |
+| `tier` | yes | `array`; may contain one or a combination of `free`, `premium` or `ultimate`. The [tier](https://about.gitlab.com/handbook/marketing/brand-and-product-marketing/product-and-solution-marketing/tiers/#definitions) where the tracked feature is available. This should be verbose and contain all tiers where a metric is available. |
| `milestone` | yes | The milestone when the metric is introduced and when it's available to self-managed instances with the official GitLab release. |
| `milestone_removed` | no | The milestone when the metric is removed. |
| `introduced_by_url` | no | The URL to the merge request that introduced the metric to be available for self-managed instances. |
diff --git a/doc/development/snowplow/event_dictionary_guide.md b/doc/development/snowplow/event_dictionary_guide.md
index 794a9a0160c..487e3e393d2 100644
--- a/doc/development/snowplow/event_dictionary_guide.md
+++ b/doc/development/snowplow/event_dictionary_guide.md
@@ -39,8 +39,8 @@ Each event is defined in a separate YAML file consisting of the following fields
| `product_category` | no | The [product category](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/categories.yml) for the event. |
| `milestone` | no | The milestone when the event is introduced. |
| `introduced_by_url` | no | The URL to the merge request that introduced the event. |
-| `distributions` | yes | The [distributions](https://about.gitlab.com/handbook/marketing/strategic-marketing/tiers/#definitions) where the tracked feature is available. Can be set to one or more of `ce` or `ee`. |
-| `tiers` | yes | The [tiers]( https://about.gitlab.com/handbook/marketing/strategic-marketing/tiers/) where the tracked feature is available. Can be set to one or more of `free`, `premium`, or `ultimate`. |
+| `distributions` | yes | The [distributions](https://about.gitlab.com/handbook/marketing/brand-and-product-marketing/product-and-solution-marketing/tiers/#definitions) where the tracked feature is available. Can be set to one or more of `ce` or `ee`. |
+| `tiers` | yes | The [tiers](https://about.gitlab.com/handbook/marketing/brand-and-product-marketing/product-and-solution-marketing/tiers/) where the tracked feature is available. Can be set to one or more of `free`, `premium`, or `ultimate`. |
### Example event definition
diff --git a/doc/development/windows.md b/doc/development/windows.md
index bf56e16344a..99085b4b5f8 100644
--- a/doc/development/windows.md
+++ b/doc/development/windows.md
@@ -13,7 +13,7 @@ This is a guide for how to get a Windows development virtual machine on Google C
## Why Windows in Google Cloud?
-Use of Microsoft Windows operating systems on company laptops is banned under the GitLab [Approved Operating Systems policy](https://about.gitlab.com/handbook/security/approved_os.html#windows).
+Use of Microsoft Windows operating systems on company laptops is banned under the GitLab [Approved Operating Systems policy](https://about.gitlab.com/handbook/it/operating-systems/#windows).
This can make it difficult to develop features for the Windows platforms. Using GCP allows us to have a temporary Windows machine that can be removed once we're done with it.
diff --git a/doc/operations/img/copy-group-id.png b/doc/operations/img/copy-group-id.png
deleted file mode 100644
index 0ede4042c7b..00000000000
--- a/doc/operations/img/copy-group-id.png
+++ /dev/null
Binary files differ
diff --git a/doc/operations/img/create-gitlab-application.png b/doc/operations/img/create-gitlab-application.png
deleted file mode 100644
index 82c7c2d226b..00000000000
--- a/doc/operations/img/create-gitlab-application.png
+++ /dev/null
Binary files differ
diff --git a/doc/operations/img/error_tracking_setting_dsn_v14_4.png b/doc/operations/img/error_tracking_setting_dsn_v14_4.png
deleted file mode 100644
index b7ecaa047b2..00000000000
--- a/doc/operations/img/error_tracking_setting_dsn_v14_4.png
+++ /dev/null
Binary files differ
diff --git a/doc/operations/img/error_tracking_setting_v14_3.png b/doc/operations/img/error_tracking_setting_v14_3.png
deleted file mode 100644
index 14306130c97..00000000000
--- a/doc/operations/img/error_tracking_setting_v14_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/operations/img/listing_groups.png b/doc/operations/img/listing_groups.png
deleted file mode 100644
index c8574a59199..00000000000
--- a/doc/operations/img/listing_groups.png
+++ /dev/null
Binary files differ
diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md
index 2c9f93886bf..a8241905379 100644
--- a/doc/subscriptions/index.md
+++ b/doc/subscriptions/index.md
@@ -176,7 +176,7 @@ To meet GitLab for Open Source Program requirements, first add an OSI-approved o
To add a license to a project:
1. On the top bar, select **Main menu > Projects** and find your project.
-1. On the overview page, select **Add LICENSE**. If the license you want is not available as a license template, manually copy the entire, unaltered [text of your chosen license](https://opensource.org/licenses/alphabetical) into the `LICENSE` file. Note that GitLab defaults to **All rights reserved** if users do not perform this action.
+1. On the overview page, select **Add LICENSE**. If the license you want is not available as a license template, manually copy the entire, unaltered [text of your chosen license](https://opensource.org/licenses/) into the `LICENSE` file. Note that GitLab defaults to **All rights reserved** if users do not perform this action.
![Add license](img/add-license.png)
diff --git a/doc/user/group/contribution_analytics/img/group_stats_cal.png b/doc/user/group/contribution_analytics/img/group_stats_cal.png
deleted file mode 100644
index 0238c7bf088..00000000000
--- a/doc/user/group/contribution_analytics/img/group_stats_cal.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/contribution_analytics/img/group_stats_table.png b/doc/user/group/contribution_analytics/img/group_stats_table.png
deleted file mode 100644
index 1f58b9717d0..00000000000
--- a/doc/user/group/contribution_analytics/img/group_stats_table.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
index 74cfa2bd6ed..96c20e5d943 100644
--- a/doc/user/group/epics/manage_epics.md
+++ b/doc/user/group/epics/manage_epics.md
@@ -504,7 +504,7 @@ You can create a spreadsheet template to manage a pattern of consistently repeat
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an introduction to epic templates, see [GitLab Epics and Epic Template Tip](https://www.youtube.com/watch?v=D74xKFNw8vg).
-For more on epic templates, see [Epic Templates - Repeatable sets of issues](https://about.gitlab.com/handbook/marketing/strategic-marketing/getting-started/104/).
+For more on epic templates, see [Epic Templates - Repeatable sets of issues](https://about.gitlab.com/handbook/marketing/brand-and-product-marketing/product-and-solution-marketing/getting-started/104/).
## Multi-level child epics **(ULTIMATE)**
diff --git a/doc/user/project/import/img/bitbucket_import_select_project_v12_3.png b/doc/user/project/import/img/bitbucket_import_select_project_v12_3.png
deleted file mode 100644
index bbc72a0b4b7..00000000000
--- a/doc/user/project/import/img/bitbucket_import_select_project_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/import/img/fogbugz_import_finished.png b/doc/user/project/import/img/fogbugz_import_finished.png
deleted file mode 100644
index 62c5c86c9b3..00000000000
--- a/doc/user/project/import/img/fogbugz_import_finished.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/import/img/manifest_status_v13_3.png b/doc/user/project/import/img/manifest_status_v13_3.png
deleted file mode 100644
index c1a55ba1f50..00000000000
--- a/doc/user/project/import/img/manifest_status_v13_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md
index f43f87119a6..10b29feb072 100644
--- a/doc/user/project/issues/design_management.md
+++ b/doc/user/project/issues/design_management.md
@@ -227,6 +227,19 @@ New discussion threads get different pin numbers, which you can use to refer to
In GitLab 12.5 and later, new discussions are output to the issue activity,
so that everyone involved can participate in the discussion.
+## Delete a comment from a design
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/385100) in GitLab 15.9.
+
+Prerequisites:
+
+- You must have at least the Reporter role for the project.
+
+To delete a comment from a design:
+
+1. On the comment you want to delete, select **More actions** **{ellipsis_v}** **> Delete comment**.
+1. On the confirmation dialog box, select **Delete comment**.
+
## Resolve a discussion thread on a design
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13049) in GitLab 13.1.
diff --git a/doc/user/project/merge_requests/img/remove_source_branch_status.png b/doc/user/project/merge_requests/img/remove_source_branch_status.png
deleted file mode 100644
index afd93207e02..00000000000
--- a/doc/user/project/merge_requests/img/remove_source_branch_status.png
+++ /dev/null
Binary files differ
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 24f81b76e96..af7b88c73e3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1231,6 +1231,9 @@ msgstr ""
msgid "%{user} created an issue: %{issue_link}"
msgstr ""
+msgid "%{user} user’s menu"
+msgstr ""
+
msgid "%{value} is not included in the list"
msgstr ""
@@ -4714,6 +4717,9 @@ msgstr ""
msgid "Analytics"
msgstr ""
+msgid "Analytics|Analytics dashboards"
+msgstr ""
+
msgid "Analyze your dependencies for known vulnerabilities."
msgstr ""
@@ -14351,10 +14357,13 @@ msgstr ""
msgid "DesignManagement|Could not create new discussion. Please try again."
msgstr ""
-msgid "DesignManagement|Could not update discussion. Please try again."
+msgid "DesignManagement|Could not delete comment. Please try again."
+msgstr ""
+
+msgid "DesignManagement|Could not update comment. Please try again."
msgstr ""
-msgid "DesignManagement|Could not update note. Please try again."
+msgid "DesignManagement|Could not update discussion. Please try again."
msgstr ""
msgid "DesignManagement|Deselect all"
@@ -28580,6 +28589,9 @@ msgstr ""
msgid "NorthstarNavigation|New navigation"
msgstr ""
+msgid "NorthstarNavigation|Provide feedback"
+msgstr ""
+
msgid "NorthstarNavigation|Toggle new navigation"
msgstr ""
@@ -40469,6 +40481,9 @@ msgstr ""
msgid "Something went wrong when deleting a comment. Please try again"
msgstr ""
+msgid "Something went wrong when deleting a comment. Please try again."
+msgstr ""
+
msgid "Something went wrong when reordering designs. Please try again"
msgstr ""
diff --git a/qa/lib/gitlab/page/group/settings/usage_quotas.rb b/qa/lib/gitlab/page/group/settings/usage_quotas.rb
index 3cb501efe13..22e61c97bdb 100644
--- a/qa/lib/gitlab/page/group/settings/usage_quotas.rb
+++ b/qa/lib/gitlab/page/group/settings/usage_quotas.rb
@@ -23,12 +23,11 @@ module Gitlab
# Storage section
link :storage_tab
link :purchase_more_storage
- div :used_storage_message
+ div :namespace_usage_total
div :group_usage_message
div :dependency_proxy_usage
span :dependency_proxy_size
div :container_registry_usage
- div :project_storage_used
div :project
div :storage_type_legend
span :container_registry_size
diff --git a/spec/features/nav/new_nav_toggle_spec.rb b/spec/features/nav/new_nav_toggle_spec.rb
index 8e5cc7df053..2cdaf12bb15 100644
--- a/spec/features/nav/new_nav_toggle_spec.rb
+++ b/spec/features/nav/new_nav_toggle_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe 'new navigation toggle', :js, feature_category: :navigation do
it 'allows to disable new nav', :aggregate_failures do
within '[data-testid="super-sidebar"] [data-testid="user-dropdown"]' do
- find('button').click
+ click_button "#{user.name} user’s menu"
expect(page).to have_content('Navigation redesign')
toggle = page.find('.gl-toggle.is-checked')
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
index 402e55347af..e2f1d6e4b10 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
@@ -70,6 +70,8 @@ exports[`Design note component should match the snapshot 1`] = `
>
<!---->
+
+ <!---->
</div>
</div>
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index 2091e1e08dd..1c2e7033488 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -2,6 +2,8 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
@@ -10,9 +12,13 @@ import ToggleRepliesWidget from '~/design_management/components/design_notes/tog
import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
+import destroyNoteMutation from '~/design_management/graphql/mutations/destroy_note.mutation.graphql';
+import { DELETE_NOTE_ERROR_MSG } from '~/design_management/constants';
import mockDiscussion from '../../mock_data/discussion';
import notes from '../../mock_data/notes';
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+
const defaultMockDiscussion = {
id: '0',
resolved: false,
@@ -59,7 +65,7 @@ describe('Design discussions component', () => {
provider: { clients: { defaultClient: { readQuery } } },
};
- function createComponent(props = {}, data = {}) {
+ function createComponent({ props = {}, data = {}, apolloConfig = {} } = {}) {
wrapper = mount(DesignDiscussion, {
propsData: {
resolvedDiscussionsExpanded: true,
@@ -82,7 +88,10 @@ describe('Design discussions component', () => {
issueIid: '1',
},
mocks: {
- $apollo,
+ $apollo: {
+ ...$apollo,
+ ...apolloConfig,
+ },
$route: {
hash: '#note_1',
params: {
@@ -103,14 +112,17 @@ describe('Design discussions component', () => {
afterEach(() => {
wrapper.destroy();
window.gon = originalGon;
+ confirmAction.mockReset();
});
describe('when discussion is not resolvable', () => {
beforeEach(() => {
createComponent({
- discussion: {
- ...defaultMockDiscussion,
- resolvable: false,
+ props: {
+ discussion: {
+ ...defaultMockDiscussion,
+ resolvable: false,
+ },
},
});
});
@@ -171,11 +183,13 @@ describe('Design discussions component', () => {
innerText: DEFAULT_TODO_COUNT,
});
createComponent({
- discussion: {
- ...defaultMockDiscussion,
- resolved: true,
- resolvedBy: notes[0].author,
- resolvedAt: '2020-05-08T07:10:45Z',
+ props: {
+ discussion: {
+ ...defaultMockDiscussion,
+ resolved: true,
+ resolvedBy: notes[0].author,
+ resolvedAt: '2020-05-08T07:10:45Z',
+ },
},
});
});
@@ -206,10 +220,10 @@ describe('Design discussions component', () => {
});
it('emit todo:toggle when discussion is resolved', async () => {
- createComponent(
- { discussionWithOpenForm: defaultMockDiscussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
+ createComponent({
+ props: { discussionWithOpenForm: defaultMockDiscussion.id },
+ data: { discussionComment: 'test', isFormRendered: true },
+ });
findResolveButton().trigger('click');
findReplyForm().vm.$emit('submitForm');
@@ -262,10 +276,10 @@ describe('Design discussions component', () => {
});
it('calls mutation on submitting form and closes the form', async () => {
- createComponent(
- { discussionWithOpenForm: defaultMockDiscussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
+ createComponent({
+ props: { discussionWithOpenForm: defaultMockDiscussion.id },
+ data: { discussionComment: 'test', isFormRendered: true },
+ });
findReplyForm().vm.$emit('submit-form');
expect(mutate).toHaveBeenCalledWith(mutationVariables);
@@ -277,10 +291,10 @@ describe('Design discussions component', () => {
});
it('clears the discussion comment on closing comment form', async () => {
- createComponent(
- { discussionWithOpenForm: defaultMockDiscussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
+ createComponent({
+ props: { discussionWithOpenForm: defaultMockDiscussion.id },
+ data: { discussionComment: 'test', isFormRendered: true },
+ });
await nextTick();
findReplyForm().vm.$emit('cancel-form');
@@ -295,15 +309,15 @@ describe('Design discussions component', () => {
it.each([notes[0], notes[0].discussion.notes.nodes[1]])(
'applies correct class to all notes in the active discussion',
(note) => {
- createComponent(
- { discussion: mockDiscussion },
- {
+ createComponent({
+ props: { discussion: mockDiscussion },
+ data: {
activeDiscussion: {
id: note.id,
source: 'pin',
},
},
- );
+ });
expect(
wrapper
@@ -329,10 +343,10 @@ describe('Design discussions component', () => {
});
it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => {
- createComponent(
- { discussionWithOpenForm: defaultMockDiscussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
+ createComponent({
+ props: { discussionWithOpenForm: defaultMockDiscussion.id },
+ data: { discussionComment: 'test', isFormRendered: true },
+ });
findResolveButton().trigger('click');
findReplyForm().vm.$emit('submitForm');
@@ -359,15 +373,15 @@ describe('Design discussions component', () => {
beforeEach(() => {
window.gon = { current_user_id: null };
- createComponent(
- {
+ createComponent({
+ props: {
discussion: {
...defaultMockDiscussion,
},
discussionWithOpenForm: defaultMockDiscussion.id,
},
- { discussionComment: 'test', isFormRendered: true },
- );
+ data: { discussionComment: 'test', isFormRendered: true },
+ });
});
it('does not render resolve discussion button', () => {
@@ -390,4 +404,64 @@ describe('Design discussions component', () => {
});
});
});
+
+ it('should open confirmation modal when the note emits `delete-note` event', async () => {
+ createComponent();
+
+ findDesignNotes().at(0).vm.$emit('delete-note', { id: '1' });
+ expect(confirmAction).toHaveBeenCalled();
+ });
+
+ describe('when confirmation modal is opened', () => {
+ const noteId = 'note-test-id';
+
+ it('sends the mutation with correct variables', async () => {
+ confirmAction.mockResolvedValueOnce(true);
+ const destroyNoteMutationSuccess = jest.fn().mockResolvedValue({
+ data: { destroyNote: { note: null, __typename: 'DestroyNote', errors: [] } },
+ });
+ createComponent({ apolloConfig: { mutate: destroyNoteMutationSuccess } });
+
+ findDesignNotes().at(0).vm.$emit('delete-note', { id: noteId });
+
+ expect(confirmAction).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(destroyNoteMutationSuccess).toHaveBeenCalledWith({
+ update: expect.any(Function),
+ mutation: destroyNoteMutation,
+ variables: {
+ input: {
+ id: noteId,
+ },
+ },
+ optimisticResponse: {
+ destroyNote: {
+ note: null,
+ errors: [],
+ __typename: 'DestroyNotePayload',
+ },
+ },
+ });
+ });
+
+ it('emits `delete-note-error` event if GraphQL mutation fails', async () => {
+ confirmAction.mockResolvedValueOnce(true);
+ const destroyNoteMutationError = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ createComponent({ apolloConfig: { mutate: destroyNoteMutationError } });
+
+ findDesignNotes().at(0).vm.$emit('delete-note', { id: noteId });
+
+ await waitForPromises();
+
+ expect(destroyNoteMutationError).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted()).toEqual({
+ 'delete-note-error': [[DELETE_NOTE_ERROR_MSG]],
+ });
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index df511586c10..ba4ee3cbe03 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -1,6 +1,6 @@
import { ApolloMutation } from 'vue-apollo';
import { nextTick } from 'vue';
-import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
+import { GlAvatar, GlAvatarLink, GlDropdown } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
@@ -38,6 +38,8 @@ describe('Design note component', () => {
const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
const findEditButton = () => wrapper.findByTestId('note-edit');
const findNoteContent = () => wrapper.findByTestId('note-text');
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-button"]');
function createComponent(props = {}, data = { isEditing: false }) {
wrapper = shallowMountExtended(DesignNote, {
@@ -112,6 +114,14 @@ describe('Design note component', () => {
expect(findEditButton().exists()).toBe(false);
});
+ it('should not display a dropdown if user does not have a permission to delete note', () => {
+ createComponent({
+ note,
+ });
+
+ expect(findDropdown().exists()).toBe(false);
+ });
+
describe('when user has a permission to edit note', () => {
it('should open an edit form on edit button click', async () => {
createComponent({
@@ -169,4 +179,38 @@ describe('Design note component', () => {
});
});
});
+
+ describe('when user has a permission to delete note', () => {
+ it('should display a dropdown', () => {
+ createComponent({
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ },
+ },
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ });
+ });
+
+ it('should emit `delete-note` event with proper payload when delete note button is clicked', async () => {
+ const payload = {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ },
+ };
+
+ createComponent({
+ note: {
+ ...payload,
+ },
+ });
+
+ findDeleteNoteButton().vm.$emit('click');
+
+ expect(wrapper.emitted()).toEqual({ 'delete-note': [[{ ...payload }]] });
+ });
});
diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js
index af995f75ddc..f6e62de5ff1 100644
--- a/spec/frontend/design_management/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management/components/design_sidebar_spec.js
@@ -143,8 +143,8 @@ describe('Design management design sidebar component', () => {
expect(findResolvedCommentsToggle().props('visible')).toBe(true);
});
- it('sends a mutation to set an active discussion when clicking on a discussion', () => {
- findFirstDiscussion().trigger('click');
+ it('emits correct event to send a mutation to set an active discussion when clicking on a discussion', () => {
+ findFirstDiscussion().vm.$emit('update-active-discussion');
expect(mutate).toHaveBeenCalledWith(updateActiveDiscussionMutationVariables);
});
diff --git a/spec/frontend/nav/components/new_nav_toggle_spec.js b/spec/frontend/nav/components/new_nav_toggle_spec.js
index bad24345f9d..e545c861d5d 100644
--- a/spec/frontend/nav/components/new_nav_toggle_spec.js
+++ b/spec/frontend/nav/components/new_nav_toggle_spec.js
@@ -1,7 +1,7 @@
import { mount, createWrapper } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { getByText as getByTextHelper } from '@testing-library/dom';
-import { GlToggle } from '@gitlab/ui';
+import { GlDisclosureDropdownItem, GlToggle } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
@@ -20,6 +20,7 @@ describe('NewNavToggle', () => {
let wrapper;
const findToggle = () => wrapper.findComponent(GlToggle);
+ const findDisclosureItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
const createComponent = (propsData = { enabled: false }) => {
wrapper = mount(NewNavToggle, {
@@ -37,76 +38,153 @@ describe('NewNavToggle', () => {
const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options));
- it('renders its title', () => {
- createComponent();
- expect(getByText('Navigation redesign').exists()).toBe(true);
- });
+ describe('When rendered in scope of the new navigation', () => {
+ it('renders the disclosure item', () => {
+ createComponent({ newNavigation: true, enabled: true });
+ expect(findDisclosureItem().exists()).toBe(true);
+ });
+
+ describe('when user preference is enabled', () => {
+ beforeEach(() => {
+ createComponent({ newNavigation: true, enabled: true });
+ });
- describe('when user preference is enabled', () => {
- beforeEach(() => {
- createComponent({ enabled: true });
+ it('renders the toggle as enabled', () => {
+ expect(findToggle().props('value')).toBe(true);
+ });
});
- it('renders the toggle as enabled', () => {
- expect(findToggle().props('value')).toBe(true);
+ describe('when user preference is disabled', () => {
+ beforeEach(() => {
+ createComponent({ enabled: false });
+ });
+
+ it('renders the toggle as disabled', () => {
+ expect(findToggle().props('value')).toBe(false);
+ });
+ });
+
+ describe.each`
+ desc | actFn
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')}
+ ${'on menu item action'} | ${() => findDisclosureItem().vm.$emit('action')}
+ `('$desc', ({ actFn }) => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent({ enabled: false, newNavigation: true });
+ });
+
+ it('reloads the page on success', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
+
+ actFn();
+ await waitForPromises();
+
+ expect(window.location.reload).toHaveBeenCalled();
+ });
+
+ it('shows an alert on error', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+
+ actFn();
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: s__(
+ 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
+ ),
+ }),
+ );
+ expect(window.location.reload).not.toHaveBeenCalled();
+ });
+
+ it('changes the toggle', async () => {
+ await actFn();
+
+ expect(findToggle().props('value')).toBe(true);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
});
});
- describe('when user preference is disabled', () => {
- beforeEach(() => {
- createComponent({ enabled: false });
+ describe('When rendered in scope of the current navigation', () => {
+ it('renders its title', () => {
+ createComponent();
+ expect(getByText('Navigation redesign').exists()).toBe(true);
});
- it('renders the toggle as disabled', () => {
- expect(findToggle().props('value')).toBe(false);
+ describe('when user preference is enabled', () => {
+ beforeEach(() => {
+ createComponent({ enabled: true });
+ });
+
+ it('renders the toggle as enabled', () => {
+ expect(findToggle().props('value')).toBe(true);
+ });
});
- });
- describe.each`
- desc | actFn
- ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')}
- ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')}
- `('$desc', ({ actFn }) => {
- let mock;
+ describe('when user preference is disabled', () => {
+ beforeEach(() => {
+ createComponent({ enabled: false });
+ });
- beforeEach(() => {
- mock = new MockAdapter(axios);
- createComponent({ enabled: false });
+ it('renders the toggle as disabled', () => {
+ expect(findToggle().props('value')).toBe(false);
+ });
});
- it('reloads the page on success', async () => {
- mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
+ describe.each`
+ desc | actFn
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')}
+ `('$desc', ({ actFn }) => {
+ let mock;
- actFn();
- await waitForPromises();
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent({ enabled: false });
+ });
- expect(window.location.reload).toHaveBeenCalled();
- });
+ it('reloads the page on success', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
- it('shows an alert on error', async () => {
- mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ actFn();
+ await waitForPromises();
- actFn();
- await waitForPromises();
+ expect(window.location.reload).toHaveBeenCalled();
+ });
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: s__(
- 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
- ),
- }),
- );
- expect(window.location.reload).not.toHaveBeenCalled();
- });
+ it('shows an alert on error', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- it('changes the toggle', async () => {
- await actFn();
+ actFn();
+ await waitForPromises();
- expect(findToggle().props('value')).toBe(true);
- });
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: s__(
+ 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
+ ),
+ }),
+ );
+ expect(window.location.reload).not.toHaveBeenCalled();
+ });
+
+ it('changes the toggle', async () => {
+ await actFn();
+
+ expect(findToggle().props('value')).toBe(true);
+ });
- afterEach(() => {
- mock.restore();
+ afterEach(() => {
+ mock.restore();
+ });
});
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
index a2ec527ce12..a2bdba87980 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
@@ -198,9 +198,13 @@ describe('PackagesListApp', () => {
});
});
- describe('empty state', () => {
+ describe.each`
+ description | resolverResponse
+ ${'empty response'} | ${packagesListQuery({ extend: { nodes: [] } })}
+ ${'error response'} | ${{ data: { group: null } }}
+ `(`$description renders empty state`, ({ resolverResponse }) => {
beforeEach(() => {
- const resolver = jest.fn().mockResolvedValue(packagesListQuery({ extend: { nodes: [] } }));
+ const resolver = jest.fn().mockResolvedValue(resolverResponse);
mountComponent({ resolver });
return waitForFirstRequest();
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
new file mode 100644
index 00000000000..9641cc40cc5
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -0,0 +1,221 @@
+import { GlAvatar } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UserMenu from '~/super_sidebar/components/user_menu.vue';
+import UserNameGroup from '~/super_sidebar/components/user_name_group.vue';
+import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
+import invalidUrl from '~/lib/utils/invalid_url';
+import { userMenuMockData, userMenuMockStatus } from '../mock_data';
+
+describe('UserMenu component', () => {
+ let wrapper;
+
+ const GlEmoji = { template: '<img/>' };
+ const toggleNewNavEndpoint = invalidUrl;
+
+ const createWrapper = (userDataChanges = {}) => {
+ wrapper = mountExtended(UserMenu, {
+ propsData: {
+ data: {
+ ...userMenuMockData,
+ ...userDataChanges,
+ },
+ },
+ stubs: {
+ GlEmoji,
+ GlAvatar: true,
+ },
+ provide: {
+ toggleNewNavEndpoint,
+ },
+ });
+ };
+
+ describe('Toggle button', () => {
+ let toggle;
+
+ beforeEach(() => {
+ createWrapper();
+ toggle = wrapper.findByTestId('base-dropdown-toggle');
+ });
+
+ it('renders User Avatar in a toggle', () => {
+ const avatar = toggle.findComponent(GlAvatar);
+ expect(avatar.exists()).toBe(true);
+ expect(avatar.props()).toMatchObject({
+ entityName: userMenuMockData.name,
+ src: userMenuMockData.avatar_url,
+ });
+ });
+
+ it('renders screen reader text', () => {
+ expect(toggle.find('.gl-sr-only').text()).toBe(`${userMenuMockData.name} user’s menu`);
+ });
+ });
+
+ describe('User Menu Group', () => {
+ it('renders and passes data to it', () => {
+ createWrapper();
+ const userNameGroup = wrapper.findComponent(UserNameGroup);
+ expect(userNameGroup.exists()).toBe(true);
+ expect(userNameGroup.props('user')).toEqual(userMenuMockData);
+ });
+ });
+
+ describe('User status item', () => {
+ let item;
+
+ const setItem = ({ can_update, busy, customized } = {}) => {
+ createWrapper({ status: { ...userMenuMockStatus, can_update, busy, customized } });
+ item = wrapper.findByTestId('status-item');
+ };
+
+ describe('When user cannot update the status', () => {
+ it('does not render the status menu item', () => {
+ setItem();
+ expect(item.exists()).toBe(false);
+ });
+ });
+
+ describe('When user can update the status', () => {
+ it('renders the status menu item', () => {
+ setItem({ can_update: true });
+ expect(item.exists()).toBe(true);
+ });
+
+ it('should set the CSS class for triggering status update modal', () => {
+ setItem({ can_update: true });
+ expect(item.find('.js-set-status-modal-trigger').exists()).toBe(true);
+ });
+
+ describe('renders correct label', () => {
+ it.each`
+ busy | customized | label
+ ${false} | ${false} | ${'Set status'}
+ ${false} | ${true} | ${'Edit status'}
+ ${true} | ${false} | ${'Edit status'}
+ ${true} | ${true} | ${'Edit status'}
+ `(
+ 'when busy is "$busy" and customized is "$customized" the label is "$label"',
+ ({ busy, customized, label }) => {
+ setItem({ can_update: true, busy, customized });
+ expect(item.text()).toBe(label);
+ },
+ );
+ });
+
+ describe('Status update modal wrapper', () => {
+ const findModalWrapper = () => wrapper.find('.js-set-status-modal-wrapper');
+
+ it('renders the modal wrapper', () => {
+ setItem({ can_update: true });
+ expect(findModalWrapper().exists()).toBe(true);
+ });
+
+ it('sets default data attributes when status is not customized', () => {
+ setItem({ can_update: true });
+ expect(findModalWrapper().attributes()).toMatchObject({
+ 'data-current-emoji': '',
+ 'data-current-message': '',
+ 'data-default-emoji': 'speech_balloon',
+ });
+ });
+
+ it('sets user status as data attributes when status is customized', () => {
+ setItem({ can_update: true, customized: true });
+ expect(findModalWrapper().attributes()).toMatchObject({
+ 'data-current-emoji': userMenuMockStatus.emoji,
+ 'data-current-message': userMenuMockStatus.message,
+ 'data-current-availability': userMenuMockStatus.availability,
+ 'data-current-clear-status-after': userMenuMockStatus.clear_after,
+ });
+ });
+ });
+ });
+ });
+
+ describe('Start Ultimate trial item', () => {
+ let item;
+
+ const setItem = ({ has_start_trial } = {}) => {
+ createWrapper({ status: { ...userMenuMockStatus, has_start_trial } });
+ item = wrapper.findByTestId('start-trial-item');
+ };
+
+ describe('When Ultimate trial is not suggested for the user', () => {
+ it('does not render the start triel menu item', () => {
+ setItem();
+ expect(item.exists()).toBe(false);
+ });
+ });
+
+ describe('When Ultimate trial can be suggested for the user', () => {
+ it('does not render the status menu item', () => {
+ setItem({ has_start_trial: true });
+ expect(item.exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Edit profile item', () => {
+ it('should render a link to the profile page', () => {
+ createWrapper();
+ const item = wrapper.findByTestId('edit-profile-item');
+ expect(item.text()).toBe(UserMenu.i18n.user.editProfile);
+ expect(item.find('a').attributes('href')).toBe(userMenuMockData.settings.profile_path);
+ });
+ });
+
+ describe('Preferences item', () => {
+ it('should render a link to the profile page', () => {
+ createWrapper();
+ const item = wrapper.findByTestId('preferences-item');
+ expect(item.text()).toBe(UserMenu.i18n.user.preferences);
+ expect(item.find('a').attributes('href')).toBe(
+ userMenuMockData.settings.profile_preferences_path,
+ );
+ });
+ });
+
+ describe('New navigation toggle item', () => {
+ it('should render menu item with new navigation toggle', () => {
+ createWrapper();
+ const toggleItem = wrapper.findComponent(NewNavToggle);
+ expect(toggleItem.exists()).toBe(true);
+ expect(toggleItem.props('endpoint')).toBe(toggleNewNavEndpoint);
+ });
+ });
+
+ describe('Feedback item', () => {
+ it('should render feedback item with a link to a new GitLab issue', () => {
+ createWrapper();
+ const feedbackItem = wrapper.findByTestId('feedback-item');
+ expect(feedbackItem.find('a').attributes('href')).toBe(UserMenu.feedbackUrl);
+ });
+ });
+
+ describe('Sign out group', () => {
+ const findSignOutGroup = () => wrapper.findByTestId('sign-out-group');
+
+ it('should not render sign out group when user cannot sign out', () => {
+ createWrapper();
+ expect(findSignOutGroup().exists()).toBe(false);
+ });
+
+ describe('when user can sign out', () => {
+ beforeEach(() => {
+ createWrapper({ can_sign_out: true });
+ });
+
+ it('should render sign out group', () => {
+ expect(findSignOutGroup().exists()).toBe(true);
+ });
+
+ it('should render the menu item with a link to sign out and correct data attribute', () => {
+ expect(findSignOutGroup().find('a').attributes('href')).toBe(
+ userMenuMockData.sign_out_link,
+ );
+ expect(findSignOutGroup().find('a').attributes('data-method')).toBe('post');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/user_name_group_spec.js b/spec/frontend/super_sidebar/components/user_name_group_spec.js
new file mode 100644
index 00000000000..c06c8c218d4
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/user_name_group_spec.js
@@ -0,0 +1,100 @@
+import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlTooltip } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import UserNameGroup from '~/super_sidebar/components/user_name_group.vue';
+import { userMenuMockData, userMenuMockStatus } from '../mock_data';
+
+describe('UserNameGroup component', () => {
+ let wrapper;
+
+ const findGlDisclosureDropdownGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup);
+ const findGlDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
+ const findGlTooltip = () => wrapper.findComponent(GlTooltip);
+ const findUserStatus = () => wrapper.findByTestId('user-menu-status');
+
+ const GlEmoji = { template: '<img/>' };
+
+ const createWrapper = (userDataChanges = {}) => {
+ wrapper = shallowMountExtended(UserNameGroup, {
+ propsData: {
+ user: {
+ ...userMenuMockData,
+ ...userDataChanges,
+ },
+ },
+ stubs: {
+ GlEmoji,
+ GlDisclosureDropdownItem,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('renders the menu item in a separate group', () => {
+ expect(findGlDisclosureDropdownGroup().exists()).toBe(true);
+ });
+
+ it('renders menu item', () => {
+ expect(findGlDisclosureDropdownItem().exists()).toBe(true);
+ });
+
+ it('passes the item to the disclosure dropdown item', () => {
+ expect(findGlDisclosureDropdownItem().props('item')).toEqual({
+ text: userMenuMockData.name,
+ href: userMenuMockData.link_to_profile,
+ });
+ });
+
+ it("renders user's name", () => {
+ expect(findGlDisclosureDropdownItem().text()).toContain(userMenuMockData.name);
+ });
+
+ it("renders user's username", () => {
+ expect(findGlDisclosureDropdownItem().text()).toContain(userMenuMockData.username);
+ });
+
+ describe('Busy status', () => {
+ it('should not render "Busy" when user is NOT busy', () => {
+ expect(findGlDisclosureDropdownItem().text()).not.toContain('Busy');
+ });
+ it('should render "Busy" when user is busy', () => {
+ createWrapper({ status: { customized: true, busy: true } });
+
+ expect(findGlDisclosureDropdownItem().text()).toContain('Busy');
+ });
+ });
+
+ describe('User status', () => {
+ describe('when not customized', () => {
+ it('should not render it', () => {
+ expect(findUserStatus().exists()).toBe(false);
+ });
+ });
+
+ describe('when customized', () => {
+ beforeEach(() => {
+ createWrapper({ status: { ...userMenuMockStatus, customized: true } });
+ });
+
+ it('should render it', () => {
+ expect(findUserStatus().exists()).toBe(true);
+ });
+
+ it('should render status emoji', () => {
+ expect(findUserStatus().findComponent(GlEmoji).attributes('data-name')).toBe(
+ userMenuMockData.status.emoji,
+ );
+ });
+
+ it('should render status message', () => {
+ expect(findUserStatus().text()).toContain(userMenuMockData.status.message);
+ });
+
+ it("sets the tooltip's target to the status container", () => {
+ expect(findGlTooltip().props('target')?.()).toBe(findUserStatus().element);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index 9ec9b0f385e..d8a6ae85f0d 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -1,3 +1,5 @@
+import invalidUrl from '~/lib/utils/invalid_url';
+
export const createNewMenuGroups = [
{
name: 'This group',
@@ -80,3 +82,31 @@ export const sidebarData = {
gitlab_version: { major: 16, minor: 0 },
gitlab_version_check: { severity: 'success' },
};
+
+export const userMenuMockStatus = {
+ can_update: false,
+ busy: false,
+ customized: false,
+ emoji: 'art',
+ message: 'Working on user menu in super sidebar',
+ availability: 'busy',
+ clear_after: '2023-02-09 20:06:35 UTC',
+};
+
+export const userMenuMockData = {
+ name: 'Orange Fox',
+ username: 'thefox',
+ avatar_url: invalidUrl,
+ has_link_to_profile: true,
+ link_to_profile: '/thefox',
+ status: userMenuMockStatus,
+ trial: {
+ has_start_trial: false,
+ },
+ settings: {
+ profile_path: invalidUrl,
+ profile_preferences_path: invalidUrl,
+ },
+ can_sign_out: false,
+ sign_out_link: invalidUrl,
+};
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index 07b61ce1cbe..1e1087e72f6 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -72,6 +72,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
before do
allow(helper).to receive(:current_user) { user }
+ allow(helper).to receive(:can?).and_return(true)
allow(panel).to receive(:super_sidebar_menu_items).and_return(nil)
allow(panel).to receive(:super_sidebar_context_header).and_return(nil)
Rails.cache.write(['users', user.id, 'assigned_open_issues_count'], 1)
@@ -88,6 +89,28 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
name: user.name,
username: user.username,
avatar_url: user.avatar_url,
+ has_link_to_profile: helper.current_user_menu?(:profile),
+ link_to_profile: user_url(user),
+ status: {
+ can_update: helper.can?(user, :update_user_status, user),
+ busy: user.status&.busy?,
+ customized: user.status&.customized?,
+ availability: user.status&.availability.to_s,
+ emoji: user.status&.emoji,
+ message: user.status&.message_html&.html_safe,
+ clear_after: user.status&.clear_status_at.to_s
+ },
+ trial: {
+ has_start_trial: helper.current_user_menu?(:start_trial),
+ url: helper.trials_link_url
+ },
+ settings: {
+ has_settings: helper.current_user_menu?(:settings),
+ profile_path: profile_path,
+ profile_preferences_path: profile_preferences_path
+ },
+ can_sign_out: helper.current_user_menu?(:sign_out),
+ sign_out_link: destroy_user_session_path,
assigned_open_issues_count: 1,
todos_pending_count: 3,
issues_dashboard_path: issues_dashboard_path(assignee_username: user.username),
@@ -145,7 +168,8 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
items: array_including(
{ href: "/projects/new", text: "New project/repository" },
{ href: "/groups/new#create-group-pane", text: "New subgroup" },
- { href: "/groups/#{group.full_path}/-/group_members", text: "Invite members" }
+ { href: "/groups/#{group.full_path}/-/group_members",
+ text: "Invite members" }
)
),
a_hash_including(
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index c2c78be6a0f..07ca362a450 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -468,4 +468,14 @@ RSpec.describe UsersHelper do
expect(data[:paths]).to match_schema('entities/admin_users_data_attributes_paths')
end
end
+
+ describe '#trials_link_url' do
+ it 'returns the correct URL' do
+ if Gitlab.ee?
+ expect(trials_link_url).to eq('/-/trial_registrations/new?glm_content=top-right-dropdown&glm_source=gitlab.com')
+ else
+ expect(trials_link_url).to eq('https://about.gitlab.com/free-trial/')
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 2b3dc97e06d..492768df14e 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -5788,12 +5788,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
expect(build.token).to be_nil
expect(build.changes).to be_empty
end
-
- it 'does not remove the token when FF is disabled' do
- stub_feature_flags(remove_job_token_on_completion: false)
-
- expect { build.remove_token! }.not_to change(build, :token)
- end
end
describe 'metadata partitioning', :ci_partitioning do