diff options
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget')
19 files changed, 405 insertions, 121 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') }} diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js index 32effb91043..d337a554663 100644 --- a/app/assets/javascripts/vue_merge_request_widget/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/constants.js @@ -68,6 +68,7 @@ const STATE_MACHINE = { states: { IDLE: 'IDLE', MERGING: 'MERGING', + MERGED: 'MERGED', AUTO_MERGE: 'AUTO_MERGE', }, transitions: { @@ -75,6 +76,7 @@ const STATE_MACHINE = { AUTO_MERGE: 'start-auto-merge', MERGE_FAILURE: 'merge-failed', MERGED: 'merge-done', + MERGING: 'merging', }, }; const { states, transitions } = STATE_MACHINE; @@ -86,11 +88,12 @@ STATE_MACHINE.definition = { on: { [transitions.MERGE]: states.MERGING, [transitions.AUTO_MERGE]: states.AUTO_MERGE, + [transitions.MERGING]: states.MERGING, }, }, [states.MERGING]: { on: { - [transitions.MERGED]: states.IDLE, + [transitions.MERGED]: states.MERGED, [transitions.MERGE_FAILURE]: states.IDLE, }, }, @@ -110,6 +113,7 @@ export const stateToTransitionMap = { }; export const stateToComponentMap = { [states.MERGING]: classStateMap[stateKey.merging], + [states.MERGED]: classStateMap[stateKey.merged], [states.AUTO_MERGE]: classStateMap[stateKey.autoMergeEnabled], }; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js new file mode 100644 index 00000000000..168f10bd148 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js @@ -0,0 +1,120 @@ +import { uniqueId } from 'lodash'; +import { __, n__, s__, sprintf } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; +import { EXTENSION_ICONS } from '../../constants'; + +export default { + name: 'WidgetAccessibility', + enablePolling: true, + i18n: { + loading: s__('Reports|Accessibility scanning results are being parsed'), + error: s__('Reports|Accessibility scanning failed loading results'), + }, + props: ['accessibilityReportPath'], + computed: { + statusIcon() { + return this.collapsedData.status === 'failed' + ? EXTENSION_ICONS.warning + : EXTENSION_ICONS.success; + }, + }, + methods: { + summary() { + const numOfResults = this.collapsedData?.summary?.errored || 0; + + const successText = s__( + 'Reports|Accessibility scanning detected no issues for the source branch only', + ); + const warningText = sprintf( + n__( + 'Reports|Accessibility scanning detected %{strong_start}%{number}%{strong_end} issue for the source branch only', + 'Reports|Accessibility scanning detected %{strong_start}%{number}%{strong_end} issues for the source branch only', + numOfResults, + ), + { + number: numOfResults, + }, + false, + ); + + return numOfResults === 0 ? successText : warningText; + }, + fetchCollapsedData() { + return axios.get(this.accessibilityReportPath); + }, + fetchFullData() { + return Promise.resolve(this.prepareReports()); + }, + parsedTECHSCode(code) { + /* + * In issue code looks like "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail" + * or "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent" + * + * The TECHS code is the "G18", "G168", "H91", etc. from the code which is used for the documentation. + * Here we simply split the string on `.` and get the code in the 5th position + */ + return code?.split('.')[4]; + }, + formatLearnMoreUrl(code) { + const parsed = this.parsedTECHSCode(code); + // eslint-disable-next-line @gitlab/require-i18n-strings + return `https://www.w3.org/TR/WCAG20-TECHS/${parsed || 'Overview'}.html`; + }, + formatText(code) { + return sprintf( + s__( + 'AccessibilityReport|The accessibility scanning found an error of the following type: %{code}', + ), + { code }, + ); + }, + formatMessage(message) { + return sprintf(s__('AccessibilityReport|Message: %{message}'), { message }); + }, + prepareReports() { + const { new_errors, existing_errors, resolved_errors } = this.collapsedData; + + const newErrors = new_errors.map((error) => { + return { + header: __('New'), + id: uniqueId('new-error-'), + text: this.formatText(error.code), + icon: { name: EXTENSION_ICONS.failed }, + link: { + href: this.formatLearnMoreUrl(error.code), + text: __('Learn more'), + }, + supportingText: this.formatMessage(error.message), + }; + }); + + const existingErrors = existing_errors.map((error) => { + return { + id: uniqueId('existing-error-'), + text: this.formatText(error.code), + icon: { name: EXTENSION_ICONS.failed }, + link: { + href: this.formatLearnMoreUrl(error.code), + text: __('Learn more'), + }, + supportingText: this.formatMessage(error.message), + }; + }); + + const resolvedErrors = resolved_errors.map((error) => { + return { + id: uniqueId('resolved-error-'), + text: this.formatText(error.code), + icon: { name: EXTENSION_ICONS.success }, + link: { + href: this.formatLearnMoreUrl(error.code), + text: __('Learn more'), + }, + supportingText: this.formatMessage(error.message), + }; + }); + + return [...newErrors, ...existingErrors, ...resolvedErrors]; + }, + }, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js index ba3336df2eb..4aeebf095c4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js @@ -25,9 +25,9 @@ export default { n__( 'ciReport|Load performance test metrics detected %{strong_start}%{changesFound}%{strong_end} change', 'ciReport|Load performance test metrics detected %{strong_start}%{changesFound}%{strong_end} changes', - changesFound, + count, ), - { changesFound }, + { changesFound: count }, ); }, // Status icon to be used next to the summary text diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js index a564acada02..8fcc4f818ec 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js @@ -73,26 +73,30 @@ export default { return `${title}${subtitle}`; }, fetchCollapsedData() { - return Promise.resolve(this.fetchPlans().then(this.prepareReports)); - }, - fetchFullData() { - const { valid, invalid } = this.collapsedData; - return Promise.resolve([...valid, ...invalid]); - }, - // Custom methods - fetchPlans() { return axios .get(this.terraformReportsPath) - .then(({ data }) => { - return Object.keys(data).map((key) => { - return data[key]; + .then((res) => { + const reports = Object.keys(res.data).map((key) => { + return res.data[key]; }); + + const formattedData = this.prepareReports(reports); + + return { + ...res, + data: formattedData, + }; }) .catch(() => { - const invalidData = { tf_report_error: 'api_error' }; - return [invalidData]; + const formattedData = this.prepareReports([{ tf_report_error: 'api_error' }]); + + return { data: formattedData }; }); }, + fetchFullData() { + const { valid, invalid } = this.collapsedData; + return Promise.resolve([...valid, ...invalid]); + }, createReportRow(report, iconName) { const addNum = Number(report.create); const changeNum = Number(report.update); diff --git a/app/assets/javascripts/vue_merge_request_widget/i18n.js b/app/assets/javascripts/vue_merge_request_widget/i18n.js index c88e795e5f3..454a14faabb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/i18n.js +++ b/app/assets/javascripts/vue_merge_request_widget/i18n.js @@ -1,4 +1,14 @@ -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; + +export const MR_WIDGET_MISSING_BRANCH_WHICH = s__( + 'mrWidget|The %{type} branch %{codeStart}%{name}%{codeEnd} does not exist.', +); +export const MR_WIDGET_MISSING_BRANCH_RESTORE = s__( + 'mrWidget|Please restore it or use a different %{type} branch.', +); +export const MR_WIDGET_MISSING_BRANCH_MANUALCLI = s__( + 'mrWidget|If the %{type} branch exists in your local repository, you can merge this merge request manually using the command line.', +); export const SQUASH_BEFORE_MERGE = { tooltipTitle: __('Required in this project.'), @@ -10,3 +20,8 @@ export const I18N_SHA_MISMATCH = { warningMessage: __('Merge blocked: new changes were just added.'), actionButtonLabel: __('Review changes'), }; + +export const MERGE_TRAIN_BUTTON_TEXT = { + failed: __('Start merge train...'), + passed: __('Start merge train'), +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js index fa618756bb5..247a3711fc8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js @@ -48,7 +48,7 @@ export default { pipelineId() { return this.pipeline.id; }, - showFailedPipelineModal() { + showFailedPipelineModalMergeTrain() { return false; }, }, diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 83a07240403..11de58aa344 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -45,6 +45,7 @@ import eventHub from './event_hub'; import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables'; import getStateQuery from './queries/get_state.query.graphql'; import terraformExtension from './extensions/terraform'; +import accessibilityExtension from './extensions/accessibility'; export default { // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25 @@ -205,7 +206,7 @@ export default { ); }, shouldShowAccessibilityReport() { - return this.mr.accessibilityReportPath; + return Boolean(this.mr?.accessibilityReportPath); }, formattedHumanAccess() { return (this.mr.humanAccess || '').toLowerCase(); @@ -240,6 +241,11 @@ export default { this.registerTerraformPlans(); } }, + shouldShowAccessibilityReport(newVal) { + if (newVal) { + this.registerAccessibilityExtension(); + } + }, }, mounted() { MRWidgetService.fetchInitialData() @@ -478,6 +484,11 @@ export default { registerExtension(terraformExtension); } }, + registerAccessibilityExtension() { + if (this.shouldShowAccessibilityReport && this.shouldShowExtension) { + registerExtension(accessibilityExtension); + } + }, }, }; </script> @@ -567,7 +578,7 @@ export default { :endpoint="mr.accessibilityReportPath" /> - <div class="mr-widget-section"> + <div class="mr-widget-section" data-qa-selector="mr_widget_content"> <component :is="componentName" :mr="mr" :service="service" /> <ready-to-merge v-if="isRestructuredMrWidgetEnabled && mr.commitsCount" diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql index 0b8396b4461..25c44beaf18 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql @@ -3,7 +3,6 @@ query getState($projectPath: ID!, $iid: String!) { id archived onlyAllowMergeIfPipelineSucceeds - mergeRequest(iid: $iid) { id autoMergeEnabled diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql index 2d79d35cf24..ad93a3a7371 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql @@ -4,6 +4,7 @@ query autoMergeEnabled($projectPath: ID!, $iid: String!) { project(fullPath: $projectPath) { id mergeRequest(iid: $iid) { + id ...autoMergeEnabled } } diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql index f713739f65a..556ecee254d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql @@ -2,6 +2,7 @@ query readyToMerge($projectPath: ID!, $iid: String!) { project(fullPath: $projectPath) { + id ...ReadyToMerge } } |