Commit be7a13f1 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '_acet-related-mrs-widget-rewrite' into 'master'

Rewrite related MRs widget with Vue

Closes gitlab-ce#58926 and gitlab-ce#57662

See merge request gitlab-org/gitlab-ee!10484
parents 33eff283 b3ddd76e
...@@ -15,7 +15,6 @@ export default class Issue { ...@@ -15,7 +15,6 @@ export default class Issue {
Issue.$btnNewBranch = $('#new-branch'); Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap'); Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
Issue.initMergeRequests();
if (document.querySelector('#related-branches')) { if (document.querySelector('#related-branches')) {
Issue.initRelatedBranches(); Issue.initRelatedBranches();
} }
...@@ -143,19 +142,6 @@ export default class Issue { ...@@ -143,19 +142,6 @@ export default class Issue {
} }
} }
static initMergeRequests() {
var $container;
$container = $('#merge-requests');
return axios
.get($container.data('url'))
.then(({ data }) => {
if ('html' in data) {
$container.html(data.html);
}
})
.catch(() => flash('Failed to load referenced merge requests'));
}
static initRelatedBranches() { static initRelatedBranches() {
var $container; var $container;
$container = $('#related-branches'); $container = $('#related-branches');
......
import Vue from 'vue'; import Vue from 'vue';
import sanitize from 'sanitize-html';
import issuableApp from './components/app.vue'; import issuableApp from './components/app.vue';
import { parseIssuableData } from './utils/parse_data';
import '../vue_shared/vue_resource_interceptor'; import '../vue_shared/vue_resource_interceptor';
export default function initIssueableApp() { export default function initIssueableApp() {
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
const props = JSON.parse(sanitize(initialDataEl.textContent).replace(/"/g, '"'));
return new Vue({ return new Vue({
el: document.getElementById('js-issuable-app'), el: document.getElementById('js-issuable-app'),
components: { components: {
...@@ -14,7 +11,7 @@ export default function initIssueableApp() { ...@@ -14,7 +11,7 @@ export default function initIssueableApp() {
}, },
render(createElement) { render(createElement) {
return createElement('issuable-app', { return createElement('issuable-app', {
props, props: parseIssuableData(),
}); });
}, },
}); });
......
import sanitize from 'sanitize-html';
export const parseIssuableData = () => {
try {
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
return JSON.parse(sanitize(initialDataEl.textContent).replace(/"/g, '"'));
} catch (e) {
console.error(e); // eslint-disable-line no-console
return {};
}
};
export default {};
...@@ -4,9 +4,11 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; ...@@ -4,9 +4,11 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
import '~/notes/index'; import '~/notes/index';
import initIssueableApp from '~/issue_show'; import initIssueableApp from '~/issue_show';
import initRelatedMergeRequestsApp from '~/related_merge_requests';
export default function() { export default function() {
initIssueableApp(); initIssueableApp();
initRelatedMergeRequestsApp();
new Issue(); // eslint-disable-line no-new new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
......
<script>
import { mapState, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { sprintf, n__, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import { parseIssuableData } from '../../issue_show/utils/parse_data';
export default {
name: 'RelatedMergeRequests',
components: {
Icon,
GlLoadingIcon,
RelatedIssuableItem,
},
props: {
endpoint: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['isFetchingMergeRequests', 'mergeRequests', 'totalCount']),
closingMergeRequestsText() {
if (!this.hasClosingMergeRequest) {
return '';
}
const mrText = n__(
'When this merge request is accepted',
'When these merge requests are accepted',
this.totalCount,
);
return sprintf(s__('%{mrText}, this issue will be closed automatically.'), { mrText });
},
},
mounted() {
this.setInitialState({ apiEndpoint: this.endpoint });
this.fetchMergeRequests();
},
created() {
this.hasClosingMergeRequest = parseIssuableData().hasClosingMergeRequest;
},
methods: {
...mapActions(['setInitialState', 'fetchMergeRequests']),
getAssignees(mr) {
if (mr.assignees) {
return mr.assignees;
}
return mr.assignee ? [mr.assignee] : [];
},
},
};
</script>
<template>
<div v-if="isFetchingMergeRequests || (!isFetchingMergeRequests && totalCount)">
<div id="merge-requests" class="card-slim mt-3">
<div class="card-header">
<div class="card-title mt-0 mb-0 h5 merge-requests-title">
<span class="mr-1">
{{ __('Related merge requests') }}
</span>
<div v-if="totalCount" class="d-inline-flex lh-100 align-middle">
<div class="mr-count-badge">
<div class="mr-count-badge-count">
<svg class="s16 mr-1 text-secondary">
<icon name="merge-request" class="mr-1 text-secondary" />
</svg>
<span class="js-items-count">{{ totalCount }}</span>
</div>
</div>
</div>
</div>
</div>
<div>
<div
v-if="isFetchingMergeRequests"
class="related-related-merge-requests-icon qa-related-merge-requests-loading-icon"
>
<gl-loading-icon label="Fetching related merge requests" class="py-2" />
</div>
<ul v-else class="content-list related-items-list">
<li v-for="mr in mergeRequests" :key="mr.id" class="list-item pt-0 pb-0">
<related-issuable-item
:id-key="mr.id"
:display-reference="mr.reference"
:title="mr.title"
:milestone="mr.milestone"
:assignees="getAssignees(mr)"
:created-at="mr.created_at"
:closed-at="mr.closed_at"
:merged-at="mr.merged_at"
:path="mr.web_url"
:state="mr.state"
:is-merge-request="true"
:pipeline-status="mr.head_pipeline && mr.head_pipeline.detailed_status"
path-id-separator="!"
/>
</li>
</ul>
</div>
</div>
<div
v-if="hasClosingMergeRequest && !isFetchingMergeRequests"
class="issue-closed-by-widget second-block"
>
{{ closingMergeRequestsText }}
</div>
</div>
</template>
import Vue from 'vue';
import RelatedMergeRequests from './components/related_merge_requests.vue';
import createStore from './store';
export default function initRelatedMergeRequests() {
const relatedMergeRequestsElement = document.querySelector('#js-related-merge-requests');
if (relatedMergeRequestsElement) {
const { endpoint, projectPath, projectNamespace } = relatedMergeRequestsElement.dataset;
// eslint-disable-next-line no-new
new Vue({
el: relatedMergeRequestsElement,
components: {
RelatedMergeRequests,
},
store: createStore(),
render: createElement =>
createElement('related-merge-requests', {
props: { endpoint, projectNamespace, projectPath },
}),
});
}
}
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
const REQUEST_PAGE_COUNT = 100;
export const setInitialState = ({ commit }, props) => {
commit(types.SET_INITIAL_STATE, props);
};
export const requestData = ({ commit }) => commit(types.REQUEST_DATA);
export const receiveDataSuccess = ({ commit }, data) => commit(types.RECEIVE_DATA_SUCCESS, data);
export const receiveDataError = ({ commit }) => commit(types.RECEIVE_DATA_ERROR);
export const fetchMergeRequests = ({ state, dispatch }) => {
dispatch('requestData');
return axios
.get(`${state.apiEndpoint}?per_page=${REQUEST_PAGE_COUNT}`)
.then(res => {
const { headers, data } = res;
const total = Number(normalizeHeaders(headers)['X-TOTAL']) || 0;
dispatch('receiveDataSuccess', { data, total });
})
.catch(() => {
dispatch('receiveDataError');
createFlash(s__('Something went wrong while fetching related merge requests.'));
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
state: createState(),
actions,
mutations,
});
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
export const REQUEST_DATA = 'REQUEST_DATA';
export const RECEIVE_DATA_SUCCESS = 'RECEIVE_DATA_SUCCESS';
export const RECEIVE_DATA_ERROR = 'RECEIVE_DATA_ERROR';
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, { apiEndpoint }) {
state.apiEndpoint = apiEndpoint;
},
[types.REQUEST_DATA](state) {
state.isFetchingMergeRequests = true;
},
[types.RECEIVE_DATA_SUCCESS](state, { data, total }) {
state.isFetchingMergeRequests = false;
state.mergeRequests = data;
state.totalCount = total;
},
[types.RECEIVE_DATA_ERROR](state) {
state.isFetchingMergeRequests = false;
state.hasErrorFetchingMergeRequests = true;
},
};
export default () => ({
apiEndpoint: '',
isFetchingMergeRequests: false,
hasErrorFetchingMergeRequests: false,
mergeRequests: [],
totalCount: 0,
});
<script> <script>
import { GlTooltipDirective } from '@gitlab/ui'; import { GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { sprintf } from '~/locale';
import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue'; import IssueMilestone from '../../components/issue/issue_milestone.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import IssueAssignees from '../../components/issue/issue_assignees.vue';
import relatedIssuableMixin from '~/vue_shared/mixins/related_issuable_mixin'; import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
import CiIcon from '../ci_icon.vue';
export default { export default {
name: 'IssueItem', name: 'IssueItem',
components: { components: {
IssueMilestone, IssueMilestone,
IssueAssignees, IssueAssignees,
CiIcon,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -27,9 +29,9 @@ export default { ...@@ -27,9 +29,9 @@ export default {
return sprintf( return sprintf(
'<span class="bold">%{state}</span> %{timeInWords}<br/><span class="text-tertiary">%{timestamp}</span>', '<span class="bold">%{state}</span> %{timeInWords}<br/><span class="text-tertiary">%{timestamp}</span>',
{ {
state: this.isOpen ? __('Opened') : __('Closed'), state: this.stateText,
timeInWords: this.isOpen ? this.createdAtInWords : this.closedAtInWords, timeInWords: this.stateTimeInWords,
timestamp: this.isOpen ? this.createdAtTimestamp : this.closedAtTimestamp, timestamp: this.stateTimestamp,
}, },
); );
}, },
...@@ -84,6 +86,11 @@ export default { ...@@ -84,6 +86,11 @@ export default {
{{ pathIdSeparator }}{{ itemId }} {{ pathIdSeparator }}{{ itemId }}
</div> </div>
<div class="item-meta-child d-flex align-items-center"> <div class="item-meta-child d-flex align-items-center">
<span v-if="hasPipeline" class="mr-ci-status pr-2">
<a :href="pipelineStatus.details_path">
<ci-icon v-gl-tooltip :status="pipelineStatus" :title="pipelineStatusTooltip" />
</a>
</span>
<issue-milestone <issue-milestone
v-if="hasMilestone" v-if="hasMilestone"
:milestone="milestone" :milestone="milestone"
......
import _ from 'underscore'; import _ from 'underscore';
import { sprintf, __ } from '~/locale';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
...@@ -58,6 +59,11 @@ const mixins = { ...@@ -58,6 +59,11 @@ const mixins = {
required: false, required: false,
default: '', default: '',
}, },
mergedAt: {
type: String,
required: false,
default: '',
},
milestone: { milestone: {
type: Object, type: Object,
required: false, required: false,
...@@ -83,6 +89,16 @@ const mixins = { ...@@ -83,6 +89,16 @@ const mixins = {
required: false, required: false,
default: false, default: false,
}, },
isMergeRequest: {
type: Boolean,
required: false,
default: false,
},
pipelineStatus: {
type: Object,
required: false,
default: () => ({}),
},
}, },
components: { components: {
icon, icon,
...@@ -95,12 +111,18 @@ const mixins = { ...@@ -95,12 +111,18 @@ const mixins = {
hasState() { hasState() {
return this.state && this.state.length > 0; return this.state && this.state.length > 0;
}, },
hasPipeline() {
return this.isMergeRequest && this.pipelineStatus && Object.keys(this.pipelineStatus).length;
},
isOpen() { isOpen() {
return this.state === 'opened'; return this.state === 'opened';
}, },
isClosed() { isClosed() {
return this.state === 'closed'; return this.state === 'closed';
}, },
isMerged() {
return this.state === 'merged';
},
hasTitle() { hasTitle() {
return this.title.length > 0; return this.title.length > 0;
}, },
...@@ -108,9 +130,17 @@ const mixins = { ...@@ -108,9 +130,17 @@ const mixins = {
return !_.isEmpty(this.milestone); return !_.isEmpty(this.milestone);
}, },
iconName() { iconName() {
if (this.isMergeRequest && this.isMerged) {
return 'merge';
}
return this.isOpen ? 'issue-open-m' : 'issue-close'; return this.isOpen ? 'issue-open-m' : 'issue-close';
}, },
iconClass() { iconClass() {
if (this.isMergeRequest && this.isClosed) {
return 'merge-request-status closed issue-token-state-icon-closed';
}
return this.isOpen ? 'issue-token-state-icon-open' : 'issue-token-state-icon-closed'; return this.isOpen ? 'issue-token-state-icon-open' : 'issue-token-state-icon-closed';
}, },
computedLinkElementType() { computedLinkElementType() {
...@@ -131,12 +161,44 @@ const mixins = { ...@@ -131,12 +161,44 @@ const mixins = {
createdAtTimestamp() { createdAtTimestamp() {
return this.createdAt ? formatDate(new Date(this.createdAt)) : ''; return this.createdAt ? formatDate(new Date(this.createdAt)) : '';
}, },
mergedAtTimestamp() {
return this.mergedAt ? formatDate(new Date(this.mergedAt)) : '';
},
mergedAtInWords() {
return this.mergedAt ? this.timeFormated(this.mergedAt) : '';
},
closedAtInWords() { closedAtInWords() {
return this.closedAt ? this.timeFormated(this.closedAt) : ''; return this.closedAt ? this.timeFormated(this.closedAt) : '';
}, },
closedAtTimestamp() { closedAtTimestamp() {
return this.closedAt ? formatDate(new Date(this.closedAt)) : ''; return this.closedAt ? formatDate(new Date(this.closedAt)) : '';
}, },
stateText() {
if (this.isMerged) {
return __('Merged');
}
return this.isOpen ? __('Opened') : __('Closed');
},
stateTimeInWords() {
if (this.isMerged) {
return this.mergedAtInWords;
}
return this.isOpen ? this.createdAtInWords : this.closedAtInWords;
},
stateTimestamp() {
if (this.isMerged) {
return this.mergedAtTimestamp;
}
return this.isOpen ? this.createdAtTimestamp : this.closedAtTimestamp;
},
pipelineStatusTooltip() {
return this.hasPipeline
? sprintf(__('Pipeline: %{status}'), { status: this.pipelineStatus.label })
: '';
},
}, },
methods: { methods: {
onRemoveRequest() { onRemoveRequest() {
......
...@@ -277,6 +277,8 @@ module IssuablesHelper ...@@ -277,6 +277,8 @@ module IssuablesHelper
initialTaskStatus: issuable.task_status initialTaskStatus: issuable.task_status
} }
data[:hasClosingMergeRequest] = issuable.merge_requests_count != 0 if issuable.is_a?(Issue)
if parent.is_a?(Group) if parent.is_a?(Group)
data[:groupPath] = parent.path data[:groupPath] = parent.path
else else
......
.issue-closed-by-widget.second-block
- pluralized_mr_this = merge_request_count > 1 ? "these" : "this"
- pluralized_mr_is = merge_request_count > 1 ? "are" : "is"
When #{pluralized_mr_this} merge #{"request".pluralize(merge_request_count)} #{pluralized_mr_is} accepted, this issue will be closed automatically.
- if @merge_requests.any?
.card-slim.mt-3
.card-header
%h2.card-title.mt-0.mb-0.h5.merge-requests-title
%span.mr-1.bold
= _('Related merge requests')
.d-inline-flex.lh-100.align-middle
.mr-count-badge
.mr-count-badge-count
= sprite_icon('merge-request', size: 16, css_class: 'mr-1 text-secondary')
= @merge_requests.count
%ul.content-list.related-items-list
- has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id)
- @merge_requests.each do |merge_request|
- merge_request = merge_request.present(current_user: current_user)
%li.list-item.py-0.px-0
.item-body.issuable-info-container.py-lg-3.px-lg-3.pl-md-3
.item-contents
.item-title.d-flex.align-items-center.mr-title
= render partial: 'projects/issues/merge_requests_status', locals: { merge_request: merge_request, css_class: 'd-none d-xl-block append-right-8' }
= link_to merge_request.title, merge_request_path(merge_request), { class: 'mr-title-link'}
.item-meta
= render partial: 'projects/issues/merge_requests_status', locals: { merge_request: merge_request, css_class: 'd-xl-none d-lg-block append-right-5' }
%span.d-flex.align-items-center.append-right-8.mr-item-path.item-path-id.mt-0
%span.path-id-text.bold.text-truncate{ data: { toggle: 'tooltip'}, title: merge_request.target_project.full_path }
= merge_request.target_project.full_path
= merge_request.to_reference
%span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2
- if merge_request.can_read_pipeline?
= render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), tooltip_placement: 'bottom'
- elsif has_any_head_pipeline
= icon('blank fw')
- if @closed_by_merge_requests.present?
%p
= render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count}
...@@ -87,12 +87,11 @@ ...@@ -87,12 +87,11 @@
%h3.card-title.mt-0.mb-0.h5 %h3.card-title.mt-0.mb-0.h5
Related issues Related issues
#merge-requests{ data: { url: referenced_merge_requests_project_issue_path(@project, @issue) } } #js-related-merge-requests{ data: { endpoint: api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid), project_namespace: @project.namespace.path, project_path: @project.path } }
// This element is filled in using JavaScript.
- if can?(current_user, :download_code, @project) - if can?(current_user, :download_code, @project)
#related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } } #related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } }
// This element is filled in using JavaScript. -# This element is filled in using JavaScript.
.content-block.emoji-block.emoji-block-sticky .content-block.emoji-block.emoji-block-sticky
.row .row
......
---
title: Rewrite related MRs widget with Vue
merge_request: 27027
author:
type: other
...@@ -195,6 +195,9 @@ msgstr "" ...@@ -195,6 +195,9 @@ msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}" msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr "" msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
msgid "%{name}'s avatar" msgid "%{name}'s avatar"
msgstr "" msgstr ""
...@@ -7767,6 +7770,9 @@ msgstr "" ...@@ -7767,6 +7770,9 @@ msgstr ""
msgid "Pipeline triggers" msgid "Pipeline triggers"
msgstr "" msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
msgid "PipelineCharts|Failed:" msgid "PipelineCharts|Failed:"
msgstr "" msgstr ""
...@@ -9976,6 +9982,9 @@ msgstr "" ...@@ -9976,6 +9982,9 @@ msgstr ""
msgid "Something went wrong while fetching group member contributions" msgid "Something went wrong while fetching group member contributions"
msgstr "" msgstr ""
msgid "Something went wrong while fetching related merge requests."
msgstr ""
msgid "Something went wrong while fetching the environments for this merge request. Please try again." msgid "Something went wrong while fetching the environments for this merge request. Please try again."
msgstr "" msgstr ""
...@@ -12136,6 +12145,11 @@ msgstr "" ...@@ -12136,6 +12145,11 @@ msgstr ""
msgid "When leaving the URL blank, classification labels can still be specified without disabling cross project features or performing external authorization checks." msgid "When leaving the URL blank, classification labels can still be specified without disabling cross project features or performing external authorization checks."
msgstr "" msgstr ""
msgid "When this merge request is accepted"
msgid_plural "When these merge requests are accepted"
msgstr[0] ""
msgstr[1] ""
msgid "When:" msgid "When:"
msgstr "" msgstr ""
......
...@@ -70,7 +70,7 @@ describe "Internal references", :js do ...@@ -70,7 +70,7 @@ describe "Internal references", :js do
page.within("#merge-requests ul") do page.within("#merge-requests ul") do
expect(page).to have_content(private_project_merge_request.title) expect(page).to have_content(private_project_merge_request.title)
expect(page).to have_css(".merge-request-status") expect(page).to have_css(".ic-issue-open-m")
end end
expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}") expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}")
......
...@@ -65,3 +65,61 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller ...@@ -65,3 +65,61 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
store_frontend_fixture(response, fixture_file_name) store_frontend_fixture(response, fixture_file_name)
end end
end end
describe API::Issues, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include JavaScriptFixturesHelpers
def get_related_merge_requests(project_id, issue_iid, user = nil)
get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user)
end
def create_referencing_mr(user, project, issue)
attributes = {
author: user,
source_project: project,
target_project: project,
source_branch: "master",
target_branch: "test",
assignee: user,
description: "See #{issue.to_reference}"
}
create(:merge_request, attributes).tap do |merge_request|
create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
end
end
it 'issues/related_merge_requests.json' do |example|
user = create(:user)
project = create(:project, :public, creator_id: user.id, namespace: user.namespace)
issue_title = 'foo'
issue_description = 'closed'
milestone = create(:milestone, title: '1.0.0', project: project)
issue = create :issue,
author: user,
assignees: [user],
project: project,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description
project.add_reporter(user)
create_referencing_mr(user, project, issue)
create(:merge_request,
:simple,
author: user,
source_project: project,
target_project: project,
description: "Some description")
project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace)
create_referencing_mr(user, project2, issue).update!(head_pipeline: create(:ci_pipeline))
get_related_merge_requests(project.id, issue.iid, user)
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
import { mount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import RelatedMergeRequests from '~/related_merge_requests/components/related_merge_requests.vue';
import createStore from '~/related_merge_requests/store/index';
const FIXTURE_PATH = 'issues/related_merge_requests.json';
const API_ENDPOINT = '/api/v4/projects/2/issues/33/related_merge_requests';
const localVue = createLocalVue();
describe('RelatedMergeRequests', () => {
let wrapper;
let mock;
let mockData;
beforeEach(done => {
loadFixtures(FIXTURE_PATH);
mockData = getJSONFixture(FIXTURE_PATH);
mock = new MockAdapter(axios);
mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 });
wrapper = mount(RelatedMergeRequests, {
localVue,
sync: false,
store: createStore(),
propsData: {
endpoint: API_ENDPOINT,
projectNamespace: 'gitlab-org',
projectPath: 'gitlab-ce',
},
});
setTimeout(done);
});
afterEach(() => {
wrapper.destroy();
mock.restore();
});
describe('methods', () => {
describe('getAssignees', () => {
const assignees = [{ name: 'foo' }, { name: 'bar' }];
describe('when there is assignees array', () => {
it('should return assignees array', () => {
const mr = { assignees };
expect(wrapper.vm.getAssignees(mr)).toEqual(assignees);
});
});
it('should return an array with single assingee', () => {
const mr = { assignee: assignees[0] };
expect(wrapper.vm.getAssignees(mr)).toEqual([assignees[0]]);
});
it('should return empty array when assignee is not set', () => {
expect(wrapper.vm.getAssignees({})).toEqual([]);
expect(wrapper.vm.getAssignees({ assignee: null })).toEqual([]);
});
});
});
describe('template', () => {
it('should render related merge request items', () => {
expect(wrapper.find('.js-items-count').text()).toEqual('2');
expect(wrapper.findAll(RelatedIssuableItem).length).toEqual(2);
const props = wrapper
.findAll(RelatedIssuableItem)
.at(1)
.props();
const data = mockData[1];
expect(props.idKey).toEqual(data.id);
expect(props.pathIdSeparator).toEqual('!');
expect(props.pipelineStatus).toBe(data.head_pipeline.detailed_status);
expect(props.assignees).toEqual([data.assignee]);
expect(props.isMergeRequest).toBe(true);
expect(props.confidential).toEqual(false);
expect(props.title).toEqual(data.title);
expect(props.state).toEqual(data.state);
expect(props.createdAt).toEqual(data.created_at);
});
});
});
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import * as types from '~/related_merge_requests/store/mutation_types';
import actionsModule, * as actions from '~/related_merge_requests/store/actions';
import testAction from 'spec/helpers/vuex_action_helper';
describe('RelatedMergeRequest store actions', () => {
let state;
let flashSpy;
let mock;
beforeEach(() => {
state = {
apiEndpoint: '/api/related_merge_requests',
};
flashSpy = spyOnDependency(actionsModule, 'createFlash');
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('setInitialState', () => {
it('commits types.SET_INITIAL_STATE with given props', done => {
const props = { a: 1, b: 2 };
testAction(
actions.setInitialState,
props,
{},
[{ type: types.SET_INITIAL_STATE, payload: props }],
[],
done,
);
});
});
describe('requestData', () => {
it('commits types.REQUEST_DATA', done => {
testAction(actions.requestData, null, {}, [{ type: types.REQUEST_DATA }], [], done);
});
});
describe('receiveDataSuccess', () => {
it('commits types.RECEIVE_DATA_SUCCESS with data', done => {
const data = { a: 1, b: 2 };
testAction(
actions.receiveDataSuccess,
data,
{},
[{ type: types.RECEIVE_DATA_SUCCESS, payload: data }],
[],
done,
);
});
});
describe('receiveDataError', () => {
it('commits types.RECEIVE_DATA_ERROR', done => {
testAction(
actions.receiveDataError,
null,
{},
[{ type: types.RECEIVE_DATA_ERROR }],
[],
done,
);
});
});
describe('fetchMergeRequests', () => {
describe('for a successful request', () => {
it('should dispatch success action', done => {
const data = { a: 1 };
mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(200, data, { 'x-total': 2 });
testAction(
actions.fetchMergeRequests,
null,
state,
[],
[{ type: 'requestData' }, { type: 'receiveDataSuccess', payload: { data, total: 2 } }],
done,
);
});
});
describe('for a failing request', () => {
it('should dispatch error action', done => {
mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(400);
testAction(
actions.fetchMergeRequests,
null,
state,
[],
[{ type: 'requestData' }, { type: 'receiveDataError' }],
() => {
expect(flashSpy).toHaveBeenCalledTimes(1);
expect(flashSpy).toHaveBeenCalledWith(jasmine.stringMatching('Something went wrong'));
done();
},
);
});
});
});
});
import mutations from '~/related_merge_requests/store/mutations';
import * as types from '~/related_merge_requests/store/mutation_types';
describe('RelatedMergeRequests Store Mutations', () => {
describe('SET_INITIAL_STATE', () => {
it('should set initial state according to given data', () => {
const apiEndpoint = '/api';
const state = {};
mutations[types.SET_INITIAL_STATE](state, { apiEndpoint });
expect(state.apiEndpoint).toEqual(apiEndpoint);
});
});
describe('REQUEST_DATA', () => {
it('should set loading flag', () => {
const state = {};
mutations[types.REQUEST_DATA](state);
expect(state.isFetchingMergeRequests).toEqual(true);
});
});
describe('RECEIVE_DATA_SUCCESS', () => {
it('should set loading flag and data', () => {
const state = {};
const mrs = [1, 2, 3];
mutations[types.RECEIVE_DATA_SUCCESS](state, { data: mrs, total: mrs.length });
expect(state.isFetchingMergeRequests).toEqual(false);
expect(state.mergeRequests).toEqual(mrs);
expect(state.totalCount).toEqual(mrs.length);
});
});
describe('RECEIVE_DATA_ERROR', () => {
it('should set loading and error flags', () => {
const state = {};
mutations[types.RECEIVE_DATA_ERROR](state);
expect(state.isFetchingMergeRequests).toEqual(false);
expect(state.hasErrorFetchingMergeRequests).toEqual(true);
});
});
});
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