Commit 60432cec authored by Phil Hughes's avatar Phil Hughes

Merge branch 'ce-10795-add-epic-tree' into 'master'

CE Backport: Show tree within Epic containing child Epics and Issues

See merge request gitlab-org/gitlab-ce!28787
parents 4bb63a8c 39a27b7c
<script>
import { GlButton } from '@gitlab/ui';
import Icon from './icon.vue';
export default {
components: {
Icon,
GlButton,
},
props: {
size: {
type: String,
required: false,
default: '',
},
primaryButtonClass: {
type: String,
required: false,
default: '',
},
dropdownClass: {
type: String,
required: false,
default: '',
},
actions: {
type: Array,
required: true,
},
defaultAction: {
type: Number,
required: true,
},
},
data() {
return {
selectedAction: this.defaultAction,
};
},
computed: {
selectedActionTitle() {
return this.actions[this.selectedAction].title;
},
buttonSizeClass() {
return `btn-${this.size}`;
},
},
methods: {
handlePrimaryActionClick() {
this.$emit('onActionClick', this.actions[this.selectedAction]);
},
handleActionClick(selectedAction) {
this.selectedAction = selectedAction;
this.$emit('onActionSelect', selectedAction);
},
},
};
</script>
<template>
<div class="btn-group droplab-dropdown comment-type-dropdown">
<gl-button :class="primaryButtonClass" :size="size" @click.prevent="handlePrimaryActionClick">
{{ selectedActionTitle }}
</gl-button>
<button
:class="buttonSizeClass"
type="button"
class="btn dropdown-toggle pl-2 pr-2"
data-display="static"
data-toggle="dropdown"
>
<icon name="arrow-down" aria-label="toggle dropdown" />
</button>
<ul :class="dropdownClass" class="dropdown-menu dropdown-open-top">
<template v-for="(action, index) in actions">
<li :key="index" :class="{ 'droplab-item-selected': selectedAction === index }">
<gl-button class="btn-transparent" @click.prevent="handleActionClick(index)">
<i aria-hidden="true" class="fa fa-check icon"> </i>
<div class="description">
<strong>{{ action.title }}</strong>
<p>{{ action.description }}</p>
</div>
</gl-button>
</li>
<li v-if="index === 0" :key="`${index}-separator`" class="divider droplab-item-ignore"></li>
</template>
</ul>
</div>
</template>
...@@ -62,6 +62,15 @@ export default { ...@@ -62,6 +62,15 @@ export default {
assigneeName: assignee.name, assigneeName: assignee.name,
}); });
}, },
// This method is for backward compat
// since Graph query would return camelCase
// props while Rails would return snake_case
webUrl(assignee) {
return assignee.web_url || assignee.webUrl;
},
avatarUrl(assignee) {
return assignee.avatar_url || assignee.avatarUrl;
},
}, },
}; };
</script> </script>
...@@ -70,9 +79,9 @@ export default { ...@@ -70,9 +79,9 @@ export default {
<user-avatar-link <user-avatar-link
v-for="assignee in assigneesToShow" v-for="assignee in assigneesToShow"
:key="assignee.id" :key="assignee.id"
:link-href="assignee.web_url" :link-href="webUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)" :img-alt="avatarUrlTitle(assignee)"
:img-src="assignee.avatar_url" :img-src="avatarUrl(assignee)"
:img-size="24" :img-size="24"
class="js-no-trigger" class="js-no-trigger"
tooltip-placement="bottom" tooltip-placement="bottom"
......
...@@ -19,10 +19,14 @@ export default { ...@@ -19,10 +19,14 @@ export default {
}, },
computed: { computed: {
milestoneDue() { milestoneDue() {
return this.milestone.due_date ? parsePikadayDate(this.milestone.due_date) : null; const dueDate = this.milestone.due_date || this.milestone.dueDate;
return dueDate ? parsePikadayDate(dueDate) : null;
}, },
milestoneStart() { milestoneStart() {
return this.milestone.start_date ? parsePikadayDate(this.milestone.start_date) : null; const startDate = this.milestone.start_date || this.milestone.startDate;
return startDate ? parsePikadayDate(startDate) : null;
}, },
isMilestoneStarted() { isMilestoneStarted() {
if (!this.milestoneStart) { if (!this.milestoneStart) {
......
import { mount, createLocalVue } from '@vue/test-utils';
import DroplabDropdownButton from '~/vue_shared/components/droplab_dropdown_button.vue';
const mockActions = [
{
title: 'Foo',
description: 'Some foo action',
},
{
title: 'Bar',
description: 'Some bar action',
},
];
const createComponent = ({
size = '',
dropdownClass = '',
actions = mockActions,
defaultAction = 0,
}) => {
const localVue = createLocalVue();
return mount(DroplabDropdownButton, {
localVue,
propsData: {
size,
dropdownClass,
actions,
defaultAction,
},
});
};
describe('DroplabDropdownButton', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent({});
});
afterEach(() => {
wrapper.destroy();
});
describe('data', () => {
it('contains `selectedAction` representing value of `defaultAction` prop', () => {
expect(wrapper.vm.selectedAction).toBe(0);
});
});
describe('computed', () => {
describe('selectedActionTitle', () => {
it('returns string containing title of selected action', () => {
wrapper.setData({ selectedAction: 0 });
expect(wrapper.vm.selectedActionTitle).toBe(mockActions[0].title);
wrapper.setData({ selectedAction: 1 });
expect(wrapper.vm.selectedActionTitle).toBe(mockActions[1].title);
});
});
describe('buttonSizeClass', () => {
it('returns string containing button sizing class based on `size` prop', done => {
const wrapperWithSize = createComponent({
size: 'sm',
});
wrapperWithSize.vm.$nextTick(() => {
expect(wrapperWithSize.vm.buttonSizeClass).toBe('btn-sm');
done();
wrapperWithSize.destroy();
});
});
});
});
describe('methods', () => {
describe('handlePrimaryActionClick', () => {
it('emits `onActionClick` event on component with selectedAction object as param', () => {
jest.spyOn(wrapper.vm, '$emit');
wrapper.setData({ selectedAction: 0 });
wrapper.vm.handlePrimaryActionClick();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onActionClick', mockActions[0]);
});
});
describe('handleActionClick', () => {
it('emits `onActionSelect` event on component with selectedAction index as param', () => {
jest.spyOn(wrapper.vm, '$emit');
wrapper.vm.handleActionClick(1);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onActionSelect', 1);
});
});
});
describe('template', () => {
it('renders default action button', () => {
const defaultButton = wrapper.findAll('.btn').at(0);
expect(defaultButton.text()).toBe(mockActions[0].title);
});
it('renders dropdown button', () => {
const dropdownButton = wrapper.findAll('.dropdown-toggle').at(0);
expect(dropdownButton.isVisible()).toBe(true);
});
it('renders dropdown actions', () => {
const dropdownActions = wrapper.findAll('.dropdown-menu li button');
Array(dropdownActions.length)
.fill()
.forEach((_, index) => {
const actionContent = dropdownActions.at(index).find('.description');
expect(actionContent.find('strong').text()).toBe(mockActions[index].title);
expect(actionContent.find('p').text()).toBe(mockActions[index].description);
});
});
it('renders divider between dropdown actions', () => {
const dropdownDivider = wrapper.find('.dropdown-menu .divider');
expect(dropdownDivider.isVisible()).toBe(true);
});
});
});
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