Commit 7940ba70 authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '9424-make-related-issues-component-more-extendable' into 'master'

Make related issues component more extendable

See merge request gitlab-org/gitlab!35598
parents 57ecf75d 2d35478c
...@@ -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() {
return Object.values(linkedIssueTypesMap) if (this.showCategorizedIssues) {
.map(linkType => ({ return Object.values(linkedIssueTypesMap)
linkType, .map(linkType => ({
issues: this.relatedIssues.filter(issue => issue.linkType === 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', () => {
const relatesTo = linkedIssueTypesTextMap[linkedIssueTypesMap.RELATES_TO];
expect(headingTextAt(2)).toBe(relatesTo);
});
}); });
it('shows "Relates to" heading', () => { describe('when showCategorizedIssues=false', () => {
const relatesTo = linkedIssueTypesTextMap[linkedIssueTypesMap.RELATES_TO]; it('should render issues as a flat list with no header', () => {
mountComponent(false);
expect(categorizedHeadings.at(2).text()).toBe(relatesTo); 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