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