Commit f687431e authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch 'ph/290850/mrStatusBoxPoll' into 'master'

Updates merge request status box through polling

See merge request gitlab-org/gitlab!50761
parents 359a256e 6ba5c363
<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 ZenMode from '~/zen_mode';
import initIssuableSidebar from '~/init_issuable_sidebar'; import initIssuableSidebar from '~/init_issuable_sidebar';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; 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 initPipelines from '~/commit/pipelines/pipelines_bundle';
import initSourcegraph from '~/sourcegraph'; import initSourcegraph from '~/sourcegraph';
import loadAwardsHandler from '~/awards_handler'; import loadAwardsHandler from '~/awards_handler';
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger'; import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
import initInviteMemberModal from '~/invite_member/init_invite_member_modal'; import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
import StatusBox from '~/merge_request/components/status_box.vue';
export default function () { export default function () {
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
...@@ -18,4 +20,19 @@ export default function () { ...@@ -18,4 +20,19 @@ export default function () {
loadAwardsHandler(); loadAwardsHandler();
initInviteMemberModal(); initInviteMemberModal();
initInviteMemberTrigger(); 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 { format } from 'timeago.js';
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key'; 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 { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility'; import { formatDate } from '../../lib/utils/datetime_utility';
import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants'; import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
...@@ -154,6 +155,12 @@ export default class MergeRequestStore { ...@@ -154,6 +155,12 @@ export default class MergeRequestStore {
this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false; this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false;
this.setState(); this.setState();
mrEventHub.$emit('mr.state.updated', {
state: this.mergeRequestState,
reverted: data.reverted,
reverted_path: data.revertedPath,
});
} }
setGraphqlData(project) { setGraphqlData(project) {
......
...@@ -114,6 +114,14 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity ...@@ -114,6 +114,14 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
end end
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 private
delegate :current_user, to: :request delegate :current_user, to: :request
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
- can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request) - can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request)
- state_human_name, state_icon_name = state_name_with_icon(@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) - 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? - if @merge_request.closed_or_merged_without_fork?
.gl-alert.gl-alert-danger.gl-mb-5 .gl-alert.gl-alert-danger.gl-mb-5
...@@ -12,11 +14,11 @@ ...@@ -12,11 +14,11 @@
.detail-page-header.border-bottom-0.pt-0.pb-0 .detail-page-header.border-bottom-0.pt-0.pb-0
.detail-page-header-body .detail-page-header-body
.issuable-status-box.status-box{ class: status_box_class(@merge_request) } .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: 'd-block d-sm-none') = sprite_icon(state_icon_name, css_class: 'gl-display-block gl-display-sm-none!')
%span.d-none.d-sm-block %span.gl-display-none.gl-display-sm-block
- if @merge_request.reverted_by_merge_request?(current_user) - 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 - else
= state_human_name = state_human_name
......
---
title: Update merge request status box without reloading page
merge_request: 50761
author:
type: changed
...@@ -17670,6 +17670,9 @@ msgstr "" ...@@ -17670,6 +17670,9 @@ msgstr ""
msgid "Merged" msgid "Merged"
msgstr "" msgstr ""
msgid "Merged (%{linkStart}reverted%{linkEnd})"
msgstr ""
msgid "Merged (%{reverted})" msgid "Merged (%{reverted})"
msgstr "" 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