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 {
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() {
return Object.values(linkedIssueTypesMap)
.map(linkType => ({
linkType,
issues: this.relatedIssues.filter(issue => issue.linkType === linkType),
}))
.filter(obj => obj.issues.length > 0);
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);
});
it('should render issue tokens items', () => {
expect(issueList()).toHaveLength(3);
});
it('shows "Blocks" heading', () => {
const blocks = linkedIssueTypesTextMap[linkedIssueTypesMap.BLOCKS];
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];
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(headingTextAt(2)).toBe(relatesTo);
});
});
it('shows "Relates to" heading', () => {
const relatesTo = linkedIssueTypesTextMap[linkedIssueTypesMap.RELATES_TO];
describe('when showCategorizedIssues=false', () => {
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', () => {
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