Commit 57af7a1e authored by Florie Guibert's avatar Florie Guibert Committed by Natalia Tepluhina

Epic Boards - Edit start and due dates in sidebar

parent c5a87715
<script>
import { GlButton, GlIcon, GlDatepicker, GlTooltipDirective } from '@gitlab/ui';
import { GlIcon, GlDatepicker, GlTooltipDirective, GlLink, GlPopover } from '@gitlab/ui';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { dueDateQueries } from '~/sidebar/constants';
import { dateFields, dateTypes, dueDateQueries, startDateQueries } from '~/sidebar/constants';
import SidebarFormattedDate from './sidebar_formatted_date.vue';
import SidebarInheritDate from './sidebar_inherit_date.vue';
const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', {
bubbles: true,
......@@ -15,34 +17,58 @@ export default {
tracking: {
event: 'click_edit_button',
label: 'right_sidebar',
property: 'dueDate',
},
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlButton,
GlIcon,
GlDatepicker,
GlLink,
GlPopover,
SidebarEditableItem,
SidebarFormattedDate,
SidebarInheritDate,
},
inject: ['fullPath', 'iid', 'canUpdate'],
inject: ['canUpdate'],
props: {
iid: {
type: String,
required: true,
},
fullPath: {
type: String,
required: true,
},
dateType: {
type: String,
required: false,
default: dateTypes.due,
},
issuableType: {
required: true,
type: String,
},
canInherit: {
required: false,
type: Boolean,
default: false,
},
},
data() {
return {
dueDate: null,
issuable: {},
loading: false,
tracking: {
...this.$options.tracking,
property: this.dateType === dateTypes.start ? 'startDate' : 'dueDate',
},
};
},
apollo: {
dueDate: {
issuable: {
query() {
return dueDateQueries[this.issuableType].query;
return this.dateQueries[this.issuableType].query;
},
variables() {
return {
......@@ -51,40 +77,60 @@ export default {
};
},
update(data) {
return data.workspace?.issuable?.dueDate || null;
return data.workspace?.issuable || {};
},
result({ data }) {
this.$emit('dueDateUpdated', data.workspace?.issuable?.dueDate);
this.$emit(`${this.dateType}Updated`, data.workspace?.issuable?.[this.dateType]);
},
error() {
createFlash({
message: sprintf(__('Something went wrong while setting %{issuableType} due date.'), {
issuableType: this.issuableType,
}),
message: sprintf(
__('Something went wrong while setting %{issuableType} %{dateType} date.'),
{
issuableType: this.issuableType,
dateType: this.dateType === dateTypes.start ? 'start' : 'due',
},
),
});
},
},
},
computed: {
dateQueries() {
return this.dateType === dateTypes.start ? startDateQueries : dueDateQueries;
},
dateLabel() {
return this.dateType === dateTypes.start
? this.$options.i18n.startDate
: this.$options.i18n.dueDate;
},
removeDateLabel() {
return this.dateType === dateTypes.start
? this.$options.i18n.removeStartDate
: this.$options.i18n.removeDueDate;
},
dateValue() {
return this.issuable?.[this.dateType] || null;
},
isLoading() {
return this.$apollo.queries.dueDate.loading || this.loading;
return this.$apollo.queries.issuable.loading || this.loading;
},
hasDueDate() {
return this.dueDate !== null;
hasDate() {
return this.dateValue !== null;
},
parsedDueDate() {
if (!this.hasDueDate) {
parsedDate() {
if (!this.hasDate) {
return null;
}
return parsePikadayDate(this.dueDate);
return parsePikadayDate(this.dateValue);
},
formattedDueDate() {
if (!this.hasDueDate) {
return this.$options.i18n.noDueDate;
formattedDate() {
if (!this.hasDate) {
return this.$options.i18n.noDate;
}
return dateInWords(this.parsedDueDate, true);
return dateInWords(this.parsedDate, true);
},
workspacePath() {
return this.issuableType === IssuableType.Issue
......@@ -95,6 +141,9 @@ export default {
groupPath: this.fullPath,
};
},
dataTestId() {
return this.dateType === dateTypes.start ? 'start-date' : 'due-date';
},
},
methods: {
closeForm() {
......@@ -105,24 +154,36 @@ export default {
openDatePicker() {
this.$refs.datePicker.calendar.show();
},
setDueDate(date) {
setFixedDate(isFixed) {
const date = this.issuable[dateFields[this.dateType].dateFixed];
this.setDate(date, isFixed);
},
setDate(date, isFixed = true) {
const formattedDate = date ? formatDate(date, 'yyyy-mm-dd') : null;
this.loading = true;
this.$refs.editable.collapse();
this.$apollo
.mutate({
mutation: dueDateQueries[this.issuableType].mutation,
mutation: this.dateQueries[this.issuableType].mutation,
variables: {
input: {
...this.workspacePath,
iid: this.iid,
dueDate: date ? formatDate(date, 'yyyy-mm-dd') : null,
...(this.canInherit
? {
[dateFields[this.dateType].dateFixed]: isFixed ? formattedDate : undefined,
[dateFields[this.dateType].isDateFixed]: isFixed,
}
: {
[this.dateType]: formattedDate,
}),
},
},
})
.then(
({
data: {
issuableSetDueDate: { errors },
issuableSetDate: { errors },
},
}) => {
if (errors.length) {
......@@ -136,9 +197,13 @@ export default {
)
.catch(() => {
createFlash({
message: sprintf(__('Something went wrong while setting %{issuableType} due date.'), {
issuableType: this.issuableType,
}),
message: sprintf(
__('Something went wrong while setting %{issuableType} %{dateType} date.'),
{
issuableType: this.issuableType,
dateType: this.dateType === dateTypes.start ? 'start' : 'due',
},
),
});
})
.finally(() => {
......@@ -148,55 +213,80 @@ export default {
},
i18n: {
dueDate: __('Due date'),
noDueDate: __('None'),
startDate: __('Start date'),
noDate: __('None'),
removeDueDate: __('remove due date'),
removeStartDate: __('remove start date'),
dateHelpValidMessage: __(
'These dates affect how your epics appear in the roadmap. Set a fixed date or one inherited from the milestones assigned to issues in this epic.',
),
help: __('Help'),
learnMore: __('Learn more'),
},
dateHelpUrl: '/help/user/group/epics/index.md#start-date-and-due-date',
};
</script>
<template>
<sidebar-editable-item
ref="editable"
:title="$options.i18n.dueDate"
:tracking="$options.tracking"
:title="dateLabel"
:tracking="tracking"
:loading="isLoading"
class="block"
data-testid="due-date"
:data-testid="dataTestId"
@open="openDatePicker"
>
<template #title-extra>
<gl-icon
ref="epicDatePopover"
name="question-o"
class="gl-ml-3 gl-cursor-pointer gl-text-blue-600"
tabindex="0"
:aria-label="$options.i18n.help"
/>
<gl-popover
:target="() => $refs.epicDatePopover.$el"
triggers="focus"
placement="left"
boundary="viewport"
>
<p>{{ $options.i18n.dateHelpValidMessage }}</p>
<gl-link :href="$options.dateHelpUrl" target="_blank">{{
$options.i18n.learnMore
}}</gl-link>
</gl-popover>
</template>
<template #collapsed>
<div v-gl-tooltip :title="$options.i18n.dueDate" class="sidebar-collapsed-icon">
<div v-gl-tooltip :title="dateLabel" class="sidebar-collapsed-icon">
<gl-icon :size="16" name="calendar" />
<span class="collapse-truncated-title">{{ formattedDueDate }}</span>
</div>
<div class="gl-display-flex gl-align-items-center hide-collapsed">
<span
:class="hasDueDate ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'"
data-testid="sidebar-duedate-value"
>
{{ formattedDueDate }}
</span>
<div v-if="hasDueDate && canUpdate" class="gl-display-flex">
<span class="gl-px-2">-</span>
<gl-button
variant="link"
class="gl-text-gray-500!"
data-testid="reset-button"
:disabled="isLoading"
@click="setDueDate(null)"
>
{{ $options.i18n.removeDueDate }}
</gl-button>
</div>
<span class="collapse-truncated-title">{{ formattedDate }}</span>
</div>
<sidebar-inherit-date
v-if="canInherit"
:issuable="issuable"
:is-loading="isLoading"
:date-type="dateType"
@reset-date="setDate(null)"
@set-date="setFixedDate"
/>
<sidebar-formatted-date
v-else
:has-date="hasDate"
:formatted-date="formattedDate"
:reset-text="removeDateLabel"
:is-loading="isLoading"
@reset-date="setDate(null)"
/>
</template>
<template #default>
<gl-datepicker
ref="datePicker"
:value="parsedDueDate"
class="gl-relative"
:value="parsedDate"
show-clear-button
@input="setDueDate"
@clear="setDueDate(null)"
@input="setDate"
@clear="setDate(null)"
/>
</template>
</sidebar-editable-item>
......
<script>
import { GlButton } from '@gitlab/ui';
export default {
components: {
GlButton,
},
inject: ['canUpdate'],
props: {
formattedDate: {
required: true,
type: String,
},
hasDate: {
required: true,
type: Boolean,
},
resetText: {
required: true,
type: String,
},
isLoading: {
required: true,
type: Boolean,
},
canDelete: {
required: false,
type: Boolean,
default: true,
},
},
};
</script>
<template>
<div class="gl-display-flex gl-align-items-center hide-collapsed">
<span
:class="hasDate ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'"
data-testid="sidebar-date-value"
>
{{ formattedDate }}
</span>
<div v-if="hasDate && canUpdate && canDelete" class="gl-display-flex">
<span class="gl-px-2">-</span>
<gl-button
variant="link"
class="gl-text-gray-500!"
data-testid="reset-button"
:disabled="isLoading"
@click="$emit('reset-date', $event)"
>
{{ resetText }}
</gl-button>
</div>
</div>
</template>
<script>
import { GlFormRadio } from '@gitlab/ui';
import { dateInWords, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import { dateFields } from '../../constants';
import SidebarFormattedDate from './sidebar_formatted_date.vue';
export default {
components: {
GlFormRadio,
SidebarFormattedDate,
},
inject: ['canUpdate'],
props: {
issuable: {
required: true,
type: Object,
},
isLoading: {
required: true,
type: Boolean,
},
dateType: {
type: String,
required: true,
},
},
computed: {
dateIsFixed: {
get() {
return this.issuable?.[dateFields[this.dateType].isDateFixed] || false;
},
set(fixed) {
this.$emit('set-date', fixed);
},
},
hasFixedDate() {
return this.issuable[dateFields[this.dateType].dateFixed] !== null;
},
formattedFixedDate() {
const dateFixed = this.issuable[dateFields[this.dateType].dateFixed];
if (!dateFixed) {
return this.$options.i18n.noDate;
}
return dateInWords(parsePikadayDate(dateFixed), true);
},
formattedInheritedDate() {
const dateFromMilestones = this.issuable[dateFields[this.dateType].dateFromMilestones];
if (!dateFromMilestones) {
return this.$options.i18n.noDate;
}
return dateInWords(parsePikadayDate(dateFromMilestones), true);
},
},
i18n: {
fixed: __('Fixed:'),
inherited: __('Inherited:'),
remove: __('remove'),
noDate: __('None'),
},
};
</script>
<template>
<div class="hide-collapsed gl-mt-3">
<div class="gl-display-flex gl-align-items-baseline" data-testid="sidebar-fixed-date">
<gl-form-radio
v-model="dateIsFixed"
:value="true"
:disabled="!canUpdate || isLoading"
class="gl-pr-2"
>
<span :class="dateIsFixed ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'">
{{ $options.i18n.fixed }}
</span>
</gl-form-radio>
<sidebar-formatted-date
:has-date="dateIsFixed"
:formatted-date="formattedFixedDate"
:reset-text="$options.i18n.remove"
:is-loading="isLoading"
:can-delete="dateIsFixed && hasFixedDate"
class="gl-line-height-normal"
@reset-date="$emit('reset-date', $event)"
/>
</div>
<div class="gl-display-flex gl-align-items-baseline" data-testid="sidebar-inherited-date">
<gl-form-radio
v-model="dateIsFixed"
:value="false"
:disabled="!canUpdate || isLoading"
class="gl-pr-2"
>
<span :class="!dateIsFixed ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'">
{{ $options.i18n.inherited }}
</span>
</gl-form-radio>
<sidebar-formatted-date
:has-date="!dateIsFixed"
:formatted-date="formattedInheritedDate"
:reset-text="$options.i18n.remove"
:is-loading="isLoading"
:can-delete="false"
class="gl-line-height-normal"
/>
</div>
</div>
</template>
......@@ -103,6 +103,7 @@ export default {
<div>
<div class="gl-display-flex gl-align-items-center" @click.self="collapse">
<span class="hide-collapsed" data-testid="title" @click="collapse">{{ title }}</span>
<slot name="title-extra"></slot>
<gl-loading-icon v-if="loading || initialLoading" inline class="gl-ml-2 hide-collapsed" />
<gl-loading-icon
v-if="loading && isClassicSidebar"
......
import { IssuableType } from '~/issue_show/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
import updateEpicMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql';
import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql';
import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql';
import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
......@@ -34,7 +38,7 @@ export const confidentialityQueries = {
},
[IssuableType.Epic]: {
query: epicConfidentialQuery,
mutation: updateEpicMutation,
mutation: updateEpicConfidentialMutation,
},
};
......@@ -47,9 +51,38 @@ export const referenceQueries = {
},
};
export const dateTypes = {
start: 'startDate',
due: 'dueDate',
};
export const dateFields = {
[dateTypes.start]: {
isDateFixed: 'startDateIsFixed',
dateFixed: 'startDateFixed',
dateFromMilestones: 'startDateFromMilestones',
},
[dateTypes.due]: {
isDateFixed: 'dueDateIsFixed',
dateFixed: 'dueDateFixed',
dateFromMilestones: 'dueDateFromMilestones',
},
};
export const dueDateQueries = {
[IssuableType.Issue]: {
query: issueDueDateQuery,
mutation: updateIssueDueDateMutation,
},
[IssuableType.Epic]: {
query: epicDueDateQuery,
mutation: updateEpicDueDateMutation,
},
};
export const startDateQueries = {
[IssuableType.Epic]: {
query: epicStartDateQuery,
mutation: updateEpicStartDateMutation,
},
};
......@@ -13,7 +13,7 @@ import { __ } from '~/locale';
import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDueDateWidget from '~/sidebar/components/due_date/sidebar_due_date_widget.vue';
import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
import { apolloProvider } from '~/sidebar/graphql';
import Translate from '../vue_shared/translate';
......@@ -225,14 +225,14 @@ function mountDueDateComponent() {
SidebarDueDateWidget,
},
provide: {
iid: String(iid),
fullPath,
canUpdate: editable,
},
render: (createElement) =>
createElement('sidebar-due-date-widget', {
props: {
iid: String(iid),
fullPath,
issuableType: IssuableType.Issue,
},
}),
......
query epicDueDate($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
issuable: epic(iid: $iid) {
__typename
id
dueDate
dueDateIsFixed
dueDateFixed
dueDateFromMilestones
}
}
}
query epicStartDate($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
issuable: epic(iid: $iid) {
__typename
id
startDate
startDateIsFixed
startDateFixed
startDateFromMilestones
}
}
}
mutation updateEpicDueDate($input: UpdateEpicInput!) {
issuableSetDate: updateEpic(input: $input) {
issuable: epic {
id
dueDateIsFixed
dueDateFixed
dueDateFromMilestones
}
errors
}
}
mutation updateEpicStartDate($input: UpdateEpicInput!) {
issuableSetDate: updateEpic(input: $input) {
issuable: epic {
id
startDateIsFixed
startDateFixed
startDateFromMilestones
}
errors
}
}
mutation updateIssueDueDate($input: UpdateIssueInput!) {
issuableSetDueDate: updateIssue(input: $input) {
issuableSetDate: updateIssue(input: $input) {
issuable: issue {
id
dueDate
......
......@@ -7,6 +7,7 @@ import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.v
import { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
export default {
headerHeight: `${contentTop()}px`,
......@@ -16,6 +17,7 @@ export default {
BoardSidebarSubscription,
BoardSidebarTitle,
SidebarConfidentialityWidget,
SidebarDateWidget,
},
computed: {
...mapGetters(['isSidebarOpen', 'activeBoardItem']),
......@@ -46,6 +48,20 @@ export default {
<template #header>{{ __('Epic details') }}</template>
<template #default>
<board-sidebar-title data-testid="sidebar-title" />
<sidebar-date-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
date-type="startDate"
issuable-type="epic"
:can-inherit="true"
/>
<sidebar-date-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
date-type="dueDate"
issuable-type="epic"
:can-inherit="true"
/>
<board-sidebar-labels-select class="labels" />
<sidebar-confidentiality-widget
:iid="activeBoardItem.iid"
......
......@@ -81,6 +81,50 @@ RSpec.describe 'Epic boards sidebar', :js do
end
end
context 'start date' do
it 'edits fixed start date' do
click_card(card)
wait_for_requests
page.within('[data-testid="start-date"]') do
edit_fixed_date
end
end
it 'removes fixed start date' do
click_card(card)
wait_for_requests
page.within('[data-testid="start-date"]') do
remove_fixed_date
end
end
end
context 'due date' do
it 'edits fixed due date' do
click_card(card)
wait_for_requests
page.within('[data-testid="due-date"]') do
edit_fixed_date
end
end
it 'removes fixed due date' do
click_card(card)
wait_for_requests
page.within('[data-testid="due-date"]') do
remove_fixed_date
end
end
end
context 'labels' do
it 'adds a single label' do
click_card(card)
......@@ -173,4 +217,48 @@ RSpec.describe 'Epic boards sidebar', :js do
click_card(card)
end
def pick_a_date
click_button 'Edit'
expect(page).to have_selector('.gl-datepicker')
page.within('.pika-lendar') do
click_button '25'
end
wait_for_requests
end
def edit_fixed_date
page.within('[data-testid="sidebar-inherited-date"]') do
expect(find_field('Inherited:')).to be_checked
end
pick_a_date
page.within('[data-testid="sidebar-fixed-date"]') do
expect(find('[data-testid="sidebar-date-value"]').text).to include('25')
expect(find_field('Fixed:')).to be_checked
end
end
def remove_fixed_date
expect(page).not_to have_button('remove')
page.within('[data-testid="sidebar-fixed-date"]') do
expect(find('[data-testid="sidebar-date-value"]').text).to include('None')
end
pick_a_date
page.within('[data-testid="sidebar-fixed-date"]') do
expect(find('[data-testid="sidebar-date-value"]').text).not_to include('None')
expect(page).to have_button('remove')
find_button('remove').click
wait_for_requests
expect(page).not_to have_button('remove')
expect(find('[data-testid="sidebar-date-value"]').text).to include('None')
end
end
end
......@@ -29924,10 +29924,10 @@ msgstr ""
msgid "Something went wrong while resolving this discussion. Please try again."
msgstr ""
msgid "Something went wrong while setting %{issuableType} confidentiality."
msgid "Something went wrong while setting %{issuableType} %{dateType} date."
msgstr ""
msgid "Something went wrong while setting %{issuableType} due date."
msgid "Something went wrong while setting %{issuableType} confidentiality."
msgstr ""
msgid "Something went wrong while stopping this environment. Please try again."
......@@ -32351,6 +32351,9 @@ msgstr ""
msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
msgstr ""
msgid "These dates affect how your epics appear in the roadmap. Set a fixed date or one inherited from the milestones assigned to issues in this epic."
msgstr ""
msgid "These examples show how to trigger this project's pipeline for a branch or tag."
msgstr ""
......@@ -38470,6 +38473,9 @@ msgstr ""
msgid "remove due date"
msgstr ""
msgid "remove start date"
msgstr ""
msgid "remove weight"
msgstr ""
......
......@@ -417,7 +417,7 @@ RSpec.describe "Issues > User edits issue", :js do
wait_for_requests
expect(find('[data-testid="sidebar-duedate-value"]').text).to have_content date.strftime('%b %-d, %Y')
expect(find('[data-testid="sidebar-date-value"]').text).to have_content date.strftime('%b %-d, %Y')
end
end
......
......@@ -4,37 +4,48 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import SidebarDueDateWidget from '~/sidebar/components/due_date/sidebar_due_date_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_date.vue';
import SidebarInheritDate from '~/sidebar/components/date/sidebar_inherit_date.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
import { issueDueDateResponse } from '../../mock_data';
import { issuableDueDateResponse, issuableStartDateResponse } from '../../mock_data';
jest.mock('~/flash');
Vue.use(VueApollo);
describe('Sidebar Due date Widget', () => {
describe('Sidebar date Widget', () => {
let wrapper;
let fakeApollo;
const date = '2021-04-15';
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findFormattedDueDate = () => wrapper.find("[data-testid='sidebar-duedate-value']");
const createComponent = ({
dueDateQueryHandler = jest.fn().mockResolvedValue(issueDueDateResponse()),
dueDateQueryHandler = jest.fn().mockResolvedValue(issuableDueDateResponse()),
startDateQueryHandler = jest.fn().mockResolvedValue(issuableStartDateResponse()),
canInherit = false,
dateType = undefined,
issuableType = 'issue',
} = {}) => {
fakeApollo = createMockApollo([[issueDueDateQuery, dueDateQueryHandler]]);
fakeApollo = createMockApollo([
[issueDueDateQuery, dueDateQueryHandler],
[epicStartDateQuery, startDateQueryHandler],
]);
wrapper = shallowMount(SidebarDueDateWidget, {
wrapper = shallowMount(SidebarDateWidget, {
apolloProvider: fakeApollo,
provide: {
fullPath: 'group/project',
iid: '1',
canUpdate: true,
},
propsData: {
issuableType: 'issue',
fullPath: 'group/project',
iid: '1',
issuableType,
canInherit,
dateType,
},
stubs: {
SidebarEditableItem,
......@@ -53,10 +64,16 @@ describe('Sidebar Due date Widget', () => {
expect(findEditableItem().props('loading')).toBe(true);
});
describe('when issue has no due date', () => {
it('dateType is due date by default', () => {
createComponent();
expect(wrapper.text()).toContain('Due date');
});
describe('when issuable has no due date', () => {
beforeEach(async () => {
createComponent({
dueDateQueryHandler: jest.fn().mockResolvedValue(issueDueDateResponse(null)),
dueDateQueryHandler: jest.fn().mockResolvedValue(issuableDueDateResponse(null)),
});
await waitForPromises();
});
......@@ -65,10 +82,6 @@ describe('Sidebar Due date Widget', () => {
expect(findEditableItem().props('loading')).toBe(false);
});
it('dueDate is null by default', () => {
expect(findFormattedDueDate().text()).toBe('None');
});
it('emits `dueDateUpdated` event with a `null` payload', () => {
expect(wrapper.emitted('dueDateUpdated')).toEqual([[null]]);
});
......@@ -77,7 +90,7 @@ describe('Sidebar Due date Widget', () => {
describe('when issue has due date', () => {
beforeEach(async () => {
createComponent({
dueDateQueryHandler: jest.fn().mockResolvedValue(issueDueDateResponse(date)),
dueDateQueryHandler: jest.fn().mockResolvedValue(issuableDueDateResponse(date)),
});
await waitForPromises();
});
......@@ -86,15 +99,26 @@ describe('Sidebar Due date Widget', () => {
expect(findEditableItem().props('loading')).toBe(false);
});
it('has dueDate', () => {
expect(findFormattedDueDate().text()).toBe('Apr 15, 2021');
});
it('emits `dueDateUpdated` event with the date payload', () => {
expect(wrapper.emitted('dueDateUpdated')).toEqual([[date]]);
});
});
it.each`
canInherit | component | componentName | expected
${true} | ${SidebarFormattedDate} | ${'SidebarFormattedDate'} | ${false}
${true} | ${SidebarInheritDate} | ${'SidebarInheritDate'} | ${true}
${false} | ${SidebarFormattedDate} | ${'SidebarFormattedDate'} | ${true}
${false} | ${SidebarInheritDate} | ${'SidebarInheritDate'} | ${false}
`(
'when canInherit is $canInherit, $componentName display is $expected',
({ canInherit, component, expected }) => {
createComponent({ canInherit });
expect(wrapper.find(component).exists()).toBe(expected);
},
);
it('displays a flash message when query is rejected', async () => {
createComponent({
dueDateQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
......@@ -103,4 +127,23 @@ describe('Sidebar Due date Widget', () => {
expect(createFlash).toHaveBeenCalled();
});
it.each`
dateType | text | event | mockedResponse | issuableType | queryHandler
${'dueDate'} | ${'Due date'} | ${'dueDateUpdated'} | ${issuableDueDateResponse} | ${'issue'} | ${'dueDateQueryHandler'}
${'startDate'} | ${'Start date'} | ${'startDateUpdated'} | ${issuableStartDateResponse} | ${'epic'} | ${'startDateQueryHandler'}
`(
'when dateType is $dateType, component renders $text and emits $event',
async ({ dateType, text, event, mockedResponse, issuableType, queryHandler }) => {
createComponent({
dateType,
issuableType,
[queryHandler]: jest.fn().mockResolvedValue(mockedResponse(date)),
});
await waitForPromises();
expect(wrapper.text()).toContain(text);
expect(wrapper.emitted(event)).toEqual([[date]]);
},
);
});
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_date.vue';
describe('SidebarFormattedDate', () => {
let wrapper;
const findFormattedDate = () => wrapper.find("[data-testid='sidebar-date-value']");
const findRemoveButton = () => wrapper.find(GlButton);
const createComponent = ({ hasDate = true } = {}) => {
wrapper = shallowMount(SidebarFormattedDate, {
provide: {
canUpdate: true,
},
propsData: {
formattedDate: 'Apr 15, 2021',
hasDate,
issuableType: 'issue',
resetText: 'remove',
isLoading: false,
canDelete: true,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('displays formatted date', () => {
expect(findFormattedDate().text()).toBe('Apr 15, 2021');
});
describe('when issue has due date', () => {
it('displays remove button', () => {
expect(findRemoveButton().exists()).toBe(true);
expect(findRemoveButton().children).toEqual(wrapper.props.resetText);
});
it('emits reset-date event on click on remove button', () => {
findRemoveButton().vm.$emit('click');
expect(wrapper.emitted('reset-date')).toEqual([[undefined]]);
});
});
describe('when issuable has no due date', () => {
beforeEach(() => {
createComponent({
hasDate: false,
});
});
it('does not display remove button', () => {
expect(findRemoveButton().exists()).toBe(false);
});
});
});
import { GlFormRadio } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_date.vue';
import SidebarInheritDate from '~/sidebar/components/date/sidebar_inherit_date.vue';
describe('SidebarInheritDate', () => {
let wrapper;
const findFixedFormattedDate = () => wrapper.findAll(SidebarFormattedDate).at(0);
const findInheritFormattedDate = () => wrapper.findAll(SidebarFormattedDate).at(1);
const findFixedRadio = () => wrapper.findAll(GlFormRadio).at(0);
const findInheritRadio = () => wrapper.findAll(GlFormRadio).at(1);
const createComponent = () => {
wrapper = shallowMount(SidebarInheritDate, {
provide: {
canUpdate: true,
},
propsData: {
issuable: {
dueDate: '2021-04-15',
dueDateIsFixed: true,
dueDateFixed: '2021-04-15',
dueDateFromMilestones: '2021-05-15',
},
isLoading: false,
dateType: 'dueDate',
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('displays formatted fixed and inherited dates with radio buttons', () => {
expect(wrapper.findAll(SidebarFormattedDate)).toHaveLength(2);
expect(wrapper.findAll(GlFormRadio)).toHaveLength(2);
expect(findFixedFormattedDate().props('formattedDate')).toBe('Apr 15, 2021');
expect(findInheritFormattedDate().props('formattedDate')).toBe('May 15, 2021');
expect(findFixedRadio().text()).toBe('Fixed:');
expect(findInheritRadio().text()).toBe('Inherited:');
});
it('emits set-date event on click on radio button', () => {
findFixedRadio().vm.$emit('input', true);
expect(wrapper.emitted('set-date')).toEqual([[true]]);
});
});
......@@ -233,7 +233,7 @@ export const issueConfidentialityResponse = (confidential = false) => ({
},
});
export const issueDueDateResponse = (dueDate = null) => ({
export const issuableDueDateResponse = (dueDate = null) => ({
data: {
workspace: {
__typename: 'Project',
......@@ -246,6 +246,22 @@ export const issueDueDateResponse = (dueDate = null) => ({
},
});
export const issuableStartDateResponse = (startDate = null) => ({
data: {
workspace: {
__typename: 'Group',
issuable: {
__typename: 'Epic',
id: 'gid://gitlab/Epic/4',
startDate,
startDateIsFixed: true,
startDateFixed: startDate,
startDateFromMilestones: null,
},
},
},
});
export const issueReferenceResponse = (reference) => ({
data: {
workspace: {
......
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