Commit 7aea067a authored by Florie Guibert's avatar Florie Guibert Committed by Natalia Tepluhina

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

parent 07858743
<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 { IssuableType } from '~/issue_show/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql'; import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import issueConfidentialQuery from '~/sidebar/queries/issue_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 issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql'; import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
import updateEpicMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql'; import updateEpicMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_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 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 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'; import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
...@@ -42,3 +44,10 @@ export const referenceQueries = { ...@@ -42,3 +44,10 @@ export const referenceQueries = {
query: mergeRequestReferenceQuery, query: mergeRequestReferenceQuery,
}, },
}; };
export const dueDateQueries = {
[IssuableType.Issue]: {
query: issueDueDateQuery,
mutation: updateIssueDueDateMutation,
},
};
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
} from '~/lib/utils/common_utils'; } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_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 SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
import { apolloProvider } from '~/sidebar/graphql'; import { apolloProvider } from '~/sidebar/graphql';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
...@@ -168,6 +169,36 @@ function mountConfidentialComponent() { ...@@ -168,6 +169,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() { function mountReferenceComponent() {
const el = document.getElementById('js-reference-entry-point'); const el = document.getElementById('js-reference-entry-point');
if (!el) { if (!el) {
...@@ -345,6 +376,7 @@ export function mountSidebar(mediator) { ...@@ -345,6 +376,7 @@ export function mountSidebar(mediator) {
mountAssigneesComponent(mediator); mountAssigneesComponent(mediator);
mountReviewersComponent(mediator); mountReviewersComponent(mediator);
mountConfidentialComponent(mediator); mountConfidentialComponent(mediator);
mountDueDateComponent(mediator);
mountReferenceComponent(mediator); mountReferenceComponent(mediator);
mountLockComponent(); mountLockComponent();
mountParticipantsComponent(mediator); 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 @@ ...@@ -70,41 +70,7 @@
= _('Time tracking') = _('Time tracking')
= loading_icon(css_class: 'gl-vertical-align-text-bottom') = loading_icon(css_class: 'gl-vertical-align-text-bottom')
- if issuable_sidebar.has_key?(:due_date) - if issuable_sidebar.has_key?(:due_date)
.block.due_date #js-due-date-entry-point
.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-sidebar-labels{ data: sidebar_labels_data(issuable_sidebar, @project) } .js-sidebar-labels{ data: sidebar_labels_data(issuable_sidebar, @project) }
......
...@@ -29041,6 +29041,9 @@ msgstr "" ...@@ -29041,6 +29041,9 @@ msgstr ""
msgid "Something went wrong while setting %{issuableType} confidentiality." msgid "Something went wrong while setting %{issuableType} confidentiality."
msgstr "" msgstr ""
msgid "Something went wrong while setting %{issuableType} due date."
msgstr ""
msgid "Something went wrong while stopping this environment. Please try again." msgid "Something went wrong while stopping this environment. Please try again."
msgstr "" msgstr ""
......
...@@ -324,24 +324,23 @@ RSpec.describe "Issues > User edits issue", :js do ...@@ -324,24 +324,23 @@ RSpec.describe "Issues > User edits issue", :js do
it 'adds due date to issue' do it 'adds due date to issue' do
date = Date.today.at_beginning_of_month + 2.days date = Date.today.at_beginning_of_month + 2.days
page.within '.due_date' do page.within '[data-testid="due-date"]' do
click_link 'Edit' click_button 'Edit'
page.within '.pika-single' do page.within '.pika-single' do
click_button date.day click_button date.day
end end
wait_for_requests 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
end end
it 'removes due date from issue' do it 'removes due date from issue' do
date = Date.today.at_beginning_of_month + 2.days date = Date.today.at_beginning_of_month + 2.days
page.within '.due_date' do page.within '[data-testid="due-date"]' do
click_link 'Edit' click_button 'Edit'
page.within '.pika-single' do page.within '.pika-single' do
click_button date.day click_button date.day
...@@ -351,7 +350,7 @@ RSpec.describe "Issues > User edits issue", :js do ...@@ -351,7 +350,7 @@ RSpec.describe "Issues > User edits issue", :js do
expect(page).to have_no_content 'None' expect(page).to have_no_content 'None'
click_link 'remove due date' click_button 'remove due date'
expect(page).to have_content 'None' expect(page).to have_content 'None'
end end
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) => ({ ...@@ -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) => ({ export const issueReferenceResponse = (reference) => ({
data: { data: {
workspace: { 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