From 9370061d7bf34fe578e17c32e3ec7784b16f09a8 Mon Sep 17 00:00:00 2001 From: SebastianKrupinski Date: Sun, 19 Oct 2025 20:15:38 -0400 Subject: [PATCH] fix: update, delete, accept, devline all occurrences Signed-off-by: SebastianKrupinski --- .../Editor/InvitationResponseButtons.vue | 2 +- src/components/Editor/SaveButtons.vue | 33 +++++++--- src/mixins/EditorMixin.js | 30 ++++----- src/store/calendarObjectInstance.js | 66 +++++++++++++++---- src/views/EditFull.vue | 36 ++++++---- src/views/EditSimple.vue | 25 ++++--- 6 files changed, 133 insertions(+), 59 deletions(-) diff --git a/src/components/Editor/InvitationResponseButtons.vue b/src/components/Editor/InvitationResponseButtons.vue index 0247030d38..7ea7d18267 100644 --- a/src/components/Editor/InvitationResponseButtons.vue +++ b/src/components/Editor/InvitationResponseButtons.vue @@ -156,7 +156,7 @@ export default { // TODO: What about recurring events? Add new buttons like "Accept this and all future"? // Currently, this will only accept a single occurrence. await this.calendarObjectInstanceStore.saveCalendarObjectInstance({ - thisAndAllFuture: false, + mode: 'all', calendarId: this.calendarId, }) } catch (error) { diff --git a/src/components/Editor/SaveButtons.vue b/src/components/Editor/SaveButtons.vue index 33045922d5..e50be32ba3 100644 --- a/src/components/Editor/SaveButtons.vue +++ b/src/components/Editor/SaveButtons.vue @@ -32,36 +32,49 @@ {{ $t('calendar', 'Update') }} + + {{ $t('calendar', 'Update this occurrence') }} + - {{ $t('calendar', 'Update this and all future') }} + {{ $t('calendar', 'Update this and future occurrences') }} - {{ $t('calendar', 'Update this occurrence') }} + @click="saveSeries"> + {{ $t('calendar', 'Update all occurrences') }} + + + {{ $t('calendar', 'Update this occurrence') }} + - {{ $t('calendar', 'Update this and all future') }} + {{ $t('calendar', 'Update this and future occurrences') }} - + - {{ $t('calendar', 'Update this occurrence') }} + {{ $t('calendar', 'Update all occurrences') }} @@ -147,6 +160,10 @@ export default { this.$emit('save-this-and-all-future') }, + saveSeries() { + this.$emit('save-series') + }, + showMore() { this.$emit('show-more') }, diff --git a/src/mixins/EditorMixin.js b/src/mixins/EditorMixin.js index 31227eed3e..5869456ae6 100644 --- a/src/mixins/EditorMixin.js +++ b/src/mixins/EditorMixin.js @@ -477,12 +477,12 @@ export default { }, keyboardSaveEvent(event) { if (event.key === 'Enter' && event.ctrlKey === true && !this.isReadOnly && !this.canCreateRecurrenceException) { - this.saveAndLeave(false) + this.saveAndLeave('single') } }, keyboardDeleteEvent(event) { if (event.key === 'Delete' && event.ctrlKey === true && this.canDelete && !this.canCreateRecurrenceException) { - this.deleteAndLeave(false) + this.deleteAndLeave('single') } }, keyboardDuplicateEvent(event) { @@ -496,10 +496,10 @@ export default { /** * Saves a calendar-object * - * @param {boolean} thisAndAllFuture Whether to modify only this or this and all future occurrences + * @param {string} mode Modification mode: 'all', 'future', or 'single' * @return {Promise} */ - async save(thisAndAllFuture = false) { + async save(mode = 'single') { if (!this.calendarObject) { logger.error('Calendar-object not found') return @@ -508,14 +508,14 @@ export default { return } if (this.forceThisAndAllFuture) { - thisAndAllFuture = true + mode = 'future' } this.isLoading = true this.isSaving = true try { await this.calendarObjectInstanceStore.saveCalendarObjectInstance({ - thisAndAllFuture, + mode, calendarId: this.calendarId, }) } catch (error) { @@ -534,11 +534,11 @@ export default { /** * Saves a calendar-object and closes the editor * - * @param {boolean} thisAndAllFuture Whether to modify only this or this and all future occurrences + * @param {string} mode Modification mode: 'all', 'future', or 'single' * @return {Promise} */ - async saveAndLeave(thisAndAllFuture = false) { - await this.save(thisAndAllFuture) + async saveAndLeave(mode = 'single') { + await this.save(mode) this.requiresActionOnRouteLeave = false this.closeEditor() }, @@ -555,10 +555,10 @@ export default { /** * Deletes a calendar-object * - * @param {boolean} thisAndAllFuture Whether to delete only this or this and all future occurrences + * @param {string} mode Deletion mode: 'all', 'future', or 'single' * @return {Promise} */ - async delete(thisAndAllFuture = false) { + async delete(mode = 'single') { if (!this.calendarObject) { logger.error('Calendar-object not found') return @@ -568,17 +568,17 @@ export default { } this.isLoading = true - await this.calendarObjectInstanceStore.deleteCalendarObjectInstance({ thisAndAllFuture }) + await this.calendarObjectInstanceStore.deleteCalendarObjectInstance({ mode }) this.isLoading = false }, /** * Deletes a calendar-object and closes the editor * - * @param {boolean} thisAndAllFuture Whether to delete only this or this and all future occurrences + * @param {string} mode Deletion mode: 'all', 'future', or 'single' * @return {Promise} */ - async deleteAndLeave(thisAndAllFuture = false) { - await this.delete(thisAndAllFuture) + async deleteAndLeave(mode = 'single') { + await this.delete(mode) this.requiresActionOnRouteLeave = false this.closeEditor() }, diff --git a/src/store/calendarObjectInstance.js b/src/store/calendarObjectInstance.js index ce5ccca780..98e4b8f462 100644 --- a/src/store/calendarObjectInstance.js +++ b/src/store/calendarObjectInstance.js @@ -1456,24 +1456,61 @@ export default defineStore('calendarObjectInstance', { * Saves changes made to a single calendar-object-instance * * @param {object} data The destructuring object - * @param {boolean} data.thisAndAllFuture Whether or not to save changes for all future occurrences or just this one + * @param {string} data.mode Modification mode: 'all', 'future', or 'single' * @param {string} data.calendarId The new calendar-id to store it in * @return {Promise} */ async saveCalendarObjectInstance({ - thisAndAllFuture, + mode, calendarId, }) { const calendarObjectsStore = useCalendarObjectsStore() const eventComponent = this.calendarObjectInstance.eventComponent const calendarObject = this.calendarObject + const isForkedItem = eventComponent.primaryItem !== null updateAlarms(eventComponent) await updateTalkParticipants(eventComponent) - if (eventComponent.isDirty()) { - const isForkedItem = eventComponent.primaryItem !== null + if (eventComponent.isDirty() && eventComponent.isRecurring() && mode === 'all' && isForkedItem) { + // Find the master component (without RECURRENCE-ID) + let masterComponent = null + for (const component of calendarObject.calendarComponent.getComponentIterator()) { + if (component.name === eventComponent.name && !component.hasProperty('RECURRENCE-ID')) { + masterComponent = component + break + } + } + + if (masterComponent) { + // construct list of properties to clone + const propertyNames = [] + for (const property of masterComponent.getPropertyIterator()) { + if (property.name === 'UID' || property.name === 'RECURRENCE-ID' || property.name === 'DTSTART' || property.name === 'DTEND') { + continue + } + propertyNames.push(property.name) + masterComponent.deleteAllProperties(property.name) + } + // clone properties from eventComponent + for (const property of eventComponent.getPropertyIterator()) { + if (propertyNames.indexOf(property.name) === -1) { + continue + } + masterComponent.addProperty(property.clone()) + } + // clone alarms + masterComponent.deleteAllComponents('VALARM') + for (const alarm of eventComponent.getAlarmIterator()) { + masterComponent.addComponent(alarm.clone()) + } + } + + await calendarObjectsStore.updateCalendarObject({ calendarObject }) + } + + if (eventComponent.isDirty() && mode !== 'all') { let original = null let fork = null @@ -1481,7 +1518,7 @@ export default defineStore('calendarObjectInstance', { // - primaryItem !== null -> Is this a fork or not? // - eventComponent.canCreateRecurrenceExceptions() - Can we create a recurrence-exception for this item if (isForkedItem && eventComponent.canCreateRecurrenceExceptions()) { - [original, fork] = eventComponent.createRecurrenceException(thisAndAllFuture) + [original, fork] = eventComponent.createRecurrenceException(mode === 'future') } await calendarObjectsStore.updateCalendarObject({ calendarObject }) @@ -1534,20 +1571,25 @@ export default defineStore('calendarObjectInstance', { * Deletes a calendar-object-instance * * @param {object} data The destructuring object - * @param {boolean} data.thisAndAllFuture Whether or not to delete all future occurrences or just this one + * @param {string} data.mode Deletion mode: 'all', 'future', or 'single' * @return {Promise} */ - async deleteCalendarObjectInstance({ thisAndAllFuture }) { + async deleteCalendarObjectInstance({ mode }) { const calendarObjectsStore = useCalendarObjectsStore() - const eventComponent = this.calendarObjectInstance.eventComponent - const isRecurrenceSetEmpty = eventComponent.removeThisOccurrence(thisAndAllFuture) - const calendarObject = this.calendarObject + // Singleton event or deleting all occurrences - delete the whole calendar-object + if (!eventComponent.isRecurring() || mode === 'all') { + await calendarObjectsStore.deleteCalendarObject({ calendarObject: this.calendarObject }) + return + } + + // Recurring event - remove this occurrence or this and all future + const isRecurrenceSetEmpty = eventComponent.removeThisOccurrence(mode === 'future') if (isRecurrenceSetEmpty) { - await calendarObjectsStore.deleteCalendarObject({ calendarObject }) + await calendarObjectsStore.deleteCalendarObject({ calendarObject: this.calendarObject }) } else { - await calendarObjectsStore.updateCalendarObject({ calendarObject }) + await calendarObjectsStore.updateCalendarObject({ calendarObject: this.calendarObject }) } }, diff --git a/src/views/EditFull.vue b/src/views/EditFull.vue index 75524358f2..197a9f8967 100644 --- a/src/views/EditFull.vue +++ b/src/views/EditFull.vue @@ -54,8 +54,10 @@ :is-new="isNew" :is-read-only="isReadOnly" :force-this-and-all-future="forceThisAndAllFuture" - @save-this-only="prepareAccessForAttachments(false)" - @save-this-and-all-future="prepareAccessForAttachments(true)" /> + @save-this-only="prepareAccessForAttachments('single')" + @save-this-and-all-future="prepareAccessForAttachments('future')" + @save-series="prepareAccessForAttachments('all')" + />
@@ -70,23 +72,29 @@ {{ $t('calendar', 'Duplicate') }} - + {{ $t('calendar', 'Delete') }} - + {{ $t('calendar', 'Delete this occurrence') }} - + - {{ $t('calendar', 'Delete this and all future') }} + {{ $t('calendar', 'Delete this and future occurrences') }} + + + + {{ $t('calendar', 'Delete this and future occurrences') }}
@@ -291,7 +299,7 @@ + @click="acceptAttachmentsModal()"> {{ t('calendar', 'Invite') }} @@ -421,7 +429,7 @@ export default { data() { return { - thisAndAllFuture: false, + saveMode: 'single', doNotShare: false, showModal: false, showModalNewAttachments: [], @@ -707,7 +715,7 @@ export default { this.showModal = false this.showModalNewAttachments = [] this.showModalUsers = [] - this.saveEvent(this.thisAndAllFuture) + this.saveEvent(this.saveMode) }, 500) // trigger save event after make each attachment access // 1) if !isPrivate get attachments NOT SHARED and SharedType is empry -> API ADD SHARE @@ -731,8 +739,8 @@ export default { return name.split('/').pop() }, - prepareAccessForAttachments(thisAndAllFuture = false) { - this.thisAndAllFuture = thisAndAllFuture + prepareAccessForAttachments(mode = false) { + this.saveMode = mode const newAttachments = this.calendarObjectInstance.attachments.filter((attachment) => { // get only new attachments // TODO get NOT only new attachments =) Maybe we should filter all attachments without share-type, 'cause event can be private and AFTER save owner could add new participant @@ -752,14 +760,14 @@ export default { return false }) } else { - this.saveEvent(thisAndAllFuture) + this.saveEvent(this.saveMode) } }, - saveEvent(thisAndAllFuture = false) { + saveEvent(mode = 'single') { // if there is new attachments and !private, then make modal with users and files/ // maybe check shared access before add file - this.saveAndLeave(thisAndAllFuture) + this.saveAndLeave(mode) this.calendarObjectInstance.attachments = this.calendarObjectInstance.attachments.map((attachment) => { if (attachment.isNew) { delete attachment.isNew diff --git a/src/views/EditSimple.vue b/src/views/EditSimple.vue index 7f5e175e3c..e3153a10e9 100644 --- a/src/views/EditSimple.vue +++ b/src/views/EditSimple.vue @@ -84,23 +84,29 @@ {{ $t('calendar', 'Duplicate') }} - + {{ $t('calendar', 'Delete') }} - + {{ $t('calendar', 'Delete this occurrence') }} - + - {{ $t('calendar', 'Delete this and all future') }} + {{ $t('calendar', 'Delete this and future occurrences') }} + + + + {{ $t('calendar', 'Delete all occurrences') }} @@ -199,8 +205,9 @@ :show-more-button="true" :more-button-type="isViewing ? 'tertiary' : undefined" :disabled="isSaving" - @save-this-only="saveAndView(false)" - @save-this-and-all-future="saveAndView(true)" + @save-this-only="saveAndView('single')" + @save-this-and-all-future="saveAndView('future')" + @save-series="saveAndView('all')" @show-more="showMore"> } */ - async saveAndView(thisAndAllFuture) { + async saveAndView(mode) { // Transitioning from new to edit routes is not implemented for now if (this.isNew) { - await this.saveAndLeave(thisAndAllFuture) + await this.saveAndLeave(mode) return }