Commit 7fa1421c authored by Kushal Pandya's avatar Kushal Pandya

Add support for start and due dates in Epic sidebar

Adds start and due date pickers within sidebar.
Supports for expand and collapsed sidebar states.
parent 6b479aa5
...@@ -5,21 +5,105 @@ import epicUtils from '../utils/epic_utils'; ...@@ -5,21 +5,105 @@ import epicUtils from '../utils/epic_utils';
import SidebarHeader from './sidebar_items/sidebar_header.vue'; import SidebarHeader from './sidebar_items/sidebar_header.vue';
import SidebarTodo from './sidebar_items/sidebar_todo.vue'; import SidebarTodo from './sidebar_items/sidebar_todo.vue';
import SidebarDatePicker from './sidebar_items/sidebar_date_picker.vue';
import SidebarDatePickerCollapsed from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import { dateTypes } from '../constants';
export default { export default {
dateTypes,
components: { components: {
SidebarHeader, SidebarHeader,
SidebarTodo, SidebarTodo,
SidebarDatePicker,
SidebarDatePickerCollapsed,
}, },
computed: { computed: {
...mapState(['sidebarCollapsed']), ...mapState([
...mapGetters(['isUserSignedIn']), 'canUpdate',
'sidebarCollapsed',
'startDateSourcingMilestoneTitle',
'startDateSourcingMilestoneDates',
'startDateIsFixed',
'startDateFixed',
'startDateFromMilestones',
'dueDateSourcingMilestoneTitle',
'dueDateSourcingMilestoneDates',
'dueDateIsFixed',
'dueDateFixed',
'dueDateFromMilestones',
'epicStartDateSaveInProgress',
'epicDueDateSaveInProgress',
]),
...mapGetters([
'isUserSignedIn',
'isDateInvalid',
'startDateTimeFixed',
'startDateTimeFromMilestones',
'startDateTime',
'startDateForCollapsedSidebar',
'dueDateTimeFixed',
'dueDateTimeFromMilestones',
'dueDateTime',
'dueDateForCollapsedSidebar',
]),
}, },
mounted() { mounted() {
this.toggleSidebarFlag(epicUtils.getCollapsedGutter()); this.toggleSidebarFlag(epicUtils.getCollapsedGutter());
}, },
methods: { methods: {
...mapActions(['toggleSidebarFlag']), ...mapActions([
'toggleSidebar',
'toggleSidebarFlag',
'toggleStartDateType',
'toggleDueDateType',
'saveDate',
]),
getDateFromMilestonesTooltip(dateType) {
return epicUtils.getDateFromMilestonesTooltip({
dateType,
startDateSourcingMilestoneTitle: this.startDateSourcingMilestoneTitle,
startDateSourcingMilestoneDates: this.startDateSourcingMilestoneDates,
startDateTimeFromMilestones: this.startDateTimeFromMilestones,
dueDateSourcingMilestoneTitle: this.dueDateSourcingMilestoneTitle,
dueDateSourcingMilestoneDates: this.dueDateSourcingMilestoneDates,
dueDateTimeFromMilestones: this.dueDateTimeFromMilestones,
});
},
changeStartDateType(dateTypeIsFixed, typeChangeOnEdit) {
this.toggleStartDateType({ dateTypeIsFixed });
if (!typeChangeOnEdit) {
this.saveDate({
newDate: dateTypeIsFixed ? this.startDateFixed : this.startDateFromMilestones,
dateType: dateTypes.start,
dateTypeIsFixed,
});
}
},
saveStartDate(date) {
this.saveDate({
dateType: dateTypes.start,
newDate: date,
dateTypeIsFixed: true,
});
},
changeDueDateType(dateTypeIsFixed, typeChangeOnEdit) {
this.toggleDueDateType({ dateTypeIsFixed });
if (!typeChangeOnEdit) {
this.saveDate({
newDate: dateTypeIsFixed ? this.dueDateFixed : this.dueDateFromMilestones,
dateType: dateTypes.due,
dateTypeIsFixed,
});
}
},
saveDueDate(date) {
this.saveDate({
dateType: dateTypes.due,
newDate: date,
dateTypeIsFixed: true,
});
},
}, },
}; };
</script> </script>
...@@ -39,6 +123,55 @@ export default { ...@@ -39,6 +123,55 @@ export default {
v-show="sidebarCollapsed && isUserSignedIn" v-show="sidebarCollapsed && isUserSignedIn"
:sidebar-collapsed="sidebarCollapsed" :sidebar-collapsed="sidebarCollapsed"
/> />
<sidebar-date-picker
v-show="!sidebarCollapsed"
:can-update="canUpdate"
:sidebar-collapsed="sidebarCollapsed"
:show-toggle-sidebar="!isUserSignedIn"
:label="__('Start date')"
:date-picker-label="__('Fixed start date')"
:date-invalid-tooltip="
__('This date is after the due date, so this epic won\'t appear in the roadmap.')
"
:date-from-milestones-tooltip="getDateFromMilestonesTooltip($options.dateTypes.start)"
:date-save-in-progress="epicStartDateSaveInProgress"
:selected-date-is-fixed="startDateIsFixed"
:date-fixed="startDateTimeFixed"
:date-from-milestones="startDateTimeFromMilestones"
:selected-date="startDateTime"
:is-date-invalid="isDateInvalid"
block-class="start-date"
@toggleCollapse="toggleSidebar({ sidebarCollapsed })"
@toggleDateType="changeStartDateType"
@saveDate="saveStartDate"
/>
<sidebar-date-picker
v-show="!sidebarCollapsed"
:can-update="canUpdate"
:sidebar-collapsed="sidebarCollapsed"
:label="__('Due date')"
:date-picker-label="__('Fixed due date')"
:date-invalid-tooltip="
__('This date is before the start date, so this epic won\'t appear in the roadmap.')
"
:date-from-milestones-tooltip="getDateFromMilestonesTooltip($options.dateTypes.due)"
:date-save-in-progress="epicDueDateSaveInProgress"
:selected-date-is-fixed="dueDateIsFixed"
:date-fixed="dueDateTimeFixed"
:date-from-milestones="dueDateTimeFromMilestones"
:selected-date="dueDateTime"
:is-date-invalid="isDateInvalid"
block-class="due-date"
@toggleDateType="changeDueDateType"
@saveDate="saveDueDate"
/>
<sidebar-date-picker-collapsed
v-show="sidebarCollapsed"
:collapsed="sidebarCollapsed"
:min-date="startDateForCollapsedSidebar"
:max-date="dueDateForCollapsedSidebar"
@toggleCollapse="toggleSidebar({ sidebarCollapsed })"
/>
</div> </div>
</aside> </aside>
</template> </template>
<script>
import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab/ui';
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';
const label = __('Date picker');
const pickerLabel = __('Fixed date');
export default {
directives: {
tooltip,
popover,
},
components: {
Icon,
DatePicker,
CollapsedCalendarIcon,
ToggleSidebar,
GlLoadingIcon,
},
props: {
sidebarCollapsed: {
type: Boolean,
required: false,
default: true,
},
label: {
type: String,
required: false,
default: label,
},
datePickerLabel: {
type: String,
required: false,
default: pickerLabel,
},
dateInvalidTooltip: {
type: String,
required: false,
default: '',
},
blockClass: {
type: String,
required: false,
default: '',
},
showToggleSidebar: {
type: Boolean,
required: false,
default: false,
},
dateSaveInProgress: {
type: Boolean,
required: false,
default: false,
},
selectedDateIsFixed: {
type: Boolean,
required: false,
default: true,
},
dateFixed: {
type: Date,
required: false,
default: null,
},
dateFromMilestones: {
type: Date,
required: false,
default: null,
},
selectedDate: {
type: Date,
required: false,
default: null,
},
dateFromMilestonesTooltip: {
type: String,
required: false,
default: '',
},
canUpdate: {
type: Boolean,
required: false,
default: false,
},
isDateInvalid: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
fieldName: _.uniqueId('dateType_'),
editing: false,
};
},
computed: {
selectedAndEditable() {
return this.selectedDate && this.canUpdate;
},
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="dateSaveInProgress" :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="canUpdate && !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="sidebarCollapsed"
@toggle="toggleSidebar"
/>
</div>
</div>
<div class="value">
<div
:class="{ 'is-option-selected': selectedDateIsFixed, 'd-flex': !editing }"
class="value-type-fixed"
>
<input
v-if="canUpdate && !editing"
: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="canUpdate"
: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>
...@@ -7,3 +7,8 @@ export const statusEvent = { ...@@ -7,3 +7,8 @@ export const statusEvent = {
close: 'close', close: 'close',
reopen: 'reopen', reopen: 'reopen',
}; };
export const dateTypes = {
start: 'start',
due: 'due',
};
import flash from '~/flash'; import flash from '~/flash';
import { __ } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import epicUtils from '../utils/epic_utils'; import epicUtils from '../utils/epic_utils';
import { statusType, statusEvent } from '../constants'; import { statusType, statusEvent, dateTypes } from '../constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -105,5 +105,50 @@ export const toggleTodo = ({ state, dispatch }) => { ...@@ -105,5 +105,50 @@ export const toggleTodo = ({ state, dispatch }) => {
}); });
}; };
/**
* Methods to handle Epic start and due date manipulations from sidebar
*/
export const toggleStartDateType = ({ commit }, data) =>
commit(types.TOGGLE_EPIC_START_DATE_TYPE, data);
export const toggleDueDateType = ({ commit }, data) =>
commit(types.TOGGLE_EPIC_DUE_DATE_TYPE, data);
export const requestEpicDateSave = ({ commit }, data) => commit(types.REQUEST_EPIC_DATE_SAVE, data);
export const requestEpicDateSaveSuccess = ({ commit }, data) =>
commit(types.REQUEST_EPIC_DATE_SAVE_SUCCESS, data);
export const requestEpicDateSaveFailure = ({ commit }, data) => {
commit(types.REQUEST_EPIC_DATE_SAVE_FAILURE, data);
flash(
sprintf(s__('Epics|An error occurred while saving the %{epicDateType} date'), {
epicDateType: dateTypes.start === data.dateType ? s__('Epics|start') : s__('Epics|due'),
}),
);
};
export const saveDate = ({ state, dispatch }, { dateType, dateTypeIsFixed, newDate }) => {
const requestBody = {
[dateType === dateTypes.start ? 'start_date_is_fixed' : 'due_date_is_fixed']: dateTypeIsFixed,
};
if (dateTypeIsFixed) {
requestBody[dateType === dateTypes.start ? 'start_date_fixed' : 'due_date_fixed'] = newDate;
}
dispatch('requestEpicDateSave', { dateType });
axios
.put(state.endpoint, requestBody)
.then(() => {
dispatch('requestEpicDateSaveSuccess', {
dateType,
dateTypeIsFixed,
newDate,
});
})
.catch(() => {
dispatch('requestEpicDateSaveFailure', {
dateType,
dateTypeIsFixed: !dateTypeIsFixed,
});
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
import { parsePikadayDate } from '~/lib/utils/datetime_utility';
import epicUtils from '../utils/epic_utils';
import { statusType } from '../constants'; import { statusType } from '../constants';
export const isEpicOpen = state => state.state === statusType.open; export const isEpicOpen = state => state.state === statusType.open;
export const isUserSignedIn = () => !!gon.current_user_id; export const isUserSignedIn = () => !!gon.current_user_id;
export const startDateTime = state => (state.startDate ? parsePikadayDate(state.startDate) : null);
export const startDateTimeFixed = state =>
state.startDateFixed ? parsePikadayDate(state.startDateFixed) : null;
export const startDateTimeFromMilestones = state =>
state.startDateFromMilestones ? parsePikadayDate(state.startDateFromMilestones) : null;
export const dueDateTime = state => (state.dueDate ? parsePikadayDate(state.dueDate) : null);
export const dueDateTimeFixed = state =>
state.dueDateFixed ? parsePikadayDate(state.dueDateFixed) : null;
export const dueDateTimeFromMilestones = state =>
state.dueDateFromMilestones ? parsePikadayDate(state.dueDateFromMilestones) : null;
export const startDateForCollapsedSidebar = (state, getters) =>
state.startDateIsFixed ? getters.startDateTime : getters.startDateTimeFromMilestones;
export const dueDateForCollapsedSidebar = (state, getters) =>
state.dueDateIsFixed ? getters.dueDateTime : getters.dueDateTimeFromMilestones;
/**
* This getter determines if epic dates
* are valid (i.e. given start date is less than given due date)
*/
export const isDateInvalid = (state, getters) => {
const { startDateIsFixed, dueDateIsFixed } = state;
if (startDateIsFixed && dueDateIsFixed) {
// When Epic start and finish dates are of type fixed.
return !epicUtils.getDateValidity(getters.startDateTime, getters.dueDateTime);
} else if (!startDateIsFixed && dueDateIsFixed) {
// When Epic start date is from milestone and finish date is of type fixed.
return !epicUtils.getDateValidity(getters.startDateTimeFromMilestones, getters.dueDateTime);
} else if (startDateIsFixed && !dueDateIsFixed) {
// When Epic start date is fixed and finish date is from milestone.
return !epicUtils.getDateValidity(getters.startDateTime, getters.dueDateTimeFromMilestones);
}
// When both Epic start date and finish date are from milestone.
return !epicUtils.getDateValidity(
getters.startDateTimeFromMilestones,
getters.dueDateTimeFromMilestones,
);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -11,3 +11,9 @@ export const TOGGLE_SIDEBAR = 'TOGGLE_SIDEBAR'; ...@@ -11,3 +11,9 @@ export const TOGGLE_SIDEBAR = 'TOGGLE_SIDEBAR';
export const REQUEST_EPIC_TODO_TOGGLE = 'REQUEST_EPIC_TODO_TOGGLE'; export const REQUEST_EPIC_TODO_TOGGLE = 'REQUEST_EPIC_TODO_TOGGLE';
export const REQUEST_EPIC_TODO_TOGGLE_SUCCESS = 'REQUEST_EPIC_TODO_TOGGLE_SUCCESS'; export const REQUEST_EPIC_TODO_TOGGLE_SUCCESS = 'REQUEST_EPIC_TODO_TOGGLE_SUCCESS';
export const REQUEST_EPIC_TODO_TOGGLE_FAILURE = 'REQUEST_EPIC_TODO_TOGGLE_FAILURE'; export const REQUEST_EPIC_TODO_TOGGLE_FAILURE = 'REQUEST_EPIC_TODO_TOGGLE_FAILURE';
export const TOGGLE_EPIC_START_DATE_TYPE = 'TOGGLE_EPIC_START_DATE_TYPE';
export const TOGGLE_EPIC_DUE_DATE_TYPE = 'TOGGLE_EPIC_DUE_DATE_TYPE';
export const REQUEST_EPIC_DATE_SAVE = 'REQUEST_EPIC_DATE_SAVE';
export const REQUEST_EPIC_DATE_SAVE_SUCCESS = 'REQUEST_EPIC_DATE_SAVE_SUCCESS';
export const REQUEST_EPIC_DATE_SAVE_FAILURE = 'REQUEST_EPIC_DATE_SAVE_FAILURE';
import { dateTypes } from '../constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
...@@ -35,4 +37,40 @@ export default { ...@@ -35,4 +37,40 @@ export default {
[types.REQUEST_EPIC_TODO_TOGGLE_FAILURE](state) { [types.REQUEST_EPIC_TODO_TOGGLE_FAILURE](state) {
state.epicTodoToggleInProgress = false; state.epicTodoToggleInProgress = false;
}, },
[types.TOGGLE_EPIC_START_DATE_TYPE](state, { dateTypeIsFixed }) {
state.startDateIsFixed = dateTypeIsFixed;
},
[types.TOGGLE_EPIC_DUE_DATE_TYPE](state, { dateTypeIsFixed }) {
state.dueDateIsFixed = dateTypeIsFixed;
},
[types.REQUEST_EPIC_DATE_SAVE](state, { dateType }) {
if (dateType === dateTypes.start) {
state.epicStartDateSaveInProgress = true;
} else {
state.epicDueDateSaveInProgress = true;
}
},
[types.REQUEST_EPIC_DATE_SAVE_SUCCESS](state, { dateType, dateTypeIsFixed, newDate }) {
if (dateType === dateTypes.start) {
state.epicStartDateSaveInProgress = false;
state.startDateIsFixed = dateTypeIsFixed;
state.startDate = newDate;
} else {
state.epicDueDateSaveInProgress = false;
state.dueDateIsFixed = dateTypeIsFixed;
state.dueDate = newDate;
}
},
[types.REQUEST_EPIC_DATE_SAVE_FAILURE](state, { dateType, dateTypeIsFixed }) {
if (dateType === dateTypes.start) {
state.epicStartDateSaveInProgress = false;
state.startDateIsFixed = dateTypeIsFixed;
} else {
state.epicDueDateSaveInProgress = false;
state.dueDateIsFixed = dateTypeIsFixed;
}
},
}; };
...@@ -35,11 +35,19 @@ export default () => ({ ...@@ -35,11 +35,19 @@ export default () => ({
todoExists: false, todoExists: false,
startDateSourcingMilestoneTitle: '', startDateSourcingMilestoneTitle: '',
startDateSourcingMilestoneDates: {
startDate: '',
dueDate: '',
},
startDateIsFixed: false, startDateIsFixed: false,
startDateFixed: '', startDateFixed: '',
startDateFromMilestones: '', startDateFromMilestones: '',
startDate: '', startDate: '',
dueDateSourcingMilestoneTitle: '', dueDateSourcingMilestoneTitle: '',
dueDateSourcingMilestoneDates: {
startDate: '',
dueDate: '',
},
dueDateIsFixed: '', dueDateIsFixed: '',
dueDateFixed: '', dueDateFixed: '',
dueDateFromMilestones: '', dueDateFromMilestones: '',
...@@ -52,5 +60,7 @@ export default () => ({ ...@@ -52,5 +60,7 @@ export default () => ({
epicStatusChangeInProgress: false, epicStatusChangeInProgress: false,
epicDeleteInProgress: false, epicDeleteInProgress: false,
epicTodoToggleInProgress: false, epicTodoToggleInProgress: false,
epicStartDateSaveInProgress: false,
epicDueDateSaveInProgress: false,
sidebarCollapsed: false, sidebarCollapsed: false,
}); });
import $ from 'jquery'; import $ from 'jquery';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { __, s__, sprintf } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { dateInWords, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { dateTypes } from '../constants';
const triggerDocumentEvent = (eventName, eventParam) => { const triggerDocumentEvent = (eventName, eventParam) => {
$(document).trigger(eventName, eventParam); $(document).trigger(eventName, eventParam);
...@@ -23,6 +28,62 @@ const getCollapsedGutter = () => parseBoolean(Cookies.get('collapsed_gutter')); ...@@ -23,6 +28,62 @@ const getCollapsedGutter = () => parseBoolean(Cookies.get('collapsed_gutter'));
const setCollapsedGutter = value => Cookies.set('collapsed_gutter', value); const setCollapsedGutter = value => Cookies.set('collapsed_gutter', value);
const getDateValidity = (startDateTime, dueDateTime) => {
// If both dates are defined
// only then compare, return true otherwise
if (startDateTime && dueDateTime) {
return startDateTime.getTime() < dueDateTime.getTime();
}
return true;
};
const getDateFromMilestonesTooltip = ({
dateType = dateTypes.start,
startDateSourcingMilestoneTitle,
startDateSourcingMilestoneDates,
startDateTimeFromMilestones,
dueDateSourcingMilestoneTitle,
dueDateSourcingMilestoneDates,
dueDateTimeFromMilestones,
}) => {
const dateSourcingMilestoneTitle =
dateType === dateTypes.start ? startDateSourcingMilestoneTitle : dueDateSourcingMilestoneTitle;
const sourcingMilestoneDates =
dateType === dateTypes.start ? startDateSourcingMilestoneDates : 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: dateTypes.start === dateType ? s__('Epics|start') : s__('Epics|due'),
},
);
};
// This is for mocking methods from this // This is for mocking methods from this
// file within tests using `spyOnDependency` // file within tests using `spyOnDependency`
// which requires first param to always // which requires first param to always
...@@ -34,6 +95,8 @@ const epicUtils = { ...@@ -34,6 +95,8 @@ const epicUtils = {
toggleContainerClass, toggleContainerClass,
getCollapsedGutter, getCollapsedGutter,
setCollapsedGutter, setCollapsedGutter,
getDateValidity,
getDateFromMilestonesTooltip,
}; };
export default epicUtils; export default epicUtils;
...@@ -3,6 +3,9 @@ import Vue from 'vue'; ...@@ -3,6 +3,9 @@ import Vue from 'vue';
import EpicSidebar from 'ee/epic/components/epic_sidebar.vue'; import EpicSidebar from 'ee/epic/components/epic_sidebar.vue';
import createStore from 'ee/epic/store'; import createStore from 'ee/epic/store';
import epicUtils from 'ee/epic/utils/epic_utils';
import { dateTypes } from 'ee/epic/constants';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { mockEpicMeta, mockEpicData } from '../mock_data'; import { mockEpicMeta, mockEpicData } from '../mock_data';
...@@ -28,6 +31,110 @@ describe('EpicSidebarComponent', () => { ...@@ -28,6 +31,110 @@ describe('EpicSidebarComponent', () => {
vm.$destroy(); vm.$destroy();
}); });
describe('methods', () => {
describe('getDateFromMilestonesTooltip', () => {
it('calls `epicUtils.getDateFromMilestonesTooltip` with `dateType` param', () => {
spyOn(epicUtils, 'getDateFromMilestonesTooltip');
vm.getDateFromMilestonesTooltip(dateTypes.start);
expect(epicUtils.getDateFromMilestonesTooltip).toHaveBeenCalledWith(
jasmine.objectContaining({
dateType: dateTypes.start,
}),
);
});
});
describe('changeStartDateType', () => {
it('calls `toggleStartDateType` on component with `dateTypeIsFixed` param', () => {
spyOn(vm, 'toggleStartDateType');
vm.changeStartDateType(true, true);
expect(vm.toggleStartDateType).toHaveBeenCalledWith(
jasmine.objectContaining({
dateTypeIsFixed: true,
}),
);
});
it('calls `saveDate` on component when `typeChangeOnEdit` param false', () => {
spyOn(vm, 'saveDate');
vm.changeStartDateType(true, false);
expect(vm.saveDate).toHaveBeenCalledWith(
jasmine.objectContaining({
dateTypeIsFixed: true,
dateType: dateTypes.start,
newDate: '2018-06-01',
}),
);
});
});
describe('saveStartDate', () => {
it('calls `saveDate` on component with `date` param set to `newDate`', () => {
spyOn(vm, 'saveDate');
vm.saveStartDate('2018-1-1');
expect(vm.saveDate).toHaveBeenCalledWith(
jasmine.objectContaining({
dateTypeIsFixed: true,
dateType: dateTypes.start,
newDate: '2018-1-1',
}),
);
});
});
describe('changeDueDateType', () => {
it('calls `toggleDueDateType` on component with `dateTypeIsFixed` param', () => {
spyOn(vm, 'toggleDueDateType');
vm.changeDueDateType(true, true);
expect(vm.toggleDueDateType).toHaveBeenCalledWith(
jasmine.objectContaining({
dateTypeIsFixed: true,
}),
);
});
it('calls `saveDate` on component when `typeChangeOnEdit` param false', () => {
spyOn(vm, 'saveDate');
vm.changeDueDateType(true, false);
expect(vm.saveDate).toHaveBeenCalledWith(
jasmine.objectContaining({
dateTypeIsFixed: true,
dateType: dateTypes.due,
newDate: '2018-08-01',
}),
);
});
});
describe('saveDueDate', () => {
it('calls `saveDate` on component with `date` param set to `newDate`', () => {
spyOn(vm, 'saveDate');
vm.saveDueDate('2018-1-1');
expect(vm.saveDate).toHaveBeenCalledWith(
jasmine.objectContaining({
dateTypeIsFixed: true,
dateType: dateTypes.due,
newDate: '2018-1-1',
}),
);
});
});
});
describe('template', () => { describe('template', () => {
beforeAll(() => { beforeAll(() => {
gon.current_user_id = 1; gon.current_user_id = 1;
...@@ -67,5 +174,29 @@ describe('EpicSidebarComponent', () => { ...@@ -67,5 +174,29 @@ describe('EpicSidebarComponent', () => {
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('renders Start date & Due date elements when sidebar is expanded', done => {
store.dispatch('toggleSidebarFlag', false);
vm.$nextTick()
.then(() => {
const startDateEl = vm.$el.querySelector('.block.date.start-date');
const dueDateEl = vm.$el.querySelector('.block.date.due-date');
expect(startDateEl).not.toBeNull();
expect(startDateEl.querySelector('.title').innerText.trim()).toContain('Start date');
expect(
startDateEl.querySelector('.value .value-type-fixed .value-content').innerText.trim(),
).toBe('Jun 1, 2018');
expect(dueDateEl).not.toBeNull();
expect(dueDateEl.querySelector('.title').innerText.trim()).toContain('Due date');
expect(
dueDateEl.querySelector('.value .value-type-fixed .value-content').innerText.trim(),
).toBe('Aug 1, 2018');
})
.then(done)
.catch(done.fail);
});
}); });
}); });
import Vue from 'vue';
import SidebarDatepicker from 'ee/epic/components/sidebar_items/sidebar_date_picker.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockDatePickerProps } from '../../mock_data';
describe('SidebarDatePicker', () => {
const orginalGitLabUrl = gon.gitlab_url;
gon.gitlab_url = gl.TEST_HOST;
let vm;
beforeEach(() => {
const Component = Vue.extend(SidebarDatepicker);
vm = mountComponent(Component, mockDatePickerProps);
});
afterEach(() => {
gon.gitlab_url = orginalGitLabUrl;
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 `canUpdate` 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.dateSaveInProgress = 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);
});
});
});
...@@ -17,3 +17,20 @@ export const mockEpicData = convertObjectPropsToCamelCase( ...@@ -17,3 +17,20 @@ export const mockEpicData = convertObjectPropsToCamelCase(
}), }),
{ deep: true }, { deep: true },
); );
export const mockDatePickerProps = {
blockClass: 'epic-date',
sidebarCollapsed: false,
showToggleSidebar: false,
dateSaveInProgress: false,
canUpdate: 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',
};
...@@ -4,7 +4,7 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -4,7 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import defaultState from 'ee/epic/store/state'; import defaultState from 'ee/epic/store/state';
import * as actions from 'ee/epic/store/actions'; import * as actions from 'ee/epic/store/actions';
import epicUtils from 'ee/epic/utils/epic_utils'; import epicUtils from 'ee/epic/utils/epic_utils';
import { statusType } from 'ee/epic/constants'; import { statusType, dateTypes } from 'ee/epic/constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import testAction from 'spec/helpers/vuex_action_helper'; import testAction from 'spec/helpers/vuex_action_helper';
...@@ -450,4 +450,291 @@ describe('Epic Store Actions', () => { ...@@ -450,4 +450,291 @@ describe('Epic Store Actions', () => {
}); });
}); });
}); });
describe('toggleStartDateType', () => {
it('should set `state.startDateIsFixed` flag to `true`', done => {
const dateTypeIsFixed = true;
testAction(
actions.toggleStartDateType,
{ dateTypeIsFixed },
state,
[{ type: 'TOGGLE_EPIC_START_DATE_TYPE', payload: { dateTypeIsFixed } }],
[],
done,
);
});
});
describe('toggleDueDateType', () => {
it('should set `state.dueDateIsFixed` flag to `true`', done => {
const dateTypeIsFixed = true;
testAction(
actions.toggleDueDateType,
{ dateTypeIsFixed },
state,
[{ type: 'TOGGLE_EPIC_DUE_DATE_TYPE', payload: { dateTypeIsFixed } }],
[],
done,
);
});
});
describe('requestEpicDateSave', () => {
it('should set `state.epicStartDateSaveInProgress` flag to `true` when called with `dateType` as `start`', done => {
const dateType = dateTypes.start;
testAction(
actions.requestEpicDateSave,
{ dateType },
state,
[{ type: 'REQUEST_EPIC_DATE_SAVE', payload: { dateType } }],
[],
done,
);
});
it('should set `state.epicDueDateSaveInProgress` flag to `true` when called with `dateType` as `due`', done => {
const dateType = dateTypes.due;
testAction(
actions.requestEpicDateSave,
{ dateType },
state,
[{ type: 'REQUEST_EPIC_DATE_SAVE', payload: { dateType } }],
[],
done,
);
});
});
describe('requestEpicDateSaveSuccess', () => {
it('should set `state.epicStartDateSaveInProgress` flag to `false` and set values of `startDateIsFixed` & `startDate` with params `dateTypeIsFixed` & `newDate` when called with `dateType` as `start`', done => {
const data = {
dateType: dateTypes.start,
dateTypeIsFixed: true,
mewDate: '2018-1-1',
};
testAction(
actions.requestEpicDateSaveSuccess,
data,
state,
[{ type: 'REQUEST_EPIC_DATE_SAVE_SUCCESS', payload: { ...data } }],
[],
done,
);
});
it('should set `state.epicDueDateSaveInProgress` flag to `false` and set values of `dueDateIsFixed` & `dueDate` with params `dateTypeIsFixed` & `newDate` when called with `dateType` as `due`', done => {
const data = {
dateType: dateTypes.due,
dateTypeIsFixed: true,
mewDate: '2018-1-1',
};
testAction(
actions.requestEpicDateSaveSuccess,
data,
state,
[{ type: 'REQUEST_EPIC_DATE_SAVE_SUCCESS', payload: { ...data } }],
[],
done,
);
});
});
describe('requestEpicDateSaveFailure', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('should set `state.epicStartDateSaveInProgress` flag to `false` and set value of `startDateIsFixed` to that of param `dateTypeIsFixed` when called with `dateType` as `start`', done => {
const data = {
dateType: dateTypes.start,
dateTypeIsFixed: true,
};
testAction(
actions.requestEpicDateSaveFailure,
data,
state,
[
{
type: 'REQUEST_EPIC_DATE_SAVE_FAILURE',
payload: { ...data },
},
],
[],
done,
);
});
it('should set `state.epicDueDateSaveInProgress` flag to `false` and set value of `dueDateIsFixed` to that of param `dateTypeIsFixed` when called with `dateType` as `due`', done => {
const data = {
dateType: dateTypes.due,
dateTypeIsFixed: true,
};
testAction(
actions.requestEpicDateSaveFailure,
data,
state,
[
{
type: 'REQUEST_EPIC_DATE_SAVE_FAILURE',
payload: { ...data },
},
],
[],
done,
);
});
it('should show flash error with message "An error occurred while saving the start date" when called with `dateType` as `start`', done => {
actions.requestEpicDateSaveFailure(
{
commit: () => {},
},
{ dateType: dateTypes.start },
);
Vue.nextTick()
.then(() => {
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
'An error occurred while saving the start date',
);
})
.then(done)
.catch(done.fail);
});
it('should show flash error with message "An error occurred while saving the due date" when called with `dateType` as `due`', done => {
actions.requestEpicDateSaveFailure(
{
commit: () => {},
},
{ dateType: dateTypes.due },
);
Vue.nextTick()
.then(() => {
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
'An error occurred while saving the due date',
);
})
.then(done)
.catch(done.fail);
});
});
describe('saveDate', () => {
let mock;
const data = {
dateType: dateTypes.start,
dateTypeIsFixed: true,
newDate: '2018-1-1',
};
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
it('dispatches requestEpicDateSave and requestEpicDateSaveSuccess when request is successful', done => {
mock.onPut(/(.*)/).replyOnce(200, {});
testAction(
actions.saveDate,
{ ...data },
state,
[],
[
{
type: 'requestEpicDateSave',
payload: { dateType: data.dateType },
},
{
type: 'requestEpicDateSaveSuccess',
payload: { ...data },
},
],
done,
);
});
it('dispatches requestEpicDateSave and requestEpicDateSaveFailure when request fails', done => {
mock.onPut(/(.*)/).replyOnce(500, {});
testAction(
actions.saveDate,
{ ...data },
state,
[],
[
{
type: 'requestEpicDateSave',
payload: { dateType: data.dateType },
},
{
type: 'requestEpicDateSaveFailure',
payload: { dateType: data.dateType, dateTypeIsFixed: !data.dateTypeIsFixed },
},
],
done,
);
});
it('calls `axios.put` with request body containing start date related payload when called with `dateType` as `start`', () => {
spyOn(axios, 'put').and.callFake(() => new Promise(() => {}));
actions.saveDate(
{
state: { endpoint: '/foo/bar' },
dispatch: () => {},
},
{
dateType: dateTypes.start,
newDate: '2018-1-1',
dateTypeIsFixed: true,
},
);
expect(axios.put).toHaveBeenCalledWith(
'/foo/bar',
jasmine.objectContaining({
start_date_is_fixed: true,
start_date_fixed: '2018-1-1',
}),
);
});
it('calls `axios.put` with request body containing due date related payload when called with `dateType` as `due`', () => {
spyOn(axios, 'put').and.callFake(() => new Promise(() => {}));
actions.saveDate(
{
state: { endpoint: '/foo/bar' },
dispatch: () => {},
},
{
dateType: dateTypes.due,
newDate: '2018-1-1',
dateTypeIsFixed: true,
},
);
expect(axios.put).toHaveBeenCalledWith(
'/foo/bar',
jasmine.objectContaining({
due_date_is_fixed: true,
due_date_fixed: '2018-1-1',
}),
);
});
});
}); });
...@@ -2,6 +2,14 @@ import * as getters from 'ee/epic/store/getters'; ...@@ -2,6 +2,14 @@ import * as getters from 'ee/epic/store/getters';
import { statusType } from 'ee/epic/constants'; import { statusType } from 'ee/epic/constants';
describe('Epic Store Getters', () => { describe('Epic Store Getters', () => {
const dateString = '2018-01-01';
const epicGetter = {
startDateTime: 'startfoo',
startDateTimeFromMilestones: 'startbar',
dueDateTime: 'duefoo',
dueDateTimeFromMilestones: 'duebar',
};
describe('isEpicOpen', () => { describe('isEpicOpen', () => {
it('returns `true` when Epic `state` is `opened`', () => { it('returns `true` when Epic `state` is `opened`', () => {
const epicState = { const epicState = {
...@@ -19,4 +27,236 @@ describe('Epic Store Getters', () => { ...@@ -19,4 +27,236 @@ describe('Epic Store Getters', () => {
expect(getters.isEpicOpen(epicState)).toBe(false); expect(getters.isEpicOpen(epicState)).toBe(false);
}); });
}); });
describe('isUserSignedIn', () => {
const originalUserId = gon.current_user_id;
afterAll(() => {
gon.current_user_id = originalUserId;
});
it('return boolean representation of the value of `gon.current_user_id`', () => {
gon.current_user_id = 0;
expect(getters.isUserSignedIn()).toBe(false);
gon.current_user_id = 1;
expect(getters.isUserSignedIn()).toBe(true);
});
});
describe('startDateTime', () => {
it('should return null when there is no startDate', () => {
const epicState = {};
expect(getters.startDateTime(epicState)).toEqual(null);
});
it('should return date', () => {
const epicState = {
startDate: dateString,
};
const date = getters.startDateTime(epicState);
expect(date.getDate()).toEqual(1);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2018);
});
});
describe('startDateTimeFixed', () => {
it('should return null when there is no startDateFixed', () => {
const epicState = {};
expect(getters.startDateTimeFixed(epicState)).toEqual(null);
});
it('should return date', () => {
const epicState = {
startDateFixed: dateString,
};
const date = getters.startDateTimeFixed(epicState);
expect(date.getDate()).toEqual(1);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2018);
});
});
describe('startDateTimeFromMilestones', () => {
it('should return null when there is no startDateFromMilestones', () => {
const epicState = {};
expect(getters.startDateTimeFromMilestones(epicState)).toEqual(null);
});
it('should return date', () => {
const epicState = {
startDateFromMilestones: dateString,
};
const date = getters.startDateTimeFromMilestones(epicState);
expect(date.getDate()).toEqual(1);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2018);
});
});
describe('dueDateTime', () => {
it('should return null when there is no dueDate', () => {
const epicState = {};
expect(getters.dueDateTime(epicState)).toEqual(null);
});
it('should return date', () => {
const epicState = {
dueDate: dateString,
};
const date = getters.dueDateTime(epicState);
expect(date.getDate()).toEqual(1);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2018);
});
});
describe('dueDateTimeFixed', () => {
it('should return null when there is no dueDateFixed', () => {
const epicState = {};
expect(getters.dueDateTimeFixed(epicState)).toEqual(null);
});
it('should return date', () => {
const epicState = {
dueDateFixed: dateString,
};
const date = getters.dueDateTimeFixed(epicState);
expect(date.getDate()).toEqual(1);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2018);
});
});
describe('dueDateTimeFromMilestones', () => {
it('should return null when there is no dueDateFromMilestones', () => {
const epicState = {};
expect(getters.dueDateTimeFromMilestones(epicState)).toEqual(null);
});
it('should return date', () => {
const epicState = {
dueDateFromMilestones: dateString,
};
const date = getters.dueDateTimeFromMilestones(epicState);
expect(date.getDate()).toEqual(1);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2018);
});
});
describe('startDateForCollapsedSidebar', () => {
it('should return startDateTime when startDateIsFixed is true', () => {
const epicState = {
startDateIsFixed: true,
};
expect(getters.startDateForCollapsedSidebar(epicState, epicGetter)).toEqual('startfoo');
});
it('should return startDateTimeFromMilestones when startDateIsFixed is false', () => {
const epicState = {
startDateIsFixed: false,
};
expect(getters.startDateForCollapsedSidebar(epicState, epicGetter)).toEqual('startbar');
});
});
describe('dueDateForCollapsedSidebar', () => {
it('should return dueDateTime when dueDateIsFixed is true', () => {
const epicState = {
dueDateIsFixed: true,
};
expect(getters.dueDateForCollapsedSidebar(epicState, epicGetter)).toEqual('duefoo');
});
it('should return dueDateTimeFromMilestones when dueDateIsFixed is false', () => {
const epicState = {
dueDateIsFixed: false,
};
expect(getters.dueDateForCollapsedSidebar(epicState, epicGetter)).toEqual('duebar');
});
});
describe('isDateInvalid', () => {
it('returns true when fixed start and due dates are invalid', () => {
const epicState = {
startDateIsFixed: true,
dueDateIsFixed: true,
};
expect(
getters.isDateInvalid(epicState, {
startDateTime: new Date(2018, 0, 1),
dueDateTime: new Date(2017, 0, 1),
}),
).toBe(true);
});
it('returns false when fixed start and due dates are valid', () => {
const epicState = {
startDateIsFixed: true,
dueDateIsFixed: true,
};
expect(
getters.isDateInvalid(epicState, {
startDateTime: new Date(2017, 0, 1),
dueDateTime: new Date(2018, 0, 1),
}),
).toBe(false);
});
it('returns true when milestone start and milestone due dates are invalid', () => {
const epicState = {
startDateIsFixed: false,
dueDateIsFixed: false,
};
expect(
getters.isDateInvalid(epicState, {
startDateTimeFromMilestones: new Date(2018, 0, 1),
dueDateTimeFromMilestones: new Date(2017, 0, 1),
}),
).toBe(true);
});
it('returns false when milestone start and milestone due dates are valid', () => {
const epicState = {
startDateIsFixed: false,
dueDateIsFixed: false,
};
expect(
getters.isDateInvalid(epicState, {
startDateTimeFromMilestones: new Date(2017, 0, 1),
dueDateTimeFromMilestones: new Date(2018, 0, 1),
}),
).toBe(false);
});
});
}); });
import mutations from 'ee/epic/store/mutations'; import mutations from 'ee/epic/store/mutations';
import * as types from 'ee/epic/store/mutation_types'; import * as types from 'ee/epic/store/mutation_types';
import { dateTypes } from 'ee/epic/constants';
import { mockEpicMeta, mockEpicData } from '../mock_data'; import { mockEpicMeta, mockEpicData } from '../mock_data';
describe('Epic Store Mutations', () => { describe('Epic Store Mutations', () => {
...@@ -117,4 +119,120 @@ describe('Epic Store Mutations', () => { ...@@ -117,4 +119,120 @@ describe('Epic Store Mutations', () => {
expect(state.epicTodoToggleInProgress).toBe(false); expect(state.epicTodoToggleInProgress).toBe(false);
}); });
}); });
describe('TOGGLE_EPIC_START_DATE_TYPE', () => {
it('Should set `startDateIsFixed` flag on state based on provided `dateTypeIsFixed` param', () => {
const state = {
startDateIsFixed: false,
};
mutations[types.TOGGLE_EPIC_START_DATE_TYPE](state, {
dateTypeIsFixed: true,
});
expect(state.startDateIsFixed).toBe(true);
});
});
describe('TOGGLE_EPIC_DUE_DATE_TYPE', () => {
it('Should set `dueDateIsFixed` flag on state based on provided `dateTypeIsFixed` param', () => {
const state = {
dueDateIsFixed: false,
};
mutations[types.TOGGLE_EPIC_DUE_DATE_TYPE](state, {
dateTypeIsFixed: true,
});
expect(state.dueDateIsFixed).toBe(true);
});
});
describe('REQUEST_EPIC_DATE_SAVE', () => {
it('Should set `epicStartDateSaveInProgress` flag on state as `true` when provided `dateType` param is `start`', () => {
const state = {
epicStartDateSaveInProgress: false,
};
mutations[types.REQUEST_EPIC_DATE_SAVE](state, {
dateType: dateTypes.start,
});
expect(state.epicStartDateSaveInProgress).toBe(true);
});
it('Should set `epicDueDateSaveInProgress` flag on state as `true` when provided `dateType` param is `due`', () => {
const state = {
epicDueDateSaveInProgress: false,
};
mutations[types.REQUEST_EPIC_DATE_SAVE](state, {
dateType: dateTypes.due,
});
expect(state.epicDueDateSaveInProgress).toBe(true);
});
});
describe('REQUEST_EPIC_DATE_SAVE_SUCCESS', () => {
it('Should set `epicStartDateSaveInProgress` flag on state to `false` and set `startDateIsFixed` & `startDate` values based on provided `dateTypeIsFixed` & `newDate` params when `dateType` param is `start`', () => {
const startDateIsFixed = true;
const startDate = '2018-1-1';
const state = {};
mutations[types.REQUEST_EPIC_DATE_SAVE_SUCCESS](state, {
dateType: dateTypes.start,
dateTypeIsFixed: startDateIsFixed,
newDate: startDate,
});
expect(state.epicStartDateSaveInProgress).toBe(false);
expect(state.startDateIsFixed).toBe(startDateIsFixed);
expect(state.startDate).toBe(startDate);
});
it('Should set `epicDueDateSaveInProgress` flag on state to `false` and set `dueDateIsFixed` & `dueDate` values based on provided `dateTypeIsFixed` & `newDate` params when `dateType` param is `due`', () => {
const dueDateIsFixed = true;
const dueDate = '2018-1-1';
const state = {};
mutations[types.REQUEST_EPIC_DATE_SAVE_SUCCESS](state, {
dateType: dateTypes.due,
dateTypeIsFixed: dueDateIsFixed,
newDate: dueDate,
});
expect(state.epicDueDateSaveInProgress).toBe(false);
expect(state.dueDateIsFixed).toBe(dueDateIsFixed);
expect(state.dueDate).toBe(dueDate);
});
});
describe('REQUEST_EPIC_DATE_SAVE_FAILURE', () => {
it('Should set `epicStartDateSaveInProgress` flag on state to `false` and set `startDateIsFixed` value with provided `dateTypeIsFixed` param when `dateType` param is `start`', () => {
const startDateIsFixed = true;
const state = {};
mutations[types.REQUEST_EPIC_DATE_SAVE_FAILURE](state, {
dateType: dateTypes.start,
dateTypeIsFixed: startDateIsFixed,
});
expect(state.epicStartDateSaveInProgress).toBe(false);
expect(state.startDateIsFixed).toBe(startDateIsFixed);
});
it('Should set `epicDueDateSaveInProgress` flag on state to `false` and set `dueDateIsFixed` value with provided `dateTypeIsFixed` param when `dateType` param is `due`', () => {
const dueDateIsFixed = true;
const state = {};
mutations[types.REQUEST_EPIC_DATE_SAVE_FAILURE](state, {
dateType: dateTypes.due,
dateTypeIsFixed: dueDateIsFixed,
});
expect(state.epicDueDateSaveInProgress).toBe(false);
expect(state.dueDateIsFixed).toBe(dueDateIsFixed);
});
});
}); });
...@@ -3497,6 +3497,9 @@ msgstr "" ...@@ -3497,6 +3497,9 @@ msgstr ""
msgid "Epics|An error occurred while saving %{epicDateType} date" msgid "Epics|An error occurred while saving %{epicDateType} date"
msgstr "" msgstr ""
msgid "Epics|An error occurred while saving the %{epicDateType} date"
msgstr ""
msgid "Epics|How can I solve this?" msgid "Epics|How can I solve this?"
msgstr "" 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