Merge branch '_acet-related-mrs-widget-rewrite' into 'master'
Rewrite related MRs widget with Vue Closes #58926 and #57662 See merge request gitlab-org/gitlab-ce!27027
This commit is contained in:
commit
8f37215277
24 changed files with 651 additions and 70 deletions
|
@ -15,7 +15,6 @@ export default class Issue {
|
|||
Issue.$btnNewBranch = $('#new-branch');
|
||||
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
|
||||
|
||||
Issue.initMergeRequests();
|
||||
if (document.querySelector('#related-branches')) {
|
||||
Issue.initRelatedBranches();
|
||||
}
|
||||
|
@ -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() {
|
||||
var $container;
|
||||
$container = $('#related-branches');
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
import sanitize from 'sanitize-html';
|
||||
import issuableApp from './components/app.vue';
|
||||
import { parseIssuableData } from './utils/parse_data';
|
||||
import '../vue_shared/vue_resource_interceptor';
|
||||
|
||||
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({
|
||||
el: document.getElementById('js-issuable-app'),
|
||||
components: {
|
||||
|
@ -14,7 +11,7 @@ export default function initIssueableApp() {
|
|||
},
|
||||
render(createElement) {
|
||||
return createElement('issuable-app', {
|
||||
props,
|
||||
props: parseIssuableData(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
15
app/assets/javascripts/issue_show/utils/parse_data.js
Normal file
15
app/assets/javascripts/issue_show/utils/parse_data.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
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';
|
|||
import ZenMode from '~/zen_mode';
|
||||
import '~/notes/index';
|
||||
import initIssueableApp from '~/issue_show';
|
||||
import initRelatedMergeRequestsApp from '~/related_merge_requests';
|
||||
|
||||
export default function() {
|
||||
initIssueableApp();
|
||||
initRelatedMergeRequestsApp();
|
||||
new Issue(); // eslint-disable-line no-new
|
||||
new ShortcutsIssuable(); // eslint-disable-line no-new
|
||||
new ZenMode(); // eslint-disable-line no-new
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
<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>
|
24
app/assets/javascripts/related_merge_requests/index.js
Normal file
24
app/assets/javascripts/related_merge_requests/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
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 },
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
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 () => {};
|
14
app/assets/javascripts/related_merge_requests/store/index.js
Normal file
14
app/assets/javascripts/related_merge_requests/store/index.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
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,
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
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';
|
|
@ -0,0 +1,19 @@
|
|||
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;
|
||||
},
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
export default () => ({
|
||||
apiEndpoint: '',
|
||||
isFetchingMergeRequests: false,
|
||||
hasErrorFetchingMergeRequests: false,
|
||||
mergeRequests: [],
|
||||
totalCount: 0,
|
||||
});
|
|
@ -1,15 +1,17 @@
|
|||
<script>
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
|
||||
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
|
||||
import relatedIssuableMixin from '~/vue_shared/mixins/related_issuable_mixin';
|
||||
import { sprintf } from '~/locale';
|
||||
import IssueMilestone from '../../components/issue/issue_milestone.vue';
|
||||
import IssueAssignees from '../../components/issue/issue_assignees.vue';
|
||||
import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
|
||||
import CiIcon from '../ci_icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'IssueItem',
|
||||
components: {
|
||||
IssueMilestone,
|
||||
IssueAssignees,
|
||||
CiIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -27,9 +29,9 @@ export default {
|
|||
return sprintf(
|
||||
'<span class="bold">%{state}</span> %{timeInWords}<br/><span class="text-tertiary">%{timestamp}</span>',
|
||||
{
|
||||
state: this.isOpen ? __('Opened') : __('Closed'),
|
||||
timeInWords: this.isOpen ? this.createdAtInWords : this.closedAtInWords,
|
||||
timestamp: this.isOpen ? this.createdAtTimestamp : this.closedAtTimestamp,
|
||||
state: this.stateText,
|
||||
timeInWords: this.stateTimeInWords,
|
||||
timestamp: this.stateTimestamp,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -84,6 +86,11 @@ export default {
|
|||
{{ pathIdSeparator }}{{ itemId }}
|
||||
</div>
|
||||
<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
|
||||
v-if="hasMilestone"
|
||||
:milestone="milestone"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import _ from 'underscore';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
|
@ -58,6 +59,11 @@ const mixins = {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
mergedAt: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
milestone: {
|
||||
type: Object,
|
||||
required: false,
|
||||
|
@ -83,6 +89,16 @@ const mixins = {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isMergeRequest: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
pipelineStatus: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
components: {
|
||||
icon,
|
||||
|
@ -95,12 +111,18 @@ const mixins = {
|
|||
hasState() {
|
||||
return this.state && this.state.length > 0;
|
||||
},
|
||||
hasPipeline() {
|
||||
return this.isMergeRequest && this.pipelineStatus && Object.keys(this.pipelineStatus).length;
|
||||
},
|
||||
isOpen() {
|
||||
return this.state === 'opened';
|
||||
},
|
||||
isClosed() {
|
||||
return this.state === 'closed';
|
||||
},
|
||||
isMerged() {
|
||||
return this.state === 'merged';
|
||||
},
|
||||
hasTitle() {
|
||||
return this.title.length > 0;
|
||||
},
|
||||
|
@ -108,9 +130,17 @@ const mixins = {
|
|||
return !_.isEmpty(this.milestone);
|
||||
},
|
||||
iconName() {
|
||||
if (this.isMergeRequest && this.isMerged) {
|
||||
return 'merge';
|
||||
}
|
||||
|
||||
return this.isOpen ? 'issue-open-m' : 'issue-close';
|
||||
},
|
||||
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';
|
||||
},
|
||||
computedLinkElementType() {
|
||||
|
@ -131,12 +161,44 @@ const mixins = {
|
|||
createdAtTimestamp() {
|
||||
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() {
|
||||
return this.closedAt ? this.timeFormated(this.closedAt) : '';
|
||||
},
|
||||
closedAtTimestamp() {
|
||||
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: {
|
||||
onRemoveRequest() {
|
||||
|
|
|
@ -277,6 +277,8 @@ module IssuablesHelper
|
|||
initialTaskStatus: issuable.task_status
|
||||
}
|
||||
|
||||
data[:hasClosingMergeRequest] = issuable.merge_requests_count != 0 if issuable.is_a?(Issue)
|
||||
|
||||
if parent.is_a?(Group)
|
||||
data[:groupPath] = parent.path
|
||||
else
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
.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.
|
|
@ -1,36 +0,0 @@
|
|||
- 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 @@
|
|||
|
||||
= 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) } }
|
||||
// This element is filled in using JavaScript.
|
||||
#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 } }
|
||||
|
||||
- if can?(current_user, :download_code, @project)
|
||||
#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
|
||||
.row
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Rewrite related MRs widget with Vue
|
||||
merge_request: 27027
|
||||
author:
|
||||
type: other
|
|
@ -141,6 +141,9 @@ msgstr ""
|
|||
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{mrText}, this issue will be closed automatically."
|
||||
msgstr ""
|
||||
|
||||
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
|
||||
msgstr ""
|
||||
|
||||
|
@ -5830,6 +5833,9 @@ msgstr ""
|
|||
msgid "Pipeline triggers"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline: %{status}"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineCharts|Failed:"
|
||||
msgstr ""
|
||||
|
||||
|
@ -7510,6 +7516,9 @@ msgstr ""
|
|||
msgid "Something went wrong while fetching comments. Please try again."
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
@ -9238,6 +9247,11 @@ msgstr ""
|
|||
msgid "When enabled, users cannot use GitLab until the terms have been accepted."
|
||||
msgstr ""
|
||||
|
||||
msgid "When this merge request is accepted"
|
||||
msgid_plural "When these merge requests are accepted"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "When:"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ describe "Internal references", :js do
|
|||
|
||||
page.within("#merge-requests ul") do
|
||||
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
|
||||
|
||||
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
|
|||
store_frontend_fixture(response, fixture_file_name)
|
||||
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
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
110
spec/javascripts/related_merge_requests/store/actions_spec.js
Normal file
110
spec/javascripts/related_merge_requests/store/actions_spec.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
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();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue