Commit d2ad55fa authored by Kushal Pandya's avatar Kushal Pandya Committed by Jarka Košanová

Frontend for epic - epic relationship

Integrates frontend with API to enable
attaching epics to an epic
parent e70fc65a
......@@ -13,6 +13,7 @@ import { status, stateEvent } from '../../constants';
export default {
name: 'EpicShowApp',
epicsPathIdSeparator: '&',
components: {
epicHeader,
epicSidebar,
......@@ -44,6 +45,11 @@ export default {
required: true,
type: Boolean,
},
subepicsSupported: {
type: Boolean,
required: false,
default: true,
},
markdownPreviewPath: {
type: String,
required: true,
......@@ -150,9 +156,10 @@ export default {
type: Array,
required: true,
},
parentEpic: {
parent: {
type: Object,
required: true,
required: false,
default: () => ({}),
},
participants: {
type: Array,
......@@ -206,7 +213,9 @@ export default {
return {
// Epics specific configuration
issuableRef: '',
hasRelatedEpicsFeature: this.subepicsSupported && gon.features && gon.features.epicLinks,
projectPath: this.groupPath,
parentEpic: this.parent ? this.parent : {},
projectNamespace: '',
service: new EpicsService({
endpoint: this.endpoint,
......@@ -301,7 +310,7 @@ export default {
:initial-participants="participants"
:initial-subscribed="subscribed"
:initial-todo-exists="todoExists"
:parent-epic="parentEpic"
:parent="parentEpic"
:namespace="namespace"
:update-path="updateEndpoint"
:labels-path="labelsPath"
......@@ -312,18 +321,23 @@ export default {
:epics-web-url="epicsWebUrl"
/>
<related-issues-root
v-if="hasRelatedEpicsFeature"
:endpoint="epicLinksEndpoint"
:can-admin="canAdmin"
:can-reorder="canAdmin"
:allow-auto-complete="false"
title="Epics"
:path-id-separator="$options.epicsPathIdSeparator"
:title="__('Epics')"
css-class="js-related-epics-block"
/>
<related-issues-root
:endpoint="issueLinksEndpoint"
:can-admin="canAdmin"
:can-reorder="canAdmin"
:allow-auto-complete="false"
title="Issues"
:title="__('Issues')"
css-class="js-related-issues-block"
path-id-separator="#"
/>
</div>
</div>
......
......@@ -15,16 +15,6 @@ export default () => {
Cookies.set('collapsed_gutter', true);
}
// TODO remove once API provides proper data
initialData.epicLinksEndpoint = '/';
metaData.parentEpic = {
id: 7,
title: 'Epic with out of range end date',
url: '/groups/gitlab-org/-/epics/7',
human_readable_timestamp: '<strong>30</strong> days remaining',
human_readable_end_date: 'Dec 28, 2018',
};
const props = Object.assign({}, initialData, metaData, el.dataset);
return new Vue({
......
......@@ -118,7 +118,7 @@ export default {
type: Boolean,
required: true,
},
parentEpic: {
parent: {
type: Object,
required: true,
},
......@@ -468,7 +468,7 @@ export default {
<div class="issuable-sidebar js-issuable-update">
<div class="block issuable-sidebar-header">
<span class="issuable-header-text hide-collapsed float-left">{{ __('Todo') }}</span>
<toggle-sidebar :collapsed="collapsed" css-classes="float-right" @toggle="toggleSidebar"/>
<toggle-sidebar :collapsed="collapsed" css-classes="float-right" @toggle="toggleSidebar" />
<sidebar-todo
v-if="!collapsed"
:collapsed="collapsed"
......@@ -553,11 +553,12 @@ export default {
@onLabelClick="handleLabelClick"
@onDropdownClose="handleDropdownClose"
@toggleCollapse="toggleSidebarRevealLabelsDropdown"
>{{ __('None') }}</sidebar-labels-select>
>{{ __('None') }}</sidebar-labels-select
>
<div class="block parent-epic">
<sidebar-item-epic :block-title="__('Parent epic')" :initial-epic="parentEpic"/>
<sidebar-item-epic :block-title="__('Parent epic')" :initial-epic="parent" />
</div>
<sidebar-participants :participants="initialParticipants" @toggleCollapse="toggleSidebar"/>
<sidebar-participants :participants="initialParticipants" @toggleCollapse="toggleSidebar" />
<sidebar-subscriptions
:loading="savingSubscription"
:subscribed="store.subscribed"
......
......@@ -2,7 +2,6 @@
import $ from 'jquery';
import GfmAutoComplete from '~/gfm_auto_complete';
import { GlLoadingIcon } from '@gitlab/ui';
import eventHub from '../event_hub';
import issueToken from './issue_token.vue';
export default {
......@@ -60,7 +59,6 @@ export default {
mounted() {
const $input = $(this.$refs.input);
if (this.allowAutoComplete) {
this.gfmAutoComplete = new GfmAutoComplete(this.autoCompleteSources);
this.gfmAutoComplete.setup($input, {
......@@ -83,7 +81,10 @@ export default {
methods: {
onInput() {
const { value } = this.$refs.input;
eventHub.$emit('addIssuableFormInput', value, $(this.$refs.input).caret('pos'));
this.$emit('addIssuableFormInput', {
newValue: value,
caretPos: $(this.$refs.input).caret('pos'),
});
},
onFocus() {
this.isInputFocused = true;
......@@ -94,7 +95,7 @@ export default {
// Avoid tokenizing partial input when clicking an autocomplete item
if (!this.isAutoCompleteOpen) {
const { value } = this.$refs.input;
eventHub.$emit('addIssuableFormBlur', value);
this.$emit('addIssuableFormBlur', value);
}
},
onAutoCompleteToggled(isOpen) {
......@@ -103,12 +104,15 @@ export default {
onInputWrapperClick() {
this.$refs.input.focus();
},
onPendingIssuableRemoveRequest(params) {
this.$emit('pendingIssuableRemoveRequest', params);
},
onFormSubmit() {
const { value } = this.$refs.input;
eventHub.$emit('addIssuableFormSubmit', value);
this.$emit('addIssuableFormSubmit', value);
},
onFormCancel() {
eventHub.$emit('addIssuableFormCancel');
this.$emit('addIssuableFormCancel');
},
},
};
......@@ -141,6 +145,7 @@ export default {
:is-condensed="true"
:path-id-separator="pathIdSeparator"
event-namespace="pendingIssuable"
@pendingIssuableRemoveRequest="onPendingIssuableRemoveRequest"
/>
</li>
<li class="add-issuable-form-input-list-item">
......
......@@ -89,7 +89,7 @@ export default {
</div>
<div class="item-meta-child d-flex align-items-center">
<issue-milestone
v-if="milestone"
v-if="hasMilestone"
:milestone="milestone"
class="d-flex align-items-center item-milestone"
/>
......
......@@ -4,7 +4,6 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import sortableConfig from 'ee/sortable/sortable_config';
import { GlLoadingIcon } from '@gitlab/ui';
import eventHub from '../event_hub';
import issueItem from './issue_item.vue';
import addIssuableForm from './add_issuable_form.vue';
......@@ -62,8 +61,7 @@ export default {
},
pathIdSeparator: {
type: String,
required: false,
default: '#',
required: true,
},
helpPath: {
type: String,
......@@ -110,9 +108,6 @@ export default {
}
},
methods: {
toggleAddRelatedIssuesForm() {
eventHub.$emit('toggleAddRelatedIssuesForm');
},
getBeforeAfterId(itemEl) {
const prevItemEl = itemEl.previousElementSibling;
const nextItemEl = itemEl.nextElementSibling;
......@@ -172,7 +167,7 @@ export default {
class="js-issue-count-badge-add-button issue-count-badge-add-button btn btn-sm btn-default qa-add-issues-button"
aria-label="Add an issue"
data-placement="top"
@click="toggleAddRelatedIssuesForm"
@click="$emit('toggleAddRelatedIssuesForm', $event);"
>
<i class="fa fa-plus" aria-hidden="true"></i>
</button>
......@@ -192,6 +187,11 @@ export default {
:pending-references="pendingReferences"
:auto-complete-sources="autoCompleteSources"
:path-id-separator="pathIdSeparator"
@pendingIssuableRemoveRequest="$emit('pendingIssuableRemoveRequest', $event);"
@addIssuableFormInput="$emit('addIssuableFormInput', $event);"
@addIssuableFormBlur="$emit('addIssuableFormBlur', $event);"
@addIssuableFormSubmit="$emit('addIssuableFormSubmit', $event);"
@addIssuableFormCancel="$emit('addIssuableFormCancel', $event);"
/>
</div>
<div
......@@ -238,6 +238,7 @@ export default {
:can-reorder="canReorder"
:path-id-separator="pathIdSeparator"
event-namespace="relatedIssue"
@relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event);"
/>
</li>
</ul>
......
......@@ -25,7 +25,6 @@ Your caret can stop touching a `rawReference` can happen in a variety of ways:
*/
import _ from 'underscore';
import Flash from '~/flash';
import eventHub from '../event_hub';
import RelatedIssuesBlock from './related_issues_block.vue';
import RelatedIssuesStore from '../stores/related_issues_store';
import RelatedIssuesService from '../services/related_issues_service';
......@@ -67,6 +66,16 @@ export default {
required: false,
default: true,
},
pathIdSeparator: {
type: String,
required: false,
default: '#',
},
cssClass: {
type: String,
required: false,
default: '',
},
},
data() {
this.store = new RelatedIssuesStore();
......@@ -86,26 +95,9 @@ export default {
},
},
created() {
eventHub.$on('relatedIssue-removeRequest', this.onRelatedIssueRemoveRequest);
eventHub.$on('toggleAddRelatedIssuesForm', this.onToggleAddRelatedIssuesForm);
eventHub.$on('pendingIssuable-removeRequest', this.onPendingIssueRemoveRequest);
eventHub.$on('addIssuableFormSubmit', this.onPendingFormSubmit);
eventHub.$on('addIssuableFormCancel', this.onPendingFormCancel);
eventHub.$on('addIssuableFormInput', this.onInput);
eventHub.$on('addIssuableFormBlur', this.onBlur);
this.service = new RelatedIssuesService(this.endpoint);
this.fetchRelatedIssues();
},
beforeDestroy() {
eventHub.$off('relatedIssue-removeRequest', this.onRelatedIssueRemoveRequest);
eventHub.$off('toggleAddRelatedIssuesForm', this.onToggleAddRelatedIssuesForm);
eventHub.$off('pendingIssuable-removeRequest', this.onPendingIssueRemoveRequest);
eventHub.$off('addIssuableFormSubmit', this.onPendingFormSubmit);
eventHub.$off('addIssuableFormCancel', this.onPendingFormCancel);
eventHub.$off('addIssuableFormInput', this.onInput);
eventHub.$off('addIssuableFormBlur', this.onBlur);
},
methods: {
onRelatedIssueRemoveRequest(idToRemove) {
const issueToRemove = _.find(this.state.relatedIssues, issue => issue.id === idToRemove);
......@@ -198,7 +190,7 @@ export default {
});
}
},
onInput(newValue, caretPos) {
onInput({ newValue, caretPos }) {
const rawReferences = newValue.split(/\s/);
let touchedReference;
......@@ -235,6 +227,7 @@ export default {
<template>
<related-issues-block
:class="cssClass"
:help-path="helpPath"
:is-fetching="isFetching"
:is-submitting="isSubmitting"
......@@ -246,7 +239,14 @@ export default {
:input-value="inputValue"
:auto-complete-sources="autoCompleteSources"
:title="title"
path-id-separator="#"
:path-id-separator="pathIdSeparator"
@saveReorder="saveIssueOrder"
@toggleAddRelatedIssuesForm="onToggleAddRelatedIssuesForm"
@addIssuableFormInput="onInput"
@addIssuableFormBlur="onBlur"
@addIssuableFormSubmit="onPendingFormSubmit"
@addIssuableFormCancel="onPendingFormCancel"
@pendingIssuableRemoveRequest="onPendingIssueRemoveRequest"
@relatedIssueRemoveRequest="onRelatedIssueRemoveRequest"
/>
</template>
import Vue from 'vue';
export default new Vue();
import _ from 'underscore';
import { formatDate } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import eventHub from '../event_hub';
const mixins = {
data() {
......@@ -104,6 +104,9 @@ const mixins = {
hasTitle() {
return this.title.length > 0;
},
hasMilestone() {
return !_.isEmpty(this.milestone);
},
iconName() {
return this.isOpen ? 'issue-open-m' : 'issue-close';
},
......@@ -139,10 +142,10 @@ const mixins = {
onRemoveRequest() {
let namespacePrefix = '';
if (this.eventNamespace && this.eventNamespace.length > 0) {
namespacePrefix = `${this.eventNamespace}-`;
namespacePrefix = `${this.eventNamespace}`;
}
eventHub.$emit(`${namespacePrefix}removeRequest`, this.idKey);
this.$emit(`${namespacePrefix}RemoveRequest`, this.idKey);
this.removeDisabled = true;
},
......
......@@ -22,29 +22,24 @@ export default {
initialEpic: {
type: Object,
required: false,
default: () => {},
default: () => null,
},
},
data() {
let store = {};
if (!this.initialEpic) {
store = new Store();
}
return {
store,
store: !this.initialEpic ? new Store() : {},
};
},
computed: {
isLoading() {
return this.initialEpic ? false : this.store.isFetching.epic;
},
epicIcon() {
return spriteIcon('epic');
},
epic() {
return this.initialEpic || this.store.epic;
},
epicIcon() {
return spriteIcon('epic');
},
epicUrl() {
return this.epic.url;
},
......@@ -55,7 +50,7 @@ export default {
return this.epicUrl && this.epicTitle;
},
collapsedTitle() {
return this.hasEpic ? this.epicTitle : 'None';
return this.hasEpic ? this.epicTitle : __('None');
},
tooltipTitle() {
if (!this.hasEpic) {
......@@ -95,11 +90,11 @@ export default {
</div>
<div class="title hide-collapsed">
{{ blockTitle }}
<gl-loading-icon v-if="isLoading" :inline="true"/>
<gl-loading-icon v-if="isLoading" :inline="true" />
</div>
<div v-if="!isLoading" class="value hide-collapsed">
<a v-if="hasEpic" :href="epicUrl" class="bold">{{ epicTitle }}</a>
<span v-else class="no-value">None</span>
<span v-else class="no-value">{{ __('None') }}</span>
</div>
</div>
</template>
......@@ -32,7 +32,7 @@ describe 'Epic Issues', :js do
end
it 'user can see issues from public project but cannot delete the associations' do
within('.related-issues-block ul.related-items-list') do
within('.js-related-issues-block ul.related-items-list') do
expect(page).to have_selector('li', count: 1)
expect(page).to have_content(public_issue.title)
expect(page).not_to have_selector('button.js-issue-item-remove-button')
......@@ -40,11 +40,11 @@ describe 'Epic Issues', :js do
end
it 'user cannot add new issues to the epic' do
expect(page).not_to have_selector('.related-issues-block h3.card-title button')
expect(page).not_to have_selector('.js-related-issues-block h3.card-title button')
end
it 'user cannot reorder issues in epic' do
expect(page).not_to have_selector('.js-related-issues-token-list-item.user-can-drag')
expect(page).not_to have_selector('.js-related-issues-block .js-related-issues-token-list-item.user-can-drag')
end
end
......@@ -53,13 +53,13 @@ describe 'Epic Issues', :js do
let(:issue_invalid) { create(:issue) }
def add_issues(references)
find('.related-issues-block h3.card-title button').click
find('.js-add-issuable-form-input').set(references)
find('.js-related-issues-block h3.card-title button').click
find('.js-related-issues-block .js-add-issuable-form-input').set(references)
# When adding long references, for some reason the input gets stuck
# waiting for more text. Send a keystroke before clicking the button to
# get out of this mode.
find('.js-add-issuable-form-input').send_keys(:tab)
find('.js-add-issuable-form-add-button').click
find('.js-related-issues-block .js-add-issuable-form-input').send_keys(:tab)
find('.js-related-issues-block .js-add-issuable-form-add-button').click
wait_for_requests
end
......@@ -70,7 +70,7 @@ describe 'Epic Issues', :js do
end
it 'user can see all issues of the group and delete the associations' do
within('.related-issues-block ul.related-items-list') do
within('.js-related-issues-block ul.related-items-list') do
expect(page).to have_selector('li', count: 2)
expect(page).to have_content(public_issue.title)
expect(page).to have_content(private_issue.title)
......@@ -80,7 +80,7 @@ describe 'Epic Issues', :js do
wait_for_requests
within('.related-issues-block ul.related-items-list') do
within('.js-related-issues-block ul.related-items-list') do
expect(page).to have_selector('li', count: 1)
end
end
......@@ -100,20 +100,20 @@ describe 'Epic Issues', :js do
expect(page).not_to have_selector('.content-wrapper .alert-wrapper .flash-text')
expect(page).not_to have_content('No Issue found for given params')
within('.related-issues-block ul.related-items-list') do
within('.js-related-issues-block ul.related-items-list') do
expect(page).to have_selector('li', count: 3)
expect(page).to have_content(issue_to_add.title)
end
end
it 'user can reorder issues in epic' do
expect(first('.js-related-issues-token-list-item')).to have_content(public_issue.title)
expect(page.all('.js-related-issues-token-list-item').last).to have_content(private_issue.title)
expect(first('.js-related-issues-block .js-related-issues-token-list-item')).to have_content(public_issue.title)
expect(page.all('.js-related-issues-block .js-related-issues-token-list-item').last).to have_content(private_issue.title)
drag_to(selector: '.related-items-list', to_index: 1)
drag_to(selector: '.js-related-issues-block .related-items-list', to_index: 1)
expect(first('.js-related-issues-token-list-item')).to have_content(private_issue.title)
expect(page.all('.js-related-issues-token-list-item').last).to have_content(public_issue.title)
expect(first('.js-related-issues-block .js-related-issues-token-list-item')).to have_content(private_issue.title)
expect(page.all('.js-related-issues-block .js-related-issues-token-list-item').last).to have_content(public_issue.title)
end
end
end
......@@ -28,6 +28,10 @@ describe 'Epic in issue sidebar', :js do
context 'when epics available' do
before do
stub_licensed_features(epics: true)
sign_in(user)
visit project_issue_path(project, issue)
wait_for_requests
end
it_behaves_like 'epic in issue sidebar'
......
......@@ -40,6 +40,7 @@ export const contentProps = {
markdownPreviewPath: '',
markdownDocsPath: '',
issueLinksEndpoint: '/',
epicLinksEndpoint: '/',
groupPath: '',
namespace: 'gitlab-org',
labelsPath: '',
......@@ -71,6 +72,15 @@ export const contentProps = {
subscribed: true,
todoExists: false,
state: 'opened',
parent: {
id: 12,
startDateIsFixed: true,
startDate: '2018-12-01',
dueDateIsFixed: true,
dueDateFixed: '2019-12-31',
title: 'Sample Parent Epic',
url: `${gl.TEST_HOST}/groups/gitlab-org/-/epics/12`,
},
};
export const headerProps = {
......
......@@ -19,6 +19,7 @@ describe('epicSidebar', () => {
labelsWebUrl,
epicsWebUrl,
labels,
parent,
participants,
subscribed,
toggleSubscriptionPath,
......@@ -55,6 +56,7 @@ describe('epicSidebar', () => {
startDateSourcingMilestoneDates,
dueDateSourcingMilestoneTitle,
dueDateSourcingMilestoneDates,
parent,
toggleSubscriptionPath,
labelsPath,
labelsWebUrl,
......@@ -115,6 +117,47 @@ describe('epicSidebar', () => {
).toEqual('Jan 1, 2018');
});
describe('parent epic', () => {
it('should render parent epic information in sidebar when `parent` is present', () => {
const parentEpicEl = vm.$el.querySelector('.block.parent-epic');
expect(parentEpicEl).not.toBeNull();
expect(parentEpicEl.querySelector('.collapse-truncated-title').innerText.trim()).toBe(
parent.title,
);
expect(parentEpicEl.querySelector('.value a').innerText.trim()).toBe(parent.title);
});
it('should render parent epic as `none` when `parent` is empty', done => {
vm.parent = {};
Vue.nextTick()
.then(() => {
const parentEpicEl = vm.$el.querySelector('.block.parent-epic');
expect(parentEpicEl.querySelector('.collapse-truncated-title').innerText.trim()).toBe(
'None',
);
expect(parentEpicEl.querySelector('.value .no-value').innerText.trim()).toBe('None');
})
.then(done)
.catch(done.fail);
});
it('should render parent epic information icon when sidebar is collapsed', () => {
const parentEpicElCollapsed = vm.$el.querySelector(
'.block.parent-epic .sidebar-collapsed-icon',
);
expect(parentEpicElCollapsed).not.toBeNull();
expect(parentEpicElCollapsed.querySelector('svg use').getAttribute('xlink:href')).toContain(
'epic',
);
});
});
describe('computed prop', () => {
const getComponent = (
customPropsData = {
......
import $ from 'jquery';
import Vue from 'vue';
import eventHub from 'ee/related_issues/event_hub';
import addIssuableForm from 'ee/related_issues/components/add_issuable_form.vue';
const issuable1 = {
......@@ -167,21 +166,7 @@ describe('AddIssuableForm', () => {
});
describe('methods', () => {
let addIssuableFormInputSpy;
let addIssuableFormBlurSpy;
let addIssuableFormSubmitSpy;
let addIssuableFormCancelSpy;
beforeEach(() => {
addIssuableFormInputSpy = jasmine.createSpy('spy');
addIssuableFormBlurSpy = jasmine.createSpy('spy');
addIssuableFormSubmitSpy = jasmine.createSpy('spy');
addIssuableFormCancelSpy = jasmine.createSpy('spy');
eventHub.$on('addIssuableFormInput', addIssuableFormInputSpy);
eventHub.$on('addIssuableFormBlur', addIssuableFormBlurSpy);
eventHub.$on('addIssuableFormSubmit', addIssuableFormSubmitSpy);
eventHub.$on('addIssuableFormCancel', addIssuableFormCancelSpy);
const el = document.createElement('div');
// We need to append to body to get focus tests working
document.body.appendChild(el);
......@@ -198,13 +183,6 @@ describe('AddIssuableForm', () => {
}).$mount(el);
});
afterEach(() => {
eventHub.$off('addIssuableFormInput', addIssuableFormInputSpy);
eventHub.$off('addIssuableFormBlur', addIssuableFormBlurSpy);
eventHub.$off('addIssuableFormSubmit', addIssuableFormSubmitSpy);
eventHub.$off('addIssuableFormCancel', addIssuableFormCancelSpy);
});
it('when clicking somewhere on the input wrapper should focus the input', done => {
vm.onInputWrapperClick();
......@@ -219,18 +197,19 @@ describe('AddIssuableForm', () => {
});
it('when filling in the input', () => {
expect(addIssuableFormInputSpy).not.toHaveBeenCalled();
spyOn(vm, '$emit');
const newInputValue = 'filling in things';
vm.$refs.input.value = newInputValue;
vm.onInput();
expect(addIssuableFormInputSpy).toHaveBeenCalledWith(newInputValue, newInputValue.length);
expect(vm.$emit).toHaveBeenCalledWith('addIssuableFormInput', {
newValue: newInputValue,
caretPos: newInputValue.length,
});
});
it('when blurring the input', done => {
expect(addIssuableFormInputSpy).not.toHaveBeenCalled();
spyOn(vm, '$emit');
const newInputValue = 'filling in things';
vm.$refs.input.value = newInputValue;
vm.onBlur();
......@@ -238,7 +217,7 @@ describe('AddIssuableForm', () => {
setTimeout(() => {
Vue.nextTick(() => {
expect(vm.$refs.issuableFormWrapper.classList.contains('focus')).toEqual(false);
expect(addIssuableFormBlurSpy).toHaveBeenCalledWith(newInputValue);
expect(vm.$emit).toHaveBeenCalledWith('addIssuableFormBlur', newInputValue);
done();
});
......@@ -266,29 +245,25 @@ describe('AddIssuableForm', () => {
setTimeout(() => {
Vue.nextTick(() => {
expect(vm.$refs.input.value).toEqual('');
expect(addIssuableFormInputSpy.calls.count()).toEqual(1);
done();
});
});
});
it('when submitting pending issues', () => {
expect(addIssuableFormSubmitSpy).not.toHaveBeenCalled();
spyOn(vm, '$emit');
const newInputValue = 'filling in things';
vm.$refs.input.value = newInputValue;
vm.onFormSubmit();
expect(addIssuableFormSubmitSpy).toHaveBeenCalledWith(newInputValue);
expect(vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', newInputValue);
});
it('when canceling form to collapse', () => {
expect(addIssuableFormCancelSpy).not.toHaveBeenCalled();
spyOn(vm, '$emit');
vm.onFormCancel();
expect(addIssuableFormCancelSpy).toHaveBeenCalled();
expect(vm.$emit).toHaveBeenCalledWith('addIssuableFormCancel');
});
});
});
import Vue from 'vue';
import issueItem from 'ee/related_issues/components/issue_item.vue';
import eventHub from 'ee/related_issues/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { defaultMilestone, defaultAssignees } from '../mock_data';
......@@ -18,6 +17,7 @@ describe('issueItem', () => {
createdAt: '2018-12-01T00:00:00.00Z',
milestone: defaultMilestone,
assignees: defaultAssignees,
eventNamespace: 'relatedIssue',
};
beforeEach(() => {
......@@ -172,11 +172,10 @@ describe('issueItem', () => {
});
it('triggers onRemoveRequest when clicked', () => {
const spy = jasmine.createSpy('spy');
eventHub.$on('removeRequest', spy);
spyOn(vm, '$emit');
removeBtn.click();
expect(spy).toHaveBeenCalled();
expect(vm.$emit).toHaveBeenCalledWith('relatedIssueRemoveRequest', props.idKey);
});
});
});
import Vue from 'vue';
import eventHub from 'ee/related_issues/event_hub';
import issueToken from 'ee/related_issues/components/issue_token.vue';
describe('IssueToken', () => {
......@@ -7,6 +6,7 @@ describe('IssueToken', () => {
const displayReference = 'foo/bar#123';
const title = 'some title';
const pathIdSeparator = '#';
const eventNamespace = 'pendingIssuable';
let IssueToken;
let vm;
......@@ -25,6 +25,7 @@ describe('IssueToken', () => {
vm = new IssueToken({
propsData: {
idKey,
eventNamespace,
displayReference,
pathIdSeparator,
},
......@@ -46,6 +47,7 @@ describe('IssueToken', () => {
vm = new IssueToken({
propsData: {
idKey,
eventNamespace,
displayReference,
pathIdSeparator,
title,
......@@ -65,6 +67,7 @@ describe('IssueToken', () => {
vm = new IssueToken({
propsData: {
idKey,
eventNamespace,
displayReference,
pathIdSeparator,
title,
......@@ -84,6 +87,7 @@ describe('IssueToken', () => {
vm = new IssueToken({
propsData: {
idKey,
eventNamespace,
displayReference,
pathIdSeparator,
state: 'opened',
......@@ -101,6 +105,7 @@ describe('IssueToken', () => {
vm = new IssueToken({
propsData: {
idKey,
eventNamespace,
displayReference,
pathIdSeparator,
state: 'reopened',
......@@ -118,6 +123,7 @@ describe('IssueToken', () => {
vm = new IssueToken({
propsData: {
idKey,
eventNamespace,
displayReference,
pathIdSeparator,
state: 'closed',
......@@ -137,6 +143,7 @@ describe('IssueToken', () => {
vm = new IssueToken({
propsData: {
idKey,
eventNamespace,
displayReference,
pathIdSeparator,
title,
......@@ -160,6 +167,7 @@ describe('IssueToken', () => {
vm = new IssueToken({
propsData: {
idKey,
eventNamespace,
displayReference,
pathIdSeparator,
},
......@@ -176,6 +184,7 @@ describe('IssueToken', () => {
vm = new IssueToken({
propsData: {
idKey,
eventNamespace,
displayReference,
pathIdSeparator,
canRemove: true,
......@@ -190,29 +199,22 @@ describe('IssueToken', () => {
});
describe('methods', () => {
let removeRequestSpy;
beforeEach(() => {
vm = new IssueToken({
propsData: {
idKey,
eventNamespace,
displayReference,
pathIdSeparator,
},
}).$mount();
removeRequestSpy = jasmine.createSpy('spy');
eventHub.$on('removeRequest', removeRequestSpy);
});
afterEach(() => {
eventHub.$off('removeRequest', removeRequestSpy);
});
it('when getting checked', () => {
expect(removeRequestSpy).not.toHaveBeenCalled();
spyOn(vm, '$emit');
vm.onRemoveRequest();
expect(removeRequestSpy).toHaveBeenCalled();
expect(vm.$emit).toHaveBeenCalledWith('pendingIssuableRemoveRequest', vm.idKey);
});
});
});
import Vue from 'vue';
import eventHub from 'ee/related_issues/event_hub';
import relatedIssuesBlock from 'ee/related_issues/components/related_issues_block.vue';
import { issuable1, issuable2, issuable3, issuable4, issuable5 } from '../mock_data';
......@@ -20,7 +19,11 @@ describe('RelatedIssuesBlock', () => {
describe('with defaults', () => {
beforeEach(() => {
vm = new RelatedIssuesBlock().$mount();
vm = new RelatedIssuesBlock({
propsData: {
pathIdSeparator: '#',
},
}).$mount();
});
it('unable to add new related issues', () => {
......@@ -40,6 +43,7 @@ describe('RelatedIssuesBlock', () => {
beforeEach(() => {
vm = new RelatedIssuesBlock({
propsData: {
pathIdSeparator: '#',
isFetching: true,
},
}).$mount();
......@@ -58,6 +62,7 @@ describe('RelatedIssuesBlock', () => {
beforeEach(() => {
vm = new RelatedIssuesBlock({
propsData: {
pathIdSeparator: '#',
canAdmin: true,
},
}).$mount();
......@@ -72,6 +77,7 @@ describe('RelatedIssuesBlock', () => {
beforeEach(() => {
vm = new RelatedIssuesBlock({
propsData: {
pathIdSeparator: '#',
isFormVisible: true,
},
}).$mount();
......@@ -86,6 +92,7 @@ describe('RelatedIssuesBlock', () => {
beforeEach(() => {
vm = new RelatedIssuesBlock({
propsData: {
pathIdSeparator: '#',
relatedIssues: [issuable1, issuable2],
},
}).$mount();
......@@ -97,20 +104,13 @@ describe('RelatedIssuesBlock', () => {
});
describe('methods', () => {
let toggleAddRelatedIssuesFormSpy;
beforeEach(() => {
vm = new RelatedIssuesBlock({
propsData: {
pathIdSeparator: '#',
relatedIssues: [issuable1, issuable2, issuable3, issuable4, issuable5],
},
}).$mount();
toggleAddRelatedIssuesFormSpy = jasmine.createSpy('spy');
eventHub.$on('toggleAddRelatedIssuesForm', toggleAddRelatedIssuesFormSpy);
});
afterEach(() => {
eventHub.$off('toggleAddRelatedIssuesForm', toggleAddRelatedIssuesFormSpy);
});
it('reorder item correctly when an item is moved to the top', () => {
......@@ -140,12 +140,5 @@ describe('RelatedIssuesBlock', () => {
expect(beforeAfterIds.beforeId).toBe(3);
expect(beforeAfterIds.afterId).toBe(5);
});
it('when expanding add related issue form', () => {
expect(toggleAddRelatedIssuesFormSpy).not.toHaveBeenCalled();
vm.toggleAddRelatedIssuesForm();
expect(toggleAddRelatedIssuesFormSpy).toHaveBeenCalled();
});
});
});
......@@ -300,7 +300,10 @@ describe('RelatedIssuesRoot', () => {
it('fill in issue number reference and adds to pending related issues', () => {
const input = '#123 ';
vm.onInput(input, input.length);
vm.onInput({
newValue: input,
caretPos: input.length,
});
expect(vm.state.pendingReferences.length).toEqual(1);
expect(vm.state.pendingReferences[0]).toEqual('#123');
......@@ -308,7 +311,7 @@ describe('RelatedIssuesRoot', () => {
it('fill in with full reference', () => {
const input = 'asdf/qwer#444 ';
vm.onInput(input, input.length);
vm.onInput({ newValue: input, caretPos: input.length });
expect(vm.state.pendingReferences.length).toEqual(1);
expect(vm.state.pendingReferences[0]).toEqual('asdf/qwer#444');
......@@ -317,7 +320,7 @@ describe('RelatedIssuesRoot', () => {
it('fill in with issue link', () => {
const link = 'http://localhost:3000/foo/bar/issues/111';
const input = `${link} `;
vm.onInput(input, input.length);
vm.onInput({ newValue: input, caretPos: input.length });
expect(vm.state.pendingReferences.length).toEqual(1);
expect(vm.state.pendingReferences[0]).toEqual(link);
......@@ -325,7 +328,7 @@ describe('RelatedIssuesRoot', () => {
it('fill in with multiple references', () => {
const input = 'asdf/qwer#444 #12 ';
vm.onInput(input, input.length);
vm.onInput({ newValue: input, caretPos: input.length });
expect(vm.state.pendingReferences.length).toEqual(2);
expect(vm.state.pendingReferences[0]).toEqual('asdf/qwer#444');
......@@ -334,7 +337,7 @@ describe('RelatedIssuesRoot', () => {
it('fill in with some invalid things', () => {
const input = 'something random ';
vm.onInput(input, input.length);
vm.onInput({ newValue: input, caretPos: input.length });
expect(vm.state.pendingReferences.length).toEqual(2);
expect(vm.state.pendingReferences[0]).toEqual('something');
......@@ -343,7 +346,7 @@ describe('RelatedIssuesRoot', () => {
it('fill in invalid and some legit references', () => {
const input = 'something random #123 ';
vm.onInput(input, input.length);
vm.onInput({ newValue: input, caretPos: input.length });
expect(vm.state.pendingReferences.length).toEqual(3);
expect(vm.state.pendingReferences[0]).toEqual('something');
......@@ -353,7 +356,7 @@ describe('RelatedIssuesRoot', () => {
it('keep reference piece in input while we are touching it', () => {
const input = 'a #123 b ';
vm.onInput(input, 3);
vm.onInput({ newValue: input, caretPos: 3 });
expect(vm.state.pendingReferences.length).toEqual(2);
expect(vm.state.pendingReferences[0]).toEqual('a');
......
......@@ -16,7 +16,9 @@ describe('sidebarItemEpic', () => {
});
const SidebarItemEpic = Vue.extend(sidebarItemEpic);
vm = mountComponent(SidebarItemEpic, {});
vm = mountComponent(SidebarItemEpic, {
initialEpic: null,
});
});
afterEach(() => {
......
......@@ -6186,6 +6186,9 @@ msgstr ""
msgid "Pagination|« First"
msgstr ""
msgid "Parent epic"
msgstr ""
msgid "Part of merge request changes"
msgstr ""
......
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