Commit 0fa83c06 authored by Kushal Pandya's avatar Kushal Pandya

Remove in favour of new Epic app

Remove all the code and references to old Epic app.
parent afc49404
export const status = {
open: 'opened',
close: 'closed',
};
export const stateEvent = {
close: 'close',
reopen: 'reopen',
};
<script>
import $ from 'jquery';
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { __, s__ } from '~/locale';
import eventHub from '../../event_hub';
import { stateEvent } from '../../constants';
export default {
name: 'EpicHeader',
directives: {
tooltip,
},
components: {
Icon,
LoadingButton,
userAvatarLink,
timeagoTooltip,
},
props: {
author: {
type: Object,
required: true,
validator: value => value.url && value.username && value.name,
},
created: {
type: String,
required: true,
},
open: {
type: Boolean,
required: true,
},
canUpdate: {
required: true,
type: Boolean,
},
},
data() {
return {
deleteLoading: false,
statusUpdating: false,
isEpicOpen: this.open,
};
},
computed: {
statusIcon() {
return this.isEpicOpen ? 'issue-open-m' : 'mobile-issue-close';
},
statusText() {
return this.isEpicOpen ? __('Open') : __('Closed');
},
actionButtonClass() {
return `btn btn-grouped js-btn-epic-action qa-close-reopen-epic-button ${
this.isEpicOpen ? 'btn-close' : 'btn-open'
}`;
},
actionButtonText() {
return this.isEpicOpen ? __('Close epic') : __('Reopen epic');
},
},
mounted() {
$(document).on('issuable_vue_app:change', (e, isClosed) => {
this.isEpicOpen = e.detail ? !e.detail.isClosed : !isClosed;
this.statusUpdating = false;
});
},
methods: {
deleteEpic() {
// eslint-disable-next-line no-alert
if (window.confirm(s__('Epic will be removed! Are you sure?'))) {
this.deleteLoading = true;
this.$emit('deleteEpic');
}
},
toggleSidebar() {
eventHub.$emit('toggleSidebar');
},
toggleStatus() {
this.statusUpdating = true;
this.$emit('toggleEpicStatus', this.isEpicOpen ? stateEvent.close : stateEvent.reopen);
},
},
};
</script>
<template>
<div class="detail-page-header">
<div class="detail-page-header-body">
<div
:class="{ 'status-box-open': isEpicOpen, 'status-box-issue-closed': !isEpicOpen }"
class="issuable-status-box status-box"
>
<icon :name="statusIcon" css-classes="d-block d-sm-none" />
<span class="d-none d-sm-block">{{ statusText }}</span>
</div>
<div class="issuable-meta">
{{ s__('Opened') }}
<timeago-tooltip :time="created" />
{{ s__('by') }}
<strong>
<user-avatar-link
:link-href="author.url"
:img-src="author.src"
:img-size="24"
:tooltip-text="author.username"
:username="author.name"
img-css-classes="avatar-inline"
/>
</strong>
</div>
</div>
<div v-if="canUpdate" class="detail-page-header-actions js-issuable-actions">
<loading-button
:label="actionButtonText"
:loading="statusUpdating"
:container-class="actionButtonClass"
@click="toggleStatus"
/>
</div>
<button
:aria-label="__('toggle collapse')"
class="btn btn-default float-right d-block d-sm-none
gutter-toggle issuable-gutter-toggle js-sidebar-toggle"
type="button"
@click="toggleSidebar"
>
<i class="fa fa-angle-double-left"></i>
</button>
</div>
</template>
<script>
/* eslint-disable vue/require-default-prop */
import $ from 'jquery';
import issuableApp from '~/issue_show/components/app.vue';
import flash from '~/flash';
import { __ } from '~/locale';
import relatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue';
import epicSidebar from '../../sidebar/components/sidebar_app.vue';
import SidebarContext from '../sidebar_context';
import epicHeader from './epic_header.vue';
import EpicsService from '../../service/epics_service';
import { status, stateEvent } from '../../constants';
export default {
name: 'EpicShowApp',
epicsPathIdSeparator: '&',
components: {
epicHeader,
epicSidebar,
issuableApp,
relatedIssuesRoot,
},
props: {
epicId: {
type: Number,
required: true,
},
endpoint: {
type: String,
required: true,
},
updateEndpoint: {
type: String,
required: true,
},
canUpdate: {
required: true,
type: Boolean,
},
canDestroy: {
required: true,
type: Boolean,
},
canAdmin: {
required: true,
type: Boolean,
},
subepicsSupported: {
type: Boolean,
required: false,
default: true,
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
groupPath: {
type: String,
required: true,
},
initialTitleHtml: {
type: String,
required: true,
},
initialTitleText: {
type: String,
required: true,
},
initialDescriptionHtml: {
type: String,
required: false,
default: '',
},
initialDescriptionText: {
type: String,
required: false,
default: '',
},
created: {
type: String,
required: true,
},
author: {
type: Object,
required: true,
},
epicLinksEndpoint: {
type: String,
required: true,
},
issueLinksEndpoint: {
type: String,
required: true,
},
startDateIsFixed: {
type: Boolean,
required: true,
},
startDateFixed: {
type: String,
required: false,
default: '',
},
startDateFromMilestones: {
type: String,
required: false,
default: '',
},
startDate: {
type: String,
required: false,
},
dueDateIsFixed: {
type: Boolean,
required: true,
},
dueDateFixed: {
type: String,
required: false,
default: '',
},
dueDateFromMilestones: {
type: String,
required: false,
default: '',
},
endDate: {
type: String,
required: false,
},
startDateSourcingMilestoneTitle: {
type: String,
required: false,
default: '',
},
startDateSourcingMilestoneDates: {
type: Object,
required: true,
default: () => ({ startDate: '', dueDate: '' }),
},
dueDateSourcingMilestoneTitle: {
type: String,
required: false,
default: '',
},
dueDateSourcingMilestoneDates: {
type: Object,
required: true,
default: () => ({ startDate: '', dueDate: '' }),
},
labels: {
type: Array,
required: true,
},
parent: {
type: Object,
required: false,
default: () => ({}),
},
participants: {
type: Array,
required: true,
},
subscribed: {
type: Boolean,
required: true,
},
todoExists: {
type: Boolean,
required: true,
},
namespace: {
type: String,
required: false,
default: '#',
},
labelsPath: {
type: String,
required: true,
},
toggleSubscriptionPath: {
type: String,
required: true,
},
todoPath: {
type: String,
required: true,
},
todoDeletePath: {
type: String,
required: false,
default: '',
},
labelsWebUrl: {
type: String,
required: true,
},
epicsWebUrl: {
type: String,
required: true,
},
state: {
type: String,
required: true,
default: status.open,
},
},
data() {
return {
// Epics specific configuration
issuableRef: '',
hasRelatedEpicsFeature: this.subepicsSupported,
projectPath: this.groupPath,
parentEpic: this.parent ? this.parent : {},
projectNamespace: '',
service: new EpicsService({
endpoint: this.endpoint,
}),
};
},
computed: {
open() {
return this.state === status.open;
},
},
mounted() {
this.sidebarContext = new SidebarContext();
},
methods: {
triggerDocumentEvent(eventName, isClosed) {
$(document).trigger(eventName, isClosed);
},
toggleEpicStatus(stateEventType) {
return this.service
.updateStatus(stateEventType)
.then(() => {
const isClosed = stateEventType === stateEvent.close;
// Ensure that status change is reflected across the page.
// As `Close`/`Reopen` button is also present under
// comment form (part of Notes app)
// We've wrapped call to `$(document).trigger` for ease of testing
this.triggerDocumentEvent('issuable_vue_app:change', isClosed);
this.triggerDocumentEvent('issuable:change', isClosed);
})
.catch(() => {
flash(__('Unable to update this epic at this time.'));
const isClosed = stateEventType !== stateEvent.close;
this.triggerDocumentEvent('issuable_vue_app:change', isClosed);
this.triggerDocumentEvent('issuable:change', isClosed);
});
},
},
};
</script>
<template>
<div class="epic-page-container">
<epic-header
:author="author"
:created="created"
:open="open"
:can-delete="canDestroy"
:can-update="canUpdate"
@toggleEpicStatus="toggleEpicStatus"
/>
<div class="issuable-details content-block">
<div class="detail-page-description">
<issuable-app
:can-update="canUpdate"
:can-destroy="canDestroy"
:endpoint="endpoint"
:update-endpoint="updateEndpoint"
:issuable-ref="issuableRef"
:initial-title-html="initialTitleHtml"
:initial-title-text="initialTitleText"
:initial-description-html="initialDescriptionHtml"
:initial-description-text="initialDescriptionText"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-inline-edit-button="true"
:show-delete-button="canDestroy"
:enable-autocomplete="true"
issuable-type="epic"
/>
</div>
<epic-sidebar
:epic-id="epicId"
:endpoint="endpoint"
:editable="canUpdate"
:initial-start-date-is-fixed="startDateIsFixed"
:initial-start-date-fixed="startDateFixed"
:start-date-from-milestones="startDateFromMilestones"
:initial-start-date="startDate"
:initial-due-date-is-fixed="dueDateIsFixed"
:initial-due-date-fixed="dueDateFixed"
:due-date-from-milestones="dueDateFromMilestones"
:initial-end-date="endDate"
:start-date-sourcing-milestone-title="startDateSourcingMilestoneTitle"
:start-date-sourcing-milestone-dates="startDateSourcingMilestoneDates"
:due-date-sourcing-milestone-title="dueDateSourcingMilestoneTitle"
:due-date-sourcing-milestone-dates="dueDateSourcingMilestoneDates"
:initial-labels="labels"
:initial-participants="participants"
:initial-subscribed="subscribed"
:initial-todo-exists="todoExists"
:parent="parentEpic"
:namespace="namespace"
:update-path="updateEndpoint"
:labels-path="labelsPath"
:toggle-subscription-path="toggleSubscriptionPath"
:todo-path="todoPath"
:todo-delete-path="todoDeletePath"
:labels-web-url="labelsWebUrl"
:epics-web-url="epicsWebUrl"
/>
<related-issues-root
v-if="hasRelatedEpicsFeature"
:endpoint="epicLinksEndpoint"
:can-admin="canAdmin"
:can-reorder="canAdmin"
:allow-auto-complete="false"
:path-id-separator="$options.epicsPathIdSeparator"
:title="__('Epics')"
:issuable-type="__('epic')"
css-class="js-related-epics-block"
/>
<related-issues-root
:endpoint="issueLinksEndpoint"
:can-admin="canAdmin"
:can-reorder="canAdmin"
:allow-auto-complete="false"
:title="__('Issues')"
:issuable-type="__('issue')"
css-class="js-related-issues-block"
path-id-separator="#"
/>
</div>
</div>
</template>
import Vue from 'vue';
import Cookies from 'js-cookie';
import bp from '~/breakpoints';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import EpicShowApp from './components/epic_show_app.vue';
export default () => {
const el = document.querySelector('#epic-show-app');
const metaData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.meta), { deep: true });
const initialData = JSON.parse(el.dataset.initial);
// Collapse the sidebar on mobile screens by default
const bpBreakpoint = bp.getBreakpointSize();
if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
Cookies.set('collapsed_gutter', true);
}
const props = Object.assign({}, initialData, metaData, el.dataset);
return new Vue({
el,
components: {
'epic-show-app': EpicShowApp,
},
render: createElement =>
createElement('epic-show-app', {
props,
}),
});
};
import $ from 'jquery';
import Cookies from 'js-cookie';
import bp from '~/breakpoints';
export default class SidebarContext {
constructor() {
const $issuableSidebar = $('.js-issuable-update');
$issuableSidebar
.off('click', '.js-sidebar-dropdown-toggle')
.on('click', '.js-sidebar-dropdown-toggle', function onClickEdit(e) {
e.preventDefault();
const $block = $(this).parents('.js-labels-block');
const $selectbox = $block.find('.js-selectbox');
// We use `:visible` to detect element visibility
// since labels dropdown itself is handled by
// labels_select.js which internally uses
// $.hide() & $.show() to toggle elements
// which requires us to use `display: none;`
// in `labels_select/base.vue` as well.
// see: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4773#note_61844731
if ($selectbox.is(':visible')) {
$selectbox.hide();
$block.find('.js-value').show();
} else {
$selectbox.show();
$block.find('.js-value').hide();
}
if ($selectbox.is(':visible')) {
setTimeout(() => $block.find('.js-label-select').trigger('click'), 0);
}
});
window.addEventListener('beforeunload', () => {
// collapsed_gutter cookie hides the sidebar
const bpBreakpoint = bp.getBreakpointSize();
if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
Cookies.set('collapsed_gutter', true);
}
});
}
}
import Vue from 'vue';
export default new Vue();
<script>
import Flash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import loadingButton from '~/vue_shared/components/loading_button.vue';
import NewEpicService from '../services/new_epic_service';
export default {
name: 'NewEpic',
components: {
loadingButton,
},
props: {
endpoint: {
type: String,
required: true,
},
alignRight: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
service: new NewEpicService(this.endpoint),
creating: false,
title: '',
};
},
computed: {
buttonLabel() {
return this.creating ? s__('Creating epic') : s__('Create epic');
},
isCreatingDisabled() {
return this.title.length === 0;
},
},
methods: {
createEpic() {
this.creating = true;
this.service
.createEpic(this.title)
.then(({ data }) => {
visitUrl(data.web_url);
})
.catch(() => {
this.creating = false;
Flash(s__('Error creating epic'));
});
},
focusInput() {
// Wait for dropdown to appear because of transition CSS
setTimeout(() => {
this.$refs.title.focus();
}, 25);
},
},
};
</script>
<template>
<div class="dropdown new-epic-dropdown">
<button
class="btn btn-success qa-new-epic-button"
type="button"
data-toggle="dropdown"
@click="focusInput"
>
{{ s__('New epic') }}
</button>
<div :class="{ 'dropdown-menu-right': alignRight }" class="dropdown-menu">
<input
ref="title"
v-model="title"
:placeholder="s__('Title')"
type="text"
class="form-control qa-epic-title"
/>
<loading-button
:disabled="isCreatingDisabled"
:loading="creating"
:label="buttonLabel"
container-class="btn btn-success btn-inverted prepend-top-10 qa-create-epic-button"
@click.stop="createEpic"
/>
</div>
</div>
</template>
import Vue from 'vue';
import NewEpicApp from './components/new_epic.vue';
export default () => {
const el = document.querySelector('#new-epic-app');
if (el) {
const props = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
'new-epic-app': NewEpicApp,
},
render: createElement =>
createElement('new-epic-app', {
props,
}),
});
}
};
import axios from '~/lib/utils/axios_utils';
export default class NewEpicService {
constructor(endpoint) {
this.endpoint = endpoint;
}
createEpic(title) {
return axios.post(this.endpoint, {
title,
});
}
}
import axios from '~/lib/utils/axios_utils';
export default class EpicsService {
constructor({ endpoint }) {
this.endpoint = endpoint;
}
updateStatus(stateEventType) {
const queryParam = `epic[state_event]=${stateEventType}`;
return axios.put(`${this.endpoint}.json?${encodeURI(queryParam)}`);
}
}
<script>
/* eslint-disable vue/require-default-prop */
import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
import flash from '~/flash';
import { __, s__, sprintf } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { dateInWords, parsePikadayDate } from '~/lib/utils/datetime_utility';
import ListLabel from '~/vue_shared/models/label';
import SidebarTodo from '~/sidebar/components/todo_toggle/todo.vue';
import SidebarCollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
import SidebarLabelsSelect from '~/vue_shared/components/sidebar/labels_select/base.vue';
import SidebarItemEpic from 'ee/sidebar/components/sidebar_item_epic.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import eventHub from '../../event_hub';
import SidebarDatePicker from './sidebar_date_picker.vue';
import SidebarParticipants from './sidebar_participants.vue';
import SidebarSubscriptions from './sidebar_subscriptions.vue';
import SidebarService from '../services/sidebar_service';
import Store from '../stores/sidebar_store';
const DateTypes = {
start: 'start',
end: 'end',
};
export default {
name: 'EpicSidebar',
components: {
ToggleSidebar,
SidebarTodo,
SidebarDatePicker,
SidebarCollapsedGroupedDatePicker,
SidebarLabelsSelect,
SidebarItemEpic,
SidebarParticipants,
SidebarSubscriptions,
},
props: {
epicId: {
type: Number,
required: true,
},
endpoint: {
type: String,
required: true,
},
editable: {
type: Boolean,
required: false,
default: false,
},
initialStartDateIsFixed: {
type: Boolean,
required: true,
},
initialStartDateFixed: {
type: String,
required: false,
},
startDateFromMilestones: {
type: String,
required: false,
},
initialStartDate: {
type: String,
required: false,
},
initialDueDateIsFixed: {
type: Boolean,
required: true,
},
initialDueDateFixed: {
type: String,
required: false,
},
dueDateFromMilestones: {
type: String,
required: false,
},
initialEndDate: {
type: String,
required: false,
},
startDateSourcingMilestoneTitle: {
type: String,
required: false,
default: '',
},
startDateSourcingMilestoneDates: {
type: Object,
required: true,
},
dueDateSourcingMilestoneTitle: {
type: String,
required: false,
default: '',
},
dueDateSourcingMilestoneDates: {
type: Object,
required: true,
},
initialLabels: {
type: Array,
required: true,
},
initialParticipants: {
type: Array,
required: true,
},
initialSubscribed: {
type: Boolean,
required: true,
},
initialTodoExists: {
type: Boolean,
required: true,
},
parent: {
type: Object,
required: true,
},
namespace: {
type: String,
required: false,
default: '#',
},
updatePath: {
type: String,
required: true,
},
labelsPath: {
type: String,
required: true,
},
toggleSubscriptionPath: {
type: String,
required: true,
},
todoPath: {
type: String,
required: true,
},
todoDeletePath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
epicsWebUrl: {
type: String,
required: true,
},
},
data() {
const store = new Store({
startDateIsFixed: this.initialStartDateIsFixed,
startDateFromMilestones: this.startDateFromMilestones,
startDateFixed: this.initialStartDateFixed,
startDate: this.initialStartDate,
dueDateIsFixed: this.initialDueDateIsFixed,
dueDateFromMilestones: this.dueDateFromMilestones,
dueDateFixed: this.initialDueDateFixed,
endDate: this.initialEndDate,
subscribed: this.initialSubscribed,
todoExists: this.initialTodoExists,
todoDeletePath: this.todoDeletePath,
});
return {
store,
// Backend will pass the appropriate css class for the contentContainer
collapsed: parseBoolean(Cookies.get('collapsed_gutter')),
isUserSignedIn: !!gon.current_user_id,
autoExpanded: false,
savingStartDate: false,
savingEndDate: false,
savingSubscription: false,
savingTodoAction: false,
service: new SidebarService({
endpoint: this.endpoint,
subscriptionEndpoint: this.toggleSubscriptionPath,
todoPath: this.todoPath,
}),
epicContext: {
labels: this.initialLabels,
},
};
},
computed: {
/**
* This prop determines if epic dates
* are valid (i.e. given start date is less than given end date)
*/
isDateValid() {
const {
startDateTime,
startDateTimeFromMilestones,
startDateIsFixed,
endDateTime,
dueDateTimeFromMilestones,
dueDateIsFixed,
} = this.store;
if (startDateIsFixed && dueDateIsFixed) {
// When Epic start and finish dates are of type fixed.
return this.getDateValidity(startDateTime, endDateTime);
} else if (!startDateIsFixed && dueDateIsFixed) {
// When Epic start date is from milestone and finish date is of type fixed.
return this.getDateValidity(startDateTimeFromMilestones, endDateTime);
} else if (startDateIsFixed && !dueDateIsFixed) {
// When Epic start date is fixed and finish date is from milestone.
return this.getDateValidity(startDateTime, dueDateTimeFromMilestones);
}
// When both Epic start date and finish date are from milestone.
return this.getDateValidity(startDateTimeFromMilestones, dueDateTimeFromMilestones);
},
collapsedSidebarStartDate() {
return this.store.startDateIsFixed
? this.store.startDateTime
: this.store.startDateTimeFromMilestones;
},
collapsedSidebarEndDate() {
return this.store.dueDateIsFixed
? this.store.endDateTime
: this.store.dueDateTimeFromMilestones;
},
},
mounted() {
eventHub.$on('toggleSidebar', this.toggleSidebar);
document.addEventListener(
'toggleSidebarRevealLabelsDropdown',
this.toggleSidebarRevealLabelsDropdown,
);
},
beforeDestroy() {
eventHub.$off('toggleSidebar', this.toggleSidebar);
document.removeEventListener(
'toggleSidebarRevealLabelsDropdown',
this.toggleSidebarRevealLabelsDropdown,
);
},
methods: {
getDateValidity(startDate, endDate) {
// If both dates are defined
// only then compare, return true otherwise
if (startDate && endDate) {
return startDate < endDate;
}
return true;
},
getDateTypeString(dateType) {
return dateType === DateTypes.start ? s__('Epics|start') : s__('Epics|due');
},
getDateFromMilestonesTooltip(dateType = DateTypes.start) {
const { startDateTimeFromMilestones, dueDateTimeFromMilestones } = this.store;
const dateSourcingMilestoneTitle = this[`${dateType}DateSourcingMilestoneTitle`];
const sourcingMilestoneDates =
dateType === DateTypes.start
? this.startDateSourcingMilestoneDates
: this.dueDateSourcingMilestoneDates;
if (startDateTimeFromMilestones && dueDateTimeFromMilestones) {
const { startDate, dueDate } = sourcingMilestoneDates;
let startDateInWords = __('No start date');
let dueDateInWords = __('No due date');
if (startDate && dueDate) {
const startDateObj = parsePikadayDate(startDate);
const dueDateObj = parsePikadayDate(dueDate);
startDateInWords = dateInWords(
startDateObj,
true,
startDateObj.getFullYear() === dueDateObj.getFullYear(),
);
dueDateInWords = dateInWords(dueDateObj, true);
} else if (startDate && !dueDate) {
startDateInWords = dateInWords(parsePikadayDate(startDate), true);
} else {
dueDateInWords = dateInWords(parsePikadayDate(dueDate), true);
}
return `${dateSourcingMilestoneTitle}<br/><span class="text-tertiary">${startDateInWords}${dueDateInWords}</span>`;
}
return sprintf(
s__(
"Epics|To schedule your epic's %{epicDateType} date based on milestones, assign a milestone with a %{epicDateType} date to any issue in the epic.",
),
{
epicDateType: this.getDateTypeString(dateType),
},
);
},
toggleSidebar() {
this.collapsed = !this.collapsed;
const contentContainer = this.$el.closest('.page-with-contextual-sidebar');
contentContainer.classList.toggle('right-sidebar-expanded');
contentContainer.classList.toggle('right-sidebar-collapsed');
Cookies.set('collapsed_gutter', this.collapsed);
},
toggleSidebarRevealLabelsDropdown() {
const contentContainer = this.$el.closest('.page-with-contextual-sidebar');
this.toggleSidebar();
// When sidebar is expanded, we need to wait
// for rendering to finish before opening
// dropdown as otherwise it causes `calc()`
// used in CSS to miscalculate collapsed
// sidebar size.
_.debounce(() => {
this.autoExpanded = true;
contentContainer
.querySelector('.js-sidebar-dropdown-toggle')
.dispatchEvent(new Event('click', { bubbles: true, cancelable: false }));
}, 100)();
},
saveDate(dateType, newDate, isFixed = true) {
const type = dateType === DateTypes.start ? dateType : 'end';
const capitalizedType = capitalizeFirstCharacter(type);
const serviceMethod = `update${capitalizedType}Date`;
const savingBoolean = `saving${capitalizedType}Date`;
this[savingBoolean] = true;
return this.service[serviceMethod]({
dateValue: newDate,
isFixed,
})
.then(() => {
this[savingBoolean] = false;
this.store[`${type}Date`] = newDate;
if (isFixed) {
// Update fixed date in store
const fixedDate = dateType === DateTypes.start ? 'startDateFixed' : 'dueDateFixed';
this.store[fixedDate] = newDate;
}
})
.catch(() => {
this[savingBoolean] = false;
flash(
sprintf(s__('Epics|An error occurred while saving %{epicDateType} date'), {
epicDateType: this.getDateTypeString(dateType),
}),
);
});
},
changeStartDateType(dateTypeIsFixed, typeChangeOnEdit) {
this.store.startDateIsFixed = dateTypeIsFixed;
if (!typeChangeOnEdit) {
this.saveDate(
DateTypes.start,
dateTypeIsFixed ? this.store.startDateFixed : this.store.startDateFromMilestones,
dateTypeIsFixed,
);
}
},
saveStartDate(date) {
return this.saveDate(DateTypes.start, date);
},
changeEndDateType(dateTypeIsFixed, typeChangeOnEdit) {
this.store.dueDateIsFixed = dateTypeIsFixed;
if (!typeChangeOnEdit) {
this.saveDate(
DateTypes.end,
dateTypeIsFixed ? this.store.dueDateFixed : this.store.dueDateFromMilestones,
dateTypeIsFixed,
);
}
},
saveEndDate(date) {
return this.saveDate(DateTypes.end, date);
},
saveTodoState({ count, deletePath }) {
this.savingTodoAction = false;
this.store.setTodoExists(!this.store.todoExists);
if (deletePath) {
this.store.setTodoDeletePath(deletePath);
}
$(document).trigger('todo:toggle', count);
},
handleLabelClick(label) {
if (label.isAny) {
this.epicContext.labels = [];
} else {
const labelIndex = this.epicContext.labels.findIndex(l => l.id === label.id);
if (labelIndex === -1) {
this.epicContext.labels.push(
new ListLabel({
id: label.id,
title: label.title,
color: label.color[0],
textColor: label.text_color,
}),
);
} else {
this.epicContext.labels.splice(labelIndex, 1);
}
}
},
handleDropdownClose() {
if (this.autoExpanded) {
this.autoExpanded = false;
this.toggleSidebar();
}
},
handleToggleSubscribed() {
this.service
.toggleSubscribed()
.then(() => {
this.store.setSubscribed(!this.store.subscribed);
})
.catch(() => {
if (this.store.subscribed) {
flash(__('An error occurred while unsubscribing to notifications.'));
} else {
flash(__('An error occurred while subscribing to notifications.'));
}
});
},
handleToggleTodo() {
this.savingTodoAction = true;
if (!this.store.todoExists) {
this.service
.addTodo(this.epicId)
.then(({ data }) => {
this.saveTodoState({
count: data.count,
deletePath: data.delete_path,
});
})
.catch(() => {
this.savingTodoAction = false;
flash(__('There was an error adding a todo.'));
});
} else {
this.service
.deleteTodo(this.store.todoDeletePath)
.then(({ data }) => {
this.saveTodoState({
count: data.count,
});
})
.catch(() => {
this.savingTodoAction = false;
flash(__('There was an error deleting the todo.'));
});
}
},
},
};
</script>
<template>
<aside
:class="{ 'right-sidebar-expanded': !collapsed, 'right-sidebar-collapsed': collapsed }"
v-bind="isUserSignedIn ? { 'data-signed-in': true } : {}"
class="right-sidebar epic-sidebar"
>
<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" />
<sidebar-todo
v-if="!collapsed"
:collapsed="collapsed"
:issuable-id="epicId"
:is-todo="store.todoExists"
:is-action-active="savingTodoAction"
issuable-type="epic"
@toggleTodo="handleToggleTodo"
/>
</div>
<div v-if="collapsed && isUserSignedIn" class="block todo">
<sidebar-todo
:collapsed="collapsed"
:issuable-id="epicId"
:is-todo="store.todoExists"
:is-action-active="savingTodoAction"
issuable-type="epic"
@toggleTodo="handleToggleTodo"
/>
</div>
<sidebar-date-picker
v-if="!collapsed"
:collapsed="collapsed"
:is-loading="savingStartDate"
:is-date-invalid="!isDateValid"
:editable="editable"
:selected-date-is-fixed="store.startDateIsFixed"
:selected-date="store.startDateTime"
:date-fixed="store.startDateTimeFixed"
:date-from-milestones="store.startDateTimeFromMilestones"
:date-from-milestones-tooltip="getDateFromMilestonesTooltip('start')"
:show-toggle-sidebar="!isUserSignedIn"
:date-picker-label="__('Fixed start date')"
:label="__('Start date')"
:date-invalid-tooltip="
__(`This date is after the due date,
so this epic won't appear in the roadmap.`)
"
block-class="start-date"
@saveDate="saveStartDate"
@toggleDateType="changeStartDateType"
@toggleCollapse="toggleSidebar"
/>
<sidebar-date-picker
v-if="!collapsed"
:collapsed="collapsed"
:is-loading="savingEndDate"
:is-date-invalid="!isDateValid"
:editable="editable"
:selected-date-is-fixed="store.dueDateIsFixed"
:selected-date="store.endDateTime"
:date-fixed="store.dueDateTimeFixed"
:date-from-milestones="store.dueDateTimeFromMilestones"
:date-from-milestones-tooltip="getDateFromMilestonesTooltip('due')"
:date-picker-label="__('Fixed due date')"
:label="__('Due date')"
:date-invalid-tooltip="
__(`This date is before the start date,
so this epic won't appear in the roadmap.`)
"
block-class="end-date"
@saveDate="saveEndDate"
@toggleDateType="changeEndDateType"
/>
<sidebar-collapsed-grouped-date-picker
v-if="collapsed"
:collapsed="collapsed"
:min-date="collapsedSidebarStartDate"
:max-date="collapsedSidebarEndDate"
@toggleCollapse="toggleSidebar"
/>
<sidebar-labels-select
:context="epicContext"
:namespace="namespace"
:update-path="updatePath"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:label-filter-base-path="epicsWebUrl"
:can-edit="editable"
:show-create="true"
ability-name="epic"
@onLabelClick="handleLabelClick"
@onDropdownClose="handleDropdownClose"
@toggleCollapse="toggleSidebarRevealLabelsDropdown"
>{{ __('None') }}</sidebar-labels-select
>
<div class="block parent-epic">
<sidebar-item-epic :block-title="__('Parent epic')" :initial-epic="parent" />
</div>
<sidebar-participants :participants="initialParticipants" @toggleCollapse="toggleSidebar" />
<sidebar-subscriptions
:loading="savingSubscription"
:subscribed="store.subscribed"
@toggleSubscription="handleToggleSubscribed"
@toggleCollapse="toggleSidebar"
/>
</div>
</aside>
</template>
<script>
import _ from 'underscore';
import { __, s__ } from '~/locale';
import { dateInWords } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import popover from '~/vue_shared/directives/popover';
import Icon from '~/vue_shared/components/icon.vue';
import DatePicker from '~/vue_shared/components/pikaday.vue';
import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
import { GlLoadingIcon } from '@gitlab/ui';
const label = __('Date picker');
const pickerLabel = __('Fixed date');
export default {
directives: {
tooltip,
popover,
},
components: {
Icon,
DatePicker,
CollapsedCalendarIcon,
ToggleSidebar,
GlLoadingIcon,
},
props: {
blockClass: {
type: String,
required: false,
default: '',
},
collapsed: {
type: Boolean,
required: false,
default: true,
},
showToggleSidebar: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
editable: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
default: label,
},
datePickerLabel: {
type: String,
required: false,
default: pickerLabel,
},
selectedDate: {
type: Date,
required: false,
default: null,
},
selectedDateIsFixed: {
type: Boolean,
required: false,
default: true,
},
dateFromMilestones: {
type: Date,
required: false,
default: null,
},
dateFixed: {
type: Date,
required: false,
default: null,
},
dateFromMilestonesTooltip: {
type: String,
required: false,
default: '',
},
isDateInvalid: {
type: Boolean,
required: false,
default: true,
},
dateInvalidTooltip: {
type: String,
required: false,
default: '',
},
},
data() {
return {
fieldName: _.uniqueId('dateType_'),
editing: false,
};
},
computed: {
selectedAndEditable() {
return this.selectedDate && this.editable;
},
selectedDateWords() {
return dateInWords(this.selectedDate, true);
},
dateFixedWords() {
return dateInWords(this.dateFixed, true);
},
dateFromMilestonesWords() {
return this.dateFromMilestones ? dateInWords(this.dateFromMilestones, true) : __('None');
},
collapsedText() {
return this.selectedDateWords ? this.selectedDateWords : __('None');
},
popoverOptions() {
return this.getPopoverConfig({
title: s__(
'Epics|These dates affect how your epics appear in the roadmap. Dates from milestones come from the milestones assigned to issues in the epic. You can also set fixed dates or remove them entirely.',
),
content: `
<a
href="${gon.gitlab_url}/help/user/group/epics/index.md#start-date-and-due-date"
target="_blank"
rel="noopener noreferrer"
>${s__('Epics|More information')}</a>
`,
});
},
dateInvalidPopoverOptions() {
return this.getPopoverConfig({
title: this.dateInvalidTooltip,
content: `
<a
href="${gon.gitlab_url}/help/user/group/epics/index.md#start-date-and-due-date"
target="_blank"
rel="noopener noreferrer"
>${s__('Epics|How can I solve this?')}</a>
`,
});
},
},
methods: {
getPopoverConfig({ title, content }) {
return {
html: true,
trigger: 'focus',
container: 'body',
boundary: 'viewport',
template: `
<div class="popover" role="tooltip">
<div class="arrow"></div>
<div class="popover-header"></div>
<div class="popover-body"></div>
</div>
`,
title,
content,
};
},
stopEditing() {
this.editing = false;
this.$emit('toggleDateType', true, true);
},
toggleDatePicker() {
this.editing = !this.editing;
},
newDateSelected(date = null) {
this.editing = false;
this.$emit('saveDate', date);
},
toggleDateType(dateTypeFixed) {
this.$emit('toggleDateType', dateTypeFixed);
},
toggleSidebar() {
this.$emit('toggleCollapse');
},
},
};
</script>
<template>
<div :class="blockClass" class="block date">
<collapsed-calendar-icon :text="collapsedText" class="sidebar-collapsed-icon" />
<div class="title">
{{ label }}
<gl-loading-icon v-if="isLoading" :inline="true" />
<div class="float-right d-flex">
<icon
v-popover="popoverOptions"
name="question-o"
css-classes="help-icon append-right-5"
tab-index="0"
/>
<button
v-show="editable && !editing"
type="button"
class="btn-blank btn-link btn-primary-hover-link btn-sidebar-action"
@click="toggleDatePicker"
>
{{ __('Edit') }}
</button>
<toggle-sidebar v-if="showToggleSidebar" :collapsed="collapsed" @toggle="toggleSidebar" />
</div>
</div>
<div class="value">
<div
:class="{ 'is-option-selected': selectedDateIsFixed, 'd-flex': !editing }"
class="value-type-fixed"
>
<input
v-if="!editing && editable"
:name="fieldName"
:checked="selectedDateIsFixed"
type="radio"
@click="toggleDateType(true)"
/>
<span v-show="!editing" class="prepend-left-5">{{ __('Fixed:') }}</span>
<date-picker
v-if="editing"
:selected-date="dateFixed"
:label="datePickerLabel"
@newDateSelected="newDateSelected"
@hidePicker="stopEditing"
/>
<span v-else class="d-flex value-content">
<template v-if="dateFixed">
<span>{{ dateFixedWords }}</span>
<icon
v-if="isDateInvalid && selectedDateIsFixed"
v-popover="dateInvalidPopoverOptions"
name="warning"
css-classes="date-warning-icon append-right-5 prepend-left-5"
tab-index="0"
/>
<span v-if="selectedAndEditable" class="no-value">
-
<button
type="button"
class="btn-blank btn-link btn-default-hover-link"
@click="newDateSelected(null)"
>
{{ __('remove') }}
</button>
</span>
</template>
<span v-else class="no-value"> {{ __('None') }} </span>
</span>
</div>
<abbr
v-tooltip
:title="dateFromMilestonesTooltip"
:class="{ 'is-option-selected': !selectedDateIsFixed }"
class="value-type-dynamic d-flex prepend-top-10"
data-placement="bottom"
data-html="true"
>
<input
v-if="editable"
:name="fieldName"
:checked="!selectedDateIsFixed"
type="radio"
@click="toggleDateType(false)"
/>
<span class="prepend-left-5">{{ __('From milestones:') }}</span>
<span class="value-content">{{ dateFromMilestonesWords }}</span>
<icon
v-if="isDateInvalid && !selectedDateIsFixed"
v-popover="dateInvalidPopoverOptions"
name="warning"
css-classes="date-warning-icon prepend-left-5"
tab-index="0"
/>
</abbr>
</div>
</div>
</template>
<script>
import Participants from '~/sidebar/components/participants/participants.vue';
export default {
components: {
Participants,
},
props: {
participants: {
type: Array,
required: true,
},
},
methods: {
onToggleSidebar() {
this.$emit('toggleCollapse');
},
},
};
</script>
<template>
<div class="block participants">
<participants :participants="participants" @toggleSidebar="onToggleSidebar" />
</div>
</template>
<script>
import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
export default {
components: {
Subscriptions,
},
props: {
loading: {
type: Boolean,
required: true,
},
subscribed: {
type: Boolean,
required: true,
},
},
methods: {
onToggleSubscription() {
this.$emit('toggleSubscription');
},
onToggleSidebar() {
this.$emit('toggleCollapse');
},
},
};
</script>
<template>
<div class="block subscriptions">
<subscriptions
:loading="loading"
:subscribed="subscribed"
@toggleSubscription="onToggleSubscription"
@toggleSidebar="onToggleSidebar"
/>
</div>
</template>
import axios from '~/lib/utils/axios_utils';
export default class SidebarService {
constructor({ endpoint, subscriptionEndpoint, todoPath }) {
this.endpoint = endpoint;
this.subscriptionEndpoint = subscriptionEndpoint;
this.todoPath = todoPath;
}
updateStartDate({ dateValue, isFixed }) {
const requestBody = {
start_date_is_fixed: isFixed,
};
if (isFixed) {
requestBody.start_date_fixed = dateValue;
}
return axios.put(this.endpoint, requestBody);
}
updateEndDate({ dateValue, isFixed }) {
const requestBody = {
due_date_is_fixed: isFixed,
};
if (isFixed) {
requestBody.due_date_fixed = dateValue;
}
return axios.put(this.endpoint, requestBody);
}
toggleSubscribed() {
return axios.post(this.subscriptionEndpoint);
}
addTodo(epicId) {
return axios.post(this.todoPath, {
issuable_id: epicId,
issuable_type: 'epic',
});
}
// eslint-disable-next-line class-methods-use-this
deleteTodo(todoDeletePath) {
return axios.delete(todoDeletePath);
}
}
import { parsePikadayDate } from '~/lib/utils/datetime_utility';
export default class SidebarStore {
constructor({
startDateIsFixed,
startDateFixed,
startDateFromMilestones,
startDate,
dueDateIsFixed,
dueDateFixed,
dueDateFromMilestones,
endDate,
subscribed,
todoExists,
todoDeletePath,
}) {
this.startDateIsFixed = startDateIsFixed;
this.startDateFixed = startDateFixed;
this.startDateFromMilestones = startDateFromMilestones;
this.startDate = startDate;
this.dueDateFixed = dueDateFixed;
this.dueDateIsFixed = dueDateIsFixed;
this.dueDateFromMilestones = dueDateFromMilestones;
this.endDate = endDate;
this.subscribed = subscribed;
this.todoExists = todoExists;
this.todoDeletePath = todoDeletePath;
}
get startDateTime() {
return this.startDate ? parsePikadayDate(this.startDate) : null;
}
get startDateTimeFixed() {
return this.startDateFixed ? parsePikadayDate(this.startDateFixed) : null;
}
get startDateTimeFromMilestones() {
return this.startDateFromMilestones ? parsePikadayDate(this.startDateFromMilestones) : null;
}
get endDateTime() {
return this.endDate ? parsePikadayDate(this.endDate) : null;
}
get dueDateTimeFixed() {
return this.dueDateFixed ? parsePikadayDate(this.dueDateFixed) : null;
}
get dueDateTimeFromMilestones() {
return this.dueDateFromMilestones ? parsePikadayDate(this.dueDateFromMilestones) : null;
}
setSubscribed(subscribed) {
this.subscribed = subscribed;
}
setTodoExists(todoExists) {
this.todoExists = todoExists;
}
setTodoDeletePath(deletePath) {
this.todoDeletePath = deletePath;
}
}
import Vue from 'vue';
import epicHeader from 'ee/epics/epic_show/components/epic_header.vue';
import { stateEvent } from 'ee/epics/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { headerProps } from '../mock_data';
describe('epicHeader', () => {
let vm;
const { author } = headerProps;
beforeEach(() => {
const EpicHeader = Vue.extend(epicHeader);
vm = mountComponent(EpicHeader, headerProps);
});
it('should render timeago tooltip', () => {
expect(vm.$el.querySelector('time')).toBeDefined();
});
it('should link to author url', () => {
expect(vm.$el.querySelector('a').href).toEqual(author.url);
});
it('should render author avatar', () => {
expect(vm.$el.querySelector('img').src).toEqual(`${author.src}?width=24`);
});
it('should render author name', () => {
expect(vm.$el.querySelector('.user-avatar-link').innerText.trim()).toEqual(author.name);
});
it('should render username tooltip', () => {
expect(vm.$el.querySelector('.js-user-avatar-link-username').dataset.originalTitle).toEqual(
author.username,
);
});
it('should render sidebar toggle button', () => {
expect(vm.$el.querySelector('button.js-sidebar-toggle')).not.toBe(null);
});
it('should render status badge', () => {
const badgeEl = vm.$el.querySelector('.issuable-status-box');
const badgeIconEl = badgeEl.querySelector('svg use');
expect(badgeEl).not.toBe(null);
expect(badgeEl.innerText.trim()).toBe('Open');
expect(badgeIconEl.getAttribute('xlink:href')).toContain('issue-open-m');
});
it('should render `Close epic` button when `isEpicOpen` & `canUpdate` props are true', () => {
vm.isEpicOpen = true;
const closeButtonEl = vm.$el.querySelector('.js-issuable-actions .js-btn-epic-action');
expect(closeButtonEl).not.toBe(null);
expect(closeButtonEl.innerText.trim()).toBe('Close epic');
});
describe('computed', () => {
describe('statusIcon', () => {
it('returns `issue-open-m` when `isEpicOpen` prop is true', () => {
vm.isEpicOpen = true;
expect(vm.statusIcon).toBe('issue-open-m');
});
it('returns `mobile-issue-close` when `isEpicOpen` prop is false', () => {
vm.isEpicOpen = false;
expect(vm.statusIcon).toBe('mobile-issue-close');
});
});
describe('statusText', () => {
it('returns `Open` when `isEpicOpen` prop is true', () => {
vm.isEpicOpen = true;
expect(vm.statusText).toBe('Open');
});
it('returns `Closed` when `isEpicOpen` prop is false', () => {
vm.isEpicOpen = false;
expect(vm.statusText).toBe('Closed');
});
});
describe('actionButtonClass', () => {
it('returns classes `btn btn-grouped js-btn-epic-action qa-close-reopen-epic-button` & `btn-close` when `isEpicOpen` prop is true', () => {
vm.isEpicOpen = true;
expect(vm.actionButtonClass).toContain(
'btn btn-grouped js-btn-epic-action qa-close-reopen-epic-button btn-close',
);
});
it('returns classes `btn btn-grouped js-btn-epic-action qa-close-reopen-epic-button` & `btn-open` when `isEpicOpen` prop is false', () => {
vm.isEpicOpen = false;
expect(vm.actionButtonClass).toContain(
'btn btn-grouped js-btn-epic-action qa-close-reopen-epic-button btn-open',
);
});
});
describe('actionButtonText', () => {
it('returns `Close epic` when `isEpicOpen` prop is true', () => {
vm.isEpicOpen = true;
expect(vm.actionButtonText).toBe('Close epic');
});
it('returns `Reopen epic` when `isEpicOpen` prop is false', () => {
vm.isEpicOpen = false;
expect(vm.actionButtonText).toBe('Reopen epic');
});
});
});
describe('methods', () => {
describe('toggleStatus', () => {
it('emits `toggleEpicStatus` on component with stateEventType param as `close` when `isEpicOpen` prop is true', () => {
spyOn(vm, '$emit');
vm.isEpicOpen = true;
vm.toggleStatus();
expect(vm.statusUpdating).toBe(true);
expect(vm.$emit).toHaveBeenCalledWith('toggleEpicStatus', stateEvent.close);
});
it('emits `toggleEpicStatus` on component with stateEventType param as `reopen` when `isEpicOpen` prop is false', () => {
spyOn(vm, '$emit');
vm.isEpicOpen = false;
vm.toggleStatus();
expect(vm.statusUpdating).toBe(true);
expect(vm.$emit).toHaveBeenCalledWith('toggleEpicStatus', stateEvent.reopen);
});
});
});
});
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import epicShowApp from 'ee/epics/epic_show/components/epic_show_app.vue';
import epicHeader from 'ee/epics/epic_show/components/epic_header.vue';
import { stateEvent } from 'ee/epics/constants';
import issuableApp from '~/issue_show/components/app.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import issueShowData from 'spec/issue_show/mock_data';
import { props } from '../mock_data';
describe('EpicShowApp', () => {
let mock;
let vm;
let headerVm;
let issuableAppVm;
beforeEach(done => {
mock = new MockAdapter(axios);
mock.onGet(`${gl.TEST_HOST}/realtime_changes`).reply(200, issueShowData.initialRequest);
const {
canUpdate,
canDestroy,
endpoint,
updateEndpoint,
initialTitleHtml,
initialTitleText,
markdownPreviewPath,
markdownDocsPath,
author,
created,
toggleSubscriptionPath,
state,
open,
} = props;
const EpicShowApp = Vue.extend(epicShowApp);
vm = mountComponent(EpicShowApp, props);
const EpicHeader = Vue.extend(epicHeader);
headerVm = mountComponent(EpicHeader, {
author,
created,
open,
canUpdate,
});
const IssuableApp = Vue.extend(issuableApp);
issuableAppVm = mountComponent(IssuableApp, {
canUpdate,
canDestroy,
endpoint,
updateEndpoint,
issuableRef: '',
initialTitleHtml,
initialTitleText,
initialDescriptionHtml: '',
initialDescriptionText: '',
markdownPreviewPath,
markdownDocsPath,
projectPath: props.groupPath,
projectNamespace: '',
showInlineEditButton: true,
toggleSubscriptionPath,
state,
});
setTimeout(done);
});
afterEach(() => {
mock.restore();
});
it('should render epic-header', () => {
expect(vm.$el.innerHTML.indexOf(headerVm.$el.innerHTML)).not.toBe(-1);
});
it('should render issuable-app', () => {
expect(vm.$el.innerHTML.indexOf(issuableAppVm.$el.innerHTML)).not.toBe(-1);
});
it('should render epic-sidebar', () => {
expect(vm.$el.querySelector('aside.right-sidebar.epic-sidebar')).not.toBe(null);
});
it('calls `updateStatus` with stateEventType param on service and triggers document events when request is successful', done => {
const queryParam = `epic[state_event]=${stateEvent.close}`;
mock.onPut(`${vm.endpoint}.json?${encodeURI(queryParam)}`).reply(200, {});
spyOn(vm.service, 'updateStatus').and.callThrough();
spyOn(vm, 'triggerDocumentEvent');
vm.toggleEpicStatus(stateEvent.close);
setTimeout(() => {
expect(vm.service.updateStatus).toHaveBeenCalledWith(stateEvent.close);
expect(vm.triggerDocumentEvent).toHaveBeenCalledWith('issuable_vue_app:change', true);
expect(vm.triggerDocumentEvent).toHaveBeenCalledWith('issuable:change', true);
done();
}, 0);
});
it('calls `updateStatus` with stateEventType param on service and shows flash error and triggers document events when request is failed', done => {
const queryParam = `epic[state_event]=${stateEvent.close}`;
mock.onPut(`${vm.endpoint}.json?${encodeURI(queryParam)}`).reply(500, {});
spyOn(vm.service, 'updateStatus').and.callThrough();
spyOn(vm, 'triggerDocumentEvent');
vm.toggleEpicStatus(stateEvent.close);
setTimeout(() => {
expect(vm.service.updateStatus).toHaveBeenCalledWith(stateEvent.close);
expect(vm.triggerDocumentEvent).toHaveBeenCalledWith('issuable_vue_app:change', false);
expect(vm.triggerDocumentEvent).toHaveBeenCalledWith('issuable:change', false);
done();
}, 0);
});
});
export const mockLabels = [
{
id: 26,
title: 'Foo Label',
description: 'Foobar',
color: '#BADA55',
text_color: '#FFFFFF',
},
];
export const mockParticipants = [
{
id: 1,
name: 'Administrator',
username: 'root',
state: 'active',
avatar_url: '',
web_url: 'http://127.0.0.1:3001/root',
},
{
id: 12,
name: 'Susy Johnson',
username: 'tana_harvey',
state: 'active',
avatar_url: '',
web_url: 'http://127.0.0.1:3001/tana_harvey',
},
];
export const contentProps = {
epicId: 1,
endpoint: gl.TEST_HOST,
toggleSubscriptionPath: gl.TEST_HOST,
updateEndpoint: gl.TEST_HOST,
todoPath: gl.TEST_HOST,
todoDeletePath: gl.TEST_HOST,
canAdmin: true,
canUpdate: true,
canDestroy: true,
markdownPreviewPath: '',
markdownDocsPath: '',
issueLinksEndpoint: '/',
epicLinksEndpoint: '/',
groupPath: '',
namespace: 'gitlab-org',
labelsPath: '',
labelsWebUrl: '',
epicsWebUrl: '',
initialTitleHtml: '',
initialTitleText: '',
startDate: '2017-01-01',
endDate: '2017-10-10',
dueDate: '2017-10-10',
startDateFixed: '2017-01-01',
startDateIsFixed: true,
startDateFromMilestones: '',
dueDateFixed: '2017-10-10',
dueDateIsFixed: true,
dueDateFromMilestones: '',
startDateSourcingMilestoneTitle: 'Milestone for Start Date',
startDateSourcingMilestoneDates: {
startDate: '2010-01-01',
dueDate: '2019-12-31',
},
dueDateSourcingMilestoneTitle: 'Milestone for End Date',
dueDateSourcingMilestoneDates: {
startDate: '2020-01-01',
dueDate: '2029-12-31',
},
labels: mockLabels,
participants: mockParticipants,
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 = {
author: {
url: `${gl.TEST_HOST}/url`,
src: `${gl.TEST_HOST}/image`,
username: '@root',
name: 'Administrator',
},
created: new Date().toISOString(),
open: true,
canUpdate: true,
canDelete: true,
};
export const mockDatePickerProps = {
blockClass: 'epic-date',
collapsed: false,
showToggleSidebar: false,
isLoading: false,
editable: true,
label: 'Date',
datePickerLabel: 'Fixed date',
selectedDate: null,
selectedDateIsFixed: true,
dateFromMilestones: null,
dateFixed: null,
dateFromMilestonesTooltip: 'Select an issue with milestone to set date',
isDateInvalid: false,
dateInvalidTooltip: 'Selected date is invalid',
};
export const props = Object.assign({}, contentProps, headerProps);
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import newEpic from 'ee/epics/new_epic/components/new_epic.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('newEpic', () => {
let vm;
let mock;
beforeEach(() => {
const NewEpic = Vue.extend(newEpic);
mock = new MockAdapter(axios);
mock.onPost(gl.TEST_HOST).reply(200, { web_url: gl.TEST_HOST });
vm = mountComponent(NewEpic, {
endpoint: gl.TEST_HOST,
});
});
afterEach(() => {
mock.restore();
vm.$destroy();
});
describe('alignRight', () => {
it('should not add dropdown-menu-right by default', () => {
expect(
vm.$el.querySelector('.dropdown-menu').classList.contains('dropdown-menu-right'),
).toEqual(false);
});
it('should add dropdown-menu-right when alignRight', done => {
vm.alignRight = true;
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.dropdown-menu').classList.contains('dropdown-menu-right'),
).toEqual(true);
done();
});
});
});
describe('creating epic', () => {
it('should call createEpic service', done => {
spyOnDependency(newEpic, 'visitUrl').and.callFake(done);
spyOn(vm.service, 'createEpic').and.callThrough();
vm.title = 'test';
Vue.nextTick(() => {
vm.$el.querySelector('.dropdown-menu .btn-success').click();
expect(vm.service.createEpic).toHaveBeenCalled();
});
});
it('should redirect to epic url after epic creation', done => {
spyOnDependency(newEpic, 'visitUrl').and.callFake(url => {
expect(url).toEqual(gl.TEST_HOST);
done();
});
vm.title = 'test';
Vue.nextTick(() => {
vm.$el.querySelector('.dropdown-menu .btn-success').click();
});
});
it('should toggle loading button while creating', done => {
spyOnDependency(newEpic, 'visitUrl').and.callFake(done);
vm.title = 'test';
Vue.nextTick(() => {
const btnSave = vm.$el.querySelector('.dropdown-menu .btn-success');
const loadingIcon = btnSave.querySelector('.js-loading-button-icon');
expect(loadingIcon).toBeNull();
btnSave.click();
expect(loadingIcon).toBeDefined();
});
});
});
});
import Vue from 'vue';
import _ from 'underscore';
import Cookies from 'js-cookie';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import epicSidebar from 'ee/epics/sidebar/components/sidebar_app.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { props } from 'ee_spec/epics/epic_show/mock_data';
describe('epicSidebar', () => {
let vm;
let originalCookieState;
let EpicSidebar;
const {
epicId,
updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
labels,
parent,
participants,
subscribed,
toggleSubscriptionPath,
todoExists,
todoPath,
todoDeletePath,
startDateIsFixed,
startDateFixed,
startDateFromMilestones,
dueDateIsFixed,
dueDateFixed,
dueDateFromMilestones,
startDateSourcingMilestoneTitle,
startDateSourcingMilestoneDates,
dueDateSourcingMilestoneTitle,
dueDateSourcingMilestoneDates,
} = props;
const defaultPropsData = {
epicId,
endpoint: gl.TEST_HOST,
initialLabels: labels,
initialParticipants: participants,
initialSubscribed: subscribed,
initialTodoExists: todoExists,
initialStartDateIsFixed: startDateIsFixed,
initialStartDateFixed: startDateFixed,
startDateFromMilestones,
initialDueDateIsFixed: dueDateIsFixed,
initialDueDateFixed: dueDateFixed,
dueDateFromMilestones,
updatePath: updateEndpoint,
startDateSourcingMilestoneTitle,
startDateSourcingMilestoneDates,
dueDateSourcingMilestoneTitle,
dueDateSourcingMilestoneDates,
parent,
toggleSubscriptionPath,
labelsPath,
labelsWebUrl,
epicsWebUrl,
todoPath,
todoDeletePath,
};
beforeEach(() => {
setFixtures(`
<div class="page-with-contextual-sidebar right-sidebar-expanded">
<div id="epic-sidebar"></div>
</div>
`);
originalCookieState = Cookies.get('collapsed_gutter');
Cookies.set('collapsed_gutter', null);
EpicSidebar = Vue.extend(epicSidebar);
vm = mountComponent(EpicSidebar, defaultPropsData, '#epic-sidebar');
});
afterEach(() => {
Cookies.set('collapsed_gutter', originalCookieState);
});
it('should initialize service with correct endpoints', () => {
expect(vm.service.endpoint.length).toBeGreaterThan(0);
expect(vm.service.subscriptionEndpoint.length).toBeGreaterThan(0);
expect(vm.service.todoPath.length).toBeGreaterThan(0);
});
it('should render right-sidebar-expanded class when not collapsed', () => {
expect(vm.$el.classList.contains('right-sidebar-expanded')).toEqual(true);
});
it('should render both sidebar-date-picker', () => {
const startDate = '2017-01-01';
const endDate = '2018-01-01';
vm = mountComponent(
EpicSidebar,
Object.assign({}, defaultPropsData, {
initialStartDate: startDate,
initialStartDateFixed: startDate,
initialEndDate: endDate,
initialDueDateFixed: endDate,
}),
);
const startDatePicker = vm.$el.querySelector('.block.start-date');
const endDatePicker = vm.$el.querySelector('.block.end-date');
expect(
startDatePicker.querySelector('.value-type-fixed .value-content').innerText.trim(),
).toEqual('Jan 1, 2017');
expect(
endDatePicker.querySelector('.value-type-fixed .value-content').innerText.trim(),
).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 = {
initialStartDateIsFixed: true,
startDateFromMilestones: '2018-01-01',
initialStartDate: '2017-01-01',
initialDueDateIsFixed: true,
dueDateFromMilestones: '2018-11-31',
initialEndDate: '2018-01-01',
},
) =>
new EpicSidebar({
propsData: Object.assign({}, defaultPropsData, customPropsData),
});
describe('isDateValid', () => {
it('returns true when fixed start and end dates are valid', () => {
const component = getComponent();
expect(component.isDateValid).toBe(true);
});
it('returns false when fixed start and end dates are invalid', () => {
const component = getComponent({
initialStartDate: '2018-01-01',
initialEndDate: '2017-01-01',
});
expect(component.isDateValid).toBe(false);
});
it('returns true when milestone start date and fixed end date is valid', () => {
const component = getComponent({
initialStartDateIsFixed: false,
initialEndDate: '2018-11-31',
});
expect(component.isDateValid).toBe(true);
});
it('returns true when milestone start date and milestone end date is valid', () => {
const component = getComponent({
initialStartDateIsFixed: false,
initialDueDateIsFixed: false,
});
expect(component.isDateValid).toBe(true);
});
});
});
describe('when collapsed', () => {
beforeEach(() => {
Cookies.set('collapsed_gutter', 'true');
vm = mountComponent(
EpicSidebar,
Object.assign({}, defaultPropsData, { initialStartDate: '2017-01-01' }),
);
});
it('should render right-sidebar-collapsed class', () => {
expect(vm.$el.classList.contains('right-sidebar-collapsed')).toEqual(true);
});
it('should render collapsed grouped date picker', () => {
expect(
vm.$el.querySelector('.sidebar-grouped-item .sidebar-collapsed-icon span').innerText.trim(),
).toEqual('From Jan 1 2017');
});
it('should render collapsed labels picker', () => {
expect(
vm.$el.querySelector('.js-labels-block .sidebar-collapsed-icon span').innerText.trim(),
).toEqual('1');
});
});
describe('getDateFromMilestonesTooltip', () => {
it('returns tooltip string for milestone', () => {
expect(vm.getDateFromMilestonesTooltip('start')).toBe(
"To schedule your epic's start date based on milestones, assign a milestone with a start date to any issue in the epic.",
);
});
it('returns tooltip string with milestone dates', () => {
const vmDatesFromMilestones = mountComponent(
EpicSidebar,
Object.assign({}, defaultPropsData, {
startDateFromMilestones: startDateSourcingMilestoneDates.startDate,
dueDateFromMilestones: dueDateSourcingMilestoneDates.dueDate,
}),
);
expect(vmDatesFromMilestones.getDateFromMilestonesTooltip('start')).toBe(
'Milestone for Start Date<br/><span class="text-tertiary">Jan 1, 2010 – Dec 31, 2019</span>',
);
vmDatesFromMilestones.$destroy();
});
it('returns tooltip string with milestone dates when dates are from same year', () => {
const startDate = '2018-01-01';
const dueDate = '2018-03-31';
const vmDatesFromMilestones = mountComponent(
EpicSidebar,
Object.assign({}, defaultPropsData, {
startDateSourcingMilestoneDates: {
startDate,
dueDate,
},
dueDateSourcingMilestoneDates: {
startDate,
dueDate,
},
startDateFromMilestones: startDate,
dueDateFromMilestones: dueDate,
}),
);
expect(vmDatesFromMilestones.getDateFromMilestonesTooltip('start')).toBe(
'Milestone for Start Date<br/><span class="text-tertiary">Jan 1 – Mar 31, 2018</span>',
);
vmDatesFromMilestones.$destroy();
});
it('returns tooltip string containing `No due date` when dueDate from dates sourcing milestone is missing', () => {
const startDate = '2018-01-01';
const dueDate = '2018-03-31';
const vmMissingDueDate = mountComponent(
EpicSidebar,
Object.assign({}, defaultPropsData, {
startDateSourcingMilestoneDates: {
startDate,
dueDate: null,
},
dueDateSourcingMilestoneDates: {
startDate,
dueDate,
},
startDateFromMilestones: startDate,
dueDateFromMilestones: dueDate,
}),
);
expect(vmMissingDueDate.getDateFromMilestonesTooltip('start')).toBe(
'Milestone for Start Date<br/><span class="text-tertiary">Jan 1, 2018 – No due date</span>',
);
vmMissingDueDate.$destroy();
});
it('returns tooltip string containing `No start date` when startDate from dates sourcing milestone is missing', () => {
const startDate = '2018-01-01';
const dueDate = '2018-03-31';
const vmMissingStartDate = mountComponent(
EpicSidebar,
Object.assign({}, defaultPropsData, {
startDateSourcingMilestoneDates: {
startDate: null,
dueDate,
},
dueDateSourcingMilestoneDates: {
startDate,
dueDate,
},
startDateFromMilestones: startDate,
dueDateFromMilestones: dueDate,
}),
);
expect(vmMissingStartDate.getDateFromMilestonesTooltip('start')).toBe(
'Milestone for Start Date<br/><span class="text-tertiary">No start date – Mar 31, 2018</span>',
);
vmMissingStartDate.$destroy();
});
});
describe('toggleSidebar', () => {
it('should toggle collapsed_gutter cookie', () => {
expect(vm.$el.classList.contains('right-sidebar-expanded')).toEqual(true);
vm.$el.querySelector('.gutter-toggle').click();
expect(Cookies.get('collapsed_gutter')).toEqual('true');
});
it('should toggle contentContainer css class', () => {
const contentContainer = document.querySelector('.page-with-contextual-sidebar');
expect(contentContainer.classList.contains('right-sidebar-expanded')).toEqual(true);
expect(contentContainer.classList.contains('right-sidebar-collapsed')).toEqual(false);
vm.$el.querySelector('.gutter-toggle').click();
expect(contentContainer.classList.contains('right-sidebar-expanded')).toEqual(false);
expect(contentContainer.classList.contains('right-sidebar-collapsed')).toEqual(true);
});
});
describe('saveDate', () => {
let component;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onPut(gl.TEST_HOST).reply(() => [200, JSON.stringify({})]);
component = new EpicSidebar({
propsData: defaultPropsData,
});
});
afterEach(() => {
mock.restore();
});
it('should save startDate', done => {
const date = '2017-01-01';
expect(component.store.startDate).toBeUndefined();
component
.saveStartDate(date)
.then(() => {
expect(component.store.startDate).toEqual(date);
done();
})
.catch(done.fail);
});
it('should save endDate', done => {
const date = '2017-01-01';
expect(component.store.endDate).toBeUndefined();
component
.saveEndDate(date)
.then(() => {
expect(component.store.endDate).toEqual(date);
done();
})
.catch(done.fail);
});
it('should change start date type as from milestones', done => {
spyOn(component.service, 'updateStartDate').and.callThrough();
const dateValue = '2017-01-01';
component.saveDate('start', dateValue, false);
Vue.nextTick()
.then(() => {
expect(component.service.updateStartDate).toHaveBeenCalledWith({
dateValue,
isFixed: false,
});
})
.then(done)
.catch(done.fail);
});
it('should change start date type as fixed', done => {
spyOn(component.service, 'updateStartDate').and.callThrough();
const dateValue = '2017-04-01';
component.saveDate('start', dateValue, true);
// Using setTimeout instead of Vue.nextTick
// as otherwise store updates are not reflected correctly
setTimeout(() => {
expect(component.service.updateStartDate).toHaveBeenCalledWith({
dateValue,
isFixed: true,
});
expect(component.store.startDateFixed).toBe(dateValue);
done();
}, 0);
});
it('should change end date type as from milestones', done => {
spyOn(component.service, 'updateEndDate').and.callThrough();
const dateValue = '2017-01-01';
component.saveDate('end', dateValue, false);
Vue.nextTick()
.then(() => {
expect(component.service.updateEndDate).toHaveBeenCalledWith({
dateValue,
isFixed: false,
});
})
.then(done)
.catch(done.fail);
});
it('should change end date type as fixed', done => {
spyOn(component.service, 'updateEndDate').and.callThrough();
const dateValue = '2017-04-01';
component.saveDate('end', dateValue, true);
// Using setTimeout instead of Vue.nextTick
// as otherwise store updates are not reflected correctly
setTimeout(() => {
expect(component.service.updateEndDate).toHaveBeenCalledWith({
dateValue,
isFixed: true,
});
expect(component.store.dueDateFixed).toBe(dateValue);
done();
}, 0);
});
});
describe('handleLabelClick', () => {
const label = {
id: 1,
title: 'Foo',
color: ['#BADA55'],
text_color: '#FFFFFF',
};
it('initializes `epicContext.labels` as empty array when `label.isAny` is `true`', () => {
const labelIsAny = { isAny: true };
vm.handleLabelClick(labelIsAny);
expect(Array.isArray(vm.epicContext.labels)).toBe(true);
expect(vm.epicContext.labels.length).toBe(0);
});
it('adds provided `label` to epicContext.labels', () => {
vm.handleLabelClick(label);
// epicContext.labels gets initialized with initialLabels, hence
// newly insert label will be at second position (index `1`)
expect(vm.epicContext.labels.length).toBe(2);
expect(vm.epicContext.labels[1].id).toBe(label.id);
vm.handleLabelClick(label);
});
it('filters epicContext.labels to exclude provided `label` if it is already present in `epicContext.labels`', () => {
vm.handleLabelClick(label); // Select
vm.handleLabelClick(label); // Un-select
expect(vm.epicContext.labels.length).toBe(1);
expect(vm.epicContext.labels[0].id).toBe(labels[0].id);
});
});
describe('handleDropdownClose', () => {
it('calls toggleSidebar when `autoExpanded` prop is true', () => {
spyOn(vm, 'toggleSidebar');
vm.autoExpanded = true;
vm.handleDropdownClose();
expect(vm.autoExpanded).toBe(false);
expect(vm.toggleSidebar).toHaveBeenCalled();
});
it('does not call toggleSidebar when `autoExpanded` prop is false', () => {
spyOn(vm, 'toggleSidebar');
vm.autoExpanded = false;
vm.handleDropdownClose();
expect(vm.autoExpanded).toBe(false);
expect(vm.toggleSidebar).not.toHaveBeenCalled();
});
});
describe('handleToggleTodo', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
setFixtures('<div class="flash-container"></div>');
});
afterEach(() => {
document.querySelector('.flash-container').remove();
mock.restore();
});
it('calls `addTodo` on service object when `todoExists` prop is `false`', () => {
spyOn(vm.service, 'addTodo').and.callThrough();
vm.store.setTodoExists(false);
expect(vm.savingTodoAction).toBe(false);
vm.handleToggleTodo();
expect(vm.savingTodoAction).toBe(true);
expect(vm.service.addTodo).toHaveBeenCalledWith(epicId);
});
it('calls `addTodo` on service and sets response on store when request is successful', done => {
mock.onPost(gl.TEST_HOST).reply(200, {
delete_path: '/foo/bar',
count: 1,
});
spyOn(vm.service, 'addTodo').and.callThrough();
vm.store.setTodoExists(false);
vm.handleToggleTodo();
setTimeout(() => {
expect(vm.savingTodoAction).toBe(false);
expect(vm.store.todoDeletePath).toBe('/foo/bar');
expect(vm.store.todoExists).toBe(true);
done();
}, 0);
});
it('calls `addTodo` on service and shows Flash error when request is unsuccessful', done => {
mock.onPost(gl.TEST_HOST).reply(500, {});
spyOn(vm.service, 'addTodo').and.callThrough();
vm.store.setTodoExists(false);
vm.handleToggleTodo();
setTimeout(() => {
expect(vm.savingTodoAction).toBe(false);
expect(document.querySelector('.flash-text').innerText.trim()).toBe(
'There was an error adding a todo.',
);
done();
}, 0);
});
it('calls `deleteTodo` on service object when `todoExists` prop is `true`', () => {
spyOn(vm.service, 'deleteTodo').and.callThrough();
vm.store.setTodoExists(true);
expect(vm.savingTodoAction).toBe(false);
vm.handleToggleTodo();
expect(vm.savingTodoAction).toBe(true);
expect(vm.service.deleteTodo).toHaveBeenCalledWith(gl.TEST_HOST);
});
it('calls `deleteTodo` on service and sets response on store when request is successful', done => {
mock.onDelete(gl.TEST_HOST).reply(200, {
count: 1,
});
spyOn(vm.service, 'deleteTodo').and.callThrough();
vm.store.setTodoExists(true);
vm.handleToggleTodo();
setTimeout(() => {
expect(vm.savingTodoAction).toBe(false);
expect(vm.store.todoExists).toBe(false);
done();
}, 0);
});
it('calls `deleteTodo` on service and shows Flash error when request is unsuccessful', done => {
mock.onDelete(gl.TEST_HOST).reply(500, {});
spyOn(vm.service, 'deleteTodo').and.callThrough();
vm.store.setTodoExists(true);
vm.handleToggleTodo();
setTimeout(() => {
expect(vm.savingTodoAction).toBe(false);
expect(document.querySelector('.flash-text').innerText.trim()).toBe(
'There was an error deleting the todo.',
);
done();
}, 0);
});
});
describe('saveDate error', () => {
let interceptor;
let component;
beforeEach(() => {
interceptor = (request, next) => {
next(
request.respondWith(JSON.stringify({}), {
status: 500,
}),
);
};
Vue.http.interceptors.push(interceptor);
component = new EpicSidebar({
propsData: defaultPropsData,
});
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should handle errors gracefully', done => {
const date = '2017-01-01';
expect(component.store.startDate).toBeUndefined();
component
.saveDate('start', date)
.then(() => {
expect(component.store.startDate).toBeUndefined();
done();
})
.catch(done.fail);
});
});
});
import Vue from 'vue';
import SidebarDatepicker from 'ee/epics/sidebar/components/sidebar_date_picker.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockDatePickerProps } from 'ee_spec/epics/epic_show/mock_data';
const createComponent = (datePickerProps = mockDatePickerProps) => {
const Component = Vue.extend(SidebarDatepicker);
return mountComponent(Component, datePickerProps);
};
describe('SidebarParticipants', () => {
window.gon = { gitlab_url: gl.TEST_HOST };
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('data', () => {
it('return data props with uniqueId for `fieldName`', () => {
expect(vm.fieldName).toContain('dateType_');
});
});
describe('computed', () => {
describe('selectedAndEditable', () => {
it('returns `true` when both `selectedDate` is defined and `editable` is true', done => {
vm.selectedDate = new Date();
Vue.nextTick()
.then(() => {
expect(vm.selectedAndEditable).toBe(true);
})
.then(done)
.catch(done.fail);
});
});
describe('selectedDateWords', () => {
it('returns full date string in words based on `selectedDate` prop value', done => {
vm.selectedDate = new Date(2018, 0, 1);
Vue.nextTick()
.then(() => {
expect(vm.selectedDateWords).toBe('Jan 1, 2018');
})
.then(done)
.catch(done.fail);
});
});
describe('dateFixedWords', () => {
it('returns full date string in words based on `dateFixed` prop value', done => {
vm.dateFixed = new Date(2018, 0, 1);
Vue.nextTick()
.then(() => {
expect(vm.dateFixedWords).toBe('Jan 1, 2018');
})
.then(done)
.catch(done.fail);
});
});
describe('dateFromMilestonesWords', () => {
it('returns full date string in words when `dateFromMilestones` is defined', done => {
vm.dateFromMilestones = new Date(2018, 0, 1);
Vue.nextTick()
.then(() => {
expect(vm.dateFromMilestonesWords).toBe('Jan 1, 2018');
})
.then(done)
.catch(done.fail);
});
it('returns `None` when `dateFromMilestones` is not defined', () => {
expect(vm.dateFromMilestonesWords).toBe('None');
});
});
describe('collapsedText', () => {
it('returns value of `selectedDateWords` when it is defined', done => {
vm.selectedDate = new Date(2018, 0, 1);
Vue.nextTick()
.then(() => {
expect(vm.collapsedText).toBe('Jan 1, 2018');
})
.then(done)
.catch(done.fail);
});
it('returns `None` when `selectedDateWords` is not defined', () => {
expect(vm.collapsedText).toBe('None');
});
});
describe('popoverOptions', () => {
it('returns popover config object containing title with appropriate string', () => {
expect(vm.popoverOptions.title).toBe(
'These dates affect how your epics appear in the roadmap. Dates from milestones come from the milestones assigned to issues in the epic. You can also set fixed dates or remove them entirely.',
);
});
it('returns popover config object containing `content` with href pointing to correct documentation', () => {
const hrefContent = vm.popoverOptions.content.trim();
expect(hrefContent).toContain(
`${gon.gitlab_url}/help/user/group/epics/index.md#start-date-and-due-date`,
);
expect(hrefContent).toContain('More information');
});
});
describe('dateInvalidPopoverOptions', () => {
it('returns popover config object containing title with appropriate string', () => {
expect(vm.dateInvalidPopoverOptions.title).toBe('Selected date is invalid');
});
it('returns popover config object containing `content` with href pointing to correct documentation', () => {
const hrefContent = vm.dateInvalidPopoverOptions.content.trim();
expect(hrefContent).toContain(
`${gon.gitlab_url}/help/user/group/epics/index.md#start-date-and-due-date`,
);
expect(hrefContent).toContain('How can I solve this?');
});
});
});
describe('methods', () => {
describe('getPopoverConfig', () => {
it('returns popover config object with provided `title` and `content` values', () => {
const title = 'Popover title';
const content = 'This is a popover content';
const popoverConfig = vm.getPopoverConfig({ title, content });
const expectedPopoverConfig = {
html: true,
trigger: 'focus',
container: 'body',
boundary: 'viewport',
template: '<div class="popover-header"></div>',
title,
content,
};
Object.keys(popoverConfig).forEach(key => {
if (key === 'template') {
expect(popoverConfig[key]).toContain(expectedPopoverConfig[key]);
} else {
expect(popoverConfig[key]).toBe(expectedPopoverConfig[key]);
}
});
});
});
describe('stopEditing', () => {
it('sets `editing` prop to `false` and emits `toggleDateType` event on component', () => {
spyOn(vm, '$emit');
vm.stopEditing();
expect(vm.editing).toBe(false);
expect(vm.$emit).toHaveBeenCalledWith('toggleDateType', true, true);
});
});
describe('toggleDatePicker', () => {
it('flips value of `editing` prop from `true` to `false` and vice-versa', () => {
vm.editing = true;
vm.toggleDatePicker();
expect(vm.editing).toBe(false);
});
});
describe('newDateSelected', () => {
it('sets `editing` prop to `false` and emits `saveDate` event on component', () => {
spyOn(vm, '$emit');
const date = new Date();
vm.newDateSelected(date);
expect(vm.editing).toBe(false);
expect(vm.$emit).toHaveBeenCalledWith('saveDate', date);
});
});
describe('toggleDateType', () => {
it('emits `toggleDateType` event on component', () => {
spyOn(vm, '$emit');
vm.toggleDateType(true);
expect(vm.$emit).toHaveBeenCalledWith('toggleDateType', true);
});
});
describe('toggleSidebar', () => {
it('emits `toggleCollapse` event on component', () => {
spyOn(vm, '$emit');
vm.toggleSidebar();
expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
});
});
});
describe('template', () => {
it('renders component container element', () => {
expect(vm.$el.classList.contains('block', 'date', 'epic-date')).toBe(true);
});
it('renders collapsed calendar icon component', () => {
expect(vm.$el.querySelector('.sidebar-collapsed-icon')).not.toBe(null);
});
it('renders collapse button when `showToggleSidebar` prop is `true`', done => {
vm.showToggleSidebar = true;
Vue.nextTick()
.then(() => {
expect(vm.$el.querySelector('button.btn-sidebar-action')).not.toBe(null);
})
.then(done)
.catch(done.fail);
});
it('renders title element', () => {
expect(vm.$el.querySelector('.title')).not.toBe(null);
});
it('renders loading icon when `isLoading` prop is true', done => {
vm.isLoading = true;
Vue.nextTick()
.then(() => {
expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
})
.then(done)
.catch(done.fail);
});
it('renders help icon', () => {
const helpIconEl = vm.$el.querySelector('.help-icon');
expect(helpIconEl).not.toBe(null);
expect(helpIconEl.getAttribute('tabindex')).toBe('0');
expect(helpIconEl.querySelector('use').getAttribute('xlink:href')).toContain('question-o');
});
it('renderts edit button', () => {
const buttonEl = vm.$el.querySelector('button.btn-sidebar-action');
expect(buttonEl).not.toBe(null);
expect(buttonEl.innerText.trim()).toBe('Edit');
});
it('renders value container element', () => {
expect(vm.$el.querySelector('.value .value-type-fixed')).not.toBe(null);
expect(vm.$el.querySelector('.value .value-type-dynamic')).not.toBe(null);
});
it('renders fixed type date selection element', () => {
const valueFixedEl = vm.$el.querySelector('.value .value-type-fixed');
expect(valueFixedEl.querySelector('input[type="radio"]')).not.toBe(null);
expect(valueFixedEl.innerText.trim()).toContain('Fixed:');
expect(valueFixedEl.querySelector('.value-content').innerText.trim()).toContain('None');
});
it('renders dynamic type date selection element', () => {
const valueDynamicEl = vm.$el.querySelector('.value abbr.value-type-dynamic');
expect(valueDynamicEl.querySelector('input[type="radio"]')).not.toBe(null);
expect(valueDynamicEl.innerText.trim()).toContain('From milestones:');
expect(valueDynamicEl.querySelector('.value-content').innerText.trim()).toContain('None');
});
it('renders date warning icon when `isDateInvalid` prop is `true`', done => {
vm.isDateInvalid = true;
vm.selectedDateIsFixed = false;
Vue.nextTick()
.then(() => {
const warningIconEl = vm.$el.querySelector('.date-warning-icon');
expect(warningIconEl).not.toBe(null);
expect(warningIconEl.getAttribute('tabindex')).toBe('0');
expect(warningIconEl.querySelector('use').getAttribute('xlink:href')).toContain(
'warning',
);
})
.then(done)
.catch(done.fail);
});
});
});
import Vue from 'vue';
import SidebarParticipants from 'ee/epics/sidebar/components/sidebar_participants.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockParticipants } from 'ee_spec/epics/epic_show/mock_data';
const createComponent = () => {
const Component = Vue.extend(SidebarParticipants);
return mountComponent(Component, {
participants: mockParticipants,
});
};
describe('SidebarParticipants', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('methods', () => {
describe('onToggleSidebar', () => {
it('emits `toggleCollapse` event on component', () => {
spyOn(vm, '$emit');
vm.onToggleSidebar();
expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
});
});
});
describe('template', () => {
it('renders component container element with classes `block participants`', () => {
expect(vm.$el.classList.contains('block', 'participants')).toBe(true);
});
it('renders participants list element', () => {
expect(vm.$el.querySelector('.participants-list')).not.toBeNull();
expect(vm.$el.querySelectorAll('.js-participants-author').length).toBe(
mockParticipants.length,
);
});
});
});
import Vue from 'vue';
import SidebarSubscriptions from 'ee/epics/sidebar/components/sidebar_subscriptions.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = () => {
const Component = Vue.extend(SidebarSubscriptions);
return mountComponent(Component, {
loading: false,
subscribed: true,
});
};
describe('SidebarSubscriptions', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('methods', () => {
describe('onToggleSubscription', () => {
it('emits `toggleSubscription` event on component', () => {
spyOn(vm, '$emit');
vm.onToggleSubscription();
expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription');
});
});
describe('onToggleSidebar', () => {
it('emits `toggleCollapse` event on component', () => {
spyOn(vm, '$emit');
vm.onToggleSidebar();
expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
});
});
});
describe('template', () => {
it('renders component container element with classes `block subscriptions`', () => {
expect(vm.$el.classList.contains('block', 'subscriptions')).toBe(true);
});
it('renders subscription toggle element', () => {
expect(vm.$el.querySelector('.project-feature-toggle')).not.toBeNull();
});
});
});
import axios from '~/lib/utils/axios_utils';
import SidebarService from 'ee/epics/sidebar/services/sidebar_service';
describe('Sidebar Service', () => {
let service;
beforeEach(() => {
service = new SidebarService({
endpoint: gl.TEST_HOST,
subscriptionEndpoint: gl.TEST_HOST,
todoPath: gl.TEST_HOST,
});
});
describe('updateStartDate', () => {
it('returns axios instance with PUT for `endpoint` with `start_date_is_fixed` and `start_date_fixed` as request body', () => {
spyOn(axios, 'put').and.stub();
const dateValue = '2018-06-21';
service.updateStartDate({ dateValue, isFixed: true });
expect(axios.put).toHaveBeenCalledWith(service.endpoint, {
start_date_is_fixed: true,
start_date_fixed: dateValue,
});
});
});
describe('updateEndDate', () => {
it('returns axios instance with PUT for `endpoint` with `due_date_is_fixed` and `due_date_fixed` as request body', () => {
spyOn(axios, 'put').and.stub();
const dateValue = '2018-06-21';
service.updateEndDate({ dateValue, isFixed: true });
expect(axios.put).toHaveBeenCalledWith(service.endpoint, {
due_date_is_fixed: true,
due_date_fixed: dateValue,
});
});
});
describe('toggleSubscribed', () => {
it('returns axios instance with POST for `subscriptionEndpoint`', () => {
spyOn(axios, 'post').and.stub();
service.toggleSubscribed();
expect(axios.post).toHaveBeenCalled();
});
});
describe('addTodo', () => {
it('returns axios instance with POST for `todoPath` with `issuable_id` and `issuable_type` as request body', () => {
spyOn(axios, 'post').and.stub();
const epicId = 1;
service.addTodo(epicId);
expect(axios.post).toHaveBeenCalledWith(service.todoPath, {
issuable_id: epicId,
issuable_type: 'epic',
});
});
});
describe('deleteTodo', () => {
it('returns axios instance with DELETE for provided `todoDeletePath` param', () => {
spyOn(axios, 'delete').and.stub();
service.deleteTodo('/foo/bar');
expect(axios.delete).toHaveBeenCalledWith('/foo/bar');
});
});
});
import SidebarStore from 'ee/epics/sidebar/stores/sidebar_store';
describe('Sidebar Store', () => {
const dateString = '2017-01-20';
describe('constructor', () => {
it('should set startDate', () => {
const store = new SidebarStore({
startDate: dateString,
});
expect(store.startDate).toEqual(dateString);
});
it('should set endDate', () => {
const store = new SidebarStore({
endDate: dateString,
});
expect(store.endDate).toEqual(dateString);
});
});
describe('startDateTime', () => {
it('should return null when there is no startDate', () => {
const store = new SidebarStore({});
expect(store.startDateTime).toEqual(null);
});
it('should return date', () => {
const store = new SidebarStore({
startDate: dateString,
});
const date = store.startDateTime;
expect(date.getDate()).toEqual(20);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2017);
});
});
describe('startDateTimeFixed', () => {
it('should return null when there is no startDateFixed', () => {
const store = new SidebarStore({});
expect(store.startDateTimeFixed).toEqual(null);
});
it('should return date', () => {
const store = new SidebarStore({
startDateFixed: dateString,
});
const date = store.startDateTimeFixed;
expect(date.getDate()).toEqual(20);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2017);
});
});
describe('endDateTime', () => {
it('should return null when there is no endDate', () => {
const store = new SidebarStore({});
expect(store.endDateTime).toEqual(null);
});
it('should return date', () => {
const store = new SidebarStore({
endDate: dateString,
});
const date = store.endDateTime;
expect(date.getDate()).toEqual(20);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2017);
});
});
describe('dueDateTimeFixed', () => {
it('should return null when there is no dueDateFixed', () => {
const store = new SidebarStore({});
expect(store.dueDateTimeFixed).toEqual(null);
});
it('should return date', () => {
const store = new SidebarStore({
dueDateFixed: dateString,
});
const date = store.dueDateTimeFixed;
expect(date.getDate()).toEqual(20);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2017);
});
});
describe('setSubscribed', () => {
it('should set store.subscribed value', () => {
const store = new SidebarStore({ subscribed: true });
store.setSubscribed(false);
expect(store.subscribed).toEqual(false);
});
});
describe('setTodoExists', () => {
it('should set store.subscribed value', () => {
const store = new SidebarStore({ todoExists: true });
store.setTodoExists(false);
expect(store.todoExists).toEqual(false);
});
});
describe('setTodoDeletePath', () => {
it('should set store.subscribed value', () => {
const store = new SidebarStore({ todoDeletePath: gl.TEST_HOST });
store.setTodoDeletePath('/foo/bar');
expect(store.todoDeletePath).toEqual('/foo/bar');
});
});
});
......@@ -3518,9 +3518,6 @@ msgstr ""
msgid "Epic"
msgstr ""
msgid "Epic will be removed! Are you sure?"
msgstr ""
msgid "Epics"
msgstr ""
......@@ -3530,9 +3527,6 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
msgid "Epics|An error occurred while saving %{epicDateType} date"
msgstr ""
msgid "Epics|An error occurred while saving the %{epicDateType} date"
msgstr ""
......@@ -11433,9 +11427,6 @@ msgstr ""
msgid "to help your contributors communicate effectively!"
msgstr ""
msgid "toggle collapse"
msgstr ""
msgid "triggered"
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