Commit 0ee7195d authored by Florie Guibert's avatar Florie Guibert Committed by Natalia Tepluhina

Migrate epic sidebar datepickers to date widget

parent 0e356e1c
...@@ -266,7 +266,7 @@ export default { ...@@ -266,7 +266,7 @@ export default {
</gl-popover> </gl-popover>
</template> </template>
<template #collapsed> <template #collapsed>
<div v-gl-tooltip :title="dateLabel" class="sidebar-collapsed-icon"> <div v-gl-tooltip.viewport.left :title="dateLabel" class="sidebar-collapsed-icon">
<gl-icon :size="16" name="calendar" /> <gl-icon :size="16" name="calendar" />
<span class="collapse-truncated-title">{{ formattedDate }}</span> <span class="collapse-truncated-title">{{ formattedDate }}</span>
</div> </div>
......
<script>
import { dateInWords, timeFor } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
export default {
name: 'SidebarCollapsedGroupedDatePicker',
components: {
collapsedCalendarIcon,
},
mixins: [timeagoMixin],
props: {
collapsed: {
type: Boolean,
required: false,
default: true,
},
minDate: {
type: Date,
required: false,
default: null,
},
maxDate: {
type: Date,
required: false,
default: null,
},
disableClickableIcons: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
hasMinAndMaxDates() {
return this.minDate && this.maxDate;
},
hasNoMinAndMaxDates() {
return !this.minDate && !this.maxDate;
},
showMinDateBlock() {
return this.minDate || this.hasNoMinAndMaxDates;
},
showFromText() {
return !this.maxDate && this.minDate;
},
iconClass() {
const disabledClass = this.disableClickableIcons ? 'disabled' : '';
return `sidebar-collapsed-icon calendar-icon ${disabledClass}`;
},
},
methods: {
toggleSidebar() {
this.$emit('toggleCollapse');
},
dateText(dateType = 'min') {
const date = this[`${dateType}Date`];
const dateWords = dateInWords(date, true);
const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
return date ? parsedDateWords : __('None');
},
tooltipText(dateType = 'min') {
const defaultText = dateType === 'min' ? __('Start date') : __('Due date');
const date = this[`${dateType}Date`];
const timeAgo = dateType === 'min' ? this.timeFormatted(date) : timeFor(date);
const dateText = date ? [this.dateText(dateType), `(${timeAgo})`].join(' ') : '';
if (date) {
return [defaultText, dateText].join('<br />');
}
return __('Start and due date');
},
},
};
</script>
<template>
<div class="block sidebar-grouped-item gl-cursor-pointer" role="button" @click="toggleSidebar">
<collapsed-calendar-icon
v-if="showMinDateBlock"
:container-class="iconClass"
:tooltip-text="tooltipText('min')"
>
<span class="sidebar-collapsed-value">
<span v-if="showFromText">{{ __('From') }}</span> <span>{{ dateText('min') }}</span>
</span>
</collapsed-calendar-icon>
<div v-if="hasMinAndMaxDates" class="text-center sidebar-collapsed-divider">-</div>
<collapsed-calendar-icon
v-if="maxDate"
:container-class="iconClass"
:tooltip-text="tooltipText('max')"
>
<span class="sidebar-collapsed-value">
<span v-if="!minDate">{{ __('Until') }}</span> <span>{{ dateText('max') }}</span>
</span>
</collapsed-calendar-icon>
</div>
</template>
...@@ -8,27 +8,25 @@ import { convertToGraphQLId } from '~/graphql_shared/utils'; ...@@ -8,27 +8,25 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issue_show/constants'; import { IssuableType } from '~/issue_show/constants';
import notesEventHub from '~/notes/event_hub'; import notesEventHub from '~/notes/event_hub';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue'; import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue'; import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue'; import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import sidebarEventHub from '~/sidebar/event_hub'; import sidebarEventHub from '~/sidebar/event_hub';
import SidebarDatePickerCollapsed from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue'; import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants'; import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { dateTypes } from '../constants'; import { dateTypes } from '../constants';
import epicUtils from '../utils/epic_utils'; import epicUtils from '../utils/epic_utils';
import SidebarDatePicker from './sidebar_items/sidebar_date_picker.vue';
import SidebarHeader from './sidebar_items/sidebar_header.vue'; import SidebarHeader from './sidebar_items/sidebar_header.vue';
export default { export default {
dateTypes, dateTypes,
components: { components: {
SidebarHeader, SidebarHeader,
SidebarDatePicker, SidebarDateWidget,
SidebarDatePickerCollapsed,
SidebarAncestorsWidget, SidebarAncestorsWidget,
SidebarParticipantsWidget, SidebarParticipantsWidget,
SidebarConfidentialityWidget, SidebarConfidentialityWidget,
...@@ -50,34 +48,11 @@ export default { ...@@ -50,34 +48,11 @@ export default {
'canUpdate', 'canUpdate',
'allowSubEpics', 'allowSubEpics',
'sidebarCollapsed', 'sidebarCollapsed',
'startDateSourcingMilestoneTitle',
'startDateSourcingMilestoneDates',
'startDateIsFixed',
'startDateFixed',
'startDateFromMilestones',
'dueDateSourcingMilestoneTitle',
'dueDateSourcingMilestoneDates',
'dueDateIsFixed',
'dueDateFixed',
'dueDateFromMilestones',
'epicStartDateSaveInProgress',
'epicDueDateSaveInProgress',
'fullPath', 'fullPath',
'epicId', 'epicId',
'epicsWebUrl', 'epicsWebUrl',
]), ]),
...mapGetters([ ...mapGetters(['isUserSignedIn']),
'isUserSignedIn',
'isDateInvalid',
'startDateTimeFixed',
'startDateTimeFromMilestones',
'startDateTime',
'startDateForCollapsedSidebar',
'dueDateTimeFixed',
'dueDateTimeFromMilestones',
'dueDateTime',
'dueDateForCollapsedSidebar',
]),
issuableType() { issuableType() {
return IssuableType.Epic; return IssuableType.Epic;
}, },
...@@ -98,56 +73,8 @@ export default { ...@@ -98,56 +73,8 @@ export default {
'fetchEpicDetails', 'fetchEpicDetails',
'toggleSidebar', 'toggleSidebar',
'toggleSidebarFlag', 'toggleSidebarFlag',
'toggleStartDateType',
'toggleDueDateType',
'saveDate',
'updateConfidentialityOnIssuable', 'updateConfidentialityOnIssuable',
]), ]),
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,
});
},
updateEpicConfidentiality(confidential) { updateEpicConfidentiality(confidential) {
notesEventHub.$emit('notesApp.updateIssuableConfidentiality', confidential); notesEventHub.$emit('notesApp.updateIssuableConfidentiality', confidential);
}, },
...@@ -185,56 +112,21 @@ export default { ...@@ -185,56 +112,21 @@ export default {
data-testid="todo" data-testid="todo"
/> />
</sidebar-header> </sidebar-header>
<sidebar-date-picker <sidebar-date-widget
v-show="!sidebarCollapsed" :iid="String(iid)"
:can-update="canUpdate" :full-path="fullPath"
:sidebar-collapsed="sidebarCollapsed" date-type="startDate"
:show-toggle-sidebar="!isUserSignedIn" :issuable-type="issuableType"
:label="__('Start date')" :can-inherit="true"
: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"
data-testid="start-date" data-testid="start-date"
block-class="start-date"
@toggleCollapse="toggleSidebar({ sidebarCollapsed })"
@toggleDateType="changeStartDateType"
@saveDate="saveStartDate"
/> />
<sidebar-date-picker <sidebar-date-widget
v-show="!sidebarCollapsed" :iid="String(iid)"
:can-update="canUpdate" :full-path="fullPath"
:sidebar-collapsed="sidebarCollapsed" date-type="dueDate"
:label="__('Due date')" :issuable-type="issuableType"
:date-picker-label="__('Fixed due date')" :can-inherit="true"
: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"
data-testid="due-date" data-testid="due-date"
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 })"
/> />
<labels-select-widget <labels-select-widget
class="block labels js-labels-block" class="block labels js-labels-block"
......
<script>
import { GlLoadingIcon, GlButton, GlIcon, GlTooltipDirective, GlPopover, GlLink } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { dateInWords } from '~/lib/utils/datetime_utility';
import { __, s__ } from '~/locale';
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 {
dateHelpUrl: '/help/user/group/epics/index.md#start-date-and-due-date',
dateHelpValidMessage: 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.',
),
dateHelpInvalidUrlText: s__('Epics|How can I solve this?'),
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlIcon,
DatePicker,
CollapsedCalendarIcon,
ToggleSidebar,
GlLoadingIcon,
GlButton,
GlPopover,
GlLink,
},
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,
},
fieldName: {
type: String,
required: false,
default: () => uniqueId('dateType_'),
},
},
data() {
return {
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');
},
},
methods: {
stopEditing() {
this.editing = false;
this.$emit('toggleDateType', { dateTypeIsFixed: true, typeChangeOnEdit: true });
},
startEditing(e) {
this.editing = true;
e.stopPropagation();
},
newDateSelected(date = null) {
this.editing = false;
this.$emit('saveDate', date);
},
toggleDateType(dateTypeFixed) {
this.$emit('toggleDateType', { dateTypeIsFixed: 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 gl-mb-2">
{{ label }}
<gl-loading-icon v-if="dateSaveInProgress" size="sm" :inline="true" />
<div class="float-right gl-display-flex gl-align-items-center">
<gl-icon
ref="epicDatePopover"
name="question-o"
class="help-icon gl-mr-2"
tabindex="0"
:aria-label="__('Help')"
/>
<gl-popover
:target="() => $refs.epicDatePopover.$el"
triggers="focus"
placement="left"
boundary="viewport"
>
<p>{{ $options.dateHelpValidMessage }}</p>
<gl-link :href="$options.dateHelpUrl" target="_blank">{{
__('More information')
}}</gl-link>
</gl-popover>
<gl-button
v-show="canUpdate && !editing"
ref="editButton"
category="tertiary"
size="small"
class="btn-sidebar-action gl-mr-n2"
@click="startEditing"
>
{{ __('Edit') }}
</gl-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 text-secondary"
>
<input
v-if="canUpdate && !editing"
:name="fieldName"
:checked="selectedDateIsFixed"
type="radio"
@click="toggleDateType(true)"
/>
<span v-show="!editing" class="gl-ml-2">{{ __('Fixed:') }}</span>
<date-picker
v-if="editing"
:selected-date="dateFixed"
:label="datePickerLabel"
@newDateSelected="newDateSelected"
@hidePicker="stopEditing"
/>
<span v-else class="d-flex value-content gl-ml-1">
<template v-if="dateFixed">
<span>{{ dateFixedWords }}</span>
<template v-if="isDateInvalid && selectedDateIsFixed">
<gl-icon
ref="fixedDatePopoverWarning"
name="warning"
class="date-warning-icon gl-mr-2 gl-ml-2"
tabindex="0"
:aria-label="__('Warning')"
/>
<gl-popover
:target="() => $refs.fixedDatePopoverWarning.$el"
triggers="focus"
placement="left"
boundary="viewport"
>
<p>
{{ dateInvalidTooltip }}
</p>
<gl-link :href="$options.dateHelpUrl" target="_blank">{{
$options.dateHelpInvalidUrlText
}}</gl-link>
</gl-popover>
</template>
<span v-if="selectedAndEditable" class="no-value d-flex">
&nbsp;&ndash;&nbsp;
<gl-button
ref="removeButton"
variant="link"
class="btn-sidebar-date-remove"
@click="newDateSelected(null)"
>
{{ __('remove') }}
</gl-button>
</span>
</template>
<span v-else class="no-value"> {{ __('None') }} </span>
</span>
</div>
<abbr
v-gl-tooltip.bottom.html
:title="dateFromMilestonesTooltip"
:class="{ 'is-option-selected': !selectedDateIsFixed }"
class="value-type-dynamic text-secondary d-flex gl-mt-3"
>
<input
v-if="canUpdate"
:name="fieldName"
:checked="!selectedDateIsFixed"
type="radio"
@click="toggleDateType(false)"
/>
<span class="gl-ml-2">{{ __('Inherited:') }}</span>
<span class="value-content gl-ml-1">{{ dateFromMilestonesWords }}</span>
<template v-if="isDateInvalid && !selectedDateIsFixed">
<gl-icon
ref="datePopoverWarning"
name="warning"
class="date-warning-icon gl-ml-2"
tabindex="0"
:aria-label="__('Warning')"
/>
<gl-popover
:target="() => $refs.datePopoverWarning.$el"
triggers="focus"
placement="left"
boundary="viewport"
>
<p>
{{ dateInvalidTooltip }}
</p>
<gl-link :href="$options.dateHelpUrl" target="_blank">{{
$options.dateHelpInvalidUrlText
}}</gl-link>
</gl-popover>
</template>
</abbr>
</div>
</div>
</template>
...@@ -148,10 +148,10 @@ RSpec.describe 'Update Epic', :js do ...@@ -148,10 +148,10 @@ RSpec.describe 'Update Epic', :js do
context 'epic sidebar' do context 'epic sidebar' do
it 'opens datepicker when clicking Edit button' do it 'opens datepicker when clicking Edit button' do
page.within('.issuable-sidebar .block.start-date') do page.within('.issuable-sidebar [data-testid="start-date"]') do
click_button('Edit') click_button('Edit')
expect(find('.value-type-fixed')).to have_selector('.gl-datepicker') expect(find('[data-testid="expanded-content"]')).to have_selector('.gl-datepicker')
expect(find('.value-type-fixed')).to have_selector('.gl-datepicker .pika-single.is-bound') expect(find('[data-testid="expanded-content"]')).to have_selector('.gl-datepicker .pika-single.is-bound')
end end
end end
end end
......
...@@ -3,15 +3,10 @@ import { nextTick } from 'vue'; ...@@ -3,15 +3,10 @@ import { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import EpicSidebar from 'ee/epic/components/epic_sidebar.vue'; import EpicSidebar from 'ee/epic/components/epic_sidebar.vue';
import { dateTypes } from 'ee/epic/constants';
import { getStoreConfig } from 'ee/epic/store'; import { getStoreConfig } from 'ee/epic/store';
import epicUtils from 'ee/epic/utils/epic_utils';
import SidebarAncestorsWidget from 'ee_component/sidebar/components/ancestors_tree/sidebar_ancestors_widget.vue'; import SidebarAncestorsWidget from 'ee_component/sidebar/components/ancestors_tree/sidebar_ancestors_widget.vue';
import { parsePikadayDate } from '~/lib/utils/datetime_utility';
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue'; import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue'; import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
...@@ -53,110 +48,6 @@ describe('EpicSidebarComponent', () => { ...@@ -53,110 +48,6 @@ describe('EpicSidebarComponent', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('methods', () => {
describe('getDateFromMilestonesTooltip', () => {
it('calls `epicUtils.getDateFromMilestonesTooltip` with `dateType` param', () => {
jest.spyOn(epicUtils, 'getDateFromMilestonesTooltip');
wrapper.vm.getDateFromMilestonesTooltip(dateTypes.start);
expect(epicUtils.getDateFromMilestonesTooltip).toHaveBeenCalledWith(
expect.objectContaining({
dateType: dateTypes.start,
}),
);
});
});
describe('changeStartDateType', () => {
it('calls `toggleStartDateType` on component with `dateTypeIsFixed` param', () => {
jest.spyOn(wrapper.vm, 'toggleStartDateType');
wrapper.vm.changeStartDateType({ dateTypeIsFixed: true, typeChangeOnEdit: true });
expect(wrapper.vm.toggleStartDateType).toHaveBeenCalledWith(
expect.objectContaining({
dateTypeIsFixed: true,
}),
);
});
it('calls `saveDate` on component when `typeChangeOnEdit` param false', () => {
jest.spyOn(wrapper.vm, 'saveDate');
wrapper.vm.changeStartDateType({ dateTypeIsFixed: true, typeChangeOnEdit: false });
expect(wrapper.vm.saveDate).toHaveBeenCalledWith(
expect.objectContaining({
dateTypeIsFixed: true,
dateType: dateTypes.start,
newDate: '2018-06-01',
}),
);
});
});
describe('saveStartDate', () => {
it('calls `saveDate` on component with `date` param set to `newDate`', () => {
jest.spyOn(wrapper.vm, 'saveDate');
wrapper.vm.saveStartDate('2018-1-1');
expect(wrapper.vm.saveDate).toHaveBeenCalledWith(
expect.objectContaining({
dateTypeIsFixed: true,
dateType: dateTypes.start,
newDate: '2018-1-1',
}),
);
});
});
describe('changeDueDateType', () => {
it('calls `toggleDueDateType` on component with `dateTypeIsFixed` param', () => {
jest.spyOn(wrapper.vm, 'toggleDueDateType');
wrapper.vm.changeDueDateType({ dateTypeIsFixed: true, typeChangeOnEdit: true });
expect(wrapper.vm.toggleDueDateType).toHaveBeenCalledWith(
expect.objectContaining({
dateTypeIsFixed: true,
}),
);
});
it('calls `saveDate` on component when `typeChangeOnEdit` param false', () => {
jest.spyOn(wrapper.vm, 'saveDate');
wrapper.vm.changeDueDateType({ dateTypeIsFixed: true, typeChangeOnEdit: false });
expect(wrapper.vm.saveDate).toHaveBeenCalledWith(
expect.objectContaining({
dateTypeIsFixed: true,
dateType: dateTypes.due,
newDate: '2018-08-01',
}),
);
});
});
describe('saveDueDate', () => {
it('calls `saveDate` on component with `date` param set to `newDate`', () => {
jest.spyOn(wrapper.vm, 'saveDate');
wrapper.vm.saveDueDate('2018-1-1');
expect(wrapper.vm.saveDate).toHaveBeenCalledWith(
expect.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;
...@@ -190,14 +81,20 @@ describe('EpicSidebarComponent', () => { ...@@ -190,14 +81,20 @@ describe('EpicSidebarComponent', () => {
expect(startDateEl.exists()).toBe(true); expect(startDateEl.exists()).toBe(true);
expect(startDateEl.props()).toMatchObject({ expect(startDateEl.props()).toMatchObject({
label: 'Start date', iid: '1',
dateFixed: parsePikadayDate(mockEpicMeta.startDateFixed), fullPath: 'frontend-fixtures-group',
issuableType: 'epic',
dateType: 'startDate',
canInherit: true,
}); });
expect(dueDateEl.exists()).toBe(true); expect(dueDateEl.exists()).toBe(true);
expect(dueDateEl.props()).toMatchObject({ expect(dueDateEl.props()).toMatchObject({
label: 'Due date', iid: '1',
dateFixed: parsePikadayDate(mockEpicMeta.dueDateFixed), fullPath: 'frontend-fixtures-group',
issuableType: 'epic',
dateType: 'dueDate',
canInherit: true,
}); });
}); });
......
import { GlLoadingIcon, GlIcon, GlPopover, GlLink, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SidebarDatepicker from 'ee/epic/components/sidebar_items/sidebar_date_picker.vue';
import { TEST_HOST } from 'helpers/test_constants';
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 { mockDatePickerProps } from '../../mock_data';
describe('SidebarDatePicker', () => {
let originalGon;
beforeAll(() => {
originalGon = global.gon;
global.gon = { gitlab_url: TEST_HOST };
});
afterAll(() => {
global.gon = originalGon;
});
let wrapper;
const findIconByName = (name) =>
wrapper
.findAll(GlIcon)
.filter((w) => w.props().name === name)
.at(0);
const findEditButton = () => wrapper.find({ ref: 'editButton' });
const findRemoveButton = () => wrapper.find({ ref: 'removeButton' });
const createFakeEvent = () => ({ stopPropagation: jest.fn() });
const startEditing = () => {
const e = createFakeEvent();
findEditButton().vm.$emit('click', e);
};
const createComponent = (props) => {
wrapper = shallowMount(SidebarDatepicker, {
propsData: {
...mockDatePickerProps,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('generates unique names for input if `fieldName` prop is not provided', () => {
createComponent();
const anotherWrapper = shallowMount(SidebarDatepicker, {
propsData: mockDatePickerProps,
});
const firstInputName = wrapper.find('input').attributes('name');
const otherInputName = anotherWrapper.find('input').attributes('name');
expect(firstInputName).toContain('dateType_');
expect(otherInputName).toContain('dateType_');
expect(firstInputName).not.toEqual(otherInputName);
anotherWrapper.destroy();
});
it('renders remove button when both `selectedDate` is defined and `canUpdate` is true', () => {
createComponent({
selectedDate: new Date(),
dateFixed: new Date(),
canUpdate: true,
});
expect(findRemoveButton().exists()).toBe(true);
});
describe('collapsed calendar icon', () => {
it('receives full date string in words based on `selectedDate` prop value', () => {
createComponent({
selectedDate: new Date(2018, 0, 1),
});
expect(wrapper.find(CollapsedCalendarIcon).props('text')).toBe('Jan 1, 2018');
});
it('receives `None` when `selectedDateWords` is not defined', () => {
createComponent();
expect(wrapper.find(CollapsedCalendarIcon).props('text')).toBe('None');
});
});
it('returns full date string in words based on `dateFixed` prop value', () => {
createComponent({
dateFixed: new Date(2018, 0, 1),
});
expect(wrapper.text()).toContain('Jan 1, 2018');
});
it('returns full date string in words when `dateFromMilestones` is defined', () => {
createComponent({ dateFromMilestones: new Date(2018, 0, 1) });
expect(wrapper.text()).toContain('Inherited: Jan 1, 2018');
});
it('returns `None` when `dateFromMilestones` is not defined', () => {
createComponent();
expect(wrapper.text()).toContain('Inherited: None');
});
it('popover has message and link', () => {
createComponent();
const message =
'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.';
const popover = wrapper.find(GlPopover);
const popoverLink = popover.find(GlLink);
expect(popover.text()).toContain(message);
expect(popoverLink.text()).toBe('More information');
expect(popoverLink.attributes('href')).toContain(
'/help/user/group/epics/index.md#start-date-and-due-date',
);
});
it('popover has different message and link when date is invalid', () => {
createComponent({ isDateInvalid: true, selectedDateIsFixed: false });
const popover = wrapper.findAll(GlPopover).at(1);
const popoverLink = popover.find(GlLink);
expect(popover.text()).toContain('Selected date is invalid');
expect(popoverLink.text()).toBe('How can I solve this?');
expect(popoverLink.attributes('href')).toContain(
'/help/user/group/epics/index.md#start-date-and-due-date',
);
});
it('stops editing and emits `toggleDateType` event on component on `hidePicker` from date picker', () => {
createComponent({ canUpdate: true });
startEditing();
return wrapper.vm
.$nextTick()
.then(() => {
wrapper.find(DatePicker).vm.$emit('hidePicker');
expect(wrapper.emitted().toggleDateType[0]).toStrictEqual([
{ dateTypeIsFixed: true, typeChangeOnEdit: true },
]);
})
.then(() => wrapper.vm.$nextTick())
.then(() => {
expect(wrapper.find(DatePicker).exists()).toBe(false);
});
});
it('starts editing when clicked on edit button', () => {
createComponent();
expect(wrapper.find(DatePicker).exists()).toBe(false);
const e = createFakeEvent();
findEditButton().vm.$emit('click', e);
return wrapper.vm.$nextTick().then(() => {
expect(e.stopPropagation).toHaveBeenCalled();
expect(wrapper.find(DatePicker).exists()).toBe(true);
});
});
it('stops editing and emits `saveDate` when `newDateSelected` emitted by date picker', () => {
const date = new Date();
createComponent();
startEditing();
return wrapper.vm.$nextTick().then(() => {
wrapper.find(DatePicker).vm.$emit('newDateSelected', date);
expect(wrapper.emitted().saveDate).toStrictEqual([[date]]);
});
});
it('emits `toggleDateType` event on component when input is clicked', () => {
createComponent({ canUpdate: true });
wrapper.find('input').trigger('click');
return wrapper.vm.$nextTick(() => {
expect(wrapper.emitted().toggleDateType).toStrictEqual([[{ dateTypeIsFixed: true }]]);
});
});
it('emits `toggleCollapse` event when toggle-sidebar emits `toggle` event', () => {
createComponent({ showToggleSidebar: true });
wrapper.find(ToggleSidebar).vm.$emit('toggle');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().toggleCollapse).toBeDefined();
});
});
it('renders help icon', () => {
createComponent();
expect(wrapper.find(GlIcon).attributes('arialabel')).toBe('Help');
});
it('renders edit button', () => {
createComponent();
expect(wrapper.find(GlButton).text()).toBe('Edit');
});
it('renders an abbreviation', () => {
createComponent();
expect(wrapper.find('abbr').attributes('title')).toBe(
'Select an issue with milestone to set date',
);
});
it('renders collapse button when `showToggleSidebar` prop is `true`', () => {
createComponent({ showToggleSidebar: true });
expect(wrapper.find(ToggleSidebar).exists()).toBe(true);
});
it('renders loading icon when `dateSaveInProgress` prop is true', () => {
createComponent({ dateSaveInProgress: true });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('renders date warning icon when `isDateInvalid` prop is `true`', () => {
createComponent({ isDateInvalid: true, selectedDateIsFixed: false });
const warningIcon = findIconByName('warning');
expect(warningIcon.exists()).toBe(true);
expect(warningIcon.attributes('tabindex')).toBe('0');
});
});
...@@ -13483,9 +13483,6 @@ msgstr "" ...@@ -13483,9 +13483,6 @@ msgstr ""
msgid "Epics|Enter a title for your epic" msgid "Epics|Enter a title for your epic"
msgstr "" msgstr ""
msgid "Epics|How can I solve this?"
msgstr ""
msgid "Epics|Leave empty to inherit from milestone dates" msgid "Epics|Leave empty to inherit from milestone dates"
msgstr "" msgstr ""
...@@ -13534,9 +13531,6 @@ msgstr "" ...@@ -13534,9 +13531,6 @@ msgstr ""
msgid "Epics|Something went wrong while removing issue from epic." msgid "Epics|Something went wrong while removing issue from epic."
msgstr "" msgstr ""
msgid "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."
msgstr ""
msgid "Epics|This epic and any containing child epics are confidential and should only be visible to team members with at least Reporter access." msgid "Epics|This epic and any containing child epics are confidential and should only be visible to team members with at least Reporter access."
msgstr "" msgstr ""
...@@ -14997,15 +14991,6 @@ msgstr "" ...@@ -14997,15 +14991,6 @@ msgstr ""
msgid "Fixed burndown chart" msgid "Fixed burndown chart"
msgstr "" msgstr ""
msgid "Fixed date"
msgstr ""
msgid "Fixed due date"
msgstr ""
msgid "Fixed start date"
msgstr ""
msgid "Fixed:" msgid "Fixed:"
msgstr "" msgstr ""
...@@ -33059,9 +33044,6 @@ msgstr "" ...@@ -33059,9 +33044,6 @@ msgstr ""
msgid "Start a review" msgid "Start a review"
msgstr "" msgstr ""
msgid "Start and due date"
msgstr ""
msgid "Start by choosing a group to start exploring the merge requests in that group. You can then proceed to filter by projects, labels, milestones and authors." msgid "Start by choosing a group to start exploring the merge requests in that group. You can then proceed to filter by projects, labels, milestones and authors."
msgstr "" msgstr ""
...@@ -35453,12 +35435,6 @@ msgstr "" ...@@ -35453,12 +35435,6 @@ msgstr ""
msgid "This credential has expired" msgid "This credential has expired"
msgstr "" msgstr ""
msgid "This date is after the due date, so this epic won't appear in the roadmap."
msgstr ""
msgid "This date is before the start date, so this epic won't appear in the roadmap."
msgstr ""
msgid "This device has already been registered with us." msgid "This device has already been registered with us."
msgstr "" msgstr ""
...@@ -37250,9 +37226,6 @@ msgstr "" ...@@ -37250,9 +37226,6 @@ msgstr ""
msgid "Unsupported todo type passed. Supported todo types are: %{todo_types}" msgid "Unsupported todo type passed. Supported todo types are: %{todo_types}"
msgstr "" msgstr ""
msgid "Until"
msgstr ""
msgid "Until revoked, expired personal access tokens pose a security risk." msgid "Until revoked, expired personal access tokens pose a security risk."
msgstr "" msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import CollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
describe('CollapsedGroupedDatePicker', () => {
let wrapper;
const defaultProps = {
showToggleSidebar: true,
};
const minDate = new Date('07/17/2016');
const maxDate = new Date('07/17/2017');
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(CollapsedGroupedDatePicker, {
propsData: { ...defaultProps, ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
const findCollapsedCalendarIcon = () => wrapper.findComponent(CollapsedCalendarIcon);
const findAllCollapsedCalendarIcons = () => wrapper.findAllComponents(CollapsedCalendarIcon);
describe('toggleCollapse events', () => {
it('should emit when collapsed-calendar-icon is clicked', () => {
createComponent();
findCollapsedCalendarIcon().trigger('click');
expect(wrapper.emitted('toggleCollapse')[0]).toBeDefined();
});
});
describe('minDate and maxDate', () => {
it('should render both collapsed-calendar-icon', () => {
createComponent({
props: {
minDate,
maxDate,
},
});
const icons = findAllCollapsedCalendarIcons();
expect(icons.length).toBe(2);
expect(icons.at(0).text()).toBe('Jul 17 2016');
expect(icons.at(1).text()).toBe('Jul 17 2017');
});
});
describe('minDate', () => {
it('should render minDate in collapsed-calendar-icon', () => {
createComponent({
props: {
minDate,
},
});
const icons = findAllCollapsedCalendarIcons();
expect(icons.length).toBe(1);
expect(icons.at(0).text()).toBe('From Jul 17 2016');
});
});
describe('maxDate', () => {
it('should render maxDate in collapsed-calendar-icon', () => {
createComponent({
props: {
maxDate,
},
});
const icons = findAllCollapsedCalendarIcons();
expect(icons.length).toBe(1);
expect(icons.at(0).text()).toBe('Until Jul 17 2017');
});
});
describe('no dates', () => {
beforeEach(() => {
createComponent();
});
it('should render None', () => {
const icons = findAllCollapsedCalendarIcons();
expect(icons.length).toBe(1);
expect(icons.at(0).text()).toBe('None');
});
it('should have tooltip as `Start and due date`', () => {
const icons = findAllCollapsedCalendarIcons();
expect(icons.at(0).props('tooltipText')).toBe('Start and due date');
});
});
});
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