Commit 6e4b1f06 authored by Eric Eastwood's avatar Eric Eastwood

Add RelatedIssuesRoot

See https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1797

Conflicts:
	app/assets/javascripts/issuable/issuable_bundle.js
parent 549beacc
import Vue from 'vue';
import RelatedIssuesRoot from './related_issues/components/related_issues_root.vue';
document.addEventListener('DOMContentLoaded', () => {
const relatedIssuesRootElement = document.querySelector('.js-related-issues-root');
if (relatedIssuesRootElement) {
// eslint-disable-next-line no-new
new Vue({
el: relatedIssuesRootElement,
components: {
relatedIssuesRoot: RelatedIssuesRoot,
},
render: createElement => createElement('related-issues-root', {
props: {
endpoint: relatedIssuesRootElement.dataset.endpoint,
currentNamespacePath: relatedIssuesRootElement.dataset.namespace,
currentProjectPath: relatedIssuesRootElement.dataset.project,
canAddRelatedIssues: gl.utils.convertPermissionToBoolean(
relatedIssuesRootElement.dataset.canAddRelatedIssues,
),
helpPath: relatedIssuesRootElement.dataset.helpPath,
},
}),
});
}
});
......@@ -65,7 +65,7 @@ export default {
},
hasTitle() {
return this.title.length > 0 || this.isFetching;
}
},
},
methods: {
......
<script>
/* global Flash */
import eventHub from '../event_hub';
import RelatedIssuesBlock from './related_issues_block.vue';
import RelatedIssuesStore from '../stores/related_issues_store';
import RelatedIssuesService from '../services/related_issues_service';
export default {
name: 'RelatedIssuesRoot',
props: {
endpoint: {
type: String,
required: true,
},
currentNamespacePath: {
type: String,
required: true,
},
currentProjectPath: {
type: String,
required: true,
},
canAddRelatedIssues: {
type: Boolean,
required: false,
default: false,
},
helpPath: {
type: String,
required: false,
default: '',
},
},
data() {
this.store = new RelatedIssuesStore();
return {
state: this.store.state,
isFormVisible: false,
inputValue: '',
};
},
components: {
relatedIssuesBlock: RelatedIssuesBlock,
},
computed: {
computedRelatedIssues() {
return this.store.getIssuesFromReferences(
this.state.relatedIssues,
this.currentNamespacePath,
this.currentProjectPath,
);
},
computedPendingRelatedIssues() {
return this.store.getIssuesFromReferences(
this.state.pendingRelatedIssues,
this.currentNamespacePath,
this.currentProjectPath,
);
},
},
methods: {
onRelatedIssueRemoveRequest(reference) {
this.store.setRelatedIssues(this.state.relatedIssues.filter(ref => ref !== reference));
this.service.removeRelatedIssue(this.state.issueMap[reference].destroy_relation_path)
.catch(() => {
// Restore issue we were unable to delete
this.store.setRelatedIssues(this.state.relatedIssues.concat(reference));
// eslint-disable-next-line no-new
new Flash('An error occurred while removing related issues.');
});
},
onShowAddRelatedIssuesForm() {
this.isFormVisible = true;
},
onAddIssuableFormIssuableRemoveRequest(reference) {
this.store.setPendingRelatedIssues(
this.state.pendingRelatedIssues.filter(ref => ref !== reference),
);
},
onAddIssuableFormSubmit() {
const currentPendingIssues = this.state.pendingRelatedIssues;
this.service.addRelatedIssues(currentPendingIssues)
.then(res => res.json())
.then(() => {
this.store.setRelatedIssues(this.state.relatedIssues.concat(currentPendingIssues));
})
.catch(() => {
// Restore issues we were unable to submit
this.store.setPendingRelatedIssues(
_.uniq(this.state.pendingRelatedIssues.concat(currentPendingIssues)),
);
// eslint-disable-next-line no-new
new Flash('An error occurred while submitting related issues.');
});
this.store.setPendingRelatedIssues([]);
},
onAddIssuableFormCancel() {
this.isFormVisible = false;
this.store.setPendingRelatedIssues([]);
this.inputValue = '';
},
fetchRelatedIssues() {
this.service.fetchRelatedIssues()
.then(res => res.json())
.then((issues) => {
const relatedIssueReferences = issues.map((issue) => {
const referenceKey = `${issue.namespace_full_path}/${issue.project_path}#${issue.iid}`;
this.store.addToIssueMap(referenceKey, issue);
return referenceKey;
});
this.store.setRelatedIssues(relatedIssueReferences);
})
.catch(() => {
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching related issues.');
});
},
},
created() {
eventHub.$on('relatedIssue-removeRequest', this.onRelatedIssueRemoveRequest);
eventHub.$on('showAddRelatedIssuesForm', this.onShowAddRelatedIssuesForm);
eventHub.$on('pendingIssuable-removeRequest', this.onAddIssuableFormIssuableRemoveRequest);
eventHub.$on('addIssuableFormSubmit', this.onAddIssuableFormSubmit);
eventHub.$on('addIssuableFormCancel', this.onAddIssuableFormCancel);
this.service = new RelatedIssuesService(this.endpoint);
this.fetchRelatedIssues();
},
beforeDestroy() {
eventHub.$off('relatedIssue-removeRequest', this.onRelatedIssueRemoveRequest);
eventHub.$off('showAddRelatedIssuesForm', this.onShowAddRelatedIssuesForm);
eventHub.$off('pendingIssuable-removeRequest', this.onAddIssuableFormIssuableRemoveRequest);
eventHub.$off('addIssuableFormSubmit', this.onAddIssuableFormSubmit);
eventHub.$off('addIssuableFormCancel', this.onAddIssuableFormCancel);
},
};
</script>
<template>
<related-issues-block
:help-path="helpPath"
:related-issues="computedRelatedIssues"
:can-add-related-issues="canAddRelatedIssues"
:pending-related-issues="computedPendingRelatedIssues"
:is-form-visible="isFormVisible"
:input-value="inputValue" />
</template>
......@@ -4,6 +4,8 @@
- page_card_attributes @issue.card_attributes
- can_update_issue = can?(current_user, :update_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
- content_for :page_specific_javascripts do
= webpack_bundle_tag('issuable')
.clearfix.detail-page-header
.issuable-header
......@@ -65,6 +67,12 @@
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
.js-related-issues-root{ data: { endpoint: namespace_project_issue_related_issues_path(@project.namespace, @project, @issue),
namespace: @project.namespace.path,
project: @project.path,
can_add_related_issues: "#{can?(current_user, :update_issue, @issue)}",
help_path: help_page_path('user/project/issues/related_issues') } }
#merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } }
// This element is filled in using JavaScript.
......
......@@ -5,6 +5,7 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('diff_notes')
= webpack_bundle_tag('issuable')
.merge-request{ 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) }
= render "projects/merge_requests/show/mr_title"
......
......@@ -42,6 +42,7 @@ var config = {
graphs: './graphs/graphs_bundle.js',
group: './group.js',
groups_list: './groups_list.js',
issuable: './issuable/issuable_bundle.js',
issues: './issues/issues_bundle.js',
issue_show: './issue_show/index.js',
integrations: './integrations',
......@@ -159,6 +160,7 @@ var config = {
'environments',
'environments_folder',
'filtered_search',
'issuable',
'issue_show',
'merge_conflicts',
'notebook_viewer',
......
import Vue from 'vue';
import RelatedIssuesRoot from '~/issuable/related_issues/components/related_issues_root.vue';
const defaultProps = {
endpoint: '/foo/bar/issues/1/related_issues',
currentNamespacePath: 'foo',
currentProjectPath: 'bar',
};
const createComponent = (propsData = {}) => {
const Component = Vue.extend(RelatedIssuesRoot);
return new Component({
propsData,
})
.$mount();
};
const issuable1 = {
namespace_full_path: 'foo',
project_path: 'bar',
iid: '123',
title: 'issue1',
path: '/foo/bar/issues/123',
state: 'opened',
destroy_relation_path: '/foo/bar/issues/123/related_issues/1',
};
const issuable1Reference = `${issuable1.namespace_full_path}/${issuable1.project_path}#${issuable1.iid}`;
const issuable2 = {
namespace_full_path: 'foo',
project_path: 'bar',
iid: '124',
title: 'issue2',
path: '/foo/bar/issues/124',
state: 'opened',
destroy_relation_path: '/foo/bar/issues/124/related_issues/2',
};
const issuable2Reference = `${issuable2.namespace_full_path}/${issuable2.project_path}#${issuable2.iid}`;
describe('RelatedIssuesRoot', () => {
let vm;
afterEach(() => {
if (vm) {
vm.$destroy();
}
});
describe('methods', () => {
describe('onRelatedIssueRemoveRequest', () => {
beforeEach(() => {
vm = createComponent(defaultProps);
vm.store.addToIssueMap(issuable1Reference, issuable1);
vm.store.setRelatedIssues([issuable1Reference]);
});
it('remove related issue and succeeds', (done) => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 200,
}));
};
Vue.http.interceptors.push(interceptor);
vm.onRelatedIssueRemoveRequest(issuable1Reference);
setTimeout(() => {
expect(vm.computedRelatedIssues).toEqual([]);
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
done();
});
});
it('remove related issue, fails, and restores to related issues', (done) => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 422,
}));
};
Vue.http.interceptors.push(interceptor);
vm.onRelatedIssueRemoveRequest(issuable1Reference);
setTimeout(() => {
expect(vm.computedRelatedIssues.length).toEqual(1);
expect(vm.computedRelatedIssues[0].reference).toEqual(issuable1Reference);
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
done();
});
});
});
describe('onShowAddRelatedIssuesForm', () => {
beforeEach(() => {
vm = createComponent(defaultProps);
});
it('show add related issues form', () => {
vm.onShowAddRelatedIssuesForm();
expect(vm.isFormVisible).toEqual(true);
});
});
describe('onAddIssuableFormIssuableRemoveRequest', () => {
beforeEach(() => {
vm = createComponent(defaultProps);
vm.store.addToIssueMap(issuable1Reference, issuable1);
vm.store.setPendingRelatedIssues([issuable1Reference]);
});
it('remove pending related issue', () => {
vm.onAddIssuableFormIssuableRemoveRequest(issuable1Reference);
expect(vm.computedPendingRelatedIssues.length).toEqual(0);
});
});
describe('onAddIssuableFormSubmit', () => {
describe('when service.addRelatedIssues is succeeding', () => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 200,
}));
};
beforeEach(() => {
vm = createComponent(defaultProps);
vm.store.addToIssueMap(issuable1Reference, issuable1);
vm.store.addToIssueMap(issuable2Reference, issuable2);
Vue.http.interceptors.push(interceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('submit pending issues as related issues', (done) => {
vm.store.setPendingRelatedIssues([issuable1Reference]);
vm.onAddIssuableFormSubmit();
setTimeout(() => {
expect(vm.computedPendingRelatedIssues.length).toEqual(0);
expect(vm.computedRelatedIssues.length).toEqual(1);
expect(vm.computedRelatedIssues[0].reference).toEqual(issuable1Reference);
done();
});
});
it('submit multiple pending issues as related issues', (done) => {
vm.store.setPendingRelatedIssues([issuable1Reference, issuable2Reference]);
vm.onAddIssuableFormSubmit();
setTimeout(() => {
expect(vm.computedPendingRelatedIssues.length).toEqual(0);
expect(vm.computedRelatedIssues.length).toEqual(2);
expect(vm.computedRelatedIssues[0].reference).toEqual(issuable1Reference);
expect(vm.computedRelatedIssues[1].reference).toEqual(issuable2Reference);
done();
});
});
});
describe('when service.addRelatedIssues fails', () => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 422,
}));
};
beforeEach(() => {
vm = createComponent(defaultProps);
vm.store.addToIssueMap(issuable1Reference, issuable1);
vm.store.addToIssueMap(issuable2Reference, issuable2);
Vue.http.interceptors.push(interceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('submit pending issues as related issues fails and restores to pending related issues', (done) => {
vm.store.setPendingRelatedIssues([issuable1Reference]);
vm.onAddIssuableFormSubmit();
setTimeout(() => {
expect(vm.computedPendingRelatedIssues.length).toEqual(1);
expect(vm.computedPendingRelatedIssues[0].reference).toEqual(issuable1Reference);
expect(vm.computedRelatedIssues.length).toEqual(0);
done();
});
});
});
});
describe('onAddIssuableFormCancel', () => {
beforeEach(() => {
vm = createComponent(defaultProps);
vm.isFormVisible = true;
vm.inputValue = 'foo';
});
it('when canceling and hiding add issuable form', () => {
vm.onAddIssuableFormCancel();
expect(vm.isFormVisible).toEqual(false);
expect(vm.inputValue).toEqual('');
expect(vm.computedPendingRelatedIssues.length).toEqual(0);
});
});
describe('fetchRelatedIssues', () => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify([issuable1, issuable2]), {
status: 200,
}));
};
beforeEach(() => {
vm = createComponent(defaultProps);
Vue.http.interceptors.push(interceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('fetching related issues', (done) => {
vm.fetchRelatedIssues();
setTimeout(() => {
Vue.nextTick(() => {
expect(vm.computedRelatedIssues.length).toEqual(2);
expect(vm.computedRelatedIssues[0].reference).toEqual(issuable1Reference);
expect(vm.computedRelatedIssues[1].reference).toEqual(issuable2Reference);
done();
});
});
});
});
});
});
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