Commit 6ba5c363 authored by Phil Hughes's avatar Phil Hughes Committed by Brandon Labuschagne

Updates merge request status box through polling

This uses the merge request widgets polling responses
to update the status box in the merge request header.

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/290850
parent 4e249f7e
<script>
import { GlIcon, GlSprintf, GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
import mrEventHub from '../eventhub';
const CLASSES = {
opened: 'status-box-open',
closed: 'status-box-mr-closed',
merged: 'status-box-mr-merged',
};
const STATUS = {
opened: [__('Open'), 'issue-open-m'],
closed: [__('Closed'), 'close'],
merged: [__('Merged'), 'git-merge'],
};
export default {
components: {
GlIcon,
GlSprintf,
GlLink,
},
props: {
initialState: {
type: String,
required: true,
},
initialIsReverted: {
type: Boolean,
required: true,
},
initialRevertedPath: {
type: String,
required: false,
default: null,
},
},
data() {
return {
state: this.initialState,
isReverted: this.initialIsReverted,
revertedPath: this.initialRevertedPath,
};
},
computed: {
statusBoxClass() {
return CLASSES[this.state];
},
statusHumanName() {
return STATUS[this.state][0];
},
statusIconName() {
return STATUS[this.state][1];
},
},
created() {
mrEventHub.$on('mr.state.updated', this.updateState);
},
beforeDestroy() {
mrEventHub.$off('mr.state.updated', this.updateState);
},
methods: {
updateState({ state, reverted, revertedPath }) {
this.state = state;
this.reverted = reverted;
this.revertedPath = revertedPath;
},
},
};
</script>
<template>
<div :class="statusBoxClass" class="issuable-status-box status-box">
<gl-icon
:name="statusIconName"
class="gl-display-block gl-display-sm-none!"
data-testid="status-icon"
/>
<span class="gl-display-none gl-display-sm-block">
<gl-sprintf v-if="isReverted" :message="__('Merged (%{linkStart}reverted%{linkEnd})')">
<template #link="{ content }">
<gl-link
:href="revertedPath"
class="gl-reset-color! gl-text-decoration-underline"
data-testid="reverted-link"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
<template v-else>{{ statusHumanName }}</template>
</span>
</div>
</template>
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();
import Vue from 'vue';
import ZenMode from '~/zen_mode';
import initIssuableSidebar from '~/init_issuable_sidebar';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { handleLocationHash } from '~/lib/utils/common_utils';
import { handleLocationHash, parseBoolean } from '~/lib/utils/common_utils';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initSourcegraph from '~/sourcegraph';
import loadAwardsHandler from '~/awards_handler';
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
import StatusBox from '~/merge_request/components/status_box.vue';
export default function () {
new ZenMode(); // eslint-disable-line no-new
......@@ -18,4 +20,19 @@ export default function () {
loadAwardsHandler();
initInviteMemberModal();
initInviteMemberTrigger();
const el = document.querySelector('.js-mr-status-box');
// eslint-disable-next-line no-new
new Vue({
el,
render(h) {
return h(StatusBox, {
props: {
initialState: el.dataset.state,
initialIsReverted: parseBoolean(el.dataset.isReverted),
initialRevertedPath: el.dataset.revertedPath,
},
});
},
});
}
import { format } from 'timeago.js';
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import mrEventHub from '~/merge_request/eventhub';
import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility';
import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
......@@ -154,6 +155,12 @@ export default class MergeRequestStore {
this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false;
this.setState();
mrEventHub.$emit('mr.state.updated', {
state: this.mergeRequestState,
reverted: data.reverted,
reverted_path: data.revertedPath,
});
}
setGraphqlData(project) {
......
......@@ -114,6 +114,14 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
end
end
expose :reverted do |merge_request|
merge_request.reverted_by_merge_request?(current_user)
end
expose :reverted_path, if: -> (mr) { mr.reverted_by_merge_request?(current_user) } do |merge_request|
merge_request_path(merge_request.reverting_merge_request(current_user))
end
private
delegate :current_user, to: :request
......
......@@ -3,6 +3,8 @@
- can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request)
- state_human_name, state_icon_name = state_name_with_icon(@merge_request)
- are_close_and_open_buttons_hidden = merge_request_button_hidden?(@merge_request, true) && merge_request_button_hidden?(@merge_request, false)
- is_reverted = @merge_request.reverted_by_merge_request?(current_user)
- reverted_mr_path = is_reverted ? merge_request_path(@merge_request.reverting_merge_request(current_user)) : nil
- if @merge_request.closed_or_merged_without_fork?
.gl-alert.gl-alert-danger.gl-mb-5
......@@ -12,11 +14,11 @@
.detail-page-header.border-bottom-0.pt-0.pb-0
.detail-page-header-body
.issuable-status-box.status-box{ class: status_box_class(@merge_request) }
= sprite_icon(state_icon_name, css_class: 'd-block d-sm-none')
%span.d-none.d-sm-block
.issuable-status-box.status-box.js-mr-status-box{ class: status_box_class(@merge_request), data: { state: @merge_request.state, is_reverted: is_reverted.to_s, reverted_path: reverted_mr_path } }
= sprite_icon(state_icon_name, css_class: 'gl-display-block gl-display-sm-none!')
%span.gl-display-none.gl-display-sm-block
- if @merge_request.reverted_by_merge_request?(current_user)
= _('Merged (%{reverted})').html_safe % { reverted: link_to(s_('MergeRequest|reverted'), merge_request_path(@merge_request.reverting_merge_request(current_user)), class: 'gl-reset-color! gl-text-decoration-underline') }
= _('Merged (%{reverted})').html_safe % { reverted: link_to(s_('MergeRequest|reverted'), reverted_mr_path, class: 'gl-reset-color! gl-text-decoration-underline') }
- else
= state_human_name
......
---
title: Update merge request status box without reloading page
merge_request: 50761
author:
type: changed
......@@ -17670,6 +17670,9 @@ msgstr ""
msgid "Merged"
msgstr ""
msgid "Merged (%{linkStart}reverted%{linkEnd})"
msgstr ""
msgid "Merged (%{reverted})"
msgstr ""
......
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import StatusBox from '~/merge_request/components/status_box.vue';
import mrEventHub from '~/merge_request/eventhub';
let wrapper;
function factory(propsData) {
wrapper = shallowMount(StatusBox, { propsData, stubs: { GlSprintf } });
}
const testCases = [
{
name: 'Open',
state: 'opened',
class: 'status-box-open',
icon: 'issue-open-m',
},
{
name: 'Closed',
state: 'closed',
class: 'status-box-mr-closed',
icon: 'close',
},
{
name: 'Merged',
state: 'merged',
class: 'status-box-mr-merged',
icon: 'git-merge',
},
];
describe('Merge request status box component', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
testCases.forEach((testCase) => {
describe(`when merge request is ${testCase.name}`, () => {
it('renders human readable test', () => {
factory({
initialState: testCase.state,
initialIsReverted: false,
});
expect(wrapper.text()).toContain(testCase.name);
});
it('sets css class', () => {
factory({
initialState: testCase.state,
initialIsReverted: false,
});
expect(wrapper.classes()).toContain(testCase.class);
});
it('renders icon', () => {
factory({
initialState: testCase.state,
initialIsReverted: false,
});
expect(wrapper.find('[data-testid="status-icon"]').props('name')).toBe(testCase.icon);
});
});
});
describe('when merge request is reverted', () => {
it('renders a link to the reverted merge request', () => {
factory({
initialState: 'merged',
initialIsReverted: true,
initialRevertedPath: 'http://test.com',
});
expect(wrapper.find('[data-testid="reverted-link"]').attributes('href')).toBe(
'http://test.com',
);
});
});
it('updates with eventhub event', async () => {
factory({
initialState: 'opened',
initialIsReverted: false,
});
expect(wrapper.text()).toContain('Open');
mrEventHub.$emit('mr.state.updated', { state: 'closed', reverted: false });
await nextTick();
expect(wrapper.text()).toContain('Closed');
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment