diff options
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget/components')
22 files changed, 503 insertions, 509 deletions
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 30a0e7c383c..5339d7faf85 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 @@ -74,9 +74,11 @@ export default { </script> <template> - <div> + <div class="gl-display-flex gl-align-items-flex-start"> <gl-dropdown v-if="tertiaryButtons.length" + v-gl-tooltip + :title="__('Options')" :text="dropdownLabel" icon="ellipsis_v" no-caret diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue index f782c28ea19..2cfeb7a4bcb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue @@ -1,6 +1,6 @@ <script> import { GlButton, GlSprintf, GlLink } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { s__, __ } from '~/locale'; @@ -139,7 +139,7 @@ export default { this.fetchingApprovals = false; }) .catch(() => - createFlash({ + createAlert({ message: FETCH_ERROR, }), ); @@ -154,7 +154,7 @@ export default { this.updateApproval( () => this.service.approveMergeRequest(), () => - createFlash({ + createAlert({ message: APPROVE_ERROR, }), ); @@ -167,7 +167,7 @@ export default { this.hasApprovalAuthError = true; return; } - createFlash({ + createAlert({ message: APPROVE_ERROR, }); }, @@ -177,7 +177,7 @@ export default { this.updateApproval( () => this.service.unapproveMergeRequest(), () => - createFlash({ + createAlert({ message: UNAPPROVE_ERROR, }), ); 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 7ba387c79b1..d6d1cae4029 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,9 +1,10 @@ <script> -import createFlash from '~/flash'; +import { createAlert } 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'; +import eventHub from '../../event_hub'; import MRWidgetService from '../../services/mr_widget_service'; import { MANUAL_DEPLOY, @@ -129,11 +130,12 @@ export default { } }) .catch(() => { - createFlash({ + createAlert({ message: errorMessage, }); }) .finally(() => { + eventHub.$emit('FetchDeployments'); this.actionInProgress = null; }); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue index 1e363b0f5fb..5efa0e2879e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue @@ -7,7 +7,10 @@ import { GlLink, GlSearchBoxByType, } from '@gitlab/ui'; +import { isSafeURL } from '~/lib/utils/url_utility'; +import { s__, __ } from '~/locale'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; +import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; import ReviewAppLink from '../review_app_link.vue'; export default { @@ -19,6 +22,7 @@ export default { GlIcon, GlLink, GlSearchBoxByType, + ModalCopyButton, ReviewAppLink, }, directives: { @@ -50,6 +54,13 @@ export default { filteredChanges() { return this.deployment?.changes?.filter((change) => change.path.includes(this.searchTerm)); }, + isSafeUrl() { + return isSafeURL(this.deploymentExternalUrl); + }, + }, + i18n: { + copy: __('Copy URL'), + copyTitle: s__('Environments|Copy live environment URL'), }, }; </script> @@ -57,11 +68,20 @@ export default { <span class="gl-display-inline-flex"> <gl-button-group v-if="shouldRenderDropdown" size="small"> <review-app-link + v-if="isSafeUrl" :display="appButtonText" :link="deploymentExternalUrl" size="small" css-class="deploy-link js-deploy-url inline gl-ml-3" /> + <modal-copy-button + v-else + :title="$options.i18n.copyTitle" + :text="deploymentExternalUrl" + size="small" + > + {{ $options.i18n.copy }} + </modal-copy-button> <gl-dropdown toggle-class="gl-px-2!" size="small" class="js-mr-wigdet-deployment-dropdown"> <template #button-content> <gl-icon @@ -90,12 +110,22 @@ export default { </gl-dropdown-item> </gl-dropdown> </gl-button-group> - <review-app-link - v-else - :display="appButtonText" - :link="deploymentExternalUrl" - size="small" - css-class="js-deploy-url deploy-link btn btn-default btn-sm inline gl-ml-3" - /> + <template v-else> + <review-app-link + v-if="isSafeUrl" + :display="appButtonText" + :link="deploymentExternalUrl" + size="small" + css-class="deploy-link js-deploy-url inline gl-ml-3" + /> + <modal-copy-button + v-else + :title="$options.i18n.copyTitle" + :text="deploymentExternalUrl" + size="small" + > + {{ $options.i18n.copy }} + </modal-copy-button> + </template> </span> </template> 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 300e2a672cb..3d03dbd9db3 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 @@ -315,7 +315,6 @@ export default { data-qa-selector="mr_widget_extension" > <state-container - :mr="mr" :status="statusIconName" :is-loading="isLoadingSummary" :class="{ 'gl-cursor-pointer': isCollapsible }" @@ -324,7 +323,7 @@ export default { @mouseup="onRowMouseUp" > <div - class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center" + class="media-body gl-display-flex gl-flex-direction-row! gl-w-full" data-testid="widget-extension-top-level" > <div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js index d67ff11f297..e3f87c08ad4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js @@ -28,7 +28,7 @@ const nonStandardEvents = { }, counter: {}, }, - testReport: { + testSummary: { uniqueUser: { expand: ['i_testing_summary_widget_total'], }, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue index 94a1b805b99..870972156c5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue @@ -26,16 +26,6 @@ export default { required: false, default: true, }, - divergedCommitsCount: { - type: Number, - required: false, - default: 0, - }, - targetBranchPath: { - type: String, - required: false, - default: '', - }, }, computed: { closesText() { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue index 822c5a68093..932659f3c89 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue @@ -16,7 +16,8 @@ export default { props: { mr: { type: Object, - required: true, + required: false, + default: null, }, isLoading: { type: Boolean, @@ -80,6 +81,7 @@ export default { </div> </div> <div + v-if="mr" class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1" > <gl-button diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue index e2a9caf5419..2b22033514f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue @@ -1,6 +1,7 @@ <script> import { s__ } from '~/locale'; import StatusIcon from '../mr_widget_status_icon.vue'; +import { DETAILED_MERGE_STATUS } from '../../constants'; export default { i18n: { @@ -22,7 +23,7 @@ export default { failedText() { if (this.mr.approvals && !this.mr.isApproved) { return this.$options.i18n.approvalNeeded; - } else if (this.mr.blockingMergeRequests?.total_count > 0) { + } else if (this.mr.detailedMergeStatus === DETAILED_MERGE_STATUS.BLOCKED_STATUS) { return this.$options.i18n.blockingMergeRequests; } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index 3c6c2a44e70..92a7fa39cdc 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -2,7 +2,7 @@ import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui'; import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge'; import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { AUTO_MERGE_STRATEGIES } from '../../constants'; @@ -113,7 +113,7 @@ export default { }) .catch(() => { this.isCancellingAutoMerge = false; - createFlash({ + createAlert({ message: __('Something went wrong. Please try again.'), }); }); @@ -141,7 +141,7 @@ export default { }) .catch(() => { this.isRemovingSourceBranch = false; - createFlash({ + createAlert({ message: __('Something went wrong. Please try again.'), }); }); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index e9298b0c856..46392565088 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -1,7 +1,7 @@ <script> import { GlTooltipDirective } from '@gitlab/ui'; import api from '~/api'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { s__, __ } from '~/locale'; import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants'; import modalEventHub from '~/projects/commit/event_hub'; @@ -131,7 +131,7 @@ export default { }) .catch(() => { this.isMakingRequest = false; - createFlash({ + createAlert({ message: __('Something went wrong. Please try again.'), }); }); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 37c8d5d15f3..f6843c1f3d3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -1,6 +1,6 @@ <script> import { GlButton, GlSkeletonLoader } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import toast from '~/vue_shared/plugins/global_toast'; @@ -111,7 +111,7 @@ export default { if (error.response && error.response.data && error.response.data.merge_error) { this.rebasingError = error.response.data.merge_error; } else { - createFlash({ + createAlert({ message: __('Something went wrong. Please try again.'), }); } @@ -142,7 +142,7 @@ export default { }) .catch(() => { this.isMakingRequest = false; - createFlash({ + createAlert({ message: __('Something went wrong. Please try again.'), }); stopPolling(); 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 3cbd171a035..853895a4296 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 @@ -11,6 +11,12 @@ export default { GlSprintf, StatusIcon, }, + props: { + mr: { + type: Object, + required: true, + }, + }, computed: { troubleshootingDocsPath() { return helpPagePath('ci/troubleshooting', { anchor: 'merge-request-status-messages' }); @@ -29,7 +35,14 @@ export default { <status-icon status="failed" /> <div class="media-body space-children"> <span class="gl-font-weight-bold"> - <gl-sprintf :message="$options.i18n.failedMessage"> + <span v-if="mr.isPipelineBlocked"> + {{ + s__( + `mrWidget|Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.`, + ) + }} + </span> + <gl-sprintf v-else :message="$options.i18n.failedMessage"> <template #link="{ content }"> <gl-link :href="troubleshootingDocsPath" target="_blank"> {{ content }} 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 78430abcfe9..1298c1316e2 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,10 +14,10 @@ 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 createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { secondsToMilliseconds } from '~/lib/utils/datetime_utility'; import simplePoll from '~/lib/utils/simple_poll'; -import { __, s__ } from '~/locale'; +import { __, s__, n__ } from '~/locale'; import SmartInterval from '~/smart_interval'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { helpPagePath } from '~/helpers/help_page_helper'; @@ -325,15 +325,20 @@ export default { ); }, sourceBranchDeletedText() { - if (this.removeSourceBranch) { - return this.mr.state === 'merged' - ? __('Deleted the source branch.') - : __('Source branch will be deleted.'); + const isPreMerge = this.mr.state !== 'merged'; + + if (isPreMerge) { + return this.mr.shouldRemoveSourceBranch + ? __('Source branch will be deleted.') + : __('Source branch will not be deleted.'); } - return this.mr.state === 'merged' - ? __('Did not delete the source branch.') - : __('Source branch will not be deleted.'); + return this.mr.sourceBranchRemoved + ? __('Deleted the source branch.') + : __('Did not delete the source branch.'); + }, + sourceHasDivergedFromTarget() { + return this.mr.divergedCommitsCount > 0; }, showMergeDetailsHeader() { return ['readyToMerge'].indexOf(this.mr.state) >= 0; @@ -439,7 +444,7 @@ export default { .catch(() => { this.isMakingRequest = false; this.mr.transitionStateMachine({ transition: MERGE_FAILURE }); - createFlash({ + createAlert({ message: __('Something went wrong. Please try again.'), }); }); @@ -483,7 +488,7 @@ export default { } }) .catch(() => { - createFlash({ + createAlert({ message: __('Something went wrong while deleting the source branch. Please try again.'), }); }); @@ -507,6 +512,8 @@ export default { mergeAndSquashCommitTemplatesHintText: s__( 'mrWidget|To change these default messages, edit the templates for both the merge and squash commit messages. %{linkStart}Learn more.%{linkEnd}', ), + sourceDivergedFromTargetText: s__('mrWidget|The source branch is %{link} the target branch'), + divergedCommits: (count) => n__('%d commit behind', '%d commits behind', count), }, }; </script> @@ -530,130 +537,148 @@ export default { <div class="mr-widget-body mr-widget-body-ready-merge media mr-widget-body-line-height-1"> <div class="media-body"> <div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap"> - <gl-button-group v-if="shouldShowMergeControls" class="gl-align-self-start"> - <gl-button - size="medium" - category="primary" - class="accept-merge-request" - data-testid="merge-button" - variant="confirm" - :disabled="isMergeButtonDisabled" - :loading="isMakingRequest" - data-qa-selector="merge_button" - @click="handleMergeButtonClick(isAutoMergeAvailable)" - >{{ mergeButtonText }}</gl-button - > - <gl-dropdown - v-if="shouldShowMergeImmediatelyDropdown" - v-gl-tooltip.hover.focus="__('Select merge moment')" - :disabled="isMergeButtonDisabled" - variant="confirm" - data-qa-selector="merge_moment_dropdown" - toggle-class="btn-icon js-merge-moment" - > - <template #button-content> - <gl-icon name="chevron-down" class="mr-0" /> - <span class="sr-only">{{ __('Select merge moment') }}</span> - </template> - <gl-dropdown-item - icon-name="warning" - button-class="accept-merge-request js-merge-immediately-button" - data-qa-selector="merge_immediately_menu_item" - @click="handleMergeImmediatelyButtonClick" + <template v-if="shouldShowMergeControls"> + <div class="gl-display-flex gl-align-items-center gl-flex-wrap gl-w-full gl-mb-5"> + <gl-form-checkbox + v-if="canRemoveSourceBranch" + id="remove-source-branch-input" + v-model="removeSourceBranch" + :disabled="isRemoveSourceBranchButtonDisabled" + class="js-remove-source-branch-checkbox gl-display-flex gl-align-items-center gl-mr-5" > - {{ __('Merge immediately') }} - </gl-dropdown-item> - <merge-immediately-confirmation-dialog - ref="confirmationDialog" - :docs-url="mr.mergeImmediatelyDocsPath" - @mergeImmediately="onMergeImmediatelyConfirmation" + {{ __('Delete source branch') }} + </gl-form-checkbox> + + <!-- Placeholder for EE extension of this component --> + <squash-before-merge + v-if="shouldShowSquashBeforeMerge" + v-model="squashBeforeMerge" + :help-path="mr.squashBeforeMergeHelpPath" + :is-disabled="isSquashReadOnly" + class="gl-mr-5" /> - </gl-dropdown> - <merge-train-failed-pipeline-confirmation-dialog - :visible="isPipelineFailedModalVisibleMergeTrain" - @startMergeTrain="onStartMergeTrainConfirmation" - @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-display-flex gl-align-items-center gl-flex-wrap gl-w-full gl-order-n1 gl-mb-5" - > - <gl-form-checkbox - v-if="canRemoveSourceBranch" - id="remove-source-branch-input" - v-model="removeSourceBranch" - :disabled="isRemoveSourceBranchButtonDisabled" - class="js-remove-source-branch-checkbox gl-display-flex gl-align-items-center gl-mr-5" - > - {{ __('Delete source branch') }} - </gl-form-checkbox> - - <!-- Placeholder for EE extension of this component --> - <squash-before-merge - v-if="shouldShowSquashBeforeMerge" - v-model="squashBeforeMerge" - :help-path="mr.squashBeforeMergeHelpPath" - :is-disabled="isSquashReadOnly" - class="gl-mr-5" - /> - - <gl-form-checkbox - v-if="shouldShowSquashEdit || shouldShowMergeEdit" - v-model="editCommitMessage" - data-testid="widget_edit_commit_message" - class="gl-display-flex gl-align-items-center" - > - {{ __('Edit commit message') }} - </gl-form-checkbox> - </div> - <div - v-if="editCommitMessage" - class="gl-w-full gl-order-n1" - data-testid="edit_commit_message" - > - <ul class="border-top commits-list flex-list gl-list-style-none gl-p-0 gl-pt-4"> - <commit-edit - v-if="shouldShowSquashEdit" - :value="squashCommitMessage" - :label="__('Squash commit message')" - input-id="squash-message-edit" - class="gl-m-0! gl-p-0!" - @input="setSquashCommitMessage" + + <gl-form-checkbox + v-if="shouldShowSquashEdit || shouldShowMergeEdit" + v-model="editCommitMessage" + data-testid="widget_edit_commit_message" + class="gl-display-flex gl-align-items-center" > - <template #header> - <commit-message-dropdown :commits="commits" @input="setSquashCommitMessage" /> + {{ __('Edit commit message') }} + </gl-form-checkbox> + </div> + <div class="gl-w-full gl-text-gray-500 gl-mb-5"> + <added-commit-message + :is-squash-enabled="squashBeforeMerge" + :is-fast-forward-enabled="!shouldShowMergeEdit" + :commits-count="commitsCount" + :target-branch="stateData.targetBranch" + /> + <template v-if="mr.relatedLinks"> + · + <related-links + :state="mr.state" + :related-links="mr.relatedLinks" + :show-assign-to-me="false" + :diverged-commits-count="mr.divergedCommitsCount" + :target-branch-path="mr.targetBranchPath" + class="mr-ready-merge-related-links gl-display-inline" + /> + </template> + </div> + <div v-if="editCommitMessage" class="gl-w-full" data-testid="edit_commit_message"> + <ul class="border-top commits-list flex-list gl-list-style-none gl-p-0 gl-pt-4"> + <commit-edit + v-if="shouldShowSquashEdit" + :value="squashCommitMessage" + :label="__('Squash commit message')" + input-id="squash-message-edit" + class="gl-m-0! gl-p-0!" + @input="setSquashCommitMessage" + > + <template #header> + <commit-message-dropdown :commits="commits" @input="setSquashCommitMessage" /> + </template> + </commit-edit> + <commit-edit + v-if="shouldShowMergeEdit" + :value="commitMessage" + :label="__('Merge commit message')" + input-id="merge-message-edit" + class="gl-m-0! gl-p-0!" + @input="setCommitMessage" + /> + <li class="gl-m-0! gl-p-0!"> + <p class="form-text text-muted"> + <gl-sprintf :message="commitTemplateHintText"> + <template #link="{ content }"> + <gl-link + :href="commitTemplateHelpPage" + class="inline-link" + target="_blank" + > + {{ content }} + </gl-link> + </template> + </gl-sprintf> + </p> + </li> + </ul> + </div> + <gl-button-group class="gl-align-self-start"> + <gl-button + size="medium" + category="primary" + class="accept-merge-request" + data-testid="merge-button" + variant="confirm" + :disabled="isMergeButtonDisabled" + :loading="isMakingRequest" + data-qa-selector="merge_button" + @click="handleMergeButtonClick(isAutoMergeAvailable)" + >{{ mergeButtonText }}</gl-button + > + <gl-dropdown + v-if="shouldShowMergeImmediatelyDropdown" + v-gl-tooltip.hover.focus="__('Select merge moment')" + :disabled="isMergeButtonDisabled" + variant="confirm" + data-qa-selector="merge_moment_dropdown" + toggle-class="btn-icon js-merge-moment" + > + <template #button-content> + <gl-icon name="chevron-down" class="mr-0" /> + <span class="sr-only">{{ __('Select merge moment') }}</span> </template> - </commit-edit> - <commit-edit - v-if="shouldShowMergeEdit" - :value="commitMessage" - :label="__('Merge commit message')" - input-id="merge-message-edit" - class="gl-m-0! gl-p-0!" - @input="setCommitMessage" + <gl-dropdown-item + icon-name="warning" + button-class="accept-merge-request js-merge-immediately-button" + data-qa-selector="merge_immediately_menu_item" + @click="handleMergeImmediatelyButtonClick" + > + {{ __('Merge immediately') }} + </gl-dropdown-item> + <merge-immediately-confirmation-dialog + ref="confirmationDialog" + :docs-url="mr.mergeImmediatelyDocsPath" + @mergeImmediately="onMergeImmediatelyConfirmation" + /> + </gl-dropdown> + <merge-train-failed-pipeline-confirmation-dialog + :visible="isPipelineFailedModalVisibleMergeTrain" + @startMergeTrain="onStartMergeTrainConfirmation" + @cancel="isPipelineFailedModalVisibleMergeTrain = false" /> - <li class="gl-m-0! gl-p-0!"> - <p class="form-text text-muted"> - <gl-sprintf :message="commitTemplateHintText"> - <template #link="{ content }"> - <gl-link :href="commitTemplateHelpPage" class="inline-link" target="_blank"> - {{ content }} - </gl-link> - </template> - </gl-sprintf> - </p> - </li> - </ul> - </div> + <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" /> + </template> <div - v-if="!shouldShowMergeControls" + v-else class="gl-w-full gl-order-n1 mr-widget-merge-details" data-qa-selector="merged_status_content" > @@ -661,13 +686,11 @@ export default { {{ __('Merge details') }} </p> <ul class="gl-pl-4 gl-mb-0 gl-ml-3 gl-text-gray-600"> - <li v-if="mr.divergedCommitsCount > 0" class="gl-line-height-normal"> - <gl-sprintf - :message="s__('mrWidget|The source branch is %{link} the target branch')" - > + <li v-if="sourceHasDivergedFromTarget" class="gl-line-height-normal"> + <gl-sprintf :message="$options.i18n.sourceDivergedFromTargetText"> <template #link> <gl-link :href="mr.targetBranchPath">{{ - n__('%d commit behind', '%d commits behind', mr.divergedCommitsCount) + $options.i18n.divergedCommits(mr.divergedCommitsCount) }}</gl-link> </template> </gl-sprintf> @@ -696,29 +719,6 @@ export default { </li> </ul> </div> - <div - v-else - :class="{ 'gl-mb-5': shouldShowMergeControls }" - class="gl-w-full gl-order-n1 gl-text-gray-500" - > - <added-commit-message - :is-squash-enabled="squashBeforeMerge" - :is-fast-forward-enabled="!shouldShowMergeEdit" - :commits-count="commitsCount" - :target-branch="stateData.targetBranch" - /> - <template v-if="mr.relatedLinks"> - · - <related-links - :state="mr.state" - :related-links="mr.relatedLinks" - :show-assign-to-me="false" - :diverged-commits-count="mr.divergedCommitsCount" - :target-branch-path="mr.targetBranchPath" - class="mr-ready-merge-related-links gl-display-inline" - /> - </template> - </div> </div> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index 0458e9dfaf5..dee27a5d5b5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -2,7 +2,7 @@ import { GlButton } from '@gitlab/ui'; import { produce } from 'immer'; import $ from 'jquery'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import toast from '~/vue_shared/plugins/global_toast'; import { __ } from '~/locale'; import MergeRequest from '~/merge_request'; @@ -77,7 +77,7 @@ export default { }, ) { if (errors?.length) { - createFlash({ + createAlert({ message: __('Something went wrong. Please try again.'), }); @@ -130,7 +130,7 @@ export default { }, ) .catch(() => - createFlash({ + createAlert({ message: __('Something went wrong. Please try again.'), }), ) @@ -152,7 +152,7 @@ export default { }) .catch(() => { this.isMakingRequest = false; - createFlash({ + createAlert({ message: __('Something went wrong. Please try again.'), }); }); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue deleted file mode 100644 index 18fdb29ba54..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue +++ /dev/null @@ -1,137 +0,0 @@ -<script> -import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui'; -import axios from '~/lib/utils/axios_utils'; -import Poll from '~/lib/utils/poll'; -import { n__ } from '~/locale'; -import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue'; -import TerraformPlan from './terraform_plan.vue'; - -export default { - name: 'MRWidgetTerraformContainer', - components: { - GlSkeletonLoader, - GlSprintf, - MrWidgetExpanableSection, - TerraformPlan, - }, - props: { - endpoint: { - type: String, - required: true, - }, - }, - data() { - return { - loading: true, - plansObject: {}, - poll: null, - }; - }, - computed: { - inValidPlanCountText() { - if (this.numberOfInvalidPlans === 0) { - return null; - } - - return n__( - 'Terraform|%{number} Terraform report failed to generate', - 'Terraform|%{number} Terraform reports failed to generate', - this.numberOfInvalidPlans, - ); - }, - numberOfInvalidPlans() { - return Object.values(this.plansObject).filter((plan) => plan.tf_report_error).length; - }, - numberOfPlans() { - return Object.keys(this.plansObject).length; - }, - numberOfValidPlans() { - return this.numberOfPlans - this.numberOfInvalidPlans; - }, - validPlanCountText() { - if (this.numberOfValidPlans === 0) { - return null; - } - - return n__( - 'Terraform|%{number} Terraform report was generated in your pipelines', - 'Terraform|%{number} Terraform reports were generated in your pipelines', - this.numberOfValidPlans, - ); - }, - }, - created() { - this.fetchPlans(); - }, - beforeDestroy() { - this.poll.stop(); - }, - methods: { - fetchPlans() { - this.loading = true; - - this.poll = new Poll({ - resource: { - fetchPlans: () => axios.get(this.endpoint), - }, - data: this.endpoint, - method: 'fetchPlans', - successCallback: ({ data }) => { - this.plansObject = data; - - if (this.numberOfPlans > 0) { - this.loading = false; - this.poll.stop(); - } - }, - errorCallback: () => { - this.plansObject = { bad_plan: { tf_report_error: 'api_error' } }; - this.loading = false; - this.poll.stop(); - }, - }); - - this.poll.makeRequest(); - }, - }, -}; -</script> - -<template> - <section class="mr-widget-section"> - <div v-if="loading" class="mr-widget-body"> - <gl-skeleton-loader /> - </div> - - <mr-widget-expanable-section v-else> - <template #header> - <div - data-testid="terraform-header-text" - class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column" - > - <p v-if="validPlanCountText" class="gl-m-0"> - <gl-sprintf :message="validPlanCountText"> - <template #number> - <strong>{{ numberOfValidPlans }}</strong> - </template> - </gl-sprintf> - </p> - - <p v-if="inValidPlanCountText" class="gl-m-0"> - <gl-sprintf :message="inValidPlanCountText"> - <template #number> - <strong>{{ numberOfInvalidPlans }}</strong> - </template> - </gl-sprintf> - </p> - </div> - </template> - - <template #content> - <div class="mr-widget-body gl-pb-1"> - <terraform-plan v-for="(plan, key) in plansObject" :key="key" :plan="plan" /> - </div> - </template> - </mr-widget-expanable-section> - </section> -</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue deleted file mode 100644 index 1e5f7361966..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue +++ /dev/null @@ -1,119 +0,0 @@ -<script> -import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; -import { s__ } from '~/locale'; - -export default { - name: 'TerraformPlan', - components: { - GlIcon, - GlLink, - GlSprintf, - }, - props: { - plan: { - required: true, - type: Object, - }, - }, - i18n: { - changes: s__( - 'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete', - ), - generationErrored: s__('Terraform|Generating the report caused an error.'), - namedReportFailed: s__('Terraform|The job %{name} failed to generate a report.'), - namedReportGenerated: s__('Terraform|The job %{name} generated a report.'), - reportFailed: s__('Terraform|A report failed to generate.'), - reportGenerated: s__('Terraform|A report was generated in your pipelines.'), - }, - computed: { - addNum() { - return Number(this.plan.create); - }, - changeNum() { - return Number(this.plan.update); - }, - deleteNum() { - return Number(this.plan.delete); - }, - iconType() { - return this.validPlanValues ? 'doc-changes' : 'warning'; - }, - reportChangeText() { - if (this.validPlanValues) { - return this.$options.i18n.changes; - } - - return this.$options.i18n.generationErrored; - }, - reportHeaderText() { - if (this.validPlanValues) { - return this.plan.job_name - ? this.$options.i18n.namedReportGenerated - : this.$options.i18n.reportGenerated; - } - - return this.plan.job_name - ? this.$options.i18n.namedReportFailed - : this.$options.i18n.reportFailed; - }, - validPlanValues() { - return this.addNum + this.changeNum + this.deleteNum >= 0; - }, - }, -}; -</script> - -<template> - <div class="gl-display-flex gl-pb-3"> - <span - class="gl-display-flex gl-align-items-center gl-justify-content-center gl-align-self-start gl-px-2" - > - <gl-icon :name="iconType" :size="16" data-testid="change-type-icon" /> - </span> - - <div class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column flex-md-row gl-pl-3"> - <div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-pr-3"> - <p class="gl-mb-3 gl-line-height-normal"> - <gl-sprintf :message="reportHeaderText"> - <template #name> - <strong>{{ plan.job_name }}</strong> - </template> - </gl-sprintf> - </p> - - <p class="gl-mb-3 gl-line-height-normal"> - <gl-sprintf :message="reportChangeText"> - <template #addNum> - <strong>{{ addNum }}</strong> - </template> - - <template #changeNum> - <strong>{{ changeNum }}</strong> - </template> - - <template #deleteNum> - <strong>{{ deleteNum }}</strong> - </template> - </gl-sprintf> - </p> - </div> - - <div> - <gl-link - v-if="plan.job_path" - :href="plan.job_path" - target="_blank" - data-testid="terraform-report-link" - data-track-action="click_terraform_mr_plan_button" - data-track-label="mr_widget_terraform_mr_plan_button" - data-track-property="terraform_mr_plan_button" - class="btn btn-sm" - rel="noopener" - > - {{ __('View full log') }} - <gl-icon name="external-link" /> - </gl-link> - </div> - </div> - </div> -</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue new file mode 100644 index 00000000000..d1ade2886f4 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue @@ -0,0 +1,98 @@ +<script> +import { GlBadge, GlLink, GlSafeHtmlDirective } from '@gitlab/ui'; +import Actions from '../action_buttons.vue'; +import { generateText } from '../extensions/utils'; +import ContentRow from './widget_content_row.vue'; + +export default { + name: 'DynamicContent', + components: { + GlBadge, + GlLink, + Actions, + ContentRow, + }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, + props: { + data: { + type: Object, + required: true, + }, + widgetName: { + type: String, + required: true, + }, + level: { + type: Number, + required: false, + default: 2, + }, + }, + computed: { + statusIcon() { + return this.data.icon?.name || undefined; + }, + generatedText() { + return generateText(this.data.text); + }, + generatedSubtext() { + return generateText(this.data.subtext); + }, + generatedSupportingText() { + return generateText(this.data.supportingText); + }, + }, + methods: { + onClickedAction(action) { + this.$emit('clickedAction', action); + }, + }, +}; +</script> + +<template> + <content-row + :level="level" + :status-icon-name="statusIcon" + :widget-name="widgetName" + :header="data.header" + > + <template #body> + <div class="gl-display-flex gl-flex-direction-column"> + <div> + <p v-safe-html="generatedText" class="gl-mb-0"></p> + <gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link> + <p v-if="data.supportingText" v-safe-html="generatedSupportingText" class="gl-mb-0"></p> + <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'"> + {{ data.badge.text }} + </gl-badge> + <actions + :widget="widgetName" + :tertiary-buttons="data.actions" + class="gl-ml-auto gl-pl-3" + @clickedAction="onClickedAction" + /> + <p v-if="data.subtext" v-safe-html="generatedSubtext" class="gl-m-0 gl-font-sm"></p> + </div> + <ul + v-if="data.children && data.children.length > 0 && level === 2" + class="gl-m-0 gl-p-0 gl-list-style-none" + > + <li> + <dynamic-content + v-for="(childData, index) in data.children" + :key="childData.id || index" + :data="childData" + :widget-name="widgetName" + :level="3" + data-qa-selector="child_content" + @clickedAction="onClickedAction" + /> + </li> + </ul> + </div> + </template> + </content-row> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue new file mode 100644 index 00000000000..ff17de343d6 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue @@ -0,0 +1,67 @@ +<script> +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; +import { EXTENSION_ICON_CLASS, EXTENSION_ICON_NAMES } from '../../constants'; + +export default { + components: { + GlLoadingIcon, + GlIcon, + }, + props: { + level: { + type: Number, + required: false, + default: 1, + }, + name: { + type: String, + required: false, + default: '', + }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, + iconName: { + type: String, + required: false, + default: null, + }, + }, + computed: { + iconAriaLabel() { + return `${capitalizeFirstCharacter(this.iconName)} ${this.name}`; + }, + iconSize() { + return this.level === 1 ? 16 : 12; + }, + }, + EXTENSION_ICON_NAMES, + EXTENSION_ICON_CLASS, +}; +</script> + +<template> + <div :class="[$options.EXTENSION_ICON_CLASS[iconName]]" class="gl-mr-3"> + <gl-loading-icon v-if="isLoading" size="md" inline /> + <div + v-else + class="gl-display-flex gl-align-items-center gl-justify-content-center gl-rounded-full gl-bg-gray-10" + :class="{ + 'gl-p-2': level === 1, + }" + > + <div class="gl-rounded-full gl-bg-white"> + <gl-icon + :name="$options.EXTENSION_ICON_NAMES[iconName]" + :size="iconSize" + :aria-label="iconAriaLabel" + :data-qa-selector="`status_${iconName}_icon`" + class="gl-display-block" + /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue index c9fc2dde0bd..94359d7d6ac 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue @@ -4,10 +4,11 @@ import * as Sentry from '@sentry/browser'; import { normalizeHeaders } from '~/lib/utils/common_utils'; import { sprintf, __ } from '~/locale'; import Poll from '~/lib/utils/poll'; -import StatusIcon from '../extensions/status_icon.vue'; import ActionButtons from '../action_buttons.vue'; import { EXTENSION_ICONS } from '../../constants'; -import ContentSection from './widget_content_section.vue'; +import ContentRow from './widget_content_row.vue'; +import DynamicContent from './dynamic_content.vue'; +import StatusIcon from './status_icon.vue'; const FETCH_TYPE_COLLAPSED = 'collapsed'; const FETCH_TYPE_EXPANDED = 'expanded'; @@ -18,7 +19,8 @@ export default { StatusIcon, GlButton, GlLoadingIcon, - ContentSection, + ContentRow, + DynamicContent, }, directives: { GlTooltip: GlTooltipDirective, @@ -59,7 +61,7 @@ export default { }, // If the content slot is not used, this value will be used as a fallback. content: { - type: Object, + type: Array, required: false, default: undefined, }, @@ -187,7 +189,7 @@ export default { <template> <section class="media-section" data-testid="widget-extension"> - <div class="media gl-p-5"> + <div class="gl-p-5 gl-align-items-center gl-display-flex"> <status-icon :level="1" :name="widgetName" @@ -227,23 +229,34 @@ export default { </div> <div v-if="!isCollapsed || contentError" - class="mr-widget-grouped-section gl-relative" + class="gl-relative gl-bg-gray-10" data-testid="widget-extension-collapsed-section" > <div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center"> - <gl-loading-icon size="sm" inline /> {{ __('Loading...') }} + <gl-loading-icon size="sm" inline /> {{ loadingText }} + </div> + <div v-else class="gl-px-5 gl-display-flex"> + <content-row + v-if="contentError" + :level="2" + :status-icon-name="$options.failedStatusIcon" + :widget-name="widgetName" + > + <template #body> + {{ contentError }} + </template> + </content-row> + <div v-else class="gl-w-full"> + <slot name="content"> + <dynamic-content + v-for="(data, index) in content" + :key="data.id || index" + :data="data" + :widget-name="widgetName" + /> + </slot> + </div> </div> - <content-section - v-else-if="contentError" - class="report-block-container" - :status-icon-name="$options.failedStatusIcon" - :widget-name="widgetName" - > - {{ contentError }} - </content-section> - <slot v-else name="content"> - {{ content }} - </slot> </div> </section> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue new file mode 100644 index 00000000000..ee81f0950a8 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue @@ -0,0 +1,68 @@ +<script> +import { GlSafeHtmlDirective } from '@gitlab/ui'; +import { EXTENSION_ICONS } from '../../constants'; +import { generateText } from '../extensions/utils'; +import StatusIcon from './status_icon.vue'; + +export default { + components: { + StatusIcon, + }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, + props: { + level: { + type: Number, + required: true, + validator: (value) => value === 2 || value === 3, + }, + statusIconName: { + type: String, + default: '', + required: false, + validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).includes(value), + }, + widgetName: { + type: String, + required: true, + }, + header: { + type: [String, Array], + default: '', + required: false, + }, + }, + computed: { + generatedHeader() { + return generateText(Array.isArray(this.header) ? this.header[0] : this.header); + }, + generatedSubheader() { + return Array.isArray(this.header) && this.header[1] ? generateText(this.header[1]) : ''; + }, + }, +}; +</script> +<template> + <div + class="gl-w-full gl-display-flex mr-widget-content-row gl-align-items-baseline" + :class="{ 'gl-border-t gl-py-3 gl-pl-7': level === 2 }" + > + <status-icon v-if="statusIconName" :level="2" :name="widgetName" :icon-name="statusIconName" /> + <div> + <slot name="header"> + <div v-if="header" class="gl-mb-2"> + <strong v-safe-html="generatedHeader" class="gl-display-block"></strong + ><span + v-if="generatedSubheader" + v-safe-html="generatedSubheader" + class="gl-display-block" + ></span> + </div> + </slot> + <div class="gl-display-flex gl-align-items-baseline gl-w-full"> + <slot name="body"></slot> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue deleted file mode 100644 index 61e3744b5dc..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue +++ /dev/null @@ -1,35 +0,0 @@ -<script> -import { EXTENSION_ICONS } from '../../constants'; -import StatusIcon from '../extensions/status_icon.vue'; - -export default { - components: { - StatusIcon, - }, - props: { - statusIconName: { - type: String, - default: '', - required: false, - validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).includes(value), - }, - widgetName: { - type: String, - required: true, - }, - }, -}; -</script> -<template> - <div class="gl-px-7"> - <div class="gl-pl-4 gl-display-flex"> - <status-icon - v-if="statusIconName" - :level="2" - :name="widgetName" - :icon-name="statusIconName" - /> - <slot name="default"></slot> - </div> - </div> -</template> |