summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/group_settings/components/shared_runners_form.vue38
-rw-r--r--app/assets/javascripts/group_settings/constants.js11
-rw-r--r--app/assets/javascripts/group_settings/mount_shared_runners.js5
-rw-r--r--app/assets/javascripts/issues/constants.js5
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue165
-rw-r--r--app/assets/javascripts/issues/show/components/new_header_actions_popover.vue82
-rw-r--r--app/assets/javascripts/issues/show/constants.js2
-rw-r--r--app/assets/javascripts/issues/show/index.js2
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js2
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue7
-rw-r--r--app/assets/javascripts/right_sidebar.js6
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue27
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue15
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue141
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue96
-rw-r--r--app/assets/stylesheets/components/detail_page.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss10
-rw-r--r--app/controllers/application_controller.rb5
-rw-r--r--app/controllers/graphql_controller.rb7
-rw-r--r--app/controllers/projects/incidents_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/finders/packages/conan/package_finder.rb28
-rw-r--r--app/helpers/ci/runners_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb4
-rw-r--r--app/helpers/issues_helper.rb5
-rw-r--r--app/helpers/merge_requests_helper.rb8
-rw-r--r--app/models/concerns/protected_branch_access.rb6
-rw-r--r--app/models/concerns/protected_ref_access.rb20
-rw-r--r--app/models/packages/package.rb1
-rw-r--r--app/models/protected_tag/create_access_level.rb28
-rw-r--r--app/services/packages/conan/search_service.rb37
-rw-r--r--app/services/packages/conan/single_package_search_service.rb50
-rw-r--r--app/views/groups/runners/register.html.haml2
-rw-r--r--app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml3
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml31
-rw-r--r--app/views/shared/issue_type/_details_header.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
38 files changed, 661 insertions, 217 deletions
diff --git a/app/assets/javascripts/group_settings/components/shared_runners_form.vue b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
index 8011090f1cb..a4ec48ffd2f 100644
--- a/app/assets/javascripts/group_settings/components/shared_runners_form.vue
+++ b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
@@ -1,7 +1,16 @@
<script>
import { GlToggle, GlAlert } from '@gitlab/ui';
+import { sprintf } from '~/locale';
import { updateGroup } from '~/api/groups_api';
-import { I18N_UPDATE_ERROR_MESSAGE, I18N_REFRESH_MESSAGE } from '../constants';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import {
+ I18N_CONFIRM_MESSAGE,
+ I18N_CONFIRM_OK,
+ I18N_CONFIRM_CANCEL,
+ I18N_CONFIRM_TITLE,
+ I18N_UPDATE_ERROR_MESSAGE,
+ I18N_REFRESH_MESSAGE,
+} from '../constants';
export default {
components: {
@@ -10,6 +19,8 @@ export default {
},
inject: [
'groupId',
+ 'groupName',
+ 'groupIsEmpty',
'sharedRunnersSetting',
'parentSharedRunnersSetting',
'runnerEnabledValue',
@@ -39,9 +50,28 @@ export default {
},
},
methods: {
- onSharedRunnersToggle(value) {
- const newSetting = value ? this.runnerEnabledValue : this.runnerDisabledValue;
- this.updateSetting(newSetting);
+ async onSharedRunnersToggle(enabled) {
+ if (enabled) {
+ this.updateSetting(this.runnerEnabledValue);
+ return;
+ }
+ if (this.groupIsEmpty) {
+ this.updateSetting(this.runnerDisabledValue);
+ return;
+ }
+
+ // Confirm when disabling for a group with subgroups or projects
+ const confirmDisabled = await confirmAction(I18N_CONFIRM_MESSAGE, {
+ title: sprintf(I18N_CONFIRM_TITLE, { groupName: this.groupName }),
+ cancelBtnText: I18N_CONFIRM_CANCEL,
+ primaryBtnText: I18N_CONFIRM_OK,
+ primaryBtnVariant: 'danger',
+ size: 'md',
+ });
+
+ if (confirmDisabled) {
+ this.updateSetting(this.runnerDisabledValue);
+ }
},
onOverrideToggle(value) {
const newSetting = value ? this.runnerAllowOverrideValue : this.runnerDisabledValue;
diff --git a/app/assets/javascripts/group_settings/constants.js b/app/assets/javascripts/group_settings/constants.js
index 1b44161903d..d4ac7d94bf4 100644
--- a/app/assets/javascripts/group_settings/constants.js
+++ b/app/assets/javascripts/group_settings/constants.js
@@ -1,4 +1,13 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+
+export const I18N_CONFIRM_MESSAGE = s__(
+ 'Runners|Shared runners will be disabled for all projects and subgroups in this group. If you proceed, you must manually re-enable shared runners in the settings of each project and subgroup.',
+);
+export const I18N_CONFIRM_OK = s__('Runners|Yes, disable shared runners');
+export const I18N_CONFIRM_CANCEL = s__('Runners|No, keep shared runners enabled');
+export const I18N_CONFIRM_TITLE = s__(
+ 'Runners|Are you sure you want to disable shared runners for %{groupName}?',
+);
export const I18N_UPDATE_ERROR_MESSAGE = __('An error occurred while updating configuration.');
export const I18N_REFRESH_MESSAGE = __('Refresh the page and try again.');
diff --git a/app/assets/javascripts/group_settings/mount_shared_runners.js b/app/assets/javascripts/group_settings/mount_shared_runners.js
index e7e104d61b3..0767330cd54 100644
--- a/app/assets/javascripts/group_settings/mount_shared_runners.js
+++ b/app/assets/javascripts/group_settings/mount_shared_runners.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import UpdateSharedRunnersForm from './components/shared_runners_form.vue';
export default (containerId = 'update-shared-runners-form') => {
@@ -6,6 +7,8 @@ export default (containerId = 'update-shared-runners-form') => {
const {
groupId,
+ groupName,
+ groupIsEmpty,
sharedRunnersSetting,
parentSharedRunnersSetting,
runnerEnabledValue,
@@ -17,6 +20,8 @@ export default (containerId = 'update-shared-runners-form') => {
el: containerEl,
provide: {
groupId,
+ groupName,
+ groupIsEmpty: parseBoolean(groupIsEmpty),
sharedRunnersSetting,
parentSharedRunnersSetting,
runnerEnabledValue,
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
index 0c563a1e952..c79612ad5d0 100644
--- a/app/assets/javascripts/issues/constants.js
+++ b/app/assets/javascripts/issues/constants.js
@@ -26,3 +26,8 @@ export const issuableStatusText = {
[STATUS_MERGED]: __('Merged'),
[STATUS_LOCKED]: __('Open'),
};
+
+export const IssuableTypeText = {
+ [TYPE_ISSUE]: __('issue'),
+ [TYPE_MERGE_REQUEST]: __('merge request'),
+};
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 84def374d13..b929c4dbae0 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -2,23 +2,36 @@
import {
GlButton,
GlDropdown,
+ GlDropdownDivider,
GlDropdownItem,
GlLink,
GlModal,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { mapActions, mapGetters, mapState } from 'vuex';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
-import { STATUS_CLOSED, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
-import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
+import { STATUS_CLOSED, TYPE_INCIDENT, TYPE_ISSUE, IssuableTypeText } from '~/issues/constants';
+import {
+ ISSUE_STATE_EVENT_CLOSE,
+ ISSUE_STATE_EVENT_REOPEN,
+ NEW_ACTIONS_POPOVER_KEY,
+} from '~/issues/show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { getCookie, parseBoolean, setCookie } from '~/lib/utils/common_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
import eventHub from '~/notes/event_hub';
import Tracking from '~/tracking';
+import toast from '~/vue_shared/plugins/global_toast';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
+import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
+import IssuableLockForm from '~/sidebar/components/lock/issuable_lock_form.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import issuesEventHub from '../event_hub';
import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql';
import updateIssueMutation from '../queries/update_issue.mutation.graphql';
@@ -44,21 +57,27 @@ export default {
'The issue was successfully promoted to an epic. Redirecting to epic...',
),
reportAbuse: __('Report abuse to administrator'),
+ referenceFetchError: __('An error occurred while fetching reference'),
+ copyReferenceText: __('Copy reference'),
},
components: {
DeleteIssueModal,
GlButton,
GlDropdown,
+ GlDropdownDivider,
GlDropdownItem,
GlLink,
GlModal,
AbuseCategorySelector,
+ NewHeaderActionsPopover,
+ SidebarSubscriptionsWidget,
+ IssuableLockForm,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
},
- mixins: [trackingMixin],
+ mixins: [trackingMixin, glFeatureFlagMixin()],
inject: {
canCreateIssue: {
default: false,
@@ -105,15 +124,46 @@ export default {
reportedFromUrl: {
default: '',
},
+ issuableEmailAddress: {
+ default: '',
+ },
+ fullPath: {
+ default: '',
+ },
},
data() {
return {
isReportAbuseDrawerOpen: false,
};
},
+ apollo: {
+ issuableReference: {
+ query: issueReferenceQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: this.iid,
+ };
+ },
+ update(data) {
+ return data.workspace?.issuable?.reference || '';
+ },
+ skip() {
+ return !this.isMrSidebarMoved;
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.referenceFetchError });
+ Sentry.captureException(error);
+ },
+ },
+ },
computed: {
...mapState(['isToggleStateButtonLoading']),
...mapGetters(['openState', 'getBlockedByIssues']),
+ ...mapGetters(['getNoteableData']),
+ isLocked() {
+ return this.getNoteableData.discussion_locked;
+ },
isClosed() {
return this.openState === STATUS_CLOSED;
},
@@ -157,6 +207,17 @@ export default {
hasMobileDropdown() {
return this.hasDesktopDropdown || this.showToggleIssueStateButton;
},
+ copyMailAddressText() {
+ return sprintf(__('Copy %{issueType} email address'), {
+ issueType: IssuableTypeText[this.issueType],
+ });
+ },
+ isMrSidebarMoved() {
+ return this.glFeatures.movedMrSidebar;
+ },
+ showLockIssueOption() {
+ return this.isMrSidebarMoved && this.issueType === TYPE_ISSUE;
+ },
},
created() {
eventHub.$on('toggle.issuable.state', this.toggleIssueState);
@@ -166,6 +227,7 @@ export default {
},
methods: {
...mapActions(['toggleStateButtonLoading']),
+ ...mapActions(['updateLockedAttribute']),
toggleIssueState() {
if (!this.isClosed && this.getBlockedByIssues?.length) {
this.$refs.blockedByIssuesModal.show();
@@ -244,7 +306,19 @@ export default {
edit() {
issuesEventHub.$emit('open.form');
},
+ dismissPopover() {
+ if (this.isMrSidebarMoved && !parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`))) {
+ setCookie(NEW_ACTIONS_POPOVER_KEY, true);
+ }
+ },
+ copyReference() {
+ toast(__('Reference copied'));
+ },
+ copyEmailAddress() {
+ toast(__('Email address copied'));
+ },
},
+ TYPE_ISSUE,
};
</script>
@@ -259,6 +333,21 @@ export default {
data-testid="mobile-dropdown"
:loading="isToggleStateButtonLoading"
>
+ <template v-if="isMrSidebarMoved">
+ <sidebar-subscriptions-widget
+ :iid="String(iid)"
+ :full-path="fullPath"
+ :issuable-type="$options.TYPE_ISSUE"
+ data-testid="notification-toggle"
+ />
+
+ <gl-dropdown-divider />
+ </template>
+
+ <template v-if="showLockIssueOption">
+ <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
+ </template>
+
<gl-dropdown-item v-if="canUpdateIssue" @click="edit">
{{ $options.i18n.edit }}
</gl-dropdown-item>
@@ -275,9 +364,21 @@ export default {
<gl-dropdown-item v-if="canPromoteToEpic" @click="promoteToEpic">
{{ __('Promote to epic') }}
</gl-dropdown-item>
- <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)">
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
+ <template v-if="isMrSidebarMoved">
+ <gl-dropdown-item
+ :data-clipboard-text="issuableReference"
+ data-testid="copy-reference"
+ @click="copyReference"
+ >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
+ >
+ <gl-dropdown-item
+ v-if="issuableEmailAddress"
+ :data-clipboard-text="issuableEmailAddress"
+ data-testid="copy-email"
+ @click="copyEmailAddress"
+ >{{ copyMailAddressText }}</gl-dropdown-item
+ >
+ </template>
<gl-dropdown-item
v-if="canReportSpam"
:href="submitAsSpamPath"
@@ -287,6 +388,7 @@ export default {
{{ __('Submit as spam') }}
</gl-dropdown-item>
<template v-if="canDestroyIssue">
+ <gl-dropdown-divider />
<gl-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
@@ -295,6 +397,13 @@ export default {
{{ deleteButtonText }}
</gl-dropdown-item>
</template>
+ <gl-dropdown-item
+ v-if="!isIssueAuthor"
+ data-testid="report-abuse-item"
+ @click="toggleReportAbuseDrawer(true)"
+ >
+ {{ $options.i18n.reportAbuse }}
+ </gl-dropdown-item>
</gl-dropdown>
<gl-button
@@ -322,6 +431,7 @@ export default {
<gl-dropdown
v-if="hasDesktopDropdown"
+ id="new-actions-header-dropdown"
v-gl-tooltip.hover
class="gl-display-none gl-sm-display-inline-flex! gl-sm-ml-3"
icon="ellipsis_v"
@@ -334,7 +444,19 @@ export default {
data-testid="desktop-dropdown"
no-caret
right
+ @shown="dismissPopover"
>
+ <template v-if="isMrSidebarMoved">
+ <sidebar-subscriptions-widget
+ :iid="String(iid)"
+ :full-path="fullPath"
+ :issuable-type="$options.TYPE_ISSUE"
+ data-testid="notification-toggle"
+ />
+
+ <gl-dropdown-divider />
+ </template>
+
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ newIssueTypeText }}
</gl-dropdown-item>
@@ -346,9 +468,24 @@ export default {
>
{{ __('Promote to epic') }}
</gl-dropdown-item>
- <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)">
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
+ <template v-if="showLockIssueOption">
+ <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
+ </template>
+ <template v-if="isMrSidebarMoved">
+ <gl-dropdown-item
+ :data-clipboard-text="issuableReference"
+ data-testid="copy-reference"
+ @click="copyReference"
+ >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
+ >
+ <gl-dropdown-item
+ v-if="issuableEmailAddress"
+ :data-clipboard-text="issuableEmailAddress"
+ data-testid="copy-email"
+ @click="copyEmailAddress"
+ >{{ copyMailAddressText }}</gl-dropdown-item
+ >
+ </template>
<gl-dropdown-item
v-if="canReportSpam"
:href="submitAsSpamPath"
@@ -357,8 +494,8 @@ export default {
>
{{ __('Submit as spam') }}
</gl-dropdown-item>
-
<template v-if="canDestroyIssue">
+ <gl-dropdown-divider />
<gl-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
@@ -368,8 +505,16 @@ export default {
{{ deleteButtonText }}
</gl-dropdown-item>
</template>
+ <gl-dropdown-item
+ v-if="!isIssueAuthor"
+ data-testid="report-abuse-item"
+ @click="toggleReportAbuseDrawer(true)"
+ >
+ {{ $options.i18n.reportAbuse }}
+ </gl-dropdown-item>
</gl-dropdown>
+ <new-header-actions-popover v-if="isMrSidebarMoved" :issue-type="issueType" />
<gl-modal
ref="blockedByIssuesModal"
modal-id="blocked-by-issues-modal"
diff --git a/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue b/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue
new file mode 100644
index 00000000000..8262b3ac0ff
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue
@@ -0,0 +1,82 @@
+<script>
+import { GlPopover, GlButton } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import { getCookie, parseBoolean, setCookie } from '~/lib/utils/common_utils';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants';
+import { IssuableTypeText } from '~/issues/constants';
+
+export default {
+ name: 'NewHeaderActionsPopover',
+ i18n: {
+ popoverText: s__(
+ 'HeaderAction|Notifications and other %{issueType} actions have moved to this menu.',
+ ),
+ confirmButtonText: s__('HeaderAction|Okay!'),
+ },
+ components: {
+ GlPopover,
+ GlButton,
+ },
+ mixins: [glFeatureFlagMixin()],
+ props: {
+ issueType: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ dismissKey: NEW_ACTIONS_POPOVER_KEY,
+ popoverDismissed: parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`)),
+ };
+ },
+ computed: {
+ popoverText() {
+ return sprintf(this.$options.i18n.popoverText, {
+ issueType: IssuableTypeText[this.issueType],
+ });
+ },
+ showPopover() {
+ return !this.popoverDismissed && this.isMrSidebarMoved;
+ },
+ isMrSidebarMoved() {
+ return this.glFeatures.movedMrSidebar;
+ },
+ },
+ methods: {
+ dismissPopover() {
+ this.popoverDismissed = true;
+ setCookie(this.dismissKey, this.popoverDismissed);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-popover
+ v-if="showPopover"
+ target="new-actions-header-dropdown"
+ container="viewport"
+ placement="left"
+ :show="showPopover"
+ triggers="manual"
+ content="text"
+ :css-classes="['gl-p-2 new-header-popover']"
+ >
+ <template #title>
+ <div class="gl-font-base gl-font-weight-normal">
+ {{ popoverText }}
+ </div>
+ </template>
+ <gl-button
+ data-testid="confirm-button"
+ variant="confirm"
+ type="submit"
+ @click="dismissPopover"
+ >{{ $options.i18n.confirmButtonText }}</gl-button
+ >
+ </gl-popover>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issues/show/constants.js b/app/assets/javascripts/issues/show/constants.js
index 4d8c11f9669..6320e4ef266 100644
--- a/app/assets/javascripts/issues/show/constants.js
+++ b/app/assets/javascripts/issues/show/constants.js
@@ -17,3 +17,5 @@ export const issueState = {
issueType: undefined,
isDirty: false,
};
+
+export const NEW_ACTIONS_POPOVER_KEY = 'new-actions-popover-viewed';
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index e677328cd2e..100abcbe1e5 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -174,6 +174,8 @@ export function initHeaderActions(store, type = '') {
reportedUserId: parseInt(el.dataset.reportedUserId, 10),
reportedFromUrl: el.dataset.reportedFromUrl,
submitAsSpamPath: el.dataset.submitAsSpamPath,
+ issuableEmailAddress: el.dataset.issuableEmailAddress,
+ fullPath: el.dataset.projectPath,
},
render: (createElement) => createElement(HeaderActions),
});
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js
index 3bfbfea7f22..a6081303bf8 100644
--- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js
@@ -12,6 +12,7 @@ export function confirmAction(
modalHtmlMessage,
title,
hideCancel,
+ size,
} = {},
) {
return new Promise((resolve) => {
@@ -36,6 +37,7 @@ export function confirmAction(
title,
modalHtmlMessage,
hideCancel,
+ size,
},
on: {
confirmed() {
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
index ea91ccec546..24be1485379 100644
--- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
@@ -56,6 +56,11 @@ export default {
required: false,
default: false,
},
+ size: {
+ type: String,
+ required: false,
+ default: 'sm',
+ },
},
computed: {
primaryAction() {
@@ -103,9 +108,9 @@ export default {
<template>
<gl-modal
ref="modal"
- size="sm"
modal-id="confirmationModal"
body-class="gl-display-flex"
+ :size="size"
:title="title"
:action-primary="primaryAction"
:action-cancel="cancelAction"
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 297b8ae1fc2..58e4553d00d 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -56,8 +56,10 @@ Sidebar.prototype.addEventListeners = function () {
const layoutPage = document.querySelector('.layout-page');
const rightSidebar = document.querySelector('.js-right-sidebar');
- updateSidebarClasses(layoutPage, rightSidebar);
- window.addEventListener('resize', () => updateSidebarClasses(layoutPage, rightSidebar));
+ if (rightSidebar.classList.contains('right-sidebar-merge-requests')) {
+ updateSidebarClasses(layoutPage, rightSidebar);
+ window.addEventListener('resize', () => updateSidebarClasses(layoutPage, rightSidebar));
+ }
}
};
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index 1eff4db3970..06876546fa4 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -1,8 +1,9 @@
<script>
import { GlIcon, GlTooltipDirective, GlOutsideDirective as Outside } from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
-import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import { __, sprintf } from '~/locale';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { createAlert } from '~/alert';
import toast from '~/vue_shared/plugins/global_toast';
@@ -45,10 +46,8 @@ export default {
},
computed: {
...mapGetters(['getNoteableData']),
- isMergeRequest() {
- return (
- this.getNoteableData.targetType === TYPE_MERGE_REQUEST && this.glFeatures.movedMrSidebar
- );
+ isMovedMrSidebar() {
+ return this.glFeatures.movedMrSidebar;
},
issuableDisplayName() {
const isInIssuePage = this.getNoteableData.targetType === TYPE_ISSUE;
@@ -60,7 +59,6 @@ export default {
lockStatus() {
return this.isLocked ? this.$options.locked : this.$options.unlocked;
},
-
tooltipLabel() {
return this.isLocked ? __('Locked') : __('Unlocked');
},
@@ -89,8 +87,13 @@ export default {
fullPath: this.fullPath,
})
.then(() => {
- if (this.isMergeRequest) {
- toast(this.isLocked ? __('Merge request locked.') : __('Merge request unlocked.'));
+ if (this.isMovedMrSidebar) {
+ toast(
+ sprintf(__('%{issuableDisplayName} %{lockStatus}.'), {
+ issuableDisplayName: capitalizeFirstCharacter(this.issuableDisplayName),
+ lockStatus: this.isLocked ? __('locked') : __('unlocked'),
+ }),
+ );
}
})
.catch(() => {
@@ -113,14 +116,14 @@ export default {
</script>
<template>
- <li v-if="isMergeRequest" class="gl-dropdown-item">
- <button type="button" class="dropdown-item" @click="toggleLocked">
+ <li v-if="isMovedMrSidebar" class="gl-dropdown-item">
+ <button type="button" class="dropdown-item" data-testid="issuable-lock" @click="toggleLocked">
<span class="gl-dropdown-item-text-wrapper">
<template v-if="isLocked">
- {{ __('Unlock merge request') }}
+ {{ sprintf(__('Unlock %{issuableType}'), { issuableType: issuableDisplayName }) }}
</template>
<template v-else>
- {{ __('Lock merge request') }}
+ {{ sprintf(__('Lock %{issuableType}'), { issuableType: issuableDisplayName }) }}
</template>
</span>
</button>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
index 344fa880131..f2b960ed02c 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -1,12 +1,7 @@
<script>
import { GlDropdownForm, GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/alert';
-import {
- TYPE_EPIC,
- TYPE_MERGE_REQUEST,
- WORKSPACE_GROUP,
- WORKSPACE_PROJECT,
-} from '~/issues/constants';
+import { TYPE_EPIC, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -91,8 +86,8 @@ export default {
},
},
computed: {
- isMergeRequest() {
- return this.issuableType === TYPE_MERGE_REQUEST && this.glFeatures.movedMrSidebar;
+ isMovedMrSidebar() {
+ return this.glFeatures.movedMrSidebar;
},
isLoading() {
return this.$apollo.queries?.subscribed?.loading || this.loading;
@@ -148,7 +143,7 @@ export default {
});
}
- if (this.isMergeRequest) {
+ if (this.isMovedMrSidebar) {
toast(subscribed ? __('Notifications turned on.') : __('Notifications turned off.'));
}
},
@@ -187,7 +182,7 @@ export default {
</script>
<template>
- <gl-dropdown-form v-if="isMergeRequest" class="gl-dropdown-item">
+ <gl-dropdown-form v-if="isMovedMrSidebar" class="gl-dropdown-item">
<div class="gl-px-5 gl-pb-2 gl-pt-1">
<gl-toggle
:value="subscribed"
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 540d57bb5ce..74843bcc006 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -17,6 +17,7 @@ import { __ } from '~/locale';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import Translate from '~/vue_shared/translate';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
+import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
import CollapsedAssigneeList from './components/assignees/collapsed_assignee_list.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarAssigneesWidget from './components/assignees/sidebar_assignees_widget.vue';
@@ -785,6 +786,21 @@ export function mountAssigneesDropdown() {
});
}
+function mountNewIssuePopover() {
+ const el = document.querySelector('.js-sidebar-header-popover');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'NewHeaderActionsPopover',
+ render: (createElement) =>
+ createElement(NewHeaderActionsPopover, { props: { issueType: TYPE_MERGE_REQUEST } }),
+ });
+}
+
const isAssigneesWidgetShown =
(isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
@@ -812,6 +828,7 @@ export function mountSidebar(mediator, store) {
mountSidebarSeverityWidget();
mountSidebarEscalationStatus();
mountMoveIssueButton();
+ mountNewIssuePopover();
}
export { getSidebarOptions };
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
index f2ec8f589ce..952ff9b18e9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
@@ -1,10 +1,21 @@
<script>
-import { GlButton, GlDropdown, GlDropdownItem, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlButton,
+ GlPopover,
+ GlSprintf,
+ GlLink,
+ GlDropdown,
+ GlDropdownItem,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
export default {
components: {
GlButton,
+ GlPopover,
+ GlSprintf,
+ GlLink,
GlDropdown,
GlDropdownItem,
},
@@ -82,30 +93,46 @@ export default {
<template>
<div class="gl-display-flex gl-align-items-flex-start">
<template v-if="hasOneOption">
- <gl-button
- v-for="(btn, index) in tertiaryButtons"
- :id="btn.id"
- :key="index"
- v-gl-tooltip.hover
- :title="setTooltip(btn)"
- :href="btn.href"
- :target="btn.target"
- :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
- :data-clipboard-text="btn.dataClipboardText"
- :data-qa-selector="actionButtonQaSelector(btn)"
- :data-method="btn.dataMethod"
- :icon="btn.icon"
- :data-testid="btn.testId || 'extension-actions-button'"
- :variant="btn.variant || 'confirm'"
- :loading="btn.loading"
- :disabled="btn.loading"
- category="tertiary"
- size="small"
- class="gl-md-display-block gl-float-left"
- @click="onClickAction(btn)"
- >
- {{ btn.text }}
- </gl-button>
+ <span v-for="(btn, index) in tertiaryButtons" :key="index">
+ <gl-button
+ :id="btn.id"
+ v-gl-tooltip.hover
+ :title="setTooltip(btn)"
+ :href="btn.href"
+ :target="btn.target"
+ :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
+ :data-clipboard-text="btn.dataClipboardText"
+ :data-qa-selector="actionButtonQaSelector(btn)"
+ :data-method="btn.dataMethod"
+ :icon="btn.icon"
+ :data-testid="btn.testId || 'extension-actions-button'"
+ :variant="btn.variant || 'confirm'"
+ :loading="btn.loading"
+ :disabled="btn.loading"
+ category="tertiary"
+ size="small"
+ class="gl-md-display-block gl-float-left"
+ @click="onClickAction(btn)"
+ >
+ {{ btn.text }}
+ </gl-button>
+ <gl-popover v-if="btn.popoverTarget" :target="btn.popoverTarget">
+ <template #title> {{ btn.popoverTitle }} </template>
+
+ <span v-if="btn.popoverLink">
+ <gl-sprintf :message="btn.popoverText">
+ <template #link="{ content }">
+ <gl-link class="gl-font-sm" :href="btn.popoverLink" target="_blank">
+ {{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </span>
+ <span v-else>
+ {{ btn.popoverText }}
+ </span>
+ </gl-popover>
+ </span>
</template>
<template v-if="hasMultipleOptions">
<gl-dropdown
@@ -134,30 +161,46 @@ export default {
{{ btn.text }}
</gl-dropdown-item>
</gl-dropdown>
- <gl-button
- v-for="(btn, index) in tertiaryButtons"
- :id="btn.id"
- :key="index"
- v-gl-tooltip.hover
- :title="setTooltip(btn)"
- :href="btn.href"
- :target="btn.target"
- :class="[{ 'gl-mr-1': index !== tertiaryButtons.length - 1 }, btn.class]"
- :data-clipboard-text="btn.dataClipboardText"
- :data-qa-selector="actionButtonQaSelector(btn)"
- :data-method="btn.dataMethod"
- :icon="btn.icon"
- :data-testid="btn.testId || 'extension-actions-button'"
- :variant="btn.variant || 'confirm'"
- :loading="btn.loading"
- :disabled="btn.loading"
- category="tertiary"
- size="small"
- class="gl-display-none gl-md-display-block gl-float-left"
- @click="onClickAction(btn)"
- >
- {{ btn.text }}
- </gl-button>
+ <span v-for="(btn, index) in tertiaryButtons" :key="index">
+ <gl-button
+ :id="btn.id"
+ v-gl-tooltip.hover
+ :title="setTooltip(btn)"
+ :href="btn.href"
+ :target="btn.target"
+ :class="[{ 'gl-mr-1': index !== tertiaryButtons.length - 1 }, btn.class]"
+ :data-clipboard-text="btn.dataClipboardText"
+ :data-qa-selector="actionButtonQaSelector(btn)"
+ :data-method="btn.dataMethod"
+ :icon="btn.icon"
+ :data-testid="btn.testId || 'extension-actions-button'"
+ :variant="btn.variant || 'confirm'"
+ :loading="btn.loading"
+ :disabled="btn.loading"
+ category="tertiary"
+ size="small"
+ class="gl-display-none gl-md-display-block gl-float-left"
+ @click="onClickAction(btn)"
+ >
+ {{ btn.text }}
+ </gl-button>
+ <gl-popover v-if="btn.popoverTarget" :target="btn.popoverTarget">
+ <template #title> {{ btn.popoverTitle }} </template>
+
+ <span v-if="btn.popoverLink">
+ <gl-sprintf :message="btn.popoverText">
+ <template #link="{ content }">
+ <gl-link class="gl-font-sm" :href="btn.popoverLink" target="_blank">
+ {{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </span>
+ <span v-else>
+ {{ btn.popoverText }}
+ </span>
+ </gl-popover>
+ </span>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
index 6b9823e0f3b..feee132629f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
@@ -1,5 +1,5 @@
<script>
-import { GlCollapsibleListbox, GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlTooltip, GlButton } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { updateText } from '~/lib/utils/text_markdown';
import savedRepliesQuery from './saved_replies.query.graphql';
@@ -16,11 +16,8 @@ export default {
},
components: {
GlCollapsibleListbox,
- GlIcon,
GlButton,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
+ GlTooltip,
},
props: {
newCommentTemplatePath: {
@@ -45,6 +42,9 @@ export default {
return savedReplies.map((r) => ({ value: r.id, text: r.name, content: r.content }));
},
},
+ mounted() {
+ this.tooltipTarget = this.$el.querySelector('.js-comment-template-toggle');
+ },
methods: {
fetchCommentTemplates() {
this.shouldFetchCommentTemplates = true;
@@ -75,53 +75,49 @@ export default {
</script>
<template>
- <gl-collapsible-listbox
- :header-text="__('Insert comment template')"
- :items="filteredSavedReplies"
- placement="right"
- searchable
- class="comment-template-dropdown"
- :searching="$apollo.queries.savedReplies.loading"
- @shown="fetchCommentTemplates"
- @search="setCommentTemplateSearch"
- @select="onSelect"
- >
- <template #toggle>
- <gl-button
- v-gl-tooltip
- :title="__('Insert comment template')"
- :aria-label="__('Insert comment template')"
- category="tertiary"
- class="gl-px-3!"
- data-testid="comment-template-dropdown-toggle"
- @keydown.prevent
- >
- <gl-icon name="comment-lines" class="gl-mr-0!" />
- <gl-icon name="chevron-down" />
- </gl-button>
- </template>
- <template #list-item="{ item }">
- <div class="gl-display-flex js-comment-template-content">
- <div class="gl-text-truncate">
- <strong>{{ item.text }}</strong
- ><span class="gl-ml-2">{{ item.content }}</span>
+ <span>
+ <gl-collapsible-listbox
+ :header-text="__('Insert comment template')"
+ :items="filteredSavedReplies"
+ :toggle-text="__('Insert comment template')"
+ text-sr-only
+ toggle-class="js-comment-template-toggle"
+ icon="comment-lines"
+ category="tertiary"
+ placement="right"
+ searchable
+ class="comment-template-dropdown"
+ :searching="$apollo.queries.savedReplies.loading"
+ @shown="fetchCommentTemplates"
+ @search="setCommentTemplateSearch"
+ @select="onSelect"
+ >
+ <template #list-item="{ item }">
+ <div class="gl-display-flex js-comment-template-content">
+ <div class="gl-text-truncate">
+ <strong>{{ item.text }}</strong
+ ><span class="gl-ml-2">{{ item.content }}</span>
+ </div>
</div>
- </div>
- </template>
- <template #footer>
- <div
- class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-display-flex gl-justify-content-center gl-p-3"
- >
- <gl-button
- :href="newCommentTemplatePath"
- category="tertiary"
- block
- class="gl-justify-content-start! gl-mt-0! gl-mb-0! gl-px-3!"
- >{{ __('Add a new comment template') }}</gl-button
+ </template>
+ <template #footer>
+ <div
+ class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-display-flex gl-justify-content-center gl-p-3"
>
- </div>
- </template>
- </gl-collapsible-listbox>
+ <gl-button
+ :href="newCommentTemplatePath"
+ category="tertiary"
+ block
+ class="gl-justify-content-start! gl-mt-0! gl-mb-0! gl-px-3!"
+ >{{ __('Add a new comment template') }}</gl-button
+ >
+ </div>
+ </template>
+ </gl-collapsible-listbox>
+ <gl-tooltip :target="() => tooltipTarget">
+ {{ __('Insert comment template') }}
+ </gl-tooltip>
+ </span>
</template>
<style>
diff --git a/app/assets/stylesheets/components/detail_page.scss b/app/assets/stylesheets/components/detail_page.scss
index de8142924f9..74f61faa9ae 100644
--- a/app/assets/stylesheets/components/detail_page.scss
+++ b/app/assets/stylesheets/components/detail_page.scss
@@ -74,3 +74,7 @@
color: $gl-text-color;
}
}
+
+.new-header-popover {
+ z-index: 999;
+}
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index e0fb95a1359..1b98fd4df07 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -165,3 +165,13 @@
border: 0;
}
}
+
+.merge-request-notification-toggle {
+ .gl-toggle {
+ @include gl-ml-auto;
+ }
+
+ .gl-toggle-label {
+ @include gl-font-weight-normal;
+ }
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index a394c59c508..711585ea713 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -110,6 +110,11 @@ class ApplicationController < ActionController::Base
render plain: e.message, status: :too_many_requests
end
+ rescue_from Gitlab::Git::ResourceExhaustedError do |e|
+ response.headers.merge!(e.headers)
+ render plain: e.message, status: :too_many_requests
+ end
+
content_security_policy do |p|
next if p.directives.blank?
next unless Gitlab::CurrentSettings.snowplow_enabled? && !Gitlab::CurrentSettings.snowplow_collector_hostname.blank?
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index bf59a0a2400..8630519e028 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -76,6 +76,13 @@ class GraphqlController < ApplicationController
render_error(exception.message, status: :forbidden)
end
+ rescue_from Gitlab::Git::ResourceExhaustedError do |exception|
+ log_exception(exception)
+
+ response.headers.merge!(exception.headers)
+ render_error(exception.message, status: :too_many_requests)
+ end
+
rescue_from Gitlab::Graphql::Variables::Invalid do |exception|
render_error(exception.message, status: :unprocessable_entity)
end
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index 3842a88d15b..7121096bd77 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -10,6 +10,7 @@ class Projects::IncidentsController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc, @project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
+ push_frontend_feature_flag(:moved_mr_sidebar, project)
end
feature_category :incident_management
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index a1cd4c49bf0..2cc2c957f21 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -67,6 +67,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items_mvc, project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:epic_widget_edit_confirmation, project)
+ push_frontend_feature_flag(:moved_mr_sidebar, project)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
diff --git a/app/finders/packages/conan/package_finder.rb b/app/finders/packages/conan/package_finder.rb
index 210b37635b3..161a3d0d409 100644
--- a/app/finders/packages/conan/package_finder.rb
+++ b/app/finders/packages/conan/package_finder.rb
@@ -3,25 +3,43 @@
module Packages
module Conan
class PackageFinder
- attr_reader :current_user, :query
+ MAX_PACKAGES_COUNT = 500
- def initialize(current_user, params)
+ def initialize(current_user, params, project: nil)
@current_user = current_user
@query = params[:query]
+ @project = project
end
def execute
- packages_for_current_user.installable.with_name_like(query).order_name_asc if query
+ return ::Packages::Package.none unless query
+
+ packages
end
private
+ attr_reader :current_user, :query, :project
+
def packages
- Packages::Package.conan
+ base
+ .conan
+ .installable
+ .preload_conan_metadatum
+ .with_name_like(query)
+ .limit_recent(MAX_PACKAGES_COUNT)
+ end
+
+ def base
+ project ? packages_of_project : packages_for_current_user
+ end
+
+ def packages_of_project
+ project.packages
end
def packages_for_current_user
- packages.for_projects(projects_visible_to_current_user)
+ Packages::Package.for_projects(projects_visible_to_current_user)
end
def projects_visible_to_current_user
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
index 5012ac29816..14ca189f795 100644
--- a/app/helpers/ci/runners_helper.rb
+++ b/app/helpers/ci/runners_helper.rb
@@ -79,6 +79,8 @@ module Ci
def group_shared_runners_settings_data(group)
{
group_id: group.id,
+ group_name: group.name,
+ group_is_empty: (group.projects.empty? && group.children.empty?).to_s,
shared_runners_setting: group.shared_runners_setting,
parent_shared_runners_setting: group.parent&.shared_runners_setting,
runner_enabled_value: Namespace::SR_ENABLED,
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 72df2608de7..45b231ebdbe 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -12,8 +12,8 @@ module IssuablesHelper
end
end
- def sidebar_gutter_collapsed_class
- return "right-sidebar-expanded" if moved_mr_sidebar_enabled?
+ def sidebar_gutter_collapsed_class(is_merge_request_with_flag)
+ return "right-sidebar-expanded" if is_merge_request_with_flag
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index db6ed91b085..2f002be632d 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -153,7 +153,7 @@ module IssuesHelper
issue.moved_from.project.service_desk_enabled? && !issue.project.service_desk_enabled?
end
- def issue_header_actions_data(project, issuable, current_user)
+ def issue_header_actions_data(project, issuable, current_user, issuable_sidebar)
new_issuable_params = { issue: {}, add_related_issue: issuable.iid }
if issuable.incident?
new_issuable_params[:issuable_template] = 'incident'
@@ -177,7 +177,8 @@ module IssuesHelper
report_abuse_path: add_category_abuse_reports_path,
reported_user_id: issuable.author.id,
reported_from_url: issue_url(issuable),
- submit_as_spam_path: mark_as_spam_project_issue_path(project, issuable)
+ submit_as_spam_path: mark_as_spam_project_issue_path(project, issuable),
+ issuable_email_address: issuable_sidebar.nil? ? '' : issuable_sidebar[:create_note_email]
}
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 4bdf8f3fbd4..b394d24fa38 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -179,6 +179,10 @@ module MergeRequestsHelper
end
end
+ def moved_mr_sidebar_enabled?
+ Feature.enabled?(:moved_mr_sidebar, @project)
+ end
+
def diffs_tab_pane_data(project, merge_request, params)
{
"is-locked": merge_request.discussion_locked?,
@@ -272,10 +276,6 @@ module MergeRequestsHelper
_('%{author} requested to merge %{source_branch} %{copy_button} into %{target_branch} %{created_at}').html_safe % { author: link_to_author.html_safe, source_branch: merge_request_source_branch(merge_request).html_safe, copy_button: copy_button.html_safe, target_branch: target_branch.html_safe, created_at: time_ago_with_tooltip(merge_request.created_at, html_class: 'gl-display-inline-block').html_safe }
end
- def moved_mr_sidebar_enabled?
- Feature.enabled?(:moved_mr_sidebar, @project) && defined?(@merge_request)
- end
-
def single_file_file_by_file?
Feature.enabled?(:single_file_file_by_file, @project)
end
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index 58761fce952..8156090fd9c 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -9,10 +9,4 @@ module ProtectedBranchAccess
delegate :project, to: :protected_branch
end
-
- def check_access(user)
- return false if access_level == Gitlab::Access::NO_ACCESS
-
- super
- end
end
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index 964a862d415..b841211c811 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -45,11 +45,23 @@ module ProtectedRefAccess
type == :role
end
- def check_access(user)
- return false unless user
+ def check_access(current_user)
+ return false if current_user.nil? || no_access?
- user.can?(:push_code, project) &&
- project.team.max_member_access(user.id) >= access_level
+ yield if block_given?
+
+ user_can_access?(current_user)
+ end
+
+ private
+
+ def no_access?
+ role? && access_level == Gitlab::Access::NO_ACCESS
+ end
+
+ def user_can_access?(current_user)
+ current_user.can?(:push_code, project) &&
+ project.team.max_member_access(current_user.id) >= access_level
end
end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 5ce44ab9388..a3946724fd3 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -156,6 +156,7 @@ class Packages::Package < ApplicationRecord
scope :preload_npm_metadatum, -> { preload(:npm_metadatum) }
scope :preload_nuget_metadatum, -> { preload(:nuget_metadatum) }
scope :preload_pypi_metadatum, -> { preload(:pypi_metadatum) }
+ scope :preload_conan_metadatum, -> { preload(:conan_metadatum) }
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
diff --git a/app/models/protected_tag/create_access_level.rb b/app/models/protected_tag/create_access_level.rb
index 785e7559212..5837f3a5afb 100644
--- a/app/models/protected_tag/create_access_level.rb
+++ b/app/models/protected_tag/create_access_level.rb
@@ -12,11 +12,9 @@ class ProtectedTag::CreateAccessLevel < ApplicationRecord
validate :validate_deploy_key_membership
def type
- if deploy_key.present?
- :deploy_key
- else
- super
- end
+ return :deploy_key if deploy_key.present?
+
+ super
end
def humanize
@@ -25,28 +23,28 @@ class ProtectedTag::CreateAccessLevel < ApplicationRecord
super
end
- def check_access(user)
- return false if access_level == Gitlab::Access::NO_ACCESS
-
- if user && deploy_key.present?
- return user.can?(:read_project, project) && enabled_deploy_key_for_user?(deploy_key, user)
+ def check_access(current_user)
+ super do
+ break enabled_deploy_key_for_user?(current_user) if deploy_key?
end
-
- super
end
private
+ def deploy_key?
+ type == :deploy_key
+ end
+
def validate_deploy_key_membership
return unless deploy_key
-
return if project.deploy_keys_projects.where(deploy_key: deploy_key).exists?
errors.add(:deploy_key, 'is not enabled for this project')
end
- def enabled_deploy_key_for_user?(deploy_key, user)
- deploy_key.user_id == user.id &&
+ def enabled_deploy_key_for_user?(current_user)
+ current_user.can?(:read_project, project) &&
+ deploy_key.user_id == current_user.id &&
DeployKey.with_write_access_for_project(protected_tag.project, deploy_key: deploy_key).any?
end
end
diff --git a/app/services/packages/conan/search_service.rb b/app/services/packages/conan/search_service.rb
index df22a895c00..c65c9a85da8 100644
--- a/app/services/packages/conan/search_service.rb
+++ b/app/services/packages/conan/search_service.rb
@@ -8,10 +8,6 @@ module Packages
WILDCARD = '*'
RECIPE_SEPARATOR = '@'
- def initialize(user, params)
- super(nil, user, params)
- end
-
def execute
ServiceResponse.success(payload: { results: search_results })
end
@@ -23,35 +19,34 @@ module Packages
return search_for_single_package(sanitized_query) if params[:query].include?(RECIPE_SEPARATOR)
- search_packages(build_query)
+ search_packages
end
def wildcard_query?
params[:query] == WILDCARD
end
- def build_query
- return "#{sanitized_query}%" if params[:query].end_with?(WILDCARD)
-
- sanitized_query
- end
-
- def search_packages(query)
- ::Packages::Conan::PackageFinder.new(current_user, query: query).execute.map(&:conan_recipe)
+ def sanitized_query
+ @sanitized_query ||= sanitize_sql_like(params[:query].delete(WILDCARD))
end
def search_for_single_package(query)
- name, version, username, _ = query.split(%r{[@/]})
- full_path = Packages::Conan::Metadatum.full_path_from(package_username: username)
- project = Project.find_by_full_path(full_path)
- return unless Ability.allowed?(current_user, :read_package, project&.packages_policy_subject)
+ ::Packages::Conan::SinglePackageSearchService
+ .new(query, current_user)
+ .execute[:results]
+ end
- result = project.packages.with_name(name).with_version(version).order_created.last
- [result&.conan_recipe].compact
+ def search_packages
+ ::Packages::Conan::PackageFinder
+ .new(current_user, { query: build_query }, project: project)
+ .execute
+ .map(&:conan_recipe)
end
- def sanitized_query
- @sanitized_query ||= sanitize_sql_like(params[:query].delete(WILDCARD))
+ def build_query
+ return "#{sanitized_query}%" if params[:query].end_with?(WILDCARD)
+
+ sanitized_query
end
end
end
diff --git a/app/services/packages/conan/single_package_search_service.rb b/app/services/packages/conan/single_package_search_service.rb
new file mode 100644
index 00000000000..e133b35c2cf
--- /dev/null
+++ b/app/services/packages/conan/single_package_search_service.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Packages
+ module Conan
+ class SinglePackageSearchService # rubocop:disable Search/NamespacedClass
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(query, current_user)
+ @name, @version, @username, _ = query.split(%r{[@/]})
+ @current_user = current_user
+ end
+
+ def execute
+ ServiceResponse.success(payload: { results: search_results })
+ end
+
+ private
+
+ attr_reader :name, :version, :username, :current_user
+
+ def search_results
+ return [] unless can_access_project_package?
+
+ [package&.conan_recipe].compact
+ end
+
+ def package
+ project
+ .packages
+ .with_name(name)
+ .with_version(version)
+ .order_created
+ .last
+ end
+
+ def project
+ Project.find_by_full_path(full_path)
+ end
+ strong_memoize_attr :project
+
+ def full_path
+ ::Packages::Conan::Metadatum.full_path_from(package_username: username)
+ end
+
+ def can_access_project_package?
+ Ability.allowed?(current_user, :read_package, project.try(:packages_policy_subject))
+ end
+ end
+ end
+end
diff --git a/app/views/groups/runners/register.html.haml b/app/views/groups/runners/register.html.haml
index a5296c38618..15d96bb80b6 100644
--- a/app/views/groups/runners/register.html.haml
+++ b/app/views/groups/runners/register.html.haml
@@ -2,6 +2,6 @@
- breadcrumb_title s_('Runners|Register')
- page_title s_('Runners|Register'), runner_name
- add_to_breadcrumbs _('Runners'), group_runners_path(@group)
-- add_to_breadcrumbs runner_name, register_group_runner_path(@runner)
+- add_to_breadcrumbs runner_name, register_group_runner_path(@group, @runner)
#js-group-register-runner{ data: { runner_id: @runner.id, runners_path: group_runners_path(@group) } }
diff --git a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
index b8ee62055f0..9bfa0e7a309 100644
--- a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
+++ b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
@@ -1,7 +1,8 @@
- display_issuable_type = issuable_display_type(@merge_request)
.btn-group.gl-md-ml-3.gl-display-flex.dropdown.gl-dropdown.gl-md-w-auto.gl-w-full
- = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret gl-display-none! gl-md-display-inline-flex!", title: _('Merge request actions'), 'aria-label': _('Merge request actions'), data: { toggle: 'dropdown', testid: 'merge-request-actions' } do
+ %span.js-sidebar-header-popover
+ = button_tag type: 'button', id: "new-actions-header-dropdown", class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret gl-display-none! gl-md-display-inline-flex!", title: _('Merge request actions'), 'aria-label': _('Merge request actions'), data: { toggle: 'dropdown', testid: 'merge-request-actions' } do
= sprite_icon "ellipsis_v", size: 16, css_class: "dropdown-icon gl-icon"
= button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md btn-block gl-button gl-dropdown-toggle gl-md-display-none!", data: { 'toggle' => 'dropdown' } do
%span.gl-dropdown-button-text= _('Merge request actions')
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index f54354674e2..82e95a6a8e8 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -9,14 +9,15 @@
- reviewers = local_assigns.fetch(:reviewers, nil)
- in_group_context_with_iterations = @project.group.present? && issuable_sidebar[:supports_iterations]
- is_merge_request = issuable_type === 'merge_request'
-- moved_sidebar_enabled = moved_mr_sidebar_enabled? && is_merge_request
+- moved_sidebar_enabled = moved_mr_sidebar_enabled?
+- is_merge_request_with_flag = is_merge_request && moved_sidebar_enabled
-%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class} #{'right-sidebar-merge-requests' if moved_sidebar_enabled}", 'aria-live' => 'polite', 'aria-label': issuable_type }
- .issuable-sidebar{ class: "#{'is-merge-request' if moved_sidebar_enabled}" }
- .issuable-sidebar-header{ class: "#{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-lg-display-none!' if moved_sidebar_enabled}" }
+%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class(is_merge_request_with_flag)} #{'right-sidebar-merge-requests' if is_merge_request_with_flag}", 'aria-live' => 'polite', 'aria-label': issuable_type }
+ .issuable-sidebar{ class: "#{'is-merge-request' if is_merge_request_with_flag}" }
+ .issuable-sidebar-header{ class: "#{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-lg-display-none!' if is_merge_request_with_flag}" }
%button.btn.gl-button.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ type: "reset", class: "gl-shadow-none! #{'gl-display-block' if moved_sidebar_enabled}", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
- - if signed_in && !moved_sidebar_enabled
+ - if signed_in && !is_merge_request_with_flag
.js-sidebar-todo-widget-root{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
= form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
@@ -81,17 +82,17 @@
.js-sidebar-participants-widget-root
- .block.with-sub-blocks
- - if !moved_sidebar_enabled
+ - if !moved_sidebar_enabled
+ .block.with-sub-blocks
.js-sidebar-reference-widget-root
- - if issuable_type == 'merge_request' && !moved_sidebar_enabled
- .sub-block.js-sidebar-source-branch
- .sidebar-collapsed-icon.js-dont-change-state
- = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
- .gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mb-2.hide-collapsed
- %span.gl-overflow-hidden.gl-text-overflow-ellipsis.gl-white-space-nowrap
- = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<span class='gl-font-monospace' data-testid='ref-name' title='#{html_escape(source_branch)}'>".html_safe, source_branch_close: "</span>".html_safe, source_branch: html_escape(source_branch) }
- = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
+ - if is_merge_request && !moved_sidebar_enabled
+ .sub-block.js-sidebar-source-branch
+ .sidebar-collapsed-icon.js-dont-change-state
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
+ .gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mb-2.hide-collapsed
+ %span.gl-overflow-hidden.gl-text-overflow-ellipsis.gl-white-space-nowrap
+ = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<span class='gl-font-monospace' data-testid='ref-name' title='#{html_escape(source_branch)}'>".html_safe, source_branch_close: "</span>".html_safe, source_branch: html_escape(source_branch) }
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
- if show_forwarding_email
.block
diff --git a/app/views/shared/issue_type/_details_header.html.haml b/app/views/shared/issue_type/_details_header.html.haml
index 9f7ed6b17c3..b6c0b73a83d 100644
--- a/app/views/shared/issue_type/_details_header.html.haml
+++ b/app/views/shared/issue_type/_details_header.html.haml
@@ -19,4 +19,4 @@
%a.btn.gl-button.btn-default.btn-icon.float-right.gl-display-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-left')
- .js-issue-header-actions{ data: issue_header_actions_data(@project, issuable, current_user) }
+ .js-issue-header-actions{ data: issue_header_actions_data(@project, issuable, current_user, @issuable_sidebar) }
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index cc1965945ac..5477b9395ea 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -1,7 +1,7 @@
- affix_offset = local_assigns.fetch(:affix_offset, "50")
- project = local_assigns[:project]
-%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite', 'aria-label': _('Milestone') }
+%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class(false), 'aria-live' => 'polite', 'aria-label': _('Milestone') }
.issuable-sidebar.milestone-sidebar
.block.milestone-progress.issuable-sidebar-header
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => s_('MilestoneSidebar|Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }