Commit 0067c3aa authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '300645-refactor-due-date-sidebar-component-to-vue-apollo-client' into 'master'

Due date issue sidebar widget [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!58621
parents e421e0c1 7aea067a
<script>
import { GlButton, GlIcon, GlDatepicker, GlTooltipDirective } 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';
const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', {
bubbles: true,
});
export default {
tracking: {
event: 'click_edit_button',
label: 'right_sidebar',
property: 'dueDate',
},
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlButton,
GlIcon,
GlDatepicker,
SidebarEditableItem,
},
inject: ['fullPath', 'iid', 'canUpdate'],
props: {
issuableType: {
required: true,
type: String,
},
},
data() {
return {
dueDate: null,
loading: false,
};
},
apollo: {
dueDate: {
query() {
return dueDateQueries[this.issuableType].query;
},
variables() {
return {
fullPath: this.fullPath,
iid: String(this.iid),
};
},
update(data) {
return data.workspace?.issuable?.dueDate || null;
},
result({ data }) {
this.$emit('dueDateUpdated', data.workspace?.issuable?.dueDate);
},
error() {
createFlash({
message: sprintf(__('Something went wrong while setting %{issuableType} due date.'), {
issuableType: this.issuableType,
}),
});
},
},
},
computed: {
isLoading() {
return this.$apollo.queries.dueDate.loading || this.loading;
},
hasDueDate() {
return this.dueDate !== null;
},
parsedDueDate() {
if (!this.hasDueDate) {
return null;
}
return parsePikadayDate(this.dueDate);
},
formattedDueDate() {
if (!this.hasDueDate) {
return this.$options.i18n.noDueDate;
}
return dateInWords(this.parsedDueDate, true);
},
workspacePath() {
return this.issuableType === IssuableType.Issue
? {
projectPath: this.fullPath,
}
: {
groupPath: this.fullPath,
};
},
},
methods: {
closeForm() {
this.$refs.editable.collapse();
this.$el.dispatchEvent(hideDropdownEvent);
this.$emit('closeForm');
},
openDatePicker() {
this.$refs.datePicker.calendar.show();
},
setDueDate(date) {
this.loading = true;
this.$refs.editable.collapse();
this.$apollo
.mutate({
mutation: dueDateQueries[this.issuableType].mutation,
variables: {
input: {
...this.workspacePath,
iid: this.iid,
dueDate: date ? formatDate(date, 'yyyy-mm-dd') : null,
},
},
})
.then(
({
data: {
issuableSetDueDate: { errors },
},
}) => {
if (errors.length) {
createFlash({
message: errors[0],
});
} else {
this.$emit('closeForm');
}
},
)
.catch(() => {
createFlash({
message: sprintf(__('Something went wrong while setting %{issuableType} due date.'), {
issuableType: this.issuableType,
}),
});
})
.finally(() => {
this.loading = false;
});
},
},
i18n: {
dueDate: __('Due date'),
noDueDate: __('None'),
removeDueDate: __('remove due date'),
},
};
</script>
<template>
<sidebar-editable-item
ref="editable"
:title="$options.i18n.dueDate"
:tracking="$options.tracking"
:loading="isLoading"
class="block"
data-testid="due-date"
@open="openDatePicker"
>
<template #collapsed>
<div v-gl-tooltip :title="$options.i18n.dueDate" 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>
</div>
</template>
<template #default>
<gl-datepicker
ref="datePicker"
:value="parsedDueDate"
show-clear-button
@input="setDueDate"
@clear="setDueDate(null)"
/>
</template>
</sidebar-editable-item>
</template>
import { IssuableType } from '~/issue_show/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.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 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';
import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql';
import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
......@@ -42,3 +44,10 @@ export const referenceQueries = {
query: mergeRequestReferenceQuery,
},
};
export const dueDateQueries = {
[IssuableType.Issue]: {
query: issueDueDateQuery,
mutation: updateIssueDueDateMutation,
},
};
......@@ -13,6 +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 SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
import { apolloProvider } from '~/sidebar/graphql';
import Translate from '../vue_shared/translate';
......@@ -206,6 +207,36 @@ function mountConfidentialComponent() {
});
}
function mountDueDateComponent() {
const el = document.getElementById('js-due-date-entry-point');
if (!el) {
return;
}
const { fullPath, iid, editable } = getSidebarOptions();
// eslint-disable-next-line no-new
new Vue({
el,
apolloProvider,
components: {
SidebarDueDateWidget,
},
provide: {
iid: String(iid),
fullPath,
canUpdate: editable,
},
render: (createElement) =>
createElement('sidebar-due-date-widget', {
props: {
issuableType: IssuableType.Issue,
},
}),
});
}
function mountReferenceComponent() {
const el = document.getElementById('js-reference-entry-point');
if (!el) {
......@@ -387,6 +418,7 @@ export function mountSidebar(mediator) {
}
mountReviewersComponent(mediator);
mountConfidentialComponent(mediator);
mountDueDateComponent(mediator);
mountReferenceComponent(mediator);
mountLockComponent();
mountParticipantsComponent(mediator);
......
query issueDueDate($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
__typename
issuable: issue(iid: $iid) {
__typename
id
dueDate
}
}
}
mutation updateIssueDueDate($input: UpdateIssueInput!) {
issuableSetDueDate: updateIssue(input: $input) {
issuable: issue {
id
dueDate
}
errors
}
}
......@@ -70,41 +70,7 @@
= _('Time tracking')
= loading_icon(css_class: 'gl-vertical-align-text-bottom')
- if issuable_sidebar.has_key?(:due_date)
.block.due_date
.sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }
= sprite_icon('calendar')
%span.js-due-date-sidebar-value
= issuable_sidebar[:due_date].try(:to_s, :medium) || _('None')
.title.hide-collapsed
= _('Due date')
= loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
%span.value-content
- if issuable_sidebar[:due_date]
%span.bold= issuable_sidebar[:due_date].to_s(:medium)
- else
%span.no-value
= _('None')
- if can_edit_issuable
%span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) }
\-
%a.js-remove-due-date{ href: "#", role: "button" }
= _('remove due date')
- if can_edit_issuable
.selectbox.hide-collapsed
= f.hidden_field :due_date, value: issuable_sidebar[:due_date].try(:strftime, 'yy-mm-dd')
.dropdown
%button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable_type}[due_date]", ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], display: 'static' } }
%span.dropdown-toggle-text
= _('Due date')
= sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
.dropdown-menu.dropdown-menu-due-date
= dropdown_title(_('Due date'))
= dropdown_content do
.js-due-date-calendar
#js-due-date-entry-point
.js-sidebar-labels{ data: sidebar_labels_data(issuable_sidebar, @project) }
......
......@@ -29158,6 +29158,9 @@ msgstr ""
msgid "Something went wrong while setting %{issuableType} confidentiality."
msgstr ""
msgid "Something went wrong while setting %{issuableType} due date."
msgstr ""
msgid "Something went wrong while stopping this environment. Please try again."
msgstr ""
......
......@@ -323,24 +323,23 @@ RSpec.describe "Issues > User edits issue", :js do
it 'adds due date to issue' do
date = Date.today.at_beginning_of_month + 2.days
page.within '.due_date' do
click_link 'Edit'
page.within '[data-testid="due-date"]' do
click_button 'Edit'
page.within '.pika-single' do
click_button date.day
end
wait_for_requests
expect(find('.value').text).to have_content date.strftime('%b %-d, %Y')
expect(find('[data-testid="sidebar-duedate-value"]').text).to have_content date.strftime('%b %-d, %Y')
end
end
it 'removes due date from issue' do
date = Date.today.at_beginning_of_month + 2.days
page.within '.due_date' do
click_link 'Edit'
page.within '[data-testid="due-date"]' do
click_button 'Edit'
page.within '.pika-single' do
click_button date.day
......@@ -350,7 +349,7 @@ RSpec.describe "Issues > User edits issue", :js do
expect(page).to have_no_content 'None'
click_link 'remove due date'
click_button 'remove due date'
expect(page).to have_content 'None'
end
end
......
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
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 SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
import { issueDueDateResponse } from '../../mock_data';
jest.mock('~/flash');
Vue.use(VueApollo);
describe('Sidebar Due 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()),
} = {}) => {
fakeApollo = createMockApollo([[issueDueDateQuery, dueDateQueryHandler]]);
wrapper = shallowMount(SidebarDueDateWidget, {
apolloProvider: fakeApollo,
provide: {
fullPath: 'group/project',
iid: '1',
canUpdate: true,
},
propsData: {
issuableType: 'issue',
},
stubs: {
SidebarEditableItem,
},
});
};
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
});
it('passes a `loading` prop as true to editable item when query is loading', () => {
createComponent();
expect(findEditableItem().props('loading')).toBe(true);
});
describe('when issue has no due date', () => {
beforeEach(async () => {
createComponent({
dueDateQueryHandler: jest.fn().mockResolvedValue(issueDueDateResponse(null)),
});
await waitForPromises();
});
it('passes a `loading` prop as false to editable item', () => {
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]]);
});
});
describe('when issue has due date', () => {
beforeEach(async () => {
createComponent({
dueDateQueryHandler: jest.fn().mockResolvedValue(issueDueDateResponse(date)),
});
await waitForPromises();
});
it('passes a `loading` prop as false to editable item', () => {
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('displays a flash message when query is rejected', async () => {
createComponent({
dueDateQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
});
});
......@@ -233,6 +233,19 @@ export const issueConfidentialityResponse = (confidential = false) => ({
},
});
export const issueDueDateResponse = (dueDate = null) => ({
data: {
workspace: {
__typename: 'Project',
issuable: {
__typename: 'Issue',
id: 'gid://gitlab/Issue/4',
dueDate,
},
},
},
});
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