diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-20 14:22:11 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-20 14:22:11 +0000 |
commit | 0c872e02b2c822e3397515ec324051ff540f0cd5 (patch) | |
tree | ce2fb6ce7030e4dad0f4118d21ab6453e5938cdd /app/assets/javascripts/vue_merge_request_widget | |
parent | f7e05a6853b12f02911494c4b3fe53d9540d74fc (diff) | |
download | gitlab-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')
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">·</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> + · + </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(); |