From 68ac2b80ff01d3ca50342a36f9d54bd196b470f6 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 8 Aug 2025 16:58:41 +0530 Subject: [PATCH] feat: keep event in sync and show discard changes option if dirty --- .../Calendar/CalendarEventPanel.vue | 167 ++++++++++++------ frontend/src/pages/Calendar.vue | 86 ++++----- 2 files changed, 148 insertions(+), 105 deletions(-) diff --git a/frontend/src/components/Calendar/CalendarEventPanel.vue b/frontend/src/components/Calendar/CalendarEventPanel.vue index 0113a141..fd1b4ac3 100644 --- a/frontend/src/components/Calendar/CalendarEventPanel.vue +++ b/frontend/src/components/Calendar/CalendarEventPanel.vue @@ -77,7 +77,7 @@
@@ -86,7 +86,7 @@
@@ -95,7 +95,7 @@
- +
{{ __('All day') }}
@@ -113,7 +113,7 @@
{{ __('Time') }}
@@ -153,7 +153,7 @@ @@ -173,8 +173,8 @@
@@ -200,6 +200,7 @@ import EditIcon from '@/components/Icons/EditIcon.vue' import DescriptionIcon from '@/components/Icons/DescriptionIcon.vue' import TimePicker from './TimePicker.vue' +import { globalStore } from '@/stores/global' import { getFormat } from '@/utils' import { TextInput, @@ -212,7 +213,7 @@ import { CalendarColorMap as colorMap, CalendarActiveEvent as activeEvent, } from 'frappe-ui' -import { ref, computed, watch, nextTick, h } from 'vue' +import { ref, computed, watch, h } from 'vue' const props = defineProps({ event: { @@ -227,6 +228,8 @@ const props = defineProps({ const emit = defineEmits(['save', 'edit', 'delete', 'details', 'close']) +const { $dialog } = globalStore() + const show = defineModel() const title = computed(() => { @@ -236,59 +239,59 @@ const title = computed(() => { return __('Duplicate event') }) -const _event = ref({}) - const eventTitle = ref(null) const error = ref(null) -watch( - () => props.event, - (newEvent) => { - error.value = null +const oldEvent = ref(null) +const dirty = computed(() => { + return JSON.stringify(oldEvent.value) !== JSON.stringify(props.event) +}) - nextTick(() => { - if (props.mode === 'create' && _event.value.id === 'new-event') { - _event.value.fromDate = newEvent.fromDate - _event.value.toDate = newEvent.toDate - _event.value.fromTime = newEvent.fromTime - _event.value.toTime = newEvent.toTime - } else { - _event.value = { ...newEvent } - } - }) - setTimeout(() => eventTitle.value?.el?.focus(), 100) +watch( + [() => props.mode, () => props.event], + () => { + focusOnTitle() + oldEvent.value = { ...props.event } }, { immediate: true }, ) +function focusOnTitle() { + setTimeout(() => { + if (['edit', 'create', 'duplicate'].includes(props.mode)) { + eventTitle.value?.el?.focus() + } + }, 100) +} + function updateDate(d) { - _event.value.fromDate = d - _event.value.toDate = d + props.event.fromDate = d + props.event.toDate = d } function updateTime(t, fromTime = false) { error.value = null - let oldTo = _event.value.toTime || _event.value.fromTime + let oldTo = props.event.toTime || props.event.fromTime if (fromTime) { - _event.value.fromTime = t - if (!_event.value.toTime) { + props.event.fromTime = t + if (!props.event.toTime) { const hour = parseInt(t.split(':')[0]) const minute = parseInt(t.split(':')[1]) - _event.value.toTime = `${hour + 1}:${minute}` + props.event.toTime = `${hour + 1}:${minute}` } } else { - _event.value.toTime = t + props.event.toTime = t } - if (_event.value.toTime && _event.value.fromTime) { - const diff = dayjs(_event.value.toDate + ' ' + _event.value.toTime).diff( - dayjs(_event.value.fromDate + ' ' + _event.value.fromTime), + if (props.event.toTime && props.event.fromTime) { + const diff = dayjs(props.event.toDate + ' ' + props.event.toTime).diff( + dayjs(props.event.fromDate + ' ' + props.event.fromTime), 'minute', ) if (diff < 0) { - _event.value.toTime = oldTo + props.event.toTime = oldTo error.value = __('End time should be after start time') return } @@ -297,49 +300,99 @@ function updateTime(t, fromTime = false) { function saveEvent() { error.value = null - if (!_event.value.title) { + if (!props.event.title) { error.value = __('Title is required') eventTitle.value.el.focus() return } - _event.value.fromDateTime = - _event.value.fromDate + ' ' + _event.value.fromTime - _event.value.toDateTime = _event.value.toDate + ' ' + _event.value.toTime - - emit('save', _event.value) + oldEvent.value = { ...props.event } + emit('save', props.event) } function editDetails() { - emit('edit', _event.value) + emit('edit', props.event) } function duplicateEvent() { - emit('duplicate', _event.value) + if (dirty.value) { + showDiscardChangesModal(() => reset()) + } else { + emit('duplicate', props.event) + } } function deleteEvent() { - emit('delete', _event.value.id) + emit('delete', props.event.id) } function details() { - emit('details', _event.value) + if (dirty.value) { + showDiscardChangesModal(() => reset()) + } else { + emit('details', props.event) + } } function close() { - show.value = false - activeEvent.value = '' - emit('close', _event.value) + const _close = () => { + show.value = false + activeEvent.value = '' + emit('close', props.event) + } + + if (dirty.value) { + showDiscardChangesModal(() => { + reset() + if (props.event.id === 'new-event') _close() + }) + } else { + if (props.event.id === 'duplicate-event') + showDiscardChangesModal(() => _close()) + else _close() + } +} + +function reset() { + Object.assign(props.event, oldEvent.value) +} + +function showDiscardChangesModal(action) { + $dialog({ + title: __('Discard unsaved changes?'), + message: __( + 'Are you sure you want to discard unsaved changes to this event?', + ), + actions: [ + { + label: __('Cancel'), + onClick: (close) => { + close() + }, + }, + { + label: __('Discard'), + variant: 'solid', + onClick: (close) => { + action() + close() + }, + }, + ], + }) } const formattedDateTime = computed(() => { + const date = dayjs(props.event.fromDate) + if (props.event.isFullDay) { - return `${__('All day')} - ${dayjs(props.event.fromDateTime).format('ddd, D MMM YYYY')}` + return `${__('All day')} - ${date.format('ddd, D MMM YYYY')}` } - const start = dayjs(props.event.fromDateTime) - const end = dayjs(props.event.toDateTime) - return `${start.format('h:mm a')} - ${end.format('h:mm a')} ${start.format('ddd, D MMM YYYY')}` + const start = dayjs(props.event.fromDate + ' ' + props.event.fromTime) + const end = dayjs(props.event.toDate + ' ' + props.event.toTime) + + return `${start.format('h:mm a')} - ${end.format('h:mm a')} ${date.format('ddd, D MMM YYYY')}` }) const colors = Object.keys(colorMap).map((color) => ({ @@ -350,7 +403,7 @@ const colors = Object.keys(colorMap).map((color) => ({ style: { backgroundColor: colorMap[color].color }, }), onClick: () => { - _event.value.color = colorMap[color].color + props.event.color = colorMap[color].color }, })) diff --git a/frontend/src/pages/Calendar.vue b/frontend/src/pages/Calendar.vue index e502dcac..e1863092 100644 --- a/frontend/src/pages/Calendar.vue +++ b/frontend/src/pages/Calendar.vue @@ -133,17 +133,26 @@ const events = createListResource({ filters: { status: 'Open', owner: user }, auto: true, transform: (data) => { - return data.map((event) => ({ - id: event.name, - title: event.subject, - description: event.description, - status: event.status, - fromDate: event.starts_on, - toDate: event.ends_on, - isFullDay: event.all_day, - eventType: event.event_type, - color: event.color, - })) + return data.map((event) => { + let fromDate = dayjs(event.starts_on).format('YYYY-MM-DD') + let toDate = dayjs(event.ends_on).format('YYYY-MM-DD') + let fromTime = dayjs(event.starts_on).format('HH:mm') + let toTime = dayjs(event.ends_on).format('HH:mm') + + return { + id: event.name, + title: event.subject, + description: event.description, + status: event.status, + fromDate, + toDate, + fromTime, + toTime, + isFullDay: event.all_day, + eventType: event.event_type, + color: event.color, + } + }) }, insert: { onSuccess: () => events.reload(), @@ -175,21 +184,13 @@ function createEvent(_event) { { subject: _event.title, description: _event.description, - starts_on: _event.fromDateTime, - ends_on: _event.toDateTime, - all_day: _event.isFullDay, + starts_on: _event.fromDate + ' ' + _event.fromTime, + ends_on: _event.toDate + ' ' + _event.toTime, + all_day: _event.isFullDay || false, event_type: _event.eventType, color: _event.color, }, - { - onSuccess: (e) => { - _event.id = e.name - event.value = _event - showEventPanel.value = true - activeEvent.value = e.name - mode.value = 'details' - }, - }, + { onSuccess: (e) => showDetails({ id: e.name }) }, ) } @@ -202,15 +203,15 @@ function updateEvent(_event) { name: _event.id, subject: _event.title, description: _event.description, - starts_on: _event.fromDateTime, - ends_on: _event.toDateTime, + starts_on: _event.fromDate + ' ' + _event.fromTime, + ends_on: _event.toDate + ' ' + _event.toTime, all_day: _event.isFullDay, event_type: _event.eventType, color: _event.color, }, { - onSuccess: () => { - mode.value = 'details' + onSuccess: (e) => { + showEventPanel.value && showDetails({ id: e.name }) }, }, ) @@ -225,8 +226,6 @@ function deleteEvent(eventID) { $dialog({ title: __('Delete'), message: __('Are you sure you want to delete this event?'), - variant: 'solid', - theme: 'red', actions: [ { label: __('Delete'), @@ -264,7 +263,7 @@ function showDetails(e) { ) showEventPanel.value = true - event.value = { ..._e } + event.value = events.data.find((ev) => ev.id === _e.id) || _e activeEvent.value = _e.id mode.value = 'details' } @@ -278,7 +277,7 @@ function editDetails(e) { ) showEventPanel.value = true - event.value = { ..._e } + event.value = events.data.find((ev) => ev.id === _e.id) || _e activeEvent.value = _e.id mode.value = 'edit' } @@ -291,43 +290,34 @@ function newEvent(e, duplicate = false) { let fromTime = e.fromTime let toTime = e.toTime let fromDate = e.fromDate + let toDate = e.toDate if (!duplicate) { let t = getFromToTime(e.time) fromTime = t[0] toTime = t[1] fromDate = dayjs(e.date).format('YYYY-MM-DD') - e = { fromDate, fromTime, toTime } + toDate = fromDate + e = { fromDate, toDate, fromTime, toTime } } - showEventPanel.value = true - event.value = { id: duplicate ? 'duplicate-event' : 'new-event', title: duplicate ? `${e.title} (Copy)` : '', description: e.description || '', date: fromDate, - fromDate: fromDate, - toDate: fromDate, + fromDate, + toDate, fromTime, toTime, - isFullDay: e.isFullDay, + isFullDay: e.isFullDay || false, eventType: e.eventType || 'Public', color: e.color || 'green', } - events.data.push({ - id: duplicate ? 'duplicate-event' : 'new-event', - title: duplicate ? `${e.title} (Copy)` : '', - description: e.description || '', - status: 'Open', - eventType: e.eventType || 'Public', - fromDate: fromDate + ' ' + fromTime, - toDate: fromDate + ' ' + toTime, - color: e.color || 'green', - isFullDay: e.isFullDay, - }) + events.data.push(event.value) + showEventPanel.value = true activeEvent.value = duplicate ? 'duplicate-event' : 'new-event' mode.value = duplicate ? 'duplicate' : 'create' }