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