Commit 7650677d authored by Fatih Acet's avatar Fatih Acet

Rewrite related MRs widget with Vue

This MR rewrites existing Related Merge Requests widget
with Vue with reusing shared Related Issues components
parent 941e0012
...@@ -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}
...@@ -77,12 +77,11 @@ ...@@ -77,12 +77,11 @@
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago') = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
#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
...@@ -141,6 +141,9 @@ msgstr "" ...@@ -141,6 +141,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 "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead" msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "" msgstr ""
...@@ -5806,6 +5809,9 @@ msgstr "" ...@@ -5806,6 +5809,9 @@ msgstr ""
msgid "Pipeline triggers" msgid "Pipeline triggers"
msgstr "" msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
msgid "PipelineCharts|Failed:" msgid "PipelineCharts|Failed:"
msgstr "" msgstr ""
...@@ -7462,6 +7468,9 @@ msgstr "" ...@@ -7462,6 +7468,9 @@ msgstr ""
msgid "Something went wrong while fetching comments. Please try again." msgid "Something went wrong while fetching comments. Please try again."
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 ""
...@@ -9190,6 +9199,11 @@ msgstr "" ...@@ -9190,6 +9199,11 @@ msgstr ""
msgid "When enabled, users cannot use GitLab until the terms have been accepted." msgid "When enabled, users cannot use GitLab until the terms have been accepted."
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