Commit 663d2e43 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'related-mrs' into 'master'

Make related issues components reusable

See merge request gitlab-org/gitlab-ee!9730
parents 80486c50 b7302be0
...@@ -3,22 +3,18 @@ import { GlTooltipDirective } from '@gitlab/ui'; ...@@ -3,22 +3,18 @@ import { GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue'; import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import IssueDueDate from '~/boards/components/issue_due_date.vue'; import relatedIssuableMixin from '~/vue_shared/mixins/related_issuable_mixin';
import IssueWeight from 'ee/boards/components/issue_card_weight.vue';
import relatedIssueMixin from '../mixins/related_issues_mixin';
export default { export default {
name: 'IssueItem', name: 'IssueItem',
components: { components: {
IssueMilestone, IssueMilestone,
IssueDueDate,
IssueAssignees, IssueAssignees,
IssueWeight,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [relatedIssueMixin], mixins: [relatedIssuableMixin],
props: { props: {
canReorder: { canReorder: {
type: Boolean, type: Boolean,
...@@ -93,18 +89,8 @@ export default { ...@@ -93,18 +89,8 @@ export default {
:milestone="milestone" :milestone="milestone"
class="d-flex align-items-center item-milestone" class="d-flex align-items-center item-milestone"
/> />
<issue-due-date <slot name="dueDate"></slot>
v-if="dueDate" <slot name="weight"></slot>
:date="dueDate"
tooltip-placement="top"
css-class="item-due-date d-flex align-items-center"
/>
<issue-weight
v-if="weight"
:weight="weight"
class="item-weight d-flex align-items-center"
tag-name="span"
/>
</div> </div>
<issue-assignees <issue-assignees
v-if="assignees.length" v-if="assignees.length"
......
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
import relatedIssueMixin from '../mixins/related_issues_mixin'; import relatedIssuableMixin from '~/vue_shared/mixins/related_issuable_mixin';
export default { export default {
name: 'IssueToken', name: 'IssueToken',
mixins: [relatedIssueMixin], mixins: [relatedIssuableMixin],
props: { props: {
isCondensed: { isCondensed: {
type: Boolean, type: Boolean,
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
}" }"
class="js-issue-token-title" class="js-issue-token-title"
> >
<span class="issue-token-title-text"> {{ title }} </span> <span class="issue-token-title-text">{{ title }}</span>
</component> </component>
<component <component
:is="innerComponentType" :is="innerComponentType"
...@@ -98,7 +98,7 @@ export default { ...@@ -98,7 +98,7 @@ export default {
class="js-issue-token-remove-button" class="js-issue-token-remove-button"
@click="onRemoveRequest" @click="onRemoveRequest"
> >
<i class="fa fa-times" aria-hidden="true"> </i> <i class="fa fa-times" aria-hidden="true"></i>
</button> </button>
</div> </div>
</template> </template>
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import IssueWeight from 'ee/boards/components/issue_card_weight.vue';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import sortableConfig from 'ee/sortable/sortable_config'; import sortableConfig from 'ee/sortable/sortable_config';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import issueItem from './issue_item.vue'; import AddIssuableForm from './add_issuable_form.vue';
import addIssuableForm from './add_issuable_form.vue';
import { issuableIconMap, issuableQaClassMap } from '../constants'; import { issuableIconMap, issuableQaClassMap } from '../constants';
export default { export default {
...@@ -15,9 +17,11 @@ export default { ...@@ -15,9 +17,11 @@ export default {
}, },
components: { components: {
Icon, Icon,
addIssuableForm, AddIssuableForm,
issueItem, RelatedIssuableItem,
GlLoadingIcon, GlLoadingIcon,
IssueWeight,
IssueDueDate,
}, },
props: { props: {
isFetching: { isFetching: {
...@@ -171,7 +175,8 @@ export default { ...@@ -171,7 +175,8 @@ export default {
class="js-related-issues-header-issue-count related-issues-header-issue-count issue-count-badge mx-1" class="js-related-issues-header-issue-count related-issues-header-issue-count issue-count-badge mx-1"
> >
<span class="issue-count-badge-count"> <span class="issue-count-badge-count">
<icon :name="issuableTypeIcon" class="mr-1 text-secondary" /> {{ badgeLabel }} <icon :name="issuableTypeIcon" class="mr-1 text-secondary" />
{{ badgeLabel }}
</span> </span>
</div> </div>
<button <button
...@@ -237,7 +242,7 @@ export default { ...@@ -237,7 +242,7 @@ export default {
:data-ordering-id="issuableOrderingId(issue)" :data-ordering-id="issuableOrderingId(issue)"
class="js-related-issues-token-list-item list-item pt-0 pb-0" class="js-related-issues-token-list-item list-item pt-0 pb-0"
> >
<issue-item <related-issuable-item
:id-key="issue.id" :id-key="issue.id"
:display-reference="issue.reference" :display-reference="issue.reference"
:confidential="issue.confidential" :confidential="issue.confidential"
...@@ -245,9 +250,7 @@ export default { ...@@ -245,9 +250,7 @@ export default {
:path="issue.path" :path="issue.path"
:state="issue.state" :state="issue.state"
:milestone="issue.milestone" :milestone="issue.milestone"
:due-date="issue.due_date"
:assignees="issue.assignees" :assignees="issue.assignees"
:weight="issue.weight"
:created-at="issue.created_at" :created-at="issue.created_at"
:closed-at="issue.closed_at" :closed-at="issue.closed_at"
:can-remove="canAdmin" :can-remove="canAdmin"
...@@ -255,7 +258,22 @@ export default { ...@@ -255,7 +258,22 @@ export default {
:path-id-separator="pathIdSeparator" :path-id-separator="pathIdSeparator"
event-namespace="relatedIssue" event-namespace="relatedIssue"
@relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)" @relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
>
<issue-weight
v-if="issue.weight"
slot="weight"
:weight="issue.weight"
class="item-weight d-flex align-items-center"
tag-name="span"
/>
<issue-due-date
v-if="issue.due_date"
slot="dueDate"
:date="issue.due_date"
tooltip-placement="top"
css-class="item-due-date d-flex align-items-center"
/> />
</related-issuable-item>
</li> </li>
</ul> </ul>
</div> </div>
......
---
title: Make related issues components reusable
merge_request: 9730
author:
type: other
import Vue from 'vue'; import Vue from 'vue';
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, issuable2, issuable3, issuable4, issuable5 } from '../mock_data'; issuable1,
issuable2,
issuable3,
issuable4,
issuable5,
} from 'spec/vue_shared/components/issue/related_issuable_mock_data';
describe('RelatedIssuesBlock', () => { describe('RelatedIssuesBlock', () => {
let RelatedIssuesBlock; let RelatedIssuesBlock;
......
...@@ -2,8 +2,11 @@ import Vue from 'vue'; ...@@ -2,8 +2,11 @@ import Vue from 'vue';
import _ from 'underscore'; import _ from 'underscore';
import relatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue'; import relatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue';
import relatedIssuesService from 'ee/related_issues/services/related_issues_service'; import relatedIssuesService from 'ee/related_issues/services/related_issues_service';
import {
import { defaultProps, issuable1, issuable2 } from '../mock_data'; defaultProps,
issuable1,
issuable2,
} from 'spec/vue_shared/components/issue/related_issuable_mock_data';
describe('RelatedIssuesRoot', () => { describe('RelatedIssuesRoot', () => {
let RelatedIssuesRoot; let RelatedIssuesRoot;
......
import RelatedIssuesStore from 'ee/related_issues/stores/related_issues_store'; import RelatedIssuesStore from 'ee/related_issues/stores/related_issues_store';
import { issuable1, issuable2, issuable3, issuable4, issuable5 } from '../mock_data'; import {
issuable1,
issuable2,
issuable3,
issuable4,
issuable5,
} from 'spec/vue_shared/components/issue/related_issuable_mock_data';
describe('RelatedIssuesStore', () => { describe('RelatedIssuesStore', () => {
let store; let store;
......
...@@ -17,7 +17,7 @@ module QA ...@@ -17,7 +17,7 @@ module QA
element :add_issue_button element :add_issue_button
end end
view 'ee/app/assets/javascripts/related_issues/components/issue_item.vue' do view 'app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue' do
element :remove_issue_button element :remove_issue_button
end end
......
import Vue from 'vue'; import Vue from 'vue';
import issueItem from 'ee/related_issues/components/issue_item.vue'; import { mount, createLocalVue } from '@vue/test-utils';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import { defaultMilestone, defaultAssignees } from '../mock_data'; import { defaultMilestone, defaultAssignees } from './related_issuable_mock_data';
describe('issueItem', () => { describe('RelatedIssuableItem', () => {
let vm; let wrapper;
const props = { const props = {
idKey: 1, idKey: 1,
displayReference: 'gitlab-org/gitlab-test#1', displayReference: 'gitlab-org/gitlab-test#1',
...@@ -19,38 +19,50 @@ describe('issueItem', () => { ...@@ -19,38 +19,50 @@ describe('issueItem', () => {
assignees: defaultAssignees, assignees: defaultAssignees,
eventNamespace: 'relatedIssue', eventNamespace: 'relatedIssue',
}; };
const slots = {
dueDate: '<div class="js-due-date-slot"></div>',
weight: '<div class="js-weight-slot"></div>',
};
beforeEach(() => { beforeEach(() => {
const IssueItem = Vue.extend(issueItem); const localVue = createLocalVue();
vm = mountComponent(IssueItem, props);
wrapper = mount(localVue.extend(RelatedIssuableItem), {
localVue,
slots,
sync: false,
propsData: props,
});
});
afterEach(() => {
wrapper.destroy();
}); });
it('contains issuable-info-container class when canReorder is false', () => { it('contains issuable-info-container class when canReorder is false', () => {
expect(vm.canReorder).toEqual(false); expect(wrapper.props('canReorder')).toBe(false);
expect(vm.$el.querySelector('.issuable-info-container')).toBeNull(); expect(wrapper.find('.issuable-info-container').exists()).toBe(true);
}); });
it('does not render token state', () => { it('does not render token state', () => {
expect(vm.$el.querySelector('.text-secondary svg')).toBeNull(); expect(wrapper.find('.text-secondary svg').exists()).toBe(false);
}); });
it('does not render remove button', () => { it('does not render remove button', () => {
expect(vm.$refs.removeButton).toBeUndefined(); expect(wrapper.find({ ref: 'removeButton' }).exists()).toBe(false);
}); });
describe('token title', () => { describe('token title', () => {
it('links to computedPath', () => { it('links to computedPath', () => {
expect(vm.$el.querySelector('.item-title a').href).toEqual(props.path); expect(wrapper.find('.item-title a').attributes('href')).toEqual(wrapper.props('path'));
}); });
it('renders confidential icon', () => { it('renders confidential icon', () => {
expect( expect(wrapper.find('.confidential-icon').exists()).toBe(true);
vm.$el.querySelector('.item-title svg.confidential-icon use').getAttribute('xlink:href'),
).toContain('eye-slash');
}); });
it('renders title', () => { it('renders title', () => {
expect(vm.$el.querySelector('.item-title a').innerText.trim()).toEqual(props.title); expect(wrapper.find('.item-title a').text()).toEqual(props.title);
}); });
}); });
...@@ -58,19 +70,21 @@ describe('issueItem', () => { ...@@ -58,19 +70,21 @@ describe('issueItem', () => {
let tokenState; let tokenState;
beforeEach(done => { beforeEach(done => {
vm.state = 'opened'; wrapper.setProps({ state: 'opened' });
Vue.nextTick(() => { Vue.nextTick(() => {
tokenState = vm.$el.querySelector('.item-meta svg'); tokenState = wrapper.find('.issue-token-state-icon-open');
done(); done();
}); });
}); });
it('renders if hasState', () => { it('renders if hasState', () => {
expect(tokenState).toBeDefined(); expect(tokenState.exists()).toBe(true);
}); });
it('renders state title', () => { it('renders state title', () => {
const stateTitle = tokenState.getAttribute('data-original-title').trim(); const stateTitle = tokenState.attributes('data-original-title');
expect(stateTitle).toContain('<span class="bold">Opened</span>'); expect(stateTitle).toContain('<span class="bold">Opened</span>');
expect(stateTitle).toContain( expect(stateTitle).toContain(
...@@ -79,19 +93,22 @@ describe('issueItem', () => { ...@@ -79,19 +93,22 @@ describe('issueItem', () => {
}); });
it('renders aria label', () => { it('renders aria label', () => {
expect(tokenState.getAttribute('aria-label')).toEqual('opened'); expect(tokenState.attributes('aria-label')).toEqual('opened');
}); });
it('renders open icon when open state', () => { it('renders open icon when open state', () => {
expect(tokenState.classList.contains('issue-token-state-icon-open')).toEqual(true); expect(tokenState.classes('issue-token-state-icon-open')).toBe(true);
}); });
it('renders close icon when close state', done => { it('renders close icon when close state', done => {
vm.state = 'closed'; wrapper.setProps({
vm.closedAt = '2018-12-01T00:00:00.00Z'; state: 'closed',
closedAt: '2018-12-01T00:00:00.00Z',
});
Vue.nextTick(() => { Vue.nextTick(() => {
expect(tokenState.classList.contains('issue-token-state-icon-closed')).toEqual(true); expect(tokenState.classes('issue-token-state-icon-closed')).toBe(true);
done(); done();
}); });
}); });
...@@ -102,49 +119,40 @@ describe('issueItem', () => { ...@@ -102,49 +119,40 @@ describe('issueItem', () => {
beforeEach(done => { beforeEach(done => {
Vue.nextTick(() => { Vue.nextTick(() => {
tokenMetadata = vm.$el.querySelector('.item-meta'); tokenMetadata = wrapper.find('.item-meta');
done(); done();
}); });
}); });
it('renders item path and ID', () => { it('renders item path and ID', () => {
const pathAndID = tokenMetadata.querySelector('.item-path-id').innerText.trim(); const pathAndID = tokenMetadata.find('.item-path-id').text();
expect(pathAndID).toContain('gitlab-org/gitlab-test'); expect(pathAndID).toContain('gitlab-org/gitlab-test');
expect(pathAndID).toContain('#1'); expect(pathAndID).toContain('#1');
}); });
it('renders milestone icon and name', () => { it('renders milestone icon and name', () => {
const milestoneIconEl = tokenMetadata.querySelector('.item-milestone svg use'); const milestoneIcon = tokenMetadata.find('.item-milestone svg use');
const milestoneTitle = tokenMetadata.querySelector('.item-milestone .milestone-title'); const milestoneTitle = tokenMetadata.find('.item-milestone .milestone-title');
expect(milestoneIconEl.getAttribute('xlink:href')).toContain('clock'); expect(milestoneIcon.attributes('href')).toContain('clock');
expect(milestoneTitle.innerText.trim()).toContain('Milestone title'); expect(milestoneTitle.text()).toContain('Milestone title');
}); });
it('renders date icon and due date', () => { it('renders due date component', () => {
const dueDateIconEl = tokenMetadata.querySelector('.item-due-date svg use'); expect(tokenMetadata.find('.js-due-date-slot').exists()).toBe(true);
const dueDateEl = tokenMetadata.querySelector('.item-due-date time');
expect(dueDateIconEl.getAttribute('xlink:href')).toContain('calendar');
expect(dueDateEl.innerText.trim()).toContain('Dec 31');
}); });
it('renders weight icon and value', () => { it('renders weight component', () => {
const dueDateIconEl = tokenMetadata.querySelector('.item-weight svg use'); expect(tokenMetadata.find('.js-weight-slot').exists()).toBe(true);
const dueDateEl = tokenMetadata.querySelector('.item-weight span');
expect(dueDateIconEl.getAttribute('xlink:href')).toContain('weight');
expect(dueDateEl.innerText.trim()).toContain('10');
}); });
}); });
describe('token assignees', () => { describe('token assignees', () => {
it('renders assignees avatars', () => { it('renders assignees avatars', () => {
const assigneesEl = vm.$el.querySelector('.item-assignees'); expect(wrapper.findAll('.item-assignees .user-avatar-link').length).toBe(2);
expect(wrapper.find('.item-assignees .avatar-counter').text()).toContain('+2');
expect(assigneesEl.querySelectorAll('.user-avatar-link').length).toBe(2);
expect(assigneesEl.querySelector('.avatar-counter').innerText.trim()).toContain('+2');
}); });
}); });
...@@ -152,30 +160,35 @@ describe('issueItem', () => { ...@@ -152,30 +160,35 @@ describe('issueItem', () => {
let removeBtn; let removeBtn;
beforeEach(done => { beforeEach(done => {
vm.canRemove = true; wrapper.setProps({ canRemove: true });
Vue.nextTick(() => { Vue.nextTick(() => {
removeBtn = vm.$refs.removeButton; removeBtn = wrapper.find({ ref: 'removeButton' });
done(); done();
}); });
}); });
it('renders if canRemove', () => { it('renders if canRemove', () => {
expect(removeBtn).toBeDefined(); expect(removeBtn.exists()).toBe(true);
}); });
it('renders disabled button when removeDisabled', done => { it('renders disabled button when removeDisabled', done => {
vm.removeDisabled = true; wrapper.vm.removeDisabled = true;
Vue.nextTick(() => { Vue.nextTick(() => {
expect(removeBtn.hasAttribute('disabled')).toEqual(true); expect(removeBtn.attributes('disabled')).toEqual('disabled');
done(); done();
}); });
}); });
it('triggers onRemoveRequest when clicked', () => { it('triggers onRemoveRequest when clicked', () => {
spyOn(vm, '$emit'); removeBtn.trigger('click');
removeBtn.click();
const { relatedIssueRemoveRequest } = wrapper.emitted();
expect(vm.$emit).toHaveBeenCalledWith('relatedIssueRemoveRequest', props.idKey); expect(relatedIssueRemoveRequest.length).toBe(1);
expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]);
}); });
}); });
}); });
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