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>
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 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