diff options
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget/components')
9 files changed, 229 insertions, 100 deletions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue index 5ef7c2f72e0..7ba387c79b1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue @@ -1,5 +1,6 @@ <script> import createFlash from '~/flash'; +import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import { visitUrl } from '~/lib/utils/url_utility'; import { __, s__ } from '~/locale'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -79,6 +80,7 @@ export default { [STOPPING]: { actionName: STOPPING, buttonText: s__('MrDeploymentActions|Stop environment'), + buttonVariant: 'danger', busyText: __('This environment is being deployed'), confirmMessage: __('Are you sure you want to stop this environment?'), errorMessage: __('Something went wrong while stopping this environment. Please try again.'), @@ -86,6 +88,7 @@ export default { [DEPLOYING]: { actionName: DEPLOYING, buttonText: s__('MrDeploymentActions|Deploy'), + buttonVariant: 'confirm', busyText: __('This environment is being deployed'), confirmMessage: __('Are you sure you want to deploy this environment?'), errorMessage: __('Something went wrong while deploying this environment. Please try again.'), @@ -93,14 +96,27 @@ export default { [REDEPLOYING]: { actionName: REDEPLOYING, buttonText: s__('MrDeploymentActions|Re-deploy'), + buttonVariant: 'confirm', busyText: __('This environment is being re-deployed'), confirmMessage: __('Are you sure you want to re-deploy this environment?'), errorMessage: __('Something went wrong while deploying this environment. Please try again.'), }, }, methods: { - executeAction(endpoint, { actionName, confirmMessage, errorMessage }) { - const isConfirmed = confirm(confirmMessage); //eslint-disable-line + async executeAction( + endpoint, + { + actionName, + buttonText: primaryBtnText, + buttonVariant: primaryBtnVariant, + confirmMessage, + errorMessage, + }, + ) { + const isConfirmed = await confirmAction(confirmMessage, { + primaryBtnVariant, + primaryBtnText, + }); if (isConfirmed) { this.actionInProgress = actionName; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index 7322958e6df..a25b4ab54e5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -128,10 +128,12 @@ export default { api.trackRedisHllUserEvent(this.$options.expandEvent); } }), - toggleCollapsed() { - this.isCollapsed = !this.isCollapsed; + toggleCollapsed(e) { + if (!e?.target?.closest('.btn:not(.btn-icon),a')) { + this.isCollapsed = !this.isCollapsed; - this.triggerRedisTracking(); + this.triggerRedisTracking(); + } }, initExtensionPolling() { const poll = new Poll({ @@ -139,7 +141,7 @@ export default { fetchData: () => this.fetchCollapsedData(this.$props), }, method: 'fetchData', - successCallback: (data) => { + successCallback: ({ data }) => { if (Object.keys(data).length > 0) { poll.stop(); this.setCollapsedData(data); @@ -207,6 +209,19 @@ export default { this.showFade = true; } }, + onRowMouseDown() { + this.down = Number(new Date()); + }, + onRowMouseUp(e) { + const up = Number(new Date()); + + // To allow for text to be selected we check if the the user is clicking + // or selecting, if they are selecting the time difference should be + // more than 200ms + if (up - this.down < 200) { + this.toggleCollapsed(e); + } + }, generateText, }, EXTENSION_ICON_CLASS, @@ -215,7 +230,7 @@ export default { <template> <section class="media-section" data-testid="widget-extension"> - <div class="media gl-p-5"> + <div class="media gl-p-5 gl-cursor-pointer" @mousedown="onRowMouseDown" @mouseup="onRowMouseUp"> <status-icon :name="$options.label || $options.name" :is-loading="isLoadingSummary" @@ -253,7 +268,7 @@ export default { category="tertiary" data-testid="toggle-button" size="small" - @click="toggleCollapsed" + @click.self="toggleCollapsed" /> </div> </div> @@ -317,9 +332,13 @@ export default { <div v-if="data.link"> <gl-link :href="data.link.href">{{ data.link.text }}</gl-link> </div> + <div v-if="data.supportingText"> + <p v-safe-html="generateText(data.supportingText)" class="gl-m-0"></p> + </div> <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'"> {{ data.badge.text }} </gl-badge> + <actions :widget="$options.label || $options.name" :tertiary-buttons="data.actions" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue index cd5b7c3110d..8b410926c46 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue @@ -90,7 +90,7 @@ export default { </template> <div class="row"> <div - class="col-md-5 order-md-last col-12 gl-mt-5 gl-mt-md-n2! gl-pt-md-2 svg-content svg-225" + class="col-md-5 order-md-last col-12 gl-mt-5 gl-md-mt-n2! gl-md-pt-2 svg-content svg-225" > <img data-testid="pipeline-image" :src="pipelineSvgPath" /> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog.vue new file mode 100644 index 00000000000..7279ad971be --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog.vue @@ -0,0 +1,66 @@ +<script> +import { GlModal, GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + name: 'MergeFailedPipelineConfirmationDialog', + i18n: { + primary: __('Merge unverified changes'), + cancel: __('Cancel'), + info: __( + 'The latest pipeline for this merge request did not succeed. The latest changes are unverified.', + ), + confirmation: __('Are you sure you want to attempt to merge?'), + title: __('Merge unverified changes?'), + }, + components: { + GlModal, + GlButton, + }, + props: { + visible: { + type: Boolean, + required: true, + }, + }, + methods: { + hide() { + this.$refs.modal.hide(); + }, + cancel() { + this.hide(); + this.$emit('cancel'); + }, + focusCancelButton() { + this.$refs.cancelButton.$el.focus(); + }, + mergeChanges() { + this.$emit('mergeWithFailedPipeline'); + this.hide(); + }, + }, +}; +</script> +<template> + <gl-modal + ref="modal" + size="sm" + modal-id="merge-train-failed-pipeline-confirmation-dialog" + :title="$options.i18n.title" + :visible="visible" + data-testid="merge-failed-pipeline-confirmation-dialog" + @shown="focusCancelButton" + @hide="$emit('cancel')" + > + <p>{{ $options.i18n.info }}</p> + <p>{{ $options.i18n.confirmation }}</p> + <template #modal-footer> + <gl-button ref="cancelButton" data-testid="merge-cancel-btn" @click="cancel">{{ + $options.i18n.cancel + }}</gl-button> + <gl-button variant="danger" data-testid="merge-unverified-changes" @click="mergeChanges"> + {{ $options.i18n.primary }} + </gl-button> + </template> + </gl-modal> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue index 247877a8235..e0c4679b983 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue @@ -1,7 +1,14 @@ <script> -import { MERGE_ACTIVE_STATUS_PHRASES } from '../../constants'; +import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; +import simplePoll from '~/lib/utils/simple_poll'; +import MergeRequest from '../../../merge_request'; +import eventHub from '../../event_hub'; +import { MERGE_ACTIVE_STATUS_PHRASES, STATE_MACHINE } from '../../constants'; import statusIcon from '../mr_widget_status_icon.vue'; +const { transitions } = STATE_MACHINE; +const { MERGE_FAILURE } = transitions; + export default { name: 'MRWidgetMerging', components: { @@ -12,6 +19,10 @@ export default { type: Object, required: true, }, + service: { + type: Object, + required: true, + }, }, data() { const statusCount = MERGE_ACTIVE_STATUS_PHRASES.length; @@ -20,6 +31,53 @@ export default { mergeStatus: MERGE_ACTIVE_STATUS_PHRASES[Math.floor(Math.random() * statusCount)], }; }, + mounted() { + this.initiateMergePolling(); + }, + methods: { + initiateMergePolling() { + simplePoll( + (continuePolling, stopPolling) => { + this.handleMergePolling(continuePolling, stopPolling); + }, + { timeout: 0 }, + ); + }, + handleMergePolling(continuePolling, stopPolling) { + this.service + .poll() + .then((res) => res.data) + .then((data) => { + if (data.state === 'merged') { + // If state is merged we should update the widget and stop the polling + eventHub.$emit('MRWidgetUpdateRequested'); + eventHub.$emit('FetchActionsContent'); + MergeRequest.hideCloseButton(); + MergeRequest.decreaseCounter(); + stopPolling(); + + refreshUserMergeRequestCounts(); + + // If user checked remove source branch and we didn't remove the branch yet + // we should start another polling for source branch remove process + if (this.removeSourceBranch && data.source_branch_exists) { + this.initiateRemoveSourceBranchPolling(); + } + } else if (data.merge_error) { + eventHub.$emit('FailedToMerge', data.merge_error); + this.mr.transitionStateMachine({ transition: MERGE_FAILURE }); + stopPolling(); + } else { + // MR is not merged yet, continue polling until the state becomes 'merged' + continuePolling(); + } + }) + .catch(() => { + this.mr.transitionStateMachine({ transition: MERGE_FAILURE }); + stopPolling(); + }); + }, + }, }; </script> <template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue index 5b03eda2eac..cadbd9c28a9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue @@ -1,9 +1,14 @@ <script> -import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; -import { sprintf, s__ } from '~/locale'; +import { GlIcon, GlTooltipDirective, GlSprintf } from '@gitlab/ui'; +import { sprintf } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import missingBranchQuery from '../../queries/states/missing_branch.query.graphql'; +import { + MR_WIDGET_MISSING_BRANCH_WHICH, + MR_WIDGET_MISSING_BRANCH_RESTORE, + MR_WIDGET_MISSING_BRANCH_MANUALCLI, +} from '../../i18n'; import statusIcon from '../mr_widget_status_icon.vue'; export default { @@ -13,6 +18,7 @@ export default { }, components: { GlIcon, + GlSprintf, statusIcon, }, mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], @@ -45,26 +51,20 @@ export default { return this.mr.sourceBranchRemoved; }, - missingBranchName() { + type() { return this.sourceBranchRemoved ? 'source' : 'target'; }, - missingBranchNameMessage() { - return sprintf( - s__('mrWidget| Please restore it or use a different %{missingBranchName} branch'), - { - missingBranchName: this.missingBranchName, - }, - ); + name() { + return this.type === 'source' ? this.mr.sourceBranch : this.mr.targetBranch; + }, + warning() { + return sprintf(MR_WIDGET_MISSING_BRANCH_WHICH, { type: this.type, name: this.name }); + }, + restore() { + return sprintf(MR_WIDGET_MISSING_BRANCH_RESTORE, { type: this.type }); }, message() { - return sprintf( - s__( - 'mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line', - ), - { - missingBranchName: this.missingBranchName, - }, - ); + return sprintf(MR_WIDGET_MISSING_BRANCH_MANUALCLI, { type: this.type }); }, }, }; @@ -79,9 +79,14 @@ export default { 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget, }" class="bold js-branch-text" + data-testid="widget-content" > - <span class="capitalize" data-testid="missingBranchName"> {{ missingBranchName }} </span> - {{ s__('mrWidget|branch does not exist.') }} {{ missingBranchNameMessage }} + <gl-sprintf :message="warning"> + <template #code="{ content }"> + <code>{{ content }}</code> + </template> + </gl-sprintf> + {{ restore }} <gl-icon v-gl-tooltip :title="message" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue index d88dad2e086..d204befef58 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue @@ -20,7 +20,7 @@ export default { }, i18n: { failedMessage: s__( - `mrWidget|The pipeline for this merge request did not complete. Push a new commit to fix the failure, or check the %{linkStart}troubleshooting documentation%{linkEnd} to see other possible actions.`, + `mrWidget|Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or %{linkStart}learn about other solutions.%{linkEnd}`, ), }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 06ce312bd4c..bc094501e89 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -14,7 +14,6 @@ import { import { isEmpty } from 'lodash'; import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge'; import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql'; -import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import createFlash from '~/flash'; import { secondsToMilliseconds } from '~/lib/utils/datetime_utility'; import simplePoll from '~/lib/utils/simple_poll'; @@ -22,11 +21,8 @@ import { __, s__ } from '~/locale'; import SmartInterval from '~/smart_interval'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { helpPagePath } from '~/helpers/help_page_helper'; -import MergeRequest from '../../../merge_request'; import { AUTO_MERGE_STRATEGIES, - DANGER, - CONFIRM, WARNING, MT_MERGE_STRATEGY, PIPELINE_FAILED_STATE, @@ -42,6 +38,7 @@ import CommitEdit from './commit_edit.vue'; import CommitMessageDropdown from './commit_message_dropdown.vue'; import CommitsHeader from './commits_header.vue'; import SquashBeforeMerge from './squash_before_merge.vue'; +import MergeFailedPipelineConfirmationDialog from './merge_failed_pipeline_confirmation_dialog.vue'; const PIPELINE_RUNNING_STATE = 'running'; const PIPELINE_PENDING_STATE = 'pending'; @@ -52,7 +49,7 @@ const MERGE_SUCCESS_STATUS = 'success'; const MERGE_HOOK_VALIDATION_ERROR_STATUS = 'hook_validation_error'; const { transitions } = STATE_MACHINE; -const { MERGE, MERGED, MERGE_FAILURE, AUTO_MERGE } = transitions; +const { MERGE, MERGE_FAILURE, AUTO_MERGE, MERGING } = transitions; export default { name: 'ReadyToMerge', @@ -106,6 +103,7 @@ export default { GlDropdownItem, GlFormCheckbox, GlSkeletonLoader, + MergeFailedPipelineConfirmationDialog, MergeTrainHelperIcon: () => import('ee_component/vue_merge_request_widget/components/merge_train_helper_icon.vue'), MergeImmediatelyConfirmationDialog: () => @@ -138,7 +136,8 @@ export default { squashBeforeMerge: this.mr.squashIsSelected, isSquashReadOnly: this.mr.squashIsReadonly, squashCommitMessage: this.mr.squashCommitMessage, - isPipelineFailedModalVisible: false, + isPipelineFailedModalVisibleMergeTrain: false, + isPipelineFailedModalVisibleNormalMerge: false, editCommitMessage: false, }; }, @@ -166,6 +165,9 @@ export default { return this.mr.isPipelineFailed; }, + showMergeFailedPipelineConfirmationDialog() { + return this.status === PIPELINE_FAILED_STATE && this.isPipelineFailed; + }, isMergeAllowed() { if (this.glFeatures.mergeRequestWidgetGraphql) { return this.state.mergeable; @@ -248,13 +250,6 @@ export default { return PIPELINE_SUCCESS_STATE; }, - mergeButtonVariant() { - if (this.status === PIPELINE_FAILED_STATE || this.isPipelineFailed) { - return DANGER; - } - - return CONFIRM; - }, iconClass() { if (this.shouldRenderMergeTrainHelperIcon && !this.mr.preventMerge) { return PIPELINE_RUNNING_STATE; @@ -279,6 +274,10 @@ export default { return this.autoMergeText; } + if (this.status === PIPELINE_FAILED_STATE || this.isPipelineFailed) { + return __('Merge...'); + } + return __('Merge'); }, hasPipelineMustSucceedConflict() { @@ -361,8 +360,13 @@ export default { return this.$apollo.queries.state.refetch(); }, handleMergeButtonClick(useAutoMerge, mergeImmediately = false, confirmationClicked = false) { - if (this.showFailedPipelineModal && !confirmationClicked) { - this.isPipelineFailedModalVisible = true; + if (this.showMergeFailedPipelineConfirmationDialog && !confirmationClicked) { + this.isPipelineFailedModalVisibleNormalMerge = true; + return; + } + + if (this.showFailedPipelineModalMergeTrain && !confirmationClicked) { + this.isPipelineFailedModalVisibleMergeTrain = true; return; } @@ -406,7 +410,7 @@ export default { eventHub.$emit('MRWidgetUpdateRequested'); this.mr.transitionStateMachine({ transition: AUTO_MERGE }); } else if (data.status === MERGE_SUCCESS_STATUS) { - this.initiateMergePolling(); + this.mr.transitionStateMachine({ transition: MERGING }); } else if (hasError) { eventHub.$emit('FailedToMerge', data.merge_error); this.mr.transitionStateMachine({ transition: MERGE_FAILURE }); @@ -434,51 +438,8 @@ export default { onMergeImmediatelyConfirmation() { this.handleMergeButtonClick(false, true, true); }, - initiateMergePolling() { - simplePoll( - (continuePolling, stopPolling) => { - this.handleMergePolling(continuePolling, stopPolling); - }, - { timeout: 0 }, - ); - }, - handleMergePolling(continuePolling, stopPolling) { - this.service - .poll() - .then((res) => res.data) - .then((data) => { - if (data.state === 'merged') { - // If state is merged we should update the widget and stop the polling - eventHub.$emit('MRWidgetUpdateRequested'); - eventHub.$emit('FetchActionsContent'); - MergeRequest.hideCloseButton(); - MergeRequest.decreaseCounter(); - this.mr.transitionStateMachine({ transition: MERGED }); - stopPolling(); - - refreshUserMergeRequestCounts(); - - // If user checked remove source branch and we didn't remove the branch yet - // we should start another polling for source branch remove process - if (this.removeSourceBranch && data.source_branch_exists) { - this.initiateRemoveSourceBranchPolling(); - } - } else if (data.merge_error) { - eventHub.$emit('FailedToMerge', data.merge_error); - this.mr.transitionStateMachine({ transition: MERGE_FAILURE }); - stopPolling(); - } else { - // MR is not merged yet, continue polling until the state becomes 'merged' - continuePolling(); - } - }) - .catch(() => { - createFlash({ - message: __('Something went wrong while merging this merge request. Please try again.'), - }); - this.mr.transitionStateMachine({ transition: MERGE_FAILURE }); - stopPolling(); - }); + onMergeWithFailedPipelineConfirmation() { + this.handleMergeButtonClick(false, true, true); }, initiateRemoveSourceBranchPolling() { // We need to show source branch is being removed spinner in another component @@ -559,7 +520,7 @@ export default { category="primary" class="accept-merge-request" data-testid="merge-button" - :variant="mergeButtonVariant" + variant="confirm" :disabled="isMergeButtonDisabled" :loading="isMakingRequest" data-qa-selector="merge_button" @@ -570,7 +531,7 @@ export default { v-if="shouldShowMergeImmediatelyDropdown" v-gl-tooltip.hover.focus="__('Select merge moment')" :disabled="isMergeButtonDisabled" - :variant="mergeButtonVariant" + variant="confirm" data-qa-selector="merge_moment_dropdown" toggle-class="btn-icon js-merge-moment" > @@ -593,18 +554,22 @@ export default { /> </gl-dropdown> <merge-train-failed-pipeline-confirmation-dialog - :visible="isPipelineFailedModalVisible" + :visible="isPipelineFailedModalVisibleMergeTrain" @startMergeTrain="onStartMergeTrainConfirmation" - @cancel="isPipelineFailedModalVisible = false" + @cancel="isPipelineFailedModalVisibleMergeTrain = false" + /> + <merge-failed-pipeline-confirmation-dialog + :visible="isPipelineFailedModalVisibleNormalMerge" + @mergeWithFailedPipeline="onMergeWithFailedPipelineConfirmation" + @cancel="isPipelineFailedModalVisibleNormalMerge = false" /> </gl-button-group> + <merge-train-helper-icon v-if="shouldRenderMergeTrainHelperIcon" class="gl-mx-3" /> <div v-if="shouldShowMergeControls" :class="{ 'gl-w-full gl-order-n1 gl-mb-5': glFeatures.restructuredMrWidget }" class="gl-display-flex gl-align-items-center gl-flex-wrap" > - <merge-train-helper-icon v-if="shouldRenderMergeTrainHelperIcon" class="gl-mx-3" /> - <gl-form-checkbox v-if="canRemoveSourceBranch" id="remove-source-branch-input" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue index 8cf6383c26a..25ba4bf12af 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue @@ -43,8 +43,8 @@ export default { class="gl-ml-3" size="small" :icon="glFeatures.restructuredMrWidget ? undefined : 'comment-next'" - :variant="glFeatures.restructuredMrWidget && 'confirm'" - :category="glFeatures.restructuredMrWidget && 'secondary'" + :variant="glFeatures.restructuredMrWidget ? 'confirm' : 'default'" + :category="glFeatures.restructuredMrWidget ? 'secondary' : 'primary'" @click="jumpToFirstUnresolvedDiscussion" > {{ s__('mrWidget|Jump to first unresolved thread') }} |