Commit 285a4d6b authored by Florie Guibert's avatar Florie Guibert

Use user config for first day of week in issue datepicker

Migrate dueDate selector to widget in issue board sidebar

Changelog: fixed
parent a43773ea
......@@ -2,7 +2,6 @@
import { GlDrawer } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
......@@ -10,6 +9,7 @@ import { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
export default {
......@@ -18,10 +18,10 @@ export default {
GlDrawer,
BoardSidebarTitle,
SidebarAssigneesWidget,
SidebarDateWidget,
SidebarConfidentialityWidget,
BoardSidebarTimeTracker,
BoardSidebarLabelsSelect,
BoardSidebarDueDate,
SidebarSubscriptionsWidget,
SidebarDropdownWidget,
BoardSidebarWeightInput: () =>
......@@ -116,7 +116,13 @@ export default {
/>
</div>
<board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
<board-sidebar-due-date />
<sidebar-date-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
date-type="dueDate"
:issuable-type="issuableType"
data-testid="sidebar-due-date"
/>
<board-sidebar-labels-select class="labels" />
<board-sidebar-weight-input v-if="weightFeatureAvailable" class="weight" />
<sidebar-confidentiality-widget
......
<script>
import { GlButton, GlDatepicker } from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
export default {
components: {
BoardEditableItem,
GlButton,
GlDatepicker,
},
data() {
return {
loading: false,
};
},
computed: {
...mapGetters(['activeBoardItem', 'projectPathForActiveIssue']),
hasDueDate() {
return this.activeBoardItem.dueDate != null;
},
parsedDueDate() {
if (!this.hasDueDate) {
return null;
}
return parsePikadayDate(this.activeBoardItem.dueDate);
},
formattedDueDate() {
if (!this.hasDueDate) {
return '';
}
return dateInWords(this.parsedDueDate, true);
},
},
methods: {
...mapActions(['setActiveIssueDueDate', 'setError']),
async openDatePicker() {
await this.$nextTick();
this.$refs.datePicker.calendar.show();
},
async setDueDate(date) {
this.loading = true;
this.$refs.sidebarItem.collapse();
try {
const dueDate = date ? formatDate(date, 'yyyy-mm-dd') : null;
await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPathForActiveIssue });
} catch (e) {
this.setError({ message: this.$options.i18n.updateDueDateError });
} finally {
this.loading = false;
}
},
},
i18n: {
dueDate: __('Due date'),
removeDueDate: __('remove due date'),
updateDueDateError: __('An error occurred when updating the issue due date'),
},
};
</script>
<template>
<board-editable-item
ref="sidebarItem"
class="board-sidebar-due-date"
data-testid="sidebar-due-date"
:title="$options.i18n.dueDate"
:loading="loading"
@open="openDatePicker"
>
<template v-if="hasDueDate" #collapsed>
<div class="gl-display-flex gl-align-items-center">
<strong class="gl-text-gray-900">{{ formattedDueDate }}</strong>
<span class="gl-mx-2">-</span>
<gl-button
variant="link"
class="gl-text-gray-500!"
data-testid="reset-button"
:disabled="loading"
@click="setDueDate(null)"
>
{{ $options.i18n.removeDueDate }}
</gl-button>
</div>
</template>
<gl-datepicker
ref="datePicker"
:value="parsedDueDate"
show-clear-button
@input="setDueDate"
@clear="setDueDate(null)"
/>
</board-editable-item>
</template>
<style>
/*
* This can be removed after closing:
* https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1048
*/
.board-sidebar-due-date .gl-datepicker,
.board-sidebar-due-date .gl-datepicker-input {
width: 100%;
}
</style>
mutation issueSetDueDate($input: UpdateIssueInput!) {
updateIssue(input: $input) {
issue {
dueDate
}
errors
}
}
......@@ -35,7 +35,6 @@ import {
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import * as types from './mutation_types';
......@@ -559,30 +558,6 @@ export default {
});
},
setActiveIssueDueDate: async ({ commit, getters }, input) => {
const { activeBoardItem } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetDueDateMutation,
variables: {
input: {
iid: String(activeBoardItem.iid),
projectPath: input.projectPath,
dueDate: input.dueDate,
},
},
});
if (data.updateIssue?.errors?.length > 0) {
throw new Error(data.updateIssue.errors);
}
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
itemId: activeBoardItem.id,
prop: 'dueDate',
value: data.updateIssue.issue.dueDate,
});
},
setActiveItemSubscribed: async ({ commit, getters, state }, input) => {
const { activeBoardItem, isEpicBoard } = getters;
const { fullPath, issuableType } = state;
......
......@@ -112,6 +112,9 @@ export default {
dateValue() {
return this.issuable?.[this.dateType] || null;
},
firstDay() {
return gon.first_day_of_week;
},
isLoading() {
return this.$apollo.queries.issuable.loading || this.loading;
},
......@@ -286,6 +289,7 @@ export default {
ref="datePicker"
class="gl-relative"
:default-date="parsedDate"
:first-day="firstDay"
show-clear-button
autocomplete="off"
@input="setDate"
......
......@@ -47,7 +47,13 @@ exports[`ee/BoardContentSidebar matches the snapshot 1`] = `
class="swimlanes-sidebar-time-tracker"
/>
<boardsidebarduedate-stub />
<sidebardatewidget-stub
data-testid="sidebar-due-date"
datetype="dueDate"
fullpath="gitlab-org/gitlab-test"
iid="27"
issuabletype="issue"
/>
<boardsidebarlabelsselect-stub
class="labels"
......
......@@ -59,7 +59,7 @@ describe('ee/BoardContentSidebar', () => {
BoardSidebarLabelsSelect: true,
SidebarAssigneesWidget: true,
SidebarConfidentialityWidget: true,
BoardSidebarDueDate: true,
SidebarDateWidget: true,
SidebarSubscriptionsWidget: true,
BoardSidebarWeightInput: true,
SidebarDropdownWidget: true,
......
......@@ -3450,9 +3450,6 @@ msgstr ""
msgid "An error occurred when toggling the notification subscription"
msgstr ""
msgid "An error occurred when updating the issue due date"
msgstr ""
msgid "An error occurred when updating the issue weight"
msgstr ""
......
......@@ -4,10 +4,10 @@ import Vuex from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import { stubComponent } from 'helpers/stub_component';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
......@@ -109,8 +109,8 @@ describe('BoardContentSidebar', () => {
expect(wrapper.findComponent(BoardSidebarTitle).exists()).toBe(true);
});
it('renders BoardSidebarDueDate', () => {
expect(wrapper.findComponent(BoardSidebarDueDate).exists()).toBe(true);
it('renders SidebarDateWidget', () => {
expect(wrapper.findComponent(SidebarDateWidget).exists()).toBe(true);
});
it('renders BoardSidebarSubscription', () => {
......
import { GlDatepicker } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import { createStore } from '~/boards/stores';
const TEST_DUE_DATE = '2020-02-20';
const TEST_FORMATTED_DUE_DATE = 'Feb 20, 2020';
const TEST_PARSED_DATE = new Date(2020, 1, 20);
const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, dueDate: null, referencePath: 'h/b#2' };
describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => {
let wrapper;
let store;
afterEach(() => {
wrapper.destroy();
store = null;
wrapper = null;
});
const createWrapper = ({ dueDate = null } = {}) => {
store = createStore();
store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, dueDate } };
store.state.activeId = TEST_ISSUE.id;
wrapper = shallowMount(BoardSidebarDueDate, {
store,
provide: {
canUpdate: true,
},
stubs: {
'board-editable-item': BoardEditableItem,
},
});
};
const findDatePicker = () => wrapper.find(GlDatepicker);
const findResetButton = () => wrapper.find('[data-testid="reset-button"]');
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
it('renders "None" when no due date is set', () => {
createWrapper();
expect(findCollapsed().text()).toBe('None');
expect(findResetButton().exists()).toBe(false);
});
it('renders formatted due date with reset button when set', () => {
createWrapper({ dueDate: TEST_DUE_DATE });
expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
expect(findResetButton().exists()).toBe(true);
});
describe('when due date is submitted', () => {
beforeEach(async () => {
createWrapper();
jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
store.state.boardItems[TEST_ISSUE.id].dueDate = TEST_DUE_DATE;
});
findDatePicker().vm.$emit('input', TEST_PARSED_DATE);
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders formatted due date with reset button', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
expect(findResetButton().exists()).toBe(true);
});
it('commits change to the server', () => {
expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalledWith({
dueDate: TEST_DUE_DATE,
projectPath: 'h/b',
});
});
});
describe('when due date is cleared', () => {
beforeEach(async () => {
createWrapper();
jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
store.state.boardItems[TEST_ISSUE.id].dueDate = null;
});
findDatePicker().vm.$emit('clear');
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders "None"', () => {
expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled();
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toBe('None');
});
});
describe('when due date is resetted', () => {
beforeEach(async () => {
createWrapper({ dueDate: TEST_DUE_DATE });
jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
store.state.boardItems[TEST_ISSUE.id].dueDate = null;
});
findResetButton().vm.$emit('click');
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders "None"', () => {
expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled();
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toBe('None');
});
});
describe('when the mutation fails', () => {
beforeEach(async () => {
createWrapper({ dueDate: TEST_DUE_DATE });
jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
throw new Error(['failed mutation']);
});
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findDatePicker().vm.$emit('input', 'Invalid date');
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders former issue due date', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
expect(wrapper.vm.setError).toHaveBeenCalled();
});
});
});
......@@ -1386,57 +1386,6 @@ describe('setActiveIssueLabels', () => {
});
});
describe('setActiveIssueDueDate', () => {
const state = { boardItems: { [mockIssue.id]: mockIssue } };
const getters = { activeBoardItem: mockIssue };
const testDueDate = '2020-02-20';
const input = {
dueDate: testDueDate,
projectPath: 'h/b',
};
it('should commit due date after setting the issue', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
updateIssue: {
issue: {
dueDate: testDueDate,
},
errors: [],
},
},
});
const payload = {
itemId: getters.activeBoardItem.id,
prop: 'dueDate',
value: testDueDate,
};
testAction(
actions.setActiveIssueDueDate,
input,
{ ...state, ...getters },
[
{
type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
},
],
[],
done,
);
});
it('throws error if fails', async () => {
jest
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
await expect(actions.setActiveIssueDueDate({ getters }, input)).rejects.toThrow(Error);
});
});
describe('setActiveItemSubscribed', () => {
const state = {
boardItems: {
......
......@@ -22,6 +22,10 @@ describe('Sidebar date Widget', () => {
let fakeApollo;
const date = '2021-04-15';
window.gon = {
first_day_of_week: 1,
};
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findPopoverIcon = () => wrapper.find('[data-testid="inherit-date-popover"]');
const findDatePicker = () => wrapper.find(GlDatepicker);
......@@ -119,11 +123,12 @@ describe('Sidebar date Widget', () => {
expect(wrapper.emitted('dueDateUpdated')).toEqual([[date]]);
});
it('uses a correct prop to set the initial date for GlDatePicker', () => {
it('uses a correct prop to set the initial date and first day of the week for GlDatePicker', () => {
expect(findDatePicker().props()).toMatchObject({
value: null,
autocomplete: 'off',
defaultDate: expect.any(Object),
firstDay: window.gon.first_day_of_week,
});
});
......
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