diff options
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget')
25 files changed, 378 insertions, 148 deletions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue index 492e68b636f..437d035fbf5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue @@ -12,6 +12,11 @@ export default { }, mixins: [glFeatureFlagMixin()], props: { + state: { + type: String, + required: false, + default: '', + }, isSquashEnabled: { type: Boolean, required: false, @@ -30,8 +35,16 @@ export default { type: String, required: true, }, + mergeCommitSha: { + type: String, + required: false, + default: '', + }, }, computed: { + isMerged() { + return this.state === 'merged'; + }, targetBranchEscaped() { return escape(this.targetBranch); }, @@ -39,6 +52,22 @@ export default { return n__('%d commit', '%d commits', this.isSquashEnabled ? 1 : this.commitsCount); }, message() { + if (this.glFeatures.restructuredMrWidget) { + if (this.state === 'closed') { + return s__('mrWidgetCommitsAdded|The changes were not merged into %{targetBranch}.'); + } else if (this.isMerged) { + return s__( + 'mrWidgetCommitsAdded|Changes merged into %{targetBranch} with %{mergeCommitSha}%{squashedCommits}.', + ); + } + + return this.isFastForwardEnabled + ? s__('mrWidgetCommitsAdded|%{commitCount} will be added to %{targetBranch}.') + : s__( + 'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}%{squashedCommits}.', + ); + } + return this.isFastForwardEnabled ? s__('mrWidgetCommitsAdded|Adds %{commitCount} to %{targetBranch}.') : s__( @@ -48,6 +77,13 @@ export default { textDecorativeComponent() { return this.glFeatures.restructuredMrWidget ? 'span' : 'strong'; }, + squashCommitMessage() { + if (this.isMerged) { + return s__('mergedCommitsAdded|(commits were squashed)'); + } + + return n__('(squashes %d commit)', '(squashes %d commits)', this.commitsCount); + }, }, mergeCommitCount, }; @@ -69,9 +105,14 @@ export default { </template> <template #squashedCommits> <template v-if="glFeatures.restructuredMrWidget && isSquashEnabled"> - {{ n__('(squashes %d commit)', '(squashes %d commits)', commitsCount) }}</template + {{ squashCommitMessage }}</template ></template > + <template #mergeCommitSha> + <template v-if="glFeatures.restructuredMrWidget" + ><span class="label-branch">{{ mergeCommitSha }}</span></template + > + </template> </gl-sprintf> </span> </template> 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 24cefd63ce3..e7d5e4086bc 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 @@ -2,8 +2,11 @@ import { GlButton } from '@gitlab/ui'; import createFlash from '~/flash'; import { BV_SHOW_MODAL } from '~/lib/utils/constants'; -import { s__ } from '~/locale'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { s__, __ } from '~/locale'; import sidebarEventHub from '~/sidebar/event_hub'; +import showToast from '~/vue_shared/plugins/global_toast'; +import SidebarMediator from '~/sidebar/sidebar_mediator'; import eventHub from '../../event_hub'; import approvalsMixin from '../../mixins/approvals'; import MrWidgetContainer from '../mr_widget_container.vue'; @@ -21,7 +24,7 @@ export default { ApprovalsSummaryOptional, GlButton, }, - mixins: [approvalsMixin], + mixins: [approvalsMixin, glFeatureFlagsMixin()], props: { mr: { type: Object, @@ -171,6 +174,14 @@ export default { return serviceFn() .then((data) => { this.mr.setApprovals(data); + + if ( + this.glFeatures.mrAttentionRequests && + SidebarMediator.singleton?.store.currentUserHasAttention + ) { + showToast(__('Approved. Your attention request was removed.')); + } + eventHub.$emit('MRWidgetUpdateRequested'); eventHub.$emit('ApprovalUpdated'); sidebarEventHub.$emit('removeCurrentUserAttentionRequested'); @@ -217,7 +228,7 @@ export default { <slot :is-approving="isApproving" :approve-with-auth="approveWithAuth" - :hasApproval-auth-error="hasApprovalAuthError" + :has-approval-auth-error="hasApprovalAuthError" ></slot> </template> </div> 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 f1b89c42fb5..0bc17de638b 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 @@ -47,6 +47,8 @@ export default { fullData: [], isCollapsed: true, showFade: false, + modalData: undefined, + modalName: undefined, }; }, computed: { @@ -116,6 +118,9 @@ export default { return summary; }, + modalId() { + return this.modalName || `modal${this.$options.name}`; + }, }, watch: { isCollapsed(newVal) { @@ -249,7 +254,7 @@ export default { class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center" data-testid="widget-extension-top-level" > - <div class="gl-flex-grow-1"> + <div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary"> <template v-if="isLoadingSummary">{{ widgetLoadingText }}</template> <template v-else-if="hasFetchError">{{ widgetErrorText }}</template> <div v-else> @@ -306,12 +311,20 @@ export default { data-testid="extension-list-item" > <gl-intersection-observer - :options="{ rootMargin: '100px', thresholds: 0.1 }" + :options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ { + rootMargin: '100px', + thresholds: 0.1, + } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" class="gl-w-full" @appear="appear(index)" @disappear="disappear(index)" > - <child-content :data="item" :widget-label="widgetLabel" :level="2" /> + <child-content + :data="item" + :widget-label="widgetLabel" + :modal-id="modalId" + :level="2" + /> </gl-intersection-observer> </div> </dynamic-scroller-item> @@ -322,5 +335,8 @@ export default { class="fade mr-extenson-scrim gl-absolute gl-left-0 gl-bottom-0 gl-w-full gl-h-7 gl-pointer-events-none" ></div> </div> + <div v-if="$options.modalComponent && modalData"> + <component :is="$options.modalComponent" :modal-id="modalId" v-bind="modalData" /> + </div> </section> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue index 5cfee21dd5e..0ca4c92a5ae 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue @@ -1,5 +1,5 @@ <script> -import { GlBadge, GlLink, GlSafeHtmlDirective } from '@gitlab/ui'; +import { GlBadge, GlLink, GlSafeHtmlDirective, GlModalDirective } from '@gitlab/ui'; import StatusIcon from './status_icon.vue'; import Actions from './actions.vue'; import { generateText } from './utils'; @@ -14,6 +14,7 @@ export default { }, directives: { SafeHtml: GlSafeHtmlDirective, + GlModal: GlModalDirective, }, props: { data: { @@ -24,6 +25,11 @@ export default { type: String, required: true, }, + modalId: { + type: String, + required: false, + default: null, + }, level: { type: Number, required: true, @@ -63,6 +69,11 @@ export default { <div v-if="data.link"> <gl-link :href="data.link.href">{{ data.link.text }}</gl-link> </div> + <div v-if="data.modal"> + <gl-link v-gl-modal="modalId" @click="data.modal.onClick"> + {{ data.modal.text }} + </gl-link> + </div> <div v-if="data.supportingText"> <p v-safe-html="generateText(data.supportingText)" class="gl-m-0"></p> </div> @@ -87,6 +98,7 @@ export default { :key="childData.id" :data="childData" :widget-label="widgetLabel" + :modal-id="modalId" :level="3" data-testid="child-content" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js index 8438f3492b2..65273678fb9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js @@ -14,6 +14,7 @@ export const registerExtension = (extension) => { i18n: extension.i18n, expandEvent: extension.expandEvent, enablePolling: extension.enablePolling, + modalComponent: extension.modalComponent, computed: { ...Object.keys(extension.computed).reduce( (acc, computedKey) => ({ diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue index 01d8de132e7..456a1f17aae 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue @@ -49,7 +49,7 @@ export default { ]" class="gl-rounded-full gl-mr-3 gl-relative gl-p-2" > - <gl-loading-icon v-if="isLoading" size="md" inline class="gl-display-block" /> + <gl-loading-icon v-if="isLoading" size="lg" inline class="gl-display-block" /> <gl-icon v-else :name="$options.EXTENSION_ICON_NAMES[iconName]" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js index 5fba070f79c..cba12507eba 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js @@ -35,9 +35,7 @@ const textStyleTags = { [getStartTag('small')]: '<span class="gl-font-sm gl-text-gray-700">', }; -export const generateText = (text) => { - if (typeof text !== 'string') return null; - +const createText = (text) => { return text .replace( new RegExp( @@ -60,3 +58,21 @@ export const generateText = (text) => { ) .replace(/%{([a-z]|_)+}/g, ''); // Filter out any tags we don't know about }; + +export const generateText = (text) => { + if (typeof text === 'string') { + return createText(text); + } else if ( + typeof text === 'object' && + typeof text.text === 'string' && + typeof text.href === 'string' + ) { + return createText( + `${ + text.prependText ? `${text.prependText} ` : '' + }<a class="gl-text-decoration-underline" href="${text.href}">${text.text}</a>`, + ); + } + + return null; +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index 8cdaa3316ee..e1d88099580 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -1,9 +1,5 @@ <script> import { - GlButton, - GlDropdown, - GlDropdownSectionHeader, - GlDropdownItem, GlLink, GlTooltipDirective, GlModalDirective, @@ -14,8 +10,6 @@ import { constructWebIDEPath } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; -import WebIdeLink from '~/vue_shared/components/web_ide_link.vue'; -import MrWidgetHowToMergeModal from './mr_widget_how_to_merge_modal.vue'; import MrWidgetIcon from './mr_widget_icon.vue'; export default { @@ -24,14 +18,8 @@ export default { clipboardButton, TooltipOnTruncate, MrWidgetIcon, - MrWidgetHowToMergeModal, - GlButton, - GlDropdown, - GlDropdownSectionHeader, - GlDropdownItem, GlLink, GlSprintf, - WebIdeLink, }, directives: { GlTooltip: GlTooltipDirective, @@ -107,71 +95,6 @@ export default { </gl-sprintf> </div> </div> - - <div class="branch-actions d-flex"> - <template v-if="mr.isOpen"> - <web-ide-link - v-if="!mr.sourceBranchRemoved" - :show-edit-button="false" - :show-web-ide-button="true" - :web-ide-url="webIdePath" - :web-ide-text="$options.i18n.webIdeText" - :show-gitpod-button="mr.showGitpodButton" - :gitpod-url="mr.gitpodUrl" - :gitpod-enabled="mr.gitpodEnabled" - :user-preferences-gitpod-path="mr.userPreferencesGitpodPath" - :user-profile-enable-gitpod-path="mr.userProfileEnableGitpodPath" - :gitpod-text="$options.i18n.gitpodText" - class="gl-display-none gl-md-display-inline-block gl-mr-3" - data-placement="bottom" - tabindex="0" - data-qa-selector="open_in_web_ide_button" - /> - <gl-button - v-gl-modal-directive="'modal-merge-info'" - :disabled="mr.sourceBranchRemoved" - class="js-check-out-branch gl-mr-3" - > - {{ s__('mrWidget|Check out branch') }} - </gl-button> - <mr-widget-how-to-merge-modal - :is-fork="isFork" - :can-merge="mr.canMerge" - :source-branch="mr.sourceBranch" - :source-project="mr.sourceProject" - :source-project-path="mr.sourceProjectFullPath" - :target-branch="mr.targetBranch" - :source-project-default-url="mr.sourceProjectDefaultUrl" - :reviewing-docs-path="mr.reviewingDocsPath" - /> - </template> - <gl-dropdown - v-gl-tooltip - :title="__('Download as')" - :aria-label="__('Download as')" - icon="download" - right - data-qa-selector="download_dropdown" - > - <gl-dropdown-section-header>{{ __('Download as') }}</gl-dropdown-section-header> - <gl-dropdown-item - :href="mr.emailPatchesPath" - class="js-download-email-patches" - download - data-qa-selector="download_email_patches_menu_item" - > - {{ s__('mrWidget|Email patches') }} - </gl-dropdown-item> - <gl-dropdown-item - :href="mr.plainDiffPath" - class="js-download-plain-diff" - download - data-qa-selector="download_plain_diff_menu_item" - > - {{ s__('mrWidget|Plain diff') }} - </gl-dropdown-item> - </gl-dropdown> - </div> </div> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue index e906b8c3b59..c2a3ae361ca 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue @@ -112,11 +112,19 @@ export default { return escapeShellString(this.sourceBranch); }, }, + mounted() { + document.addEventListener('click', (e) => { + if (e.target.closest('.js-check-out-modal-trigger')) { + this.$refs.modal.show(); + } + }); + }, }; </script> <template> <gl-modal + ref="modal" modal-id="modal-merge-info" :no-enforce-focus="true" :title="$options.i18n.title" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index c0b80eef082..3b3b46e9772 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -276,12 +276,11 @@ export default { </div> </div> <div> - <span class="mr-widget-pipeline-graph"> - <span class="stage-cell"> + <span class="gl-align-items-center gl-display-inline-flex mr-widget-pipeline-graph"> + <span class="gl-align-items-center gl-display-inline-flex gl-flex-wrap stage-cell"> <linked-pipelines-mini-list v-if="triggeredBy.length" :triggered-by="triggeredBy" /> <pipeline-mini-graph v-if="hasStages" - class="gl-display-inline-block" stages-class="mr-widget-pipeline-stages" :stages="pipeline.details.stages" :is-merge-train="isMergeTrain" 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 2cef37d5c2e..b8a1f89d232 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 @@ -1,5 +1,5 @@ <script> -import { GlSafeHtmlDirective as SafeHtml, GlLink } from '@gitlab/ui'; +import { GlSafeHtmlDirective as SafeHtml, GlLink, GlSprintf } from '@gitlab/ui'; import { s__, n__ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -10,6 +10,7 @@ export default { }, components: { GlLink, + GlSprintf, }, mixins: [glFeatureFlagMixin()], props: { @@ -28,6 +29,16 @@ export default { required: false, default: true, }, + divergedCommitsCount: { + type: Number, + required: false, + default: 0, + }, + targetBranchPath: { + type: String, + required: false, + default: '', + }, }, computed: { closesText() { @@ -81,5 +92,19 @@ export default { }}</gl-link> </span> </p> + <div + v-if=" + divergedCommitsCount > 0 && glFeatures.updatedMrHeader && !glFeatures.restructuredMrWidget + " + class="diverged-commits-count" + > + <gl-sprintf :message="s__('mrWidget|The source branch is %{link} the target branch')"> + <template #link> + <gl-link :href="targetBranchPath">{{ + n__('%d commit behind', '%d commits behind', divergedCommitsCount) + }}</gl-link> + </template> + </gl-sprintf> + </div> </section> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue index 9499603163b..7ff1eb6e73a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue @@ -46,7 +46,7 @@ export default { <gl-button v-if="!glFeatures.restructuredMrWidget && showDisabledButton" category="primary" - variant="success" + variant="confirm" data-testid="disabled-merge-button" :disabled="true" > 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 a44caf886a4..aabbeac564a 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 @@ -175,7 +175,7 @@ export default { {{ cancelButtonText }} </gl-button> </h4> - <section class="mr-info-list"> + <section v-if="!glFeatures.restructuredMrWidget" class="mr-info-list"> <p v-if="shouldRemoveSourceBranch"> {{ s__('mrWidget|Deletes the source branch') }} </p> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue index 6d5ca58aa20..d50e52f5ac1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue @@ -1,4 +1,5 @@ <script> +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import MrWidgetAuthorTime from '../mr_widget_author_time.vue'; import statusIcon from '../mr_widget_status_icon.vue'; @@ -8,6 +9,7 @@ export default { MrWidgetAuthorTime, statusIcon, }, + mixins: [glFeatureFlagMixin()], props: { /* TODO: This is providing all store and service down when it only needs metrics and targetBranch */ @@ -29,7 +31,7 @@ export default { :date-readable="mr.metrics.readableClosedAt" /> - <section class="mr-info-list"> + <section v-if="!glFeatures.restructuredMrWidget" class="mr-info-list"> <p> {{ s__('mrWidget|The changes were not merged into') }} <a :href="mr.targetBranchPath" class="label-branch"> {{ mr.targetBranch }} </a> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index 7435f578852..def30dacf8a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlModalDirective, GlSkeletonLoader } from '@gitlab/ui'; +import { GlButton, GlSkeletonLoader } from '@gitlab/ui'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import userPermissionsQuery from '../../queries/permissions.query.graphql'; @@ -13,9 +13,6 @@ export default { StatusIcon, GlButton, }, - directives: { - GlModalDirective, - }, mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], apollo: { userPermissions: { @@ -131,9 +128,9 @@ export default { </gl-button> <gl-button v-if="canMerge" - v-gl-modal-directive="'modal-merge-info'" :size="glFeatures.restructuredMrWidget ? 'small' : 'medium'" data-testid="merge-locally-button" + class="js-check-out-modal-trigger" > {{ s__('mrWidget|Resolve locally') }} </gl-button> 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 84dac95ce74..bf036f562ed 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,6 +1,7 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ import { GlLoadingIcon, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import api from '~/api'; import createFlash from '~/flash'; import { s__, __ } from '~/locale'; @@ -22,6 +23,7 @@ export default { GlLoadingIcon, GlButton, }, + mixins: [glFeatureFlagMixin()], props: { mr: { type: Object, @@ -181,7 +183,11 @@ export default { {{ s__('mrWidget|Delete source branch') }} </gl-button> </div> - <section class="mr-info-list" data-qa-selector="merged_status_content"> + <section + v-if="!glFeatures.restructuredMrWidget" + class="mr-info-list" + data-qa-selector="merged_status_content" + > <p> {{ s__('mrWidget|The changes were merged into') }} <span class="label-branch"> 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 887d1aab524..b86ab69af3f 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,5 +1,6 @@ <script> import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import simplePoll from '~/lib/utils/simple_poll'; import MergeRequest from '~/merge_request'; import eventHub from '../../event_hub'; @@ -14,6 +15,7 @@ export default { components: { statusIcon, }, + mixins: [glFeatureFlagMixin()], props: { mr: { type: Object, @@ -88,7 +90,7 @@ export default { {{ mergeStatus.message }} <gl-emoji :data-name="mergeStatus.emoji" /> </h4> - <section class="mr-info-list"> + <section v-if="!glFeatures.restructuredMrWidget" class="mr-info-list"> <p> {{ s__('mrWidget|Merges changes into') }} <span class="label-branch"> 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 4f8faeb877f..4fb95fe635c 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 @@ -322,11 +322,33 @@ export default { }, restructuredWidgetShowMergeButtons() { if (this.glFeatures.restructuredMrWidget) { - return this.isMergeAllowed && this.state.userPermissions.canMerge; + return ( + (this.isMergeAllowed || this.isAutoMergeAvailable) && + this.state.userPermissions.canMerge && + !this.mr.mergeOngoing && + !this.mr.autoMergeEnabled + ); } return true; }, + sourceBranchDeletedText() { + if (this.glFeatures.restructuredMrWidget) { + if (this.removeSourceBranch) { + return this.mr.state === 'merged' + ? __('Deleted the source branch.') + : __('Source branch will be deleted.'); + } + + return this.mr.state === 'merged' + ? __('Did not delete the source branch.') + : __('Source branch will not be deleted.'); + } + + return this.removeSourceBranch + ? __('Deletes the source branch.') + : __('Does not delete the source branch.'); + }, }, mounted() { if (this.glFeatures.mergeRequestWidgetGraphql) { @@ -421,6 +443,8 @@ export default { if (this.glFeatures.mergeRequestWidgetGraphql) { this.updateGraphqlState(); } + + this.isMakingRequest = false; }) .catch(() => { this.isMakingRequest = false; @@ -499,6 +523,7 @@ export default { <template> <div + data-testid="ready_to_merge_state" :class="{ 'gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7 gl-rounded-bottom-left-base gl-rounded-bottom-right-base': glFeatures.restructuredMrWidget, @@ -611,6 +636,7 @@ export default { glFeatures.restructuredMrWidget && (shouldShowSquashEdit || shouldShowMergeEdit) " v-model="editCommitMessage" + data-testid="widget_edit_commit_message" class="gl-display-flex gl-align-items-center" > {{ __('Edit commit message') }} @@ -686,25 +712,36 @@ export default { v-if="!restructuredWidgetShowMergeButtons" class="gl-w-full gl-order-n1 gl-text-gray-500" > - <strong> + <strong v-if="mr.state !== 'closed'"> {{ __('Merge details') }} </strong> <ul class="gl-pl-4 gl-m-0"> + <li + v-if="mr.divergedCommitsCount > 0 && glFeatures.updatedMrHeader" + class="gl-line-height-normal" + > + <gl-sprintf + :message="s__('mrWidget|The source branch is %{link} the target branch')" + > + <template #link> + <gl-link :href="mr.targetBranchPath">{{ + n__('%d commit behind', '%d commits behind', mr.divergedCommitsCount) + }}</gl-link> + </template> + </gl-sprintf> + </li> <li class="gl-line-height-normal"> <added-commit-message + :state="mr.state" + :merge-commit-sha="mr.shortMergeCommitSha" :is-squash-enabled="squashBeforeMerge" :is-fast-forward-enabled="!shouldShowMergeEdit" :commits-count="commitsCount" :target-branch="stateData.targetBranch" /> </li> - <li class="gl-line-height-normal"> - <template v-if="removeSourceBranch"> - {{ __('Deletes the source branch.') }} - </template> - <template v-else> - {{ __('Does not delete the source branch.') }} - </template> + <li v-if="mr.state !== 'closed'" class="gl-line-height-normal"> + {{ sourceBranchDeletedText }} </li> <li v-if="mr.relatedLinks" class="gl-line-height-normal"> <related-links @@ -733,6 +770,8 @@ export default { :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> diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js index d32db50874c..cea8df2484b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js @@ -83,14 +83,11 @@ export default { this.collapsedData.newErrors.map((e) => { return fullData.push({ text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`, - subtext: sprintf( - s__(`ciReport|in %{open_link}${e.file_path}:${e.line}%{close_link}`), - { - open_link: `<a class="gl-text-decoration-underline" href="${e.urlPath}">`, - close_link: '</a>', - }, - false, - ), + subtext: { + prependText: s__(`ciReport|in`), + text: `${e.file_path}:${e.line}`, + href: e.urlPath, + }, icon: { name: SEVERITY_ICONS_EXTENSION[e.severity], }, diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js index cd5cfb6837c..23f14bea4e1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js @@ -10,6 +10,8 @@ export const i18n = { label: s__('Reports|Test summary'), loading: s__('Reports|Test summary results are loading'), error: s__('Reports|Test summary failed to load results'), + newHeader: s__('Reports|New'), + fixedHeader: s__('Reports|Fixed'), fullReport: s__('Reports|Full report'), noChanges: (bold) => s__(`Reports|${noText(bold)} changed test results`), @@ -36,4 +38,32 @@ export const i18n = { sprintf(s__('Reports|An error occurred while loading %{name} results'), { name }), headReportParsingError: s__('Reports|Head report parsing error:'), baseReportParsingError: s__('Reports|Base report parsing error:'), + + recentFailureSummary: (recentlyFailed, failed) => { + if (failed < 2) { + return sprintf( + s__( + 'Reports|%{recentlyFailed} out of %{failed} failed test has failed more than once in the last 14 days', + ), + { recentlyFailed, failed }, + ); + } + return sprintf( + n__( + 'Reports|%{recentlyFailed} out of %{failed} failed tests has failed more than once in the last 14 days', + 'Reports|%{recentlyFailed} out of %{failed} failed tests have failed more than once in the last 14 days', + recentlyFailed, + ), + { recentlyFailed, failed }, + ); + }, + recentFailureCount: (recentFailures) => + sprintf( + n__( + 'Reports|Failed %{count} time in %{base_branch} in the last 14 days', + 'Reports|Failed %{count} times in %{base_branch} in the last 14 days', + recentFailures.count, + ), + recentFailures, + ), }; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js index 65d9257903f..577b2cbfc5c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js @@ -1,7 +1,13 @@ import { uniqueId } from 'lodash'; import axios from '~/lib/utils/axios_utils'; import { EXTENSION_ICONS } from '../../constants'; -import { summaryTextBuilder, reportTextBuilder, reportSubTextBuilder } from './utils'; +import { + summaryTextBuilder, + reportTextBuilder, + reportSubTextBuilder, + countRecentlyFailedTests, + recentFailuresTextBuilder, +} from './utils'; import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants'; export default { @@ -18,7 +24,10 @@ export default { if (data.hasSuiteError) { return this.$options.i18n.error; } - return summaryTextBuilder(this.$options.i18n.label, data.summary); + return { + subject: summaryTextBuilder(this.$options.i18n.label, data.summary), + meta: recentFailuresTextBuilder(data.summary), + }; }, statusIcon(data) { if (data.parsingInProgress) { @@ -50,6 +59,10 @@ export default { hasSuiteError: data.suites?.some((suite) => suite.status === ERROR_STATUS), parsingInProgress: status === 204, ...data, + summary: { + recentlyFailed: countRecentlyFailedTests(data.suites), + ...data.summary, + }, }, }; }); @@ -66,17 +79,66 @@ export default { } return EXTENSION_ICONS.success; }, - prepareReports() { - return this.collapsedData.suites.map((suite) => { + testHeader(test, sectionHeader, index) { + const headers = []; + if (index === 0) { + headers.push(sectionHeader); + } + if (test.recent_failures?.count && test.recent_failures?.base_branch) { + headers.push(i18n.recentFailureCount(test.recent_failures)); + } + return headers; + }, + mapTestAsChild({ iconName, sectionHeader }) { + return (test, index) => { return { - id: uniqueId('suite-'), - text: reportTextBuilder(suite), - subtext: reportSubTextBuilder(suite), - icon: { - name: this.suiteIcon(suite), - }, + id: uniqueId('test-'), + header: this.testHeader(test, sectionHeader, index), + icon: { name: iconName }, + text: test.name, }; - }); + }; + }, + prepareReports() { + return this.collapsedData.suites + .map((suite) => { + return { + ...suite, + summary: { + recentlyFailed: countRecentlyFailedTests(suite), + ...suite.summary, + }, + }; + }) + .map((suite) => { + return { + id: uniqueId('suite-'), + text: reportTextBuilder(suite), + subtext: reportSubTextBuilder(suite), + icon: { + name: this.suiteIcon(suite), + }, + children: [ + ...[...suite.new_failures, ...suite.new_errors].map( + this.mapTestAsChild({ + sectionHeader: i18n.newHeader, + iconName: EXTENSION_ICONS.failed, + }), + ), + ...[...suite.existing_failures, ...suite.existing_errors].map( + this.mapTestAsChild({ + iconName: EXTENSION_ICONS.failed, + }), + ), + ...[...suite.resolved_failures, ...suite.resolved_errors].map( + this.mapTestAsChild({ + sectionHeader: i18n.fixedHeader, + iconName: EXTENSION_ICONS.success, + }), + ), + ], + }; + }); }, }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js index a74ed20362f..9e4b0ac581c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js @@ -43,13 +43,42 @@ export const reportTextBuilder = ({ name = '', summary = {}, status }) => { return i18n.summaryText(name, resultsString); }; -export const reportSubTextBuilder = ({ suite_errors }) => { - const errors = []; - if (suite_errors?.head) { - errors.push(`${i18n.headReportParsingError} ${suite_errors.head}`); - } - if (suite_errors?.base) { - errors.push(`${i18n.baseReportParsingError} ${suite_errors.base}`); +export const recentFailuresTextBuilder = (summary = {}) => { + const { failed, recentlyFailed } = summary; + if (!failed || !recentlyFailed) return ''; + + return i18n.recentFailureSummary(recentlyFailed, failed); +}; + +export const reportSubTextBuilder = ({ suite_errors, summary }) => { + if (suite_errors?.head || suite_errors?.base) { + const errors = []; + if (suite_errors?.head) { + errors.push(`${i18n.headReportParsingError} ${suite_errors.head}`); + } + if (suite_errors?.base) { + errors.push(`${i18n.baseReportParsingError} ${suite_errors.base}`); + } + return errors.join('<br />'); } - return errors.join('<br />'); + return recentFailuresTextBuilder(summary); +}; + +export const countRecentlyFailedTests = (subject) => { + // handle either a single report or an array of reports + const reports = !subject.length ? [subject] : subject; + + return reports + .map((report) => { + return ( + [report.new_failures, report.existing_failures, report.resolved_failures] + // only count tests which have failed more than once + .map( + (failureArray) => + failureArray.filter((failure) => failure.recent_failures?.count > 1).length, + ) + .reduce((total, count) => total + count, 0) + ); + }) + .reduce((total, count) => total + count, 0); }; 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 4b3ad288768..8ebb7f6f159 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 @@ -165,7 +165,10 @@ export default { return this.mr?.codequalityReportsPath; }, shouldRenderRelatedLinks() { - return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState; + return ( + (Boolean(this.mr.relatedLinks) || this.mr.divergedCommitsCount > 0) && + !this.mr.isNothingToMergeState + ); }, shouldRenderSourceBranchRemovalStatus() { return ( @@ -195,6 +198,9 @@ export default { shouldRenderTestReport() { return Boolean(this.mr?.testResultsPath); }, + shouldRenderRefactoredTestReport() { + return window.gon?.features?.refactorMrWidgetTestSummary; + }, mergeError() { let { mergeError } = this.mr; @@ -228,6 +234,9 @@ export default { isRestructuredMrWidgetEnabled() { return window.gon?.features?.restructuredMrWidget; }, + isUpdatedHeaderEnabled() { + return window.gon?.features?.updatedMrHeader; + }, }, watch: { 'mr.machineValue': { @@ -512,7 +521,7 @@ export default { } }, registerTestReportExtension() { - if (this.shouldRenderTestReport && this.shouldShowExtension) { + if (this.shouldRenderTestReport && this.shouldRenderRefactoredTestReport) { registerExtension(testReportExtension); } }, @@ -521,11 +530,15 @@ export default { </script> <template> <div v-if="isLoaded" class="mr-state-widget gl-mt-3"> - <header class="gl-rounded-base gl-border-solid gl-border-1 gl-border-gray-100"> + <header + v-if="shouldRenderCollaborationStatus || !isUpdatedHeaderEnabled" + :class="{ 'mr-widget-workflow gl-mt-0!': isUpdatedHeaderEnabled }" + class="gl-rounded-base gl-border-solid gl-border-1 gl-border-gray-100 gl-overflow-hidden" + > <mr-widget-alert-message v-if="shouldRenderCollaborationStatus" type="info"> {{ s__('mrWidget|Members who can merge are allowed to add commits.') }} </mr-widget-alert-message> - <mr-widget-header :mr="mr" /> + <mr-widget-header v-if="!isUpdatedHeaderEnabled" :mr="mr" /> </header> <mr-widget-suggest-pipeline v-if="shouldSuggestPipelines" @@ -588,7 +601,7 @@ export default { /> <grouped-test-reports-app - v-if="mr.testResultsPath && !shouldShowExtension" + v-if="shouldRenderTestReport && !shouldRenderRefactoredTestReport" class="js-reports-container" :endpoint="mr.testResultsPath" :head-blob-path="mr.headBlobPath" @@ -617,6 +630,8 @@ export default { v-if="shouldRenderRelatedLinks" :state="mr.state" :related-links="mr.relatedLinks" + :diverged-commits-count="mr.divergedCommitsCount" + :target-branch-path="mr.targetBranchPath" class="mr-info-list gl-ml-7 gl-pb-5" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql index 99e6f4e9beb..efc0673bc26 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql @@ -11,7 +11,6 @@ fragment ReadyToMerge on Project { shouldRemoveSourceBranch forceRemoveSourceBranch defaultMergeCommitMessage - defaultMergeCommitMessageWithDescription defaultSquashCommitMessage squash squashOnMerge diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index eb07609d5d6..146cf7e11a7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -1,5 +1,5 @@ import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key'; -import { statusBoxState } from '~/issuable/components/status_box.vue'; +import { badgeState } from '~/issuable/components/status_box.vue'; import { formatDate, getTimeago } from '~/lib/utils/datetime_utility'; import { machine } from '~/lib/utils/finite_state_machine'; import { @@ -221,8 +221,8 @@ export default class MergeRequestStore { } updateStatusState(state) { - if (this.mergeRequestState !== state && statusBoxState.updateStatus) { - statusBoxState.updateStatus(); + if (this.mergeRequestState !== state && badgeState.updateStatus) { + badgeState.updateStatus(); } } |