Commit 2d35478c authored by Daniel Tian's avatar Daniel Tian Committed by Miguel Rincon

Make related issues component more extendable

Make the related issues component used on the Issues page more
extendable so that it can be reused on the standalone vulnerabilities
page
parent a04c8e24
...@@ -324,7 +324,7 @@ export default { ...@@ -324,7 +324,7 @@ export default {
v-if="showRelatedIssues" v-if="showRelatedIssues"
:endpoint="featureFlagIssuesEndpoint" :endpoint="featureFlagIssuesEndpoint"
:can-admin="true" :can-admin="true"
:is-linked-issue-block="false" :show-categorized-issues="false"
/> />
<template v-if="supportsStrategies"> <template v-if="supportsStrategies">
......
...@@ -33,7 +33,7 @@ export default { ...@@ -33,7 +33,7 @@ export default {
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
isLinkedIssueBlock: { showCategorizedIssues: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
...@@ -128,7 +128,7 @@ export default { ...@@ -128,7 +128,7 @@ export default {
<template> <template>
<form @submit.prevent="onFormSubmit"> <form @submit.prevent="onFormSubmit">
<template v-if="isLinkedIssueBlock"> <template v-if="showCategorizedIssues">
<gl-form-group <gl-form-group
:label="__('The current issue')" :label="__('The current issue')"
label-for="linked-issue-type-radio" label-for="linked-issue-type-radio"
......
...@@ -77,7 +77,7 @@ export default { ...@@ -77,7 +77,7 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
isLinkedIssueBlock: { showCategorizedIssues: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true, default: true,
...@@ -88,12 +88,16 @@ export default { ...@@ -88,12 +88,16 @@ export default {
return this.relatedIssues.length > 0; return this.relatedIssues.length > 0;
}, },
categorisedIssues() { categorisedIssues() {
if (this.showCategorizedIssues) {
return Object.values(linkedIssueTypesMap) return Object.values(linkedIssueTypesMap)
.map(linkType => ({ .map(linkType => ({
linkType, linkType,
issues: this.relatedIssues.filter(issue => issue.linkType === linkType), issues: this.relatedIssues.filter(issue => issue.linkType === linkType),
})) }))
.filter(obj => obj.issues.length > 0); .filter(obj => obj.issues.length > 0);
}
return [{ issues: this.relatedIssues }];
}, },
shouldShowTokenBody() { shouldShowTokenBody() {
return this.hasRelatedIssues || this.isFetching; return this.hasRelatedIssues || this.isFetching;
...@@ -114,9 +118,7 @@ export default { ...@@ -114,9 +118,7 @@ export default {
return issuableQaClassMap[this.issuableType]; return issuableQaClassMap[this.issuableType];
}, },
}, },
created() { linkedIssueTypesTextMap,
this.linkedIssueTypesTextMap = linkedIssueTypesTextMap;
},
}; };
</script> </script>
...@@ -131,7 +133,7 @@ export default { ...@@ -131,7 +133,7 @@ export default {
href="#related-issues" href="#related-issues"
aria-hidden="true" aria-hidden="true"
/> />
{{ __('Linked issues') }} <slot name="headerText">{{ __('Linked issues') }}</slot>
<a v-if="hasHelpPath" :href="helpPath"> <a v-if="hasHelpPath" :href="helpPath">
<i <i
class="related-issues-header-help-icon fa fa-question-circle" class="related-issues-header-help-icon fa fa-question-circle"
...@@ -174,7 +176,7 @@ export default { ...@@ -174,7 +176,7 @@ export default {
class="js-add-related-issues-form-area card-body bordered-box bg-white" class="js-add-related-issues-form-area card-body bordered-box bg-white"
> >
<add-issuable-form <add-issuable-form
:is-linked-issue-block="isLinkedIssueBlock" :show-categorized-issues="showCategorizedIssues"
:is-submitting="isSubmitting" :is-submitting="isSubmitting"
:issuable-type="issuableType" :issuable-type="issuableType"
:input-value="inputValue" :input-value="inputValue"
...@@ -192,7 +194,7 @@ export default { ...@@ -192,7 +194,7 @@ export default {
<related-issues-list <related-issues-list
v-for="category in categorisedIssues" v-for="category in categorisedIssues"
:key="category.linkType" :key="category.linkType"
:heading="linkedIssueTypesTextMap[category.linkType]" :heading="$options.linkedIssueTypesTextMap[category.linkType]"
:can-admin="canAdmin" :can-admin="canAdmin"
:can-reorder="canReorder" :can-reorder="canReorder"
:is-fetching="isFetching" :is-fetching="isFetching"
......
...@@ -81,7 +81,7 @@ export default { ...@@ -81,7 +81,7 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
isLinkedIssueBlock: { showCategorizedIssues: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true, default: true,
...@@ -147,17 +147,18 @@ export default { ...@@ -147,17 +147,18 @@ export default {
this.store.setPendingReferences([]); this.store.setPendingReferences([]);
this.store.setRelatedIssues(data.issuables); this.store.setRelatedIssues(data.issuables);
this.isSubmitting = false;
// Close the form on submission // Close the form on submission
this.isFormVisible = false; this.isFormVisible = false;
}) })
.catch(({ response }) => { .catch(({ response }) => {
this.isSubmitting = false;
let errorMessage = addRelatedIssueErrorMap[this.issuableType]; let errorMessage = addRelatedIssueErrorMap[this.issuableType];
if (response && response.data && response.data.message) { if (response && response.data && response.data.message) {
errorMessage = response.data.message; errorMessage = response.data.message;
} }
Flash(errorMessage); Flash(errorMessage);
})
.finally(() => {
this.isSubmitting = false;
}); });
} }
}, },
...@@ -172,12 +173,13 @@ export default { ...@@ -172,12 +173,13 @@ export default {
.fetchRelatedIssues() .fetchRelatedIssues()
.then(({ data }) => { .then(({ data }) => {
this.store.setRelatedIssues(data); this.store.setRelatedIssues(data);
this.isFetching = false;
}) })
.catch(() => { .catch(() => {
this.store.setRelatedIssues([]); this.store.setRelatedIssues([]);
this.isFetching = false;
Flash(__('An error occurred while fetching issues.')); Flash(__('An error occurred while fetching issues.'));
})
.finally(() => {
this.isFetching = false;
}); });
}, },
saveIssueOrder({ issueId, beforeId, afterId, oldIndex, newIndex }) { saveIssueOrder({ issueId, beforeId, afterId, oldIndex, newIndex }) {
...@@ -200,7 +202,8 @@ export default { ...@@ -200,7 +202,8 @@ export default {
} }
}, },
onInput({ untouchedRawReferences, touchedReference }) { onInput({ untouchedRawReferences, touchedReference }) {
this.store.setPendingReferences(this.state.pendingReferences.concat(untouchedRawReferences)); this.store.addPendingReferences(untouchedRawReferences);
this.inputValue = `${touchedReference}`; this.inputValue = `${touchedReference}`;
}, },
onBlur(newValue) { onBlur(newValue) {
...@@ -209,7 +212,7 @@ export default { ...@@ -209,7 +212,7 @@ export default {
processAllReferences(value = '') { processAllReferences(value = '') {
const rawReferences = value.split(/\s+/).filter(reference => reference.trim().length > 0); const rawReferences = value.split(/\s+/).filter(reference => reference.trim().length > 0);
this.store.setPendingReferences(this.state.pendingReferences.concat(rawReferences)); this.store.addPendingReferences(rawReferences);
this.inputValue = ''; this.inputValue = '';
}, },
}, },
...@@ -231,7 +234,7 @@ export default { ...@@ -231,7 +234,7 @@ export default {
:auto-complete-sources="autoCompleteSources" :auto-complete-sources="autoCompleteSources"
:issuable-type="issuableType" :issuable-type="issuableType"
:path-id-separator="pathIdSeparator" :path-id-separator="pathIdSeparator"
:is-linked-issue-block="isLinkedIssueBlock" :show-categorized-issues="showCategorizedIssues"
@saveReorder="saveIssueOrder" @saveReorder="saveIssueOrder"
@toggleAddRelatedIssuesForm="onToggleAddRelatedIssuesForm" @toggleAddRelatedIssuesForm="onToggleAddRelatedIssuesForm"
@addIssuableFormInput="onInput" @addIssuableFormInput="onInput"
......
...@@ -14,8 +14,12 @@ class RelatedIssuesStore { ...@@ -14,8 +14,12 @@ class RelatedIssuesStore {
this.state.relatedIssues = convertObjectPropsToCamelCase(issues, { deep: true }); this.state.relatedIssues = convertObjectPropsToCamelCase(issues, { deep: true });
} }
removeRelatedIssue(idToRemove) { addRelatedIssues(issues = []) {
this.state.relatedIssues = this.state.relatedIssues.filter(issue => issue.id !== idToRemove); this.setRelatedIssues(this.state.relatedIssues.concat(issues));
}
removeRelatedIssue(issue) {
this.state.relatedIssues = this.state.relatedIssues.filter(x => x.id !== issue.id);
} }
updateIssueOrder(oldIndex, newIndex) { updateIssueOrder(oldIndex, newIndex) {
...@@ -31,6 +35,11 @@ class RelatedIssuesStore { ...@@ -31,6 +35,11 @@ class RelatedIssuesStore {
this.state.pendingReferences = issues.filter((ref, idx) => issues.indexOf(ref) === idx); this.state.pendingReferences = issues.filter((ref, idx) => issues.indexOf(ref) === idx);
} }
addPendingReferences(references = []) {
const issues = this.state.pendingReferences.concat(references);
this.setPendingReferences(issues);
}
removePendingRelatedIssue(indexToRemove) { removePendingRelatedIssue(indexToRemove) {
this.state.pendingReferences = this.state.pendingReferences.filter( this.state.pendingReferences = this.state.pendingReferences.filter(
(reference, index) => index !== indexToRemove, (reference, index) => index !== indexToRemove,
......
...@@ -145,7 +145,7 @@ describe('AddIssuableForm', () => { ...@@ -145,7 +145,7 @@ describe('AddIssuableForm', () => {
wrapper = mount(AddIssuableForm, { wrapper = mount(AddIssuableForm, {
propsData: { propsData: {
inputValue: '', inputValue: '',
isLinkedIssueBlock: true, showCategorizedIssues: true,
issuableType: issuableTypesMap.ISSUE, issuableType: issuableTypesMap.ISSUE,
pathIdSeparator, pathIdSeparator,
pendingReferences: [], pendingReferences: [],
......
import { mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import RelatedIssuesBlock from 'ee/related_issues/components/related_issues_block.vue'; import RelatedIssuesBlock from 'ee/related_issues/components/related_issues_block.vue';
import { import {
issuable1, issuable1,
...@@ -29,7 +29,7 @@ describe('RelatedIssuesBlock', () => { ...@@ -29,7 +29,7 @@ describe('RelatedIssuesBlock', () => {
}); });
it('displays "Linked issues" in the header', () => { it('displays "Linked issues" in the header', () => {
expect(wrapper.find('h3').text()).toContain('Linked issues'); expect(wrapper.find('.card-title').text()).toContain('Linked issues');
}); });
it('unable to add new related issues', () => { it('unable to add new related issues', () => {
...@@ -41,6 +41,22 @@ describe('RelatedIssuesBlock', () => { ...@@ -41,6 +41,22 @@ describe('RelatedIssuesBlock', () => {
}); });
}); });
describe('with headerText slot', () => {
it('displays header text slot data', () => {
const headerText = '<div>custom header text</div>';
wrapper = shallowMount(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
},
slots: { headerText },
});
expect(wrapper.find('.card-title').html()).toContain(headerText);
});
});
describe('with isFetching=true', () => { describe('with isFetching=true', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mount(RelatedIssuesBlock, { wrapper = mount(RelatedIssuesBlock, {
...@@ -89,41 +105,57 @@ describe('RelatedIssuesBlock', () => { ...@@ -89,41 +105,57 @@ describe('RelatedIssuesBlock', () => {
}); });
}); });
describe('with relatedIssues', () => { describe('showCategorizedIssues prop', () => {
let categorizedHeadings; const issueList = () => wrapper.findAll('.js-related-issues-token-list-item');
const categorizedHeadings = () => wrapper.findAll('h4');
beforeEach(() => { const headingTextAt = index =>
categorizedHeadings()
.at(index)
.text();
const mountComponent = showCategorizedIssues => {
wrapper = mount(RelatedIssuesBlock, { wrapper = mount(RelatedIssuesBlock, {
propsData: { propsData: {
pathIdSeparator: PathIdSeparator.Issue, pathIdSeparator: PathIdSeparator.Issue,
relatedIssues: [issuable1, issuable2, issuable3], relatedIssues: [issuable1, issuable2, issuable3],
issuableType: 'issue', issuableType: 'issue',
showCategorizedIssues,
}, },
}); });
};
categorizedHeadings = wrapper.findAll('h4'); describe('when showCategorizedIssues=true', () => {
}); beforeEach(() => mountComponent(true));
it('should render issue tokens items', () => { it('should render issue tokens items', () => {
expect(wrapper.findAll('.js-related-issues-token-list-item')).toHaveLength(3); expect(issueList()).toHaveLength(3);
}); });
it('shows "Blocks" heading', () => { it('shows "Blocks" heading', () => {
const blocks = linkedIssueTypesTextMap[linkedIssueTypesMap.BLOCKS]; const blocks = linkedIssueTypesTextMap[linkedIssueTypesMap.BLOCKS];
expect(categorizedHeadings.at(0).text()).toBe(blocks); expect(headingTextAt(0)).toBe(blocks);
}); });
it('shows "Is blocked by" heading', () => { it('shows "Is blocked by" heading', () => {
const isBlockedBy = linkedIssueTypesTextMap[linkedIssueTypesMap.IS_BLOCKED_BY]; const isBlockedBy = linkedIssueTypesTextMap[linkedIssueTypesMap.IS_BLOCKED_BY];
expect(categorizedHeadings.at(1).text()).toBe(isBlockedBy); expect(headingTextAt(1)).toBe(isBlockedBy);
}); });
it('shows "Relates to" heading', () => { it('shows "Relates to" heading', () => {
const relatesTo = linkedIssueTypesTextMap[linkedIssueTypesMap.RELATES_TO]; const relatesTo = linkedIssueTypesTextMap[linkedIssueTypesMap.RELATES_TO];
expect(categorizedHeadings.at(2).text()).toBe(relatesTo); expect(headingTextAt(2)).toBe(relatesTo);
});
});
describe('when showCategorizedIssues=false', () => {
it('should render issues as a flat list with no header', () => {
mountComponent(false);
expect(issueList()).toHaveLength(3);
expect(categorizedHeadings()).toHaveLength(0);
});
}); });
}); });
......
...@@ -20,7 +20,7 @@ describe('RelatedIssuesStore', () => { ...@@ -20,7 +20,7 @@ describe('RelatedIssuesStore', () => {
expect(store.state.relatedIssues).toEqual([]); expect(store.state.relatedIssues).toEqual([]);
}); });
it('add issue', () => { it('sets issues', () => {
const relatedIssues = [issuable1]; const relatedIssues = [issuable1];
store.setRelatedIssues(relatedIssues); store.setRelatedIssues(relatedIssues);
...@@ -28,21 +28,28 @@ describe('RelatedIssuesStore', () => { ...@@ -28,21 +28,28 @@ describe('RelatedIssuesStore', () => {
}); });
}); });
describe('addRelatedIssues', () => {
it('adds related issues', () => {
store.state.relatedIssues = [issuable1];
store.addRelatedIssues([issuable2, issuable3]);
expect(store.state.relatedIssues).toEqual([issuable1, issuable2, issuable3]);
});
});
describe('removeRelatedIssue', () => { describe('removeRelatedIssue', () => {
it('remove issue', () => { it('removes issue', () => {
const relatedIssues = [issuable1]; store.state.relatedIssues = [issuable1];
store.state.relatedIssues = relatedIssues;
store.removeRelatedIssue(issuable1.id); store.removeRelatedIssue(issuable1);
expect(store.state.relatedIssues).toEqual([]); expect(store.state.relatedIssues).toEqual([]);
}); });
it('remove issue with multiple in store', () => { it('removes issue with multiple in store', () => {
const relatedIssues = [issuable1, issuable2]; store.state.relatedIssues = [issuable1, issuable2];
store.state.relatedIssues = relatedIssues;
store.removeRelatedIssue(issuable1.id); store.removeRelatedIssue(issuable1);
expect(store.state.relatedIssues).toEqual([issuable2]); expect(store.state.relatedIssues).toEqual([issuable2]);
}); });
...@@ -50,8 +57,7 @@ describe('RelatedIssuesStore', () => { ...@@ -50,8 +57,7 @@ describe('RelatedIssuesStore', () => {
describe('updateIssueOrder', () => { describe('updateIssueOrder', () => {
it('updates issue order', () => { it('updates issue order', () => {
const relatedIssues = [issuable1, issuable2, issuable3, issuable4, issuable5]; store.state.relatedIssues = [issuable1, issuable2, issuable3, issuable4, issuable5];
store.state.relatedIssues = relatedIssues;
expect(store.state.relatedIssues[3].id).toBe(issuable4.id); expect(store.state.relatedIssues[3].id).toBe(issuable4.id);
store.updateIssueOrder(3, 0); store.updateIssueOrder(3, 0);
...@@ -65,7 +71,7 @@ describe('RelatedIssuesStore', () => { ...@@ -65,7 +71,7 @@ describe('RelatedIssuesStore', () => {
expect(store.state.pendingReferences).toEqual([]); expect(store.state.pendingReferences).toEqual([]);
}); });
it('add reference', () => { it('sets pending references', () => {
const relatedIssues = [issuable1.reference]; const relatedIssues = [issuable1.reference];
store.setPendingReferences(relatedIssues); store.setPendingReferences(relatedIssues);
...@@ -73,19 +79,30 @@ describe('RelatedIssuesStore', () => { ...@@ -73,19 +79,30 @@ describe('RelatedIssuesStore', () => {
}); });
}); });
describe('addPendingReferences', () => {
it('adds a reference', () => {
store.state.pendingReferences = [issuable1.reference];
store.addPendingReferences([issuable2.reference, issuable3.reference]);
expect(store.state.pendingReferences).toEqual([
issuable1.reference,
issuable2.reference,
issuable3.reference,
]);
});
});
describe('removePendingRelatedIssue', () => { describe('removePendingRelatedIssue', () => {
it('remove issue', () => { it('removes issue', () => {
const relatedIssues = [issuable1.reference]; store.state.pendingReferences = [issuable1.reference];
store.state.pendingReferences = relatedIssues;
store.removePendingRelatedIssue(0); store.removePendingRelatedIssue(0);
expect(store.state.pendingReferences).toEqual([]); expect(store.state.pendingReferences).toEqual([]);
}); });
it('remove issue with multiple in store', () => { it('removes issue with multiple in store', () => {
const relatedIssues = [issuable1.reference, issuable2.reference]; store.state.pendingReferences = [issuable1.reference, issuable2.reference];
store.state.pendingReferences = relatedIssues;
store.removePendingRelatedIssue(0); store.removePendingRelatedIssue(0);
......
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