diff options
Diffstat (limited to 'app')
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' } } |