summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/work_items/components/work_item_due_date.vue
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/work_items/components/work_item_due_date.vue')
-rw-r--r--app/assets/javascripts/work_items/components/work_item_due_date.vue257
1 files changed, 257 insertions, 0 deletions
diff --git a/app/assets/javascripts/work_items/components/work_item_due_date.vue b/app/assets/javascripts/work_items/components/work_item_due_date.vue
new file mode 100644
index 00000000000..05f8fa8f5e1
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_due_date.vue
@@ -0,0 +1,257 @@
+<script>
+import { GlButton, GlDatepicker, GlFormGroup } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { getDateWithUTC, newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility';
+import { s__ } from '~/locale';
+import Tracking from '~/tracking';
+import {
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ sprintfWorkItem,
+ TRACKING_CATEGORY_SHOW,
+} from '~/work_items/constants';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+
+const nullObjectDate = new Date(0);
+
+export default {
+ i18n: {
+ addDueDate: s__('WorkItem|Add due date'),
+ addStartDate: s__('WorkItem|Add start date'),
+ dates: s__('WorkItem|Dates'),
+ dueDate: s__('WorkItem|Due date'),
+ none: s__('WorkItem|None'),
+ startDate: s__('WorkItem|Start date'),
+ },
+ dueDateInputId: 'due-date-input',
+ startDateInputId: 'start-date-input',
+ components: {
+ GlButton,
+ GlDatepicker,
+ GlFormGroup,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ dueDate: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ startDate: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ workItemType: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ dirtyDueDate: null,
+ dirtyStartDate: null,
+ isUpdating: false,
+ showDueDateInput: false,
+ showStartDateInput: false,
+ };
+ },
+ computed: {
+ datesUnchanged() {
+ const dirtyDueDate = this.dirtyDueDate || nullObjectDate;
+ const dirtyStartDate = this.dirtyStartDate || nullObjectDate;
+ const dueDate = this.dueDate ? newDateAsLocaleTime(this.dueDate) : nullObjectDate;
+ const startDate = this.startDate ? newDateAsLocaleTime(this.startDate) : nullObjectDate;
+ return (
+ dirtyDueDate.getTime() === dueDate.getTime() &&
+ dirtyStartDate.getTime() === startDate.getTime()
+ );
+ },
+ isDatepickerDisabled() {
+ return !this.canUpdate || this.isUpdating;
+ },
+ isReadonlyWithOnlyDueDate() {
+ return !this.canUpdate && this.dueDate && !this.startDate;
+ },
+ isReadonlyWithOnlyStartDate() {
+ return !this.canUpdate && !this.dueDate && this.startDate;
+ },
+ isReadonlyWithNoDates() {
+ return !this.canUpdate && !this.dueDate && !this.startDate;
+ },
+ labelClass() {
+ return this.isReadonlyWithNoDates ? 'gl-align-self-center gl-pb-0!' : 'gl-mt-3 gl-pb-0!';
+ },
+ showDueDateButton() {
+ return this.canUpdate && !this.showDueDateInput;
+ },
+ showStartDateButton() {
+ return this.canUpdate && !this.showStartDateInput;
+ },
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_dates',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ },
+ watch: {
+ dueDate: {
+ handler(newDueDate) {
+ this.dirtyDueDate = newDateAsLocaleTime(newDueDate);
+ this.showDueDateInput = Boolean(newDueDate);
+ },
+ immediate: true,
+ },
+ startDate: {
+ handler(newStartDate) {
+ this.dirtyStartDate = newDateAsLocaleTime(newStartDate);
+ this.showStartDateInput = Boolean(newStartDate);
+ },
+ immediate: true,
+ },
+ },
+ methods: {
+ clearDueDatePicker() {
+ this.dirtyDueDate = null;
+ this.showDueDateInput = false;
+ this.updateDates();
+ },
+ clearStartDatePicker() {
+ this.dirtyStartDate = null;
+ this.showStartDateInput = false;
+ this.updateDates();
+ },
+ async clickShowDueDate() {
+ this.showDueDateInput = true;
+ await this.$nextTick();
+ this.$refs.dueDatePicker.calendar.show();
+ },
+ async clickShowStartDate() {
+ this.showStartDateInput = true;
+ await this.$nextTick();
+ this.$refs.startDatePicker.calendar.show();
+ },
+ handleStartDateInput() {
+ if (this.dirtyDueDate && this.dirtyStartDate > this.dirtyDueDate) {
+ this.dirtyDueDate = this.dirtyStartDate;
+ this.clickShowDueDate();
+ return;
+ }
+
+ this.updateDates();
+ },
+ updateDates() {
+ if (!this.canUpdate || this.datesUnchanged) {
+ return;
+ }
+
+ this.track('updated_dates');
+
+ this.isUpdating = true;
+
+ this.$apollo
+ .mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ startAndDueDateWidget: {
+ dueDate: getDateWithUTC(this.dirtyDueDate),
+ startDate: getDateWithUTC(this.dirtyStartDate),
+ },
+ },
+ },
+ })
+ .then(({ data }) => {
+ if (data.workItemUpdate.errors.length) {
+ throw new Error(data.workItemUpdate.errors.join('; '));
+ }
+ })
+ .catch((error) => {
+ const message = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
+ this.$emit('error', message);
+ Sentry.captureException(error);
+ })
+ .finally(() => {
+ this.isUpdating = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ class="work-item-due-date"
+ :label="$options.i18n.dates"
+ :label-class="labelClass"
+ label-cols="3"
+ label-cols-lg="2"
+ >
+ <span v-if="isReadonlyWithNoDates" class="gl-text-gray-400 gl-ml-4">
+ {{ $options.i18n.none }}
+ </span>
+ <div v-else class="gl-display-flex gl-flex-wrap gl-gap-5">
+ <gl-form-group
+ class="gl-display-flex gl-align-items-center gl-m-0"
+ :class="{ 'gl-ml-n3': isReadonlyWithOnlyDueDate }"
+ :label="$options.i18n.startDate"
+ :label-for="$options.startDateInputId"
+ :label-sr-only="!showStartDateInput"
+ label-class="gl-flex-shrink-0 gl-text-secondary gl-font-weight-normal! gl-pb-0! gl-ml-4 gl-mr-3"
+ >
+ <gl-datepicker
+ v-if="showStartDateInput"
+ ref="startDatePicker"
+ v-model="dirtyStartDate"
+ container="body"
+ :disabled="isDatepickerDisabled"
+ :input-id="$options.startDateInputId"
+ show-clear-button
+ :target="null"
+ @clear="clearStartDatePicker"
+ @close="handleStartDateInput"
+ />
+ <gl-button v-if="showStartDateButton" category="tertiary" @click="clickShowStartDate">
+ {{ $options.i18n.addStartDate }}
+ </gl-button>
+ </gl-form-group>
+ <gl-form-group
+ v-if="!isReadonlyWithOnlyStartDate"
+ class="gl-display-flex gl-align-items-center gl-m-0"
+ :class="{ 'gl-ml-n3': isReadonlyWithOnlyDueDate }"
+ :label="$options.i18n.dueDate"
+ :label-for="$options.dueDateInputId"
+ :label-sr-only="!showDueDateInput"
+ label-class="gl-flex-shrink-0 gl-text-secondary gl-font-weight-normal! gl-pb-0! gl-ml-4 gl-mr-3"
+ >
+ <gl-datepicker
+ v-if="showDueDateInput"
+ ref="dueDatePicker"
+ v-model="dirtyDueDate"
+ container="body"
+ :disabled="isDatepickerDisabled"
+ :input-id="$options.dueDateInputId"
+ :min-date="dirtyStartDate"
+ show-clear-button
+ :target="null"
+ @clear="clearDueDatePicker"
+ @close="updateDates"
+ />
+ <gl-button v-if="showDueDateButton" category="tertiary" @click="clickShowDueDate">
+ {{ $options.i18n.addDueDate }}
+ </gl-button>
+ </gl-form-group>
+ </div>
+ </gl-form-group>
+</template>