summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_merge_request_widget
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-12-20 14:22:11 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-12-20 14:22:11 +0000
commit0c872e02b2c822e3397515ec324051ff540f0cd5 (patch)
treece2fb6ce7030e4dad0f4118d21ab6453e5938cdd /app/assets/javascripts/vue_merge_request_widget
parentf7e05a6853b12f02911494c4b3fe53d9540d74fc (diff)
downloadgitlab-ce-0c872e02b2c822e3397515ec324051ff540f0cd5.tar.gz
Add latest changes from gitlab-org/gitlab@15-7-stable-eev15.7.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget')
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/state_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue64
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue52
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue134
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue44
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue35
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js31
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js88
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/i18n.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue38
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge_merge_request.fragment.graphql39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js8
29 files changed, 546 insertions, 178 deletions
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 2cfeb7a4bcb..eb93f42e2f3 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
@@ -189,8 +189,11 @@ export default {
.then((data) => {
this.mr.setApprovals(data);
- eventHub.$emit('MRWidgetUpdateRequested');
- eventHub.$emit('ApprovalUpdated');
+ if (!window.gon?.features?.realtimeMrStatusChange) {
+ eventHub.$emit('MRWidgetUpdateRequested');
+ eventHub.$emit('ApprovalUpdated');
+ }
+
this.$emit('updated');
})
.catch(errFn)
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
index 1256b3a8e52..c7d34d45f06 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlSprintf, GlLink } from '@gitlab/ui';
import { backOff } from '~/lib/utils/common_utils';
-import statusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import { bytesToMiB } from '~/lib/utils/number_utils';
import { s__ } from '~/locale';
import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
@@ -107,7 +107,7 @@ export default {
backOff((next, stop) => {
MRWidgetService.fetchMetrics(this.metricsUrl)
.then((res) => {
- if (res.status === statusCodes.NO_CONTENT) {
+ if (res.status === HTTP_STATUS_NO_CONTENT) {
this.backOffRequestCounter += 1;
/* eslint-disable no-unused-expressions */
this.backOffRequestCounter < 3 ? next() : stop(res);
@@ -118,7 +118,7 @@ export default {
.catch(stop);
})
.then((res) => {
- if (res.status === statusCodes.NO_CONTENT) {
+ if (res.status === HTTP_STATUS_NO_CONTENT) {
return res;
}
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 3d03dbd9db3..e8cc9b2eb2a 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
@@ -1,12 +1,7 @@
<script>
-import {
- GlButton,
- GlLoadingIcon,
- GlSafeHtmlDirective,
- GlTooltipDirective,
- GlIntersectionObserver,
-} from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIntersectionObserver } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import { sprintf, s__, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
@@ -40,7 +35,7 @@ export default {
StateContainer,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
GlTooltip: GlTooltipDirective,
},
data() {
@@ -323,19 +318,23 @@ export default {
@mouseup="onRowMouseUp"
>
<div
+ :class="{ 'gl-h-full': isLoadingSummary }"
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">
+ <div
+ class="gl-flex-grow-1 gl-display-flex gl-align-items-center"
+ data-testid="widget-extension-top-level-summary"
+ >
<template v-if="isLoadingSummary">{{ widgetLoadingText }}</template>
<template v-else-if="hasFetchError">{{ widgetErrorText }}</template>
- <div v-else>
+ <template v-else>
<span v-safe-html="hydratedSummary.subject"></span>
<template v-if="hydratedSummary.meta">
<br />
<span v-safe-html="hydratedSummary.meta" class="gl-font-sm"></span>
</template>
- </div>
+ </template>
</div>
<actions
:widget="$options.label || $options.name"
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 a10e5efa0e7..fa369d23b6c 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,6 +1,7 @@
<script>
-import { GlBadge, GlLink, GlSafeHtmlDirective, GlModalDirective } from '@gitlab/ui';
+import { GlBadge, GlLink, GlModalDirective } from '@gitlab/ui';
import { isArray } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import Actions from '../action_buttons.vue';
import StatusIcon from './status_icon.vue';
import { generateText } from './utils';
@@ -14,7 +15,7 @@ export default {
Actions,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
GlModal: GlModalDirective,
},
props: {
@@ -97,7 +98,12 @@ export default {
<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'">
+ <gl-badge
+ v-if="data.badge"
+ :variant="data.badge.variant || 'info'"
+ size="sm"
+ class="gl-ml-2"
+ >
{{ data.badge.text }}
</gl-badge>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
index f71b1fbc539..79ea2624ec5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
@@ -1,8 +1,11 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink } from '@gitlab/ui';
export default {
name: 'MrWidgetAuthor',
+ components: {
+ GlLink,
+ },
directives: {
GlTooltip: GlTooltipDirective,
},
@@ -28,13 +31,16 @@ export default {
};
</script>
<template>
- <a
+ <gl-link
v-gl-tooltip
:href="authorUrl"
:title="showAuthorName ? null : author.name"
- class="author-link inline"
+ class="mr-widget-author"
>
- <img :src="avatarUrl" class="avatar avatar-inline s16" />
- <span v-if="showAuthorName" class="author">{{ author.name }}</span>
- </a>
+ <img :src="avatarUrl" :alt="author.name" class="avatar avatar-inline s16" /><span
+ v-if="showAuthorName"
+ class="author"
+ >{{ author.name }}</span
+ >
+ </gl-link>
</template>
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 97c6de37054..d8a361066f4 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
@@ -7,8 +7,8 @@ import {
GlSprintf,
GlTooltip,
GlTooltipDirective,
- GlSafeHtmlDirective,
} from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
@@ -33,7 +33,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
pipeline: {
@@ -190,7 +190,7 @@ export default {
</template>
<template v-else-if="hasPipeline">
<a :href="status.details_path" class="gl-align-self-center gl-mr-3">
- <ci-icon :status="status" :size="24" />
+ <ci-icon :status="status" :size="24" class="gl-display-flex" />
</a>
<div class="ci-widget-container d-flex">
<div class="ci-widget-content">
@@ -277,9 +277,9 @@ export default {
v-if="pipeline.details.stages"
:downstream-pipelines="pipeline.triggered"
:is-merge-train="isMergeTrain"
+ :pipeline-path="pipeline.path"
:stages="pipeline.details.stages"
:upstream-pipeline="pipeline.triggered_by"
- stages-class="mr-widget-pipeline-stages"
/>
<pipeline-artifacts :pipeline-id="pipeline.id" :artifacts="artifacts" class="gl-ml-3" />
</span>
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 870972156c5..1fd1e264c25 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,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlLink } from '@gitlab/ui';
+import { GlLink } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
export default {
@@ -54,16 +55,16 @@ export default {
</script>
<template>
<section>
- <p v-if="relatedLinks.closing" class="gl-display-inline gl-m-0">
+ <p v-if="relatedLinks.closing" class="gl-display-inline gl-m-0 gl-font-sm!">
{{ closesText }}
<span v-safe-html="relatedLinks.closing"></span>
</p>
- <p v-if="relatedLinks.mentioned" class="gl-display-inline gl-m-0">
+ <p v-if="relatedLinks.mentioned" class="gl-display-inline gl-m-0 gl-font-sm!">
<span v-if="relatedLinks.closing">&middot;</span>
{{ n__('mrWidget|Mentions issue', 'mrWidget|Mentions issues', relatedLinks.mentionedCount) }}
<span v-safe-html="relatedLinks.mentioned"></span>
</p>
- <p v-if="shouldShowAssignToMeLink" class="gl-display-inline gl-m-0">
+ <p v-if="shouldShowAssignToMeLink" class="gl-display-inline gl-m-0 gl-font-sm!">
<span>
<gl-link rel="nofollow" data-method="post" :href="relatedLinks.assignToMe">{{
assignIssueText
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 66e33a08a12..9a3555d3e11 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
@@ -54,7 +54,7 @@ export default {
<template>
<div
- class="mr-widget-body media mr-widget-body-line-height-1 gl-line-height-normal"
+ class="mr-widget-body media gl-display-flex gl-align-items-center"
:class="wrapperClasses"
v-on="$listeners"
>
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 38b99dae264..e5688091cc7 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,6 @@
<script>
import { s__ } from '~/locale';
-import StatusIcon from '../mr_widget_status_icon.vue';
+import StateContainer from '../state_container.vue';
import { DETAILED_MERGE_STATUS } from '../../constants';
export default {
@@ -12,7 +12,7 @@ export default {
externalStatusChecksFailed: s__('mrWidget|Merge blocked: all status checks must pass.'),
},
components: {
- StatusIcon,
+ StateContainer,
},
props: {
mr: {
@@ -37,10 +37,11 @@ export default {
</script>
<template>
- <div class="mr-widget-body media gl-flex-wrap">
- <status-icon status="failed" />
- <p class="media-body gl-m-0! gl-font-weight-bold gl-text-black-normal!">
+ <state-container :mr="mr" status="failed">
+ <span
+ class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!"
+ >
{{ failedText }}
- </p>
- </div>
+ </span>
+ </state-container>
</template>
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 806f8f939a6..6bcf88713a5 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,7 +1,17 @@
<script>
+import api from '~/api';
+import showGlobalToast from '~/vue_shared/plugins/global_toast';
+
import MrWidgetAuthorTime from '../mr_widget_author_time.vue';
import StateContainer from '../state_container.vue';
+import {
+ MR_WIDGET_CLOSED_REOPEN,
+ MR_WIDGET_CLOSED_REOPENING,
+ MR_WIDGET_CLOSED_RELOADING,
+ MR_WIDGET_CLOSED_REOPEN_FAILURE,
+} from '../../i18n';
+
export default {
name: 'MRWidgetClosed',
components: {
@@ -14,10 +24,62 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ isPending: false,
+ isReloading: false,
+ };
+ },
+ computed: {
+ reopenText() {
+ let text = MR_WIDGET_CLOSED_REOPEN;
+
+ if (this.isPending) {
+ text = MR_WIDGET_CLOSED_REOPENING;
+ } else if (this.isReloading) {
+ text = MR_WIDGET_CLOSED_RELOADING;
+ }
+
+ return text;
+ },
+ actions() {
+ if (!window.gon?.current_user_id) {
+ return [];
+ }
+
+ return [
+ {
+ text: this.reopenText,
+ loading: this.isPending || this.isReloading,
+ onClick: this.reopen,
+ testId: 'extension-actions-reopen-button',
+ },
+ ];
+ },
+ },
+ methods: {
+ reopen() {
+ this.isPending = true;
+
+ api
+ .updateMergeRequest(this.mr.targetProjectId, this.mr.iid, { state_event: 'reopen' })
+ .then(() => {
+ this.isReloading = true;
+
+ window.location.reload();
+ })
+ .catch(() => {
+ showGlobalToast(MR_WIDGET_CLOSED_REOPEN_FAILURE);
+ })
+ .finally(() => {
+ this.isPending = false;
+ });
+ },
+ },
};
</script>
<template>
- <state-container :mr="mr" status="closed">
+ <state-container :mr="mr" status="closed" :actions="actions">
<mr-widget-author-time
:action-text="s__('mrWidget|Closed by')"
:author="mr.metrics.closedBy"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 4902c9b45e8..850a4e2fd56 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -1,5 +1,6 @@
<script>
-import { GlButton, GlSprintf, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
import api from '~/api';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -12,7 +13,7 @@ export default {
GlLink,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
mr: {
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 c54672cd0f8..23b163e2c6a 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
@@ -20,6 +20,8 @@ import simplePoll from '~/lib/utils/simple_poll';
import { __, s__, n__ } from '~/locale';
import SmartInterval from '~/smart_interval';
import { helpPagePath } from '~/helpers/help_page_helper';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import readyToMergeSubscription from '~/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql';
import {
AUTO_MERGE_STRATEGIES,
WARNING,
@@ -87,6 +89,31 @@ export default {
this.initPolling();
}
},
+ subscribeToMore: {
+ document() {
+ return readyToMergeSubscription;
+ },
+ skip() {
+ return !this.mr?.id || this.loading || !window.gon?.features?.realtimeMrStatusChange;
+ },
+ variables() {
+ return {
+ issuableId: convertToGraphQLId('MergeRequest', this.mr?.id),
+ };
+ },
+ updateQuery(
+ _,
+ {
+ subscriptionData: {
+ data: { mergeRequestMergeStatusUpdated },
+ },
+ },
+ ) {
+ if (mergeRequestMergeStatusUpdated) {
+ this.state = mergeRequestMergeStatusUpdated;
+ }
+ },
+ },
},
},
components: {
@@ -295,7 +322,7 @@ export default {
return this.mr.divergedCommitsCount > 0;
},
showMergeDetailsHeader() {
- return ['readyToMerge'].indexOf(this.mr.state) >= 0;
+ return !['readyToMerge'].includes(this.mr.state);
},
},
mounted() {
@@ -467,8 +494,9 @@ export default {
<template>
<div
+ :class="{ 'gl-bg-gray-10': mr.state !== 'closed' && mr.state !== 'merged' }"
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"
+ class="gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-pl-7"
>
<div v-if="loading" class="mr-widget-body">
<div class="gl-w-full mr-ready-to-merge-loader">
@@ -481,7 +509,9 @@ export default {
</div>
</div>
<template v-else>
- <div class="mr-widget-body mr-widget-body-ready-merge media mr-widget-body-line-height-1">
+ <div
+ class="mr-widget-body mr-widget-body-ready-merge media gl-display-flex gl-align-items-center"
+ >
<div class="media-body">
<div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap">
<template v-if="shouldShowMergeControls">
@@ -555,7 +585,19 @@ export default {
</li>
</ul>
</div>
- <div class="gl-w-full gl-text-gray-500 gl-mb-3 gl-md-mb-0 gl-md-pb-5">
+ <div
+ class="gl-w-full gl-text-gray-500 gl-mb-3 gl-md-mb-0 gl-md-pb-5 mr-widget-merge-details"
+ >
+ <template v-if="sourceHasDivergedFromTarget">
+ <gl-sprintf :message="$options.i18n.sourceDivergedFromTargetText">
+ <template #link>
+ <gl-link :href="mr.targetBranchPath">{{
+ $options.i18n.divergedCommits(mr.divergedCommitsCount)
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ &middot;
+ </template>
<added-commit-message
:is-squash-enabled="squashBeforeMerge"
:is-fast-forward-enabled="!shouldShowMergeEdit"
@@ -631,7 +673,7 @@ export default {
class="gl-w-full gl-order-n1 mr-widget-merge-details"
data-qa-selector="merged_status_content"
>
- <p v-if="showMergeDetailsHeader" class="gl-mb-3 gl-text-gray-900">
+ <p v-if="showMergeDetailsHeader" class="gl-mb-2 gl-text-gray-900">
{{ __('Merge details') }}
</p>
<ul class="gl-pl-4 gl-mb-0 gl-ml-3 gl-text-gray-600">
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 074758e33b2..9f3748599dc 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
@@ -26,7 +26,7 @@ export default {
<template>
<state-container :mr="mr" status="failed">
<span
- class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body! gl-align-self-start"
+ class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!"
>
{{ s__('mrWidget|Merge blocked: all threads must be resolved.') }}
</span>
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 ef5be0fbfcd..01f9b4757a0 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
@@ -94,6 +94,7 @@ export default {
errors: [],
mergeRequest: {
__typename: 'MergeRequest',
+ id: this.mr.issuableId,
mergeableDiscussionsState: true,
title: this.mr.title,
draft: false,
@@ -111,7 +112,10 @@ export default {
}) => {
toast(__('Marked as ready. Merging is now allowed.'));
$('.merge-request .detail-page-description .title').text(title);
- eventHub.$emit('MRWidgetUpdateRequested');
+
+ if (!window.gon?.features?.realtimeMrStatusChange) {
+ eventHub.$emit('MRWidgetUpdateRequested');
+ }
},
)
.catch(() =>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
new file mode 100644
index 00000000000..6655af92a55
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
@@ -0,0 +1,134 @@
+<script>
+import { GlButton, GlDropdown, GlDropdownItem, GlTooltipDirective } from '@gitlab/ui';
+import { sprintf, __ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ widget: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ tertiaryButtons: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data: () => {
+ return {
+ timeout: null,
+ updatingTooltip: false,
+ };
+ },
+ computed: {
+ dropdownLabel() {
+ if (!this.widget) return undefined;
+
+ return sprintf(__('%{widget} options'), { widget: this.widget });
+ },
+ },
+ methods: {
+ onClickAction(action) {
+ this.$emit('clickedAction', action);
+
+ if (action.onClick) {
+ action.onClick();
+ }
+
+ if (action.tooltipOnClick) {
+ this.updatingTooltip = true;
+ this.$root.$emit('bv::show::tooltip', action.id);
+
+ clearTimeout(this.timeout);
+
+ this.timeout = setTimeout(() => {
+ this.updatingTooltip = false;
+ this.$root.$emit('bv::hide::tooltip', action.id);
+ }, 1000);
+ }
+ },
+ setTooltip(btn) {
+ if (this.updatingTooltip && btn.tooltipOnClick) {
+ return btn.tooltipOnClick;
+ }
+
+ return btn.tooltipText;
+ },
+ actionButtonQaSelector(btn) {
+ if (btn.dataQaSelector) {
+ return btn.dataQaSelector;
+ }
+ return 'mr_widget_extension_actions_button';
+ },
+ },
+};
+</script>
+
+<template>
+ <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
+ category="tertiary"
+ right
+ lazy
+ text-sr-only
+ size="small"
+ toggle-class="gl-p-2!"
+ class="gl-display-block gl-md-display-none!"
+ >
+ <gl-dropdown-item
+ v-for="(btn, index) in tertiaryButtons"
+ :key="index"
+ :href="btn.href"
+ :target="btn.target"
+ :data-clipboard-text="btn.dataClipboardText"
+ :data-method="btn.dataMethod"
+ @click="onClickAction(btn)"
+ >
+ {{ btn.text }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <template v-if="tertiaryButtons.length">
+ <gl-button
+ v-for="(btn, index) in tertiaryButtons"
+ :id="btn.id"
+ :key="index"
+ v-gl-tooltip.hover
+ :title="setTooltip(btn)"
+ :href="btn.href"
+ :target="btn.target"
+ :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
+ :data-clipboard-text="btn.dataClipboardText"
+ :data-qa-selector="actionButtonQaSelector(btn)"
+ :data-method="btn.dataMethod"
+ :icon="btn.icon"
+ :data-testid="btn.testId || 'extension-actions-button'"
+ :variant="btn.variant || 'confirm'"
+ :loading="btn.loading"
+ :disabled="btn.loading"
+ category="tertiary"
+ size="small"
+ class="gl-display-none gl-md-display-block gl-float-left"
+ @click="onClickAction(btn)"
+ >
+ <template v-if="btn.text">
+ {{ btn.text }}
+ </template>
+ </gl-button>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
index 2f52ac70833..18aa85484ea 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
@@ -20,13 +20,14 @@ export default {
role="region"
:aria-label="__('Merge request reports')"
data-testid="mr-widget-app"
+ class="mr-widget-section"
>
<component
:is="widget"
v-for="(widget, index) in widgets"
:key="widget.name || index"
:mr="mr"
- :class="{ 'mr-widget-border-top': index === 0 }"
+ class="mr-widget-section"
/>
</section>
</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
index 4d66c75719b..cdce7c6625a 100644
--- 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
@@ -1,8 +1,9 @@
<script>
-import { GlBadge, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
-import Actions from '../action_buttons.vue';
+import { GlBadge, GlLink } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { generateText } from '../extensions/utils';
import ContentRow from './widget_content_row.vue';
+import Actions from './action_buttons.vue';
export default {
name: 'DynamicContent',
@@ -13,7 +14,7 @@ export default {
ContentRow,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
data: {
@@ -81,10 +82,8 @@ export default {
v-if="data.children && data.children.length > 0 && level === 2"
class="gl-m-0 gl-p-0 gl-list-style-none"
>
- <li>
+ <li v-for="(childData, index) in data.children" :key="childData.id || index">
<dynamic-content
- v-for="(childData, index) in data.children"
- :key="childData.id || index"
:data="childData"
:widget-name="widgetName"
:level="3"
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
index 181b8cfad9a..6d17ac98d7f 100644
--- 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
@@ -48,9 +48,9 @@ export default {
:class="{
[iconClassNameText]: !isLoading,
[`mr-widget-status-icon-level-${level}`]: !isLoading,
- 'gl-mr-3': level === 1,
+ 'gl-w-6 gl-h-6 gl--flex-center': level === 1,
}"
- class="gl-relative gl-w-6 gl-h-6 gl-rounded-full gl--flex-center"
+ class="gl-relative gl-rounded-full gl-mr-3"
>
<gl-loading-icon v-if="isLoading" size="md" inline />
<gl-icon
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 cea7fb8260a..cdf35033021 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
@@ -1,22 +1,18 @@
<script>
-import {
- GlButton,
- GlLink,
- GlTooltipDirective,
- GlLoadingIcon,
- GlSafeHtmlDirective,
-} from '@gitlab/ui';
+import { GlButton, GlLink, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { normalizeHeaders } from '~/lib/utils/common_utils';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { sprintf, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import ActionButtons from '../action_buttons.vue';
+import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import { EXTENSION_ICONS } from '../../constants';
import { createTelemetryHub } from '../extensions/telemetry';
import ContentRow from './widget_content_row.vue';
import DynamicContent from './dynamic_content.vue';
import StatusIcon from './status_icon.vue';
+import ActionButtons from './action_buttons.vue';
const FETCH_TYPE_COLLAPSED = 'collapsed';
const FETCH_TYPE_EXPANDED = 'expanded';
@@ -31,11 +27,13 @@ export default {
GlLoadingIcon,
ContentRow,
DynamicContent,
+ DynamicScroller,
+ DynamicScrollerItem,
HelpPopover,
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
/**
@@ -258,6 +256,7 @@ export default {
<div class="gl-display-flex">
<help-popover
v-if="helpPopover"
+ icon="information-o"
:options="helpPopover.options"
:class="{ 'gl-mr-3': actionButtons.length > 0 }"
>
@@ -309,7 +308,7 @@ export default {
<div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center">
<gl-loading-icon size="sm" inline /> {{ loadingText }}
</div>
- <div v-else class="gl-px-5 gl-display-flex">
+ <div v-else class="gl-pl-5 gl-display-flex" :class="{ 'gl-pr-5': $scopedSlots.content }">
<content-row
v-if="contentError"
:level="2"
@@ -322,12 +321,25 @@ export default {
</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"
- />
+ <dynamic-scroller
+ v-if="content"
+ :items="content"
+ :min-item-size="32"
+ :style="{ maxHeight: '170px' }"
+ data-testid="dynamic-content-scroller"
+ class="gl-pr-5"
+ >
+ <template #default="{ item, index, active }">
+ <dynamic-scroller-item :item="item" :active="active">
+ <dynamic-content
+ :key="item.id || index"
+ :data="item"
+ :widget-name="widgetName"
+ :level="2"
+ />
+ </dynamic-scroller-item>
+ </template>
+ </dynamic-scroller>
</slot>
</div>
</div>
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
index 1fd1e325863..543136dc659 100644
--- 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
@@ -1,10 +1,11 @@
<script>
-import { GlSafeHtmlDirective, GlLink } from '@gitlab/ui';
+import { GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import ActionButtons from '../action_buttons.vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { EXTENSION_ICONS } from '../../constants';
import { generateText } from '../extensions/utils';
+import ActionButtons from './action_buttons.vue';
import StatusIcon from './status_icon.vue';
export default {
@@ -15,7 +16,7 @@ export default {
ActionButtons,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
level: {
@@ -67,6 +68,9 @@ export default {
shouldShowHeaderActions() {
return Boolean(this.helpPopover) || this.actionButtons?.length > 0;
},
+ hasActionButtons() {
+ return this.actionButtons.length > 0;
+ },
},
i18n: {
learnMore: __('Learn more'),
@@ -75,10 +79,15 @@ export default {
</script>
<template>
<div
- class="gl-w-full gl-display-flex mr-widget-content-row gl-align-items-baseline"
+ class="gl-w-full gl-display-flex 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" />
+ <status-icon
+ v-if="statusIconName && !header"
+ :level="2"
+ :name="widgetName"
+ :icon-name="statusIconName"
+ />
<div class="gl-w-full">
<div class="gl-display-flex">
<slot name="header">
@@ -95,7 +104,12 @@ export default {
v-if="shouldShowHeaderActions"
class="gl-ml-auto gl-display-flex gl-align-items-baseline"
>
- <help-popover v-if="helpPopover" :options="helpPopover.options">
+ <help-popover
+ v-if="helpPopover"
+ :options="helpPopover.options"
+ :class="{ 'gl-mr-3': hasActionButtons }"
+ icon="information-o"
+ >
<template v-if="helpPopover.content">
<p
v-if="helpPopover.content.text"
@@ -112,14 +126,19 @@ export default {
</template>
</help-popover>
<action-buttons
- v-if="actionButtons.length > 0"
+ v-if="hasActionButtons"
:widget="widgetName"
:tertiary-buttons="actionButtons"
- :class="{ 'gl-ml-2': helpPopover }"
/>
</div>
</div>
<div class="gl-display-flex gl-align-items-baseline gl-w-full">
+ <status-icon
+ v-if="statusIconName && header"
+ :level="2"
+ :name="widgetName"
+ :icon-name="statusIconName"
+ />
<slot name="body"></slot>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js
new file mode 100644
index 00000000000..03af21a5019
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js
@@ -0,0 +1,31 @@
+import { n__, s__, sprintf } from '~/locale';
+
+export const i18n = {
+ label: s__('ciReport|Code Quality'),
+ loading: s__('ciReport|Code Quality is loading'),
+ error: s__('ciReport|Code Quality failed to load results'),
+ noChanges: s__(`ciReport|Code Quality hasn't changed.`),
+ prependText: s__(`ciReport|in`),
+ fixed: s__(`ciReport|Fixed`),
+ pluralReport: (errors) =>
+ sprintf(
+ n__(
+ '%{strong_start}%{errors}%{strong_end} point',
+ '%{strong_start}%{errors}%{strong_end} points',
+ errors.length,
+ ),
+ {
+ errors: errors.length,
+ },
+ false,
+ ),
+ singularReport: (errors) => n__('%d point', '%d points', errors.length),
+ improvementAndDegradationCopy: (improvement, degradation) =>
+ sprintf(
+ s__(`ciReport|Code Quality improved on ${improvement} and degraded on ${degradation}.`),
+ ),
+ improvedCopy: (improvements) =>
+ sprintf(s__(`ciReport|Code Quality improved on ${improvements}.`)),
+ degradedCopy: (degradations) =>
+ sprintf(s__(`ciReport|Code Quality degraded on ${degradations}.`)),
+};
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 68347ac269e..394f8979a53 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
@@ -1,54 +1,33 @@
-import { n__, s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
-import { SEVERITY_ICONS_EXTENSION } from '~/reports/codequality_report/constants';
-import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_parser';
+import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/constants';
+import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
+import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { i18n } from './constants';
export default {
name: 'WidgetCodeQuality',
+ enablePolling: true,
props: ['codeQuality', 'blobPath'],
- i18n: {
- label: s__('ciReport|Code Quality'),
- loading: s__('ciReport|Code Quality test metrics results are being parsed'),
- error: s__('ciReport|Code Quality failed loading results'),
- },
+ i18n,
computed: {
- summary() {
- const { newErrors, resolvedErrors, errorSummary } = this.collapsedData;
- if (errorSummary.errored >= 1 && errorSummary.resolved >= 1) {
- const improvements = sprintf(
- n__(
- '%{strong_start}%{errors}%{strong_end} point',
- '%{strong_start}%{errors}%{strong_end} points',
- resolvedErrors.length,
- ),
- {
- errors: resolvedErrors.length,
- },
- false,
- );
+ summary(data) {
+ const { newErrors, resolvedErrors, errorSummary, parsingInProgress } = data;
- const degradations = sprintf(
- n__(
- '%{strong_start}%{errors}%{strong_end} point',
- '%{strong_start}%{errors}%{strong_end} points',
- newErrors.length,
- ),
- { errors: newErrors.length },
- false,
- );
- return sprintf(
- s__(`ciReport|Code Quality improved on ${improvements} and degraded on ${degradations}.`),
+ if (parsingInProgress) {
+ return i18n.loading;
+ } else if (errorSummary.errored >= 1 && errorSummary.resolved >= 1) {
+ return i18n.improvementAndDegradationCopy(
+ i18n.pluralReport(resolvedErrors),
+ i18n.pluralReport(newErrors),
);
} else if (errorSummary.resolved >= 1) {
- const improvements = n__('%d point', '%d points', resolvedErrors.length);
- return sprintf(s__(`ciReport|Code Quality improved on ${improvements}.`));
+ return i18n.improvedCopy(i18n.singularReport(resolvedErrors));
} else if (errorSummary.errored >= 1) {
- const degradations = n__('%d point', '%d points', newErrors.length);
- return sprintf(s__(`ciReport|Code Quality degraded on ${degradations}.`));
+ return i18n.degradedCopy(i18n.singularReport(newErrors));
}
- return s__(`ciReport|No changes to Code Quality.`);
+ return i18n.noChanges;
},
statusIcon() {
if (this.collapsedData.errorSummary?.errored >= 1) {
@@ -59,18 +38,17 @@ export default {
},
methods: {
fetchCollapsedData() {
- return Promise.all([this.fetchReport(this.codeQuality)]).then((values) => {
+ return axios.get(this.codeQuality).then((response) => {
+ const { data = {}, status } = response;
return {
- resolvedErrors: parseCodeclimateMetrics(
- values[0].resolved_errors,
- this.blobPath.head_path,
- ),
- newErrors: parseCodeclimateMetrics(values[0].new_errors, this.blobPath.head_path),
- existingErrors: parseCodeclimateMetrics(
- values[0].existing_errors,
- this.blobPath.head_path,
- ),
- errorSummary: values[0].summary,
+ ...response,
+ data: {
+ parsingInProgress: status === HTTP_STATUS_NO_CONTENT,
+ resolvedErrors: parseCodeclimateMetrics(data.resolved_errors, this.blobPath.head_path),
+ newErrors: parseCodeclimateMetrics(data.new_errors, this.blobPath.head_path),
+ existingErrors: parseCodeclimateMetrics(data.existing_errors, this.blobPath.head_path),
+ errorSummary: data.summary,
+ },
};
});
},
@@ -81,12 +59,12 @@ export default {
return fullData.push({
text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
subtext: {
- prependText: s__(`ciReport|in`),
+ prependText: i18n.prependText,
text: `${e.file_path}:${e.line}`,
href: e.urlPath,
},
icon: {
- name: SEVERITY_ICONS_EXTENSION[e.severity],
+ name: SEVERITY_ICONS_MR_WIDGET[e.severity],
},
});
});
@@ -95,12 +73,16 @@ export default {
return fullData.push({
text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
subtext: {
- prependText: s__(`ciReport|in`),
+ prependText: i18n.prependText,
text: `${e.file_path}:${e.line}`,
href: e.urlPath,
},
icon: {
- name: SEVERITY_ICONS_EXTENSION[e.severity],
+ name: SEVERITY_ICONS_MR_WIDGET[e.severity],
+ },
+ badge: {
+ variant: 'neutral',
+ text: i18n.fixed,
},
});
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/i18n.js b/app/assets/javascripts/vue_merge_request_widget/i18n.js
index 454a14faabb..5380bcae003 100644
--- a/app/assets/javascripts/vue_merge_request_widget/i18n.js
+++ b/app/assets/javascripts/vue_merge_request_widget/i18n.js
@@ -25,3 +25,10 @@ export const MERGE_TRAIN_BUTTON_TEXT = {
failed: __('Start merge train...'),
passed: __('Start merge train'),
};
+
+export const MR_WIDGET_CLOSED_REOPEN = __('Reopen');
+export const MR_WIDGET_CLOSED_REOPENING = __('Reopening...');
+export const MR_WIDGET_CLOSED_RELOADING = __('Refreshing...');
+export const MR_WIDGET_CLOSED_REOPEN_FAILURE = __(
+ 'An error occurred. Unable to reopen this merge request.',
+);
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 b96bdcb3833..00024a594dc 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
@@ -1,10 +1,10 @@
<script>
-import { GlSafeHtmlDirective } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import {
registerExtension,
registeredExtensions,
} from '~/vue_merge_request_widget/components/extensions';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
@@ -15,6 +15,7 @@ import notify from '~/lib/utils/notify';
import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import { setFaviconOverlay } from '../lib/utils/favicon';
import Loading from './components/loading.vue';
import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
@@ -46,18 +47,20 @@ import { STATE_MACHINE, stateToComponentMap } from './constants';
import eventHub from './event_hub';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import getStateQuery from './queries/get_state.query.graphql';
+import getStateSubscription from './queries/get_state.subscription.graphql';
import terraformExtension from './extensions/terraform';
import accessibilityExtension from './extensions/accessibility';
import codeQualityExtension from './extensions/code_quality';
import testReportExtension from './extensions/test_report';
import ReportWidgetContainer from './components/report_widget_container.vue';
+import MrWidgetReadyToMerge from './components/states/new_ready_to_merge.vue';
export default {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'MRWidget',
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
components: {
Loading,
@@ -76,7 +79,7 @@ export default {
MrWidgetNothingToMerge: NothingToMergeState,
MrWidgetNotAllowed: NotAllowedState,
MrWidgetMissingBranch: MissingBranchState,
- MrWidgetReadyToMerge: () => import('./components/states/new_ready_to_merge.vue'),
+ MrWidgetReadyToMerge,
ShaMismatch,
MrWidgetChecking: CheckingState,
MrWidgetUnresolvedDiscussions: UnresolvedDiscussionsState,
@@ -108,6 +111,31 @@ export default {
this.loading = false;
}
},
+ subscribeToMore: {
+ document() {
+ return getStateSubscription;
+ },
+ skip() {
+ return !this.mr?.id || this.loading || !window.gon?.features?.realtimeMrStatusChange;
+ },
+ variables() {
+ return {
+ issuableId: convertToGraphQLId('MergeRequest', this.mr?.id),
+ };
+ },
+ updateQuery(
+ _,
+ {
+ subscriptionData: {
+ data: { mergeRequestMergeStatusUpdated },
+ },
+ },
+ ) {
+ if (mergeRequestMergeStatusUpdated) {
+ this.mr.setGraphqlSubscriptionData(mergeRequestMergeStatusUpdated);
+ }
+ },
+ },
},
},
mixins: [mergeRequestQueryVariablesMixin],
@@ -128,6 +156,7 @@ export default {
machineState: store?.machineValue || STATE_MACHINE.definition.initial,
loading: true,
recomputeComponentName: 0,
+ issuableId: false,
};
},
computed: {
@@ -545,6 +574,7 @@ export default {
<mr-widget-approvals v-if="shouldRenderApprovals" :mr="mr" :service="service" />
<report-widget-container>
<extensions-container v-if="hasExtensions" :mr="mr" />
+ <widget-container v-if="mr && shouldShowSecurityExtension" :mr="mr" />
<security-reports-app
v-if="shouldRenderSecurityReport && !shouldShowSecurityExtension"
:pipeline-id="mr.pipeline.id"
@@ -580,8 +610,6 @@ export default {
</mr-widget-alert-message>
</div>
- <widget-container v-if="mr" :mr="mr" />
-
<div class="mr-widget-section" data-qa-selector="mr_widget_content">
<component :is="componentName" :mr="mr" :service="service" />
<ready-to-merge
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql
new file mode 100644
index 00000000000..c7b53db1221
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql
@@ -0,0 +1,7 @@
+subscription getStateSubscription($issuableId: IssuableID!) {
+ mergeRequestMergeStatusUpdated(issuableId: $issuableId) {
+ ... on MergeRequest {
+ detailedMergeStatus
+ }
+ }
+}
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 54770e6579a..9b0420cc7fa 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
@@ -1,44 +1,11 @@
+#import "./ready_to_merge_merge_request.fragment.graphql"
+
fragment ReadyToMerge on Project {
id
onlyAllowMergeIfPipelineSucceeds
mergeRequestsFfOnlyEnabled
squashReadOnly
mergeRequest(iid: $iid) {
- id
- autoMergeEnabled
- shouldRemoveSourceBranch
- forceRemoveSourceBranch
- defaultMergeCommitMessage
- defaultSquashCommitMessage
- squash
- squashOnMerge
- availableAutoMergeStrategies
- hasCi
- mergeable
- mergeWhenPipelineSucceeds
- commitCount
- diffHeadSha
- userPermissions {
- canMerge
- removeSourceBranch
- updateMergeRequest
- }
- targetBranch
- mergeError
- commitsWithoutMergeCommits {
- nodes {
- id
- sha
- shortId
- title
- message
- }
- }
- headPipeline {
- id
- status
- path
- active
- }
+ ...ReadyToMergeMergeRequest
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql
new file mode 100644
index 00000000000..8aba172e09c
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql
@@ -0,0 +1,9 @@
+#import "./ready_to_merge_merge_request.fragment.graphql"
+
+subscription readyToMergeSubscription($issuableId: IssuableID!) {
+ mergeRequestMergeStatusUpdated(issuableId: $issuableId) {
+ ... on MergeRequest {
+ ...ReadyToMergeMergeRequest
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge_merge_request.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge_merge_request.fragment.graphql
new file mode 100644
index 00000000000..276e2d4d63f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge_merge_request.fragment.graphql
@@ -0,0 +1,39 @@
+fragment ReadyToMergeMergeRequest on MergeRequest {
+ id
+ detailedMergeStatus
+ autoMergeEnabled
+ shouldRemoveSourceBranch
+ forceRemoveSourceBranch
+ defaultMergeCommitMessage
+ defaultSquashCommitMessage
+ squash
+ squashOnMerge
+ availableAutoMergeStrategies
+ hasCi
+ mergeable
+ mergeWhenPipelineSucceeds
+ commitCount
+ diffHeadSha
+ userPermissions {
+ canMerge
+ removeSourceBranch
+ updateMergeRequest
+ }
+ targetBranch
+ mergeError
+ commitsWithoutMergeCommits {
+ nodes {
+ id
+ sha
+ shortId
+ title
+ message
+ }
+ }
+ headPipeline {
+ id
+ status
+ path
+ active
+ }
+}
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 86ce032ea3d..85df2ea63c8 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
@@ -30,6 +30,7 @@ export default class MergeRequestStore {
this.machineValue = this.stateMachine.value;
this.mergeDetailsCollapsed = window.innerWidth < 768;
this.mergeError = data.mergeError;
+ this.id = data.id;
this.setPaths(data);
@@ -177,6 +178,7 @@ export default class MergeRequestStore {
this.updateStatusState(mergeRequest.state);
+ this.issuableId = mergeRequest.id;
this.projectArchived = project.archived;
this.onlyAllowMergeIfPipelineSucceeds = project.onlyAllowMergeIfPipelineSucceeds;
this.allowMergeOnSkippedPipeline = project.allowMergeOnSkippedPipeline;
@@ -206,6 +208,12 @@ export default class MergeRequestStore {
this.setState();
}
+ setGraphqlSubscriptionData(data) {
+ this.detailedMergeStatus = data.detailedMergeStatus;
+
+ this.setState();
+ }
+
updateStatusState(state) {
if (this.mergeRequestState !== state && badgeState.updateStatus) {
badgeState.updateStatus();