Skip to content

Commit cd83847

Browse files
committed
feat(editor): Preserve unknown properties and components when duplicating events
Resolves #8605 Signed-off-by: Oleksandr Dzhychko <hey@oleks.dev>
1 parent 54af44e commit cd83847

5 files changed

Lines changed: 189 additions & 59 deletions

File tree

REUSE.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ SPDX-FileCopyrightText = "2014-2016 ownCloud, Inc., 2016-2024 Nextcloud GmbH and
2424
SPDX-License-Identifier = "AGPL-3.0-or-later"
2525

2626
[[annotations]]
27-
path = ["tests/assets/ics/alarms/absoluteAlarm.ics", "tests/assets/ics/alarms/relativeAlarmAfter.ics", "tests/assets/ics/alarms/relativeAlarmAfterWithin24hours.ics", "tests/assets/ics/alarms/relativeAlarmBefore.ics", "tests/assets/ics/alarms/relativeAlarmRelatedEnd.ics", "tests/assets/ics/alarms/relativeAlarmWeekBefore.ics", "tests/assets/ics/attendees/attendee1.ics", "tests/assets/ics/attendees/attendee2.ics", "tests/assets/ics/attendees/attendee3.ics", "tests/assets/ics/attendees/attendee4.ics", "tests/assets/ics/attendees/attendee5.ics", "tests/assets/ics/attendees/attendee6.ics", "tests/assets/ics/attendees/attendee7.ics", "tests/assets/ics/rrules/rrule-count-and-until.ics", "tests/assets/ics/rrules/rrule-count.ics", "tests/assets/ics/rrules/rrule-until.ics", "tests/assets/ics/rrules/rrules1.ics", "tests/assets/ics/rrules/rrules10.ics", "tests/assets/ics/rrules/rrules11.ics", "tests/assets/ics/rrules/rrules12.ics", "tests/assets/ics/rrules/rrules13.ics", "tests/assets/ics/rrules/rrules14.ics", "tests/assets/ics/rrules/rrules15.ics", "tests/assets/ics/rrules/rrules16.ics", "tests/assets/ics/rrules/rrules17.ics", "tests/assets/ics/rrules/rrules18.ics", "tests/assets/ics/rrules/rrules19.ics", "tests/assets/ics/rrules/rrules2.ics", "tests/assets/ics/rrules/rrules21.ics", "tests/assets/ics/rrules/rrules22.ics", "tests/assets/ics/rrules/rrules23.ics", "tests/assets/ics/rrules/rrules24.ics", "tests/assets/ics/rrules/rrules25.ics", "tests/assets/ics/rrules/rrules26.ics", "tests/assets/ics/rrules/rrules27.ics", "tests/assets/ics/rrules/rrules28.ics", "tests/assets/ics/rrules/rrules29.ics", "tests/assets/ics/rrules/rrules3.ics", "tests/assets/ics/rrules/rrules30.ics", "tests/assets/ics/rrules/rrules31.ics", "tests/assets/ics/rrules/rrules32.ics", "tests/assets/ics/rrules/rrules4.ics", "tests/assets/ics/rrules/rrules5.ics", "tests/assets/ics/rrules/rrules6.ics", "tests/assets/ics/rrules/rrules7.ics", "tests/assets/ics/rrules/rrules8.ics", "tests/assets/ics/rrules/rrules9.ics", "tests/assets/ics/vcalendars/vcalendar-empty.ics", "tests/assets/ics/vcalendars/vcalendar-event-alarms.ics", "tests/assets/ics/vcalendars/vcalendar-event-allday.ics", "tests/assets/ics/vcalendars/vcalendar-event-attendees.ics", "tests/assets/ics/vcalendars/vcalendar-event-categories.ics", "tests/assets/ics/vcalendars/vcalendar-event-custom-color.ics", "tests/assets/ics/vcalendars/vcalendar-event-floating-time.ics", "tests/assets/ics/vcalendars/vcalendar-event-multiple-rrules.ics", "tests/assets/ics/vcalendars/vcalendar-event-recurring-allday.ics", "tests/assets/ics/vcalendars/vcalendar-event-recurring.ics", "tests/assets/ics/vcalendars/vcalendar-event-timed.ics", "tests/assets/ics/vcalendars/vcalendar-event-utc-time.ics", "tests/assets/ics/vcalendars/vcalendar-journal.ics", "tests/assets/ics/vcalendars/vcalendar-todo.ics", "tests/assets/ics/vcalendars/vcalendar-without-vobjects.ics", "tests/assets/ics/vcalendars-scheduling/add.ics", "tests/assets/ics/vcalendars-scheduling/cancel.ics", "tests/assets/ics/vcalendars-scheduling/counter.ics", "tests/assets/ics/vcalendars-scheduling/declinecounter.ics", "tests/assets/ics/vcalendars-scheduling/freebusy-reply.ics", "tests/assets/ics/vcalendars-scheduling/freebusy-request.ics", "tests/assets/ics/vcalendars-scheduling/publish.ics", "tests/assets/ics/vcalendars-scheduling/refresh.ics", "tests/assets/ics/vcalendars-scheduling/reply.ics", "tests/assets/ics/vcalendars-scheduling/request.ics"]
27+
path = ["tests/assets/ics/alarms/absoluteAlarm.ics", "tests/assets/ics/alarms/relativeAlarmAfter.ics", "tests/assets/ics/alarms/relativeAlarmAfterWithin24hours.ics", "tests/assets/ics/alarms/relativeAlarmBefore.ics", "tests/assets/ics/alarms/relativeAlarmRelatedEnd.ics", "tests/assets/ics/alarms/relativeAlarmWeekBefore.ics", "tests/assets/ics/attendees/attendee1.ics", "tests/assets/ics/attendees/attendee2.ics", "tests/assets/ics/attendees/attendee3.ics", "tests/assets/ics/attendees/attendee4.ics", "tests/assets/ics/attendees/attendee5.ics", "tests/assets/ics/attendees/attendee6.ics", "tests/assets/ics/attendees/attendee7.ics", "tests/assets/ics/rrules/rrule-count-and-until.ics", "tests/assets/ics/rrules/rrule-count.ics", "tests/assets/ics/rrules/rrule-until.ics", "tests/assets/ics/rrules/rrules1.ics", "tests/assets/ics/rrules/rrules10.ics", "tests/assets/ics/rrules/rrules11.ics", "tests/assets/ics/rrules/rrules12.ics", "tests/assets/ics/rrules/rrules13.ics", "tests/assets/ics/rrules/rrules14.ics", "tests/assets/ics/rrules/rrules15.ics", "tests/assets/ics/rrules/rrules16.ics", "tests/assets/ics/rrules/rrules17.ics", "tests/assets/ics/rrules/rrules18.ics", "tests/assets/ics/rrules/rrules19.ics", "tests/assets/ics/rrules/rrules2.ics", "tests/assets/ics/rrules/rrules21.ics", "tests/assets/ics/rrules/rrules22.ics", "tests/assets/ics/rrules/rrules23.ics", "tests/assets/ics/rrules/rrules24.ics", "tests/assets/ics/rrules/rrules25.ics", "tests/assets/ics/rrules/rrules26.ics", "tests/assets/ics/rrules/rrules27.ics", "tests/assets/ics/rrules/rrules28.ics", "tests/assets/ics/rrules/rrules29.ics", "tests/assets/ics/rrules/rrules3.ics", "tests/assets/ics/rrules/rrules30.ics", "tests/assets/ics/rrules/rrules31.ics", "tests/assets/ics/rrules/rrules32.ics", "tests/assets/ics/rrules/rrules4.ics", "tests/assets/ics/rrules/rrules5.ics", "tests/assets/ics/rrules/rrules6.ics", "tests/assets/ics/rrules/rrules7.ics", "tests/assets/ics/rrules/rrules8.ics", "tests/assets/ics/rrules/rrules9.ics", "tests/assets/ics/vcalendars/vcalendar-empty.ics", "tests/assets/ics/vcalendars/vcalendar-event-alarms.ics", "tests/assets/ics/vcalendars/vcalendar-event-allday.ics", "tests/assets/ics/vcalendars/vcalendar-event-attendees.ics", "tests/assets/ics/vcalendars/vcalendar-event-categories.ics", "tests/assets/ics/vcalendars/vcalendar-event-custom-color.ics", "tests/assets/ics/vcalendars/vcalendar-event-floating-time.ics", "tests/assets/ics/vcalendars/vcalendar-event-minimal.ics","tests/assets/ics/vcalendars/vcalendar-event-multiple-rrules.ics", "tests/assets/ics/vcalendars/vcalendar-event-recurring-allday.ics", "tests/assets/ics/vcalendars/vcalendar-event-recurring.ics", "tests/assets/ics/vcalendars/vcalendar-event-timed.ics", "tests/assets/ics/vcalendars/vcalendar-event-utc-time.ics", "tests/assets/ics/vcalendars/vcalendar-journal.ics", "tests/assets/ics/vcalendars/vcalendar-todo.ics", "tests/assets/ics/vcalendars/vcalendar-without-vobjects.ics", "tests/assets/ics/vcalendars-scheduling/add.ics", "tests/assets/ics/vcalendars-scheduling/cancel.ics", "tests/assets/ics/vcalendars-scheduling/counter.ics", "tests/assets/ics/vcalendars-scheduling/declinecounter.ics", "tests/assets/ics/vcalendars-scheduling/freebusy-reply.ics", "tests/assets/ics/vcalendars-scheduling/freebusy-request.ics", "tests/assets/ics/vcalendars-scheduling/publish.ics", "tests/assets/ics/vcalendars-scheduling/refresh.ics", "tests/assets/ics/vcalendars-scheduling/reply.ics", "tests/assets/ics/vcalendars-scheduling/request.ics"]
2828
precedence = "aggregate"
2929
SPDX-FileCopyrightText = "2020 Nextcloud GmbH and Nextcloud contributors"
3030
SPDX-License-Identifier = "AGPL-3.0-or-later"

src/models/event.js

Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6-
import { DateTimeValue, DurationValue } from '@nextcloud/calendar-js'
6+
import { DurationValue } from '@nextcloud/calendar-js'
77
import { markRaw } from 'vue'
8-
import { getClosestCSS3ColorNameForHex, getHexForColorName } from '../utils/color.js'
8+
import { getHexForColorName } from '../utils/color.js'
99
import { getDateFromDateTimeValue } from '../utils/date.js'
1010
import { mapAlarmComponentToAlarmObject } from './alarm.js'
1111
import { mapAttachmentPropertyToAttchmentObject } from './attachment.js'
@@ -187,53 +187,61 @@ function mapEventComponentToEventObject(eventComponent) {
187187
*
188188
* @param {object} eventObject The calendar-object-instance object
189189
* @param {EventComponent} eventComponent The calendar-js EventComponent object
190-
* @param {boolean} resetAttendeeStatus Whether or not to reset the attendee status
191190
*/
192-
function copyCalendarObjectInstanceIntoEventComponent(eventObject, eventComponent, resetAttendeeStatus = false) {
193-
eventComponent.title = eventObject.title
194-
eventComponent.location = eventObject.location
195-
eventComponent.description = eventObject.description
196-
eventComponent.accessClass = eventObject.accessClass
197-
eventComponent.status = eventObject.status
198-
eventComponent.timeTransparency = eventObject.timeTransparency
199-
200-
for (const category of eventObject.categories) {
201-
eventComponent.addCategory(category)
202-
}
203-
204-
if (eventObject.organizer) {
205-
eventComponent.setOrganizerFromNameAndEMail(eventObject.organizer.commonName, eventObject.organizer.uri)
206-
}
207-
208-
for (const alarm of eventObject.alarms) {
209-
if (alarm.isRelative) {
210-
const duration = DurationValue.fromSeconds(alarm.relativeTrigger)
211-
eventComponent.addRelativeAlarm(alarm.type, duration, alarm.relativeIsRelatedToStart)
212-
} else {
213-
const date = DateTimeValue.fromJSDate(alarm.absoluteDate)
214-
eventComponent.addAbsoluteAlarm(alarm.type, date)
191+
function copyCalendarObjectInstanceIntoEventComponent(eventObject, eventComponent) {
192+
const sourceEventComponent = eventObject.eventComponent
193+
194+
const unexpectedRecurrenceProperties = new Set([
195+
'RRULE',
196+
'EXRULE',
197+
'RDATE',
198+
'EXDATE',
199+
])
200+
for (const property of sourceEventComponent.getPropertyIterator()) {
201+
if (unexpectedRecurrenceProperties.has(property.name)) {
202+
throw new Error(`Illegal argument: Event objects has recurrence related property ${property.name}.`)
215203
}
216204
}
217205

218-
for (const attendee of eventObject.attendees) {
219-
if (resetAttendeeStatus) {
220-
attendee.attendeeProperty.participationStatus = 'NEEDS-ACTION'
221-
attendee.attendeeProperty.rsvp = true
206+
const propertiesExcludedFromCopying = new Set([
207+
// These properties are regenerated for the new copy.
208+
'UID',
209+
'SEQUENCE',
210+
'CREATED',
211+
'DTSTAMP',
212+
'LAST-MODIFIED',
213+
// Currently only copying as exact occurrences.
214+
// Therefore, do not preserve any RECURRENCE-ID.
215+
'RECURRENCE-ID',
216+
])
217+
for (const property of sourceEventComponent.getPropertyIterator()) {
218+
if (unexpectedRecurrenceProperties.has(property.name)) {
219+
continue
220+
}
221+
if (propertiesExcludedFromCopying.has(property.name)) {
222+
continue
223+
}
224+
const successful = eventComponent.addProperty(property.clone())
225+
if (!successful) {
226+
throw new Error(`Illegal state: Property ${property.name} could not be copied.`)
222227
}
223-
224-
eventComponent.addProperty(attendee.attendeeProperty)
225-
}
226-
227-
for (const rule of eventObject.eventComponent.getPropertyIterator('RRULE')) {
228-
eventComponent.addProperty(rule)
229228
}
230229

231-
if (eventObject.eventComponent.hasProperty('X-NC-INVITATION-FORWARDING')) {
232-
eventComponent.updatePropertyWithValue('X-NC-INVITATION-FORWARDING', eventObject.invitationForwarding)
230+
// VALARM is supported in our app.
231+
// Other sub components could be PARTICIPANT, VLOCATION, VRESOURCE.
232+
// They might be set by other clients or apps.
233+
// So preservsem when copying.
234+
// Same behaviour applies events are copied for recurrence exceptions.
235+
for (const subComponent of sourceEventComponent.getComponentIterator()) {
236+
const successful = eventComponent.addComponent(subComponent.clone())
237+
if (!successful) {
238+
throw new Error(`Illegal state: Component ${subComponent.name} could not be copied.`)
239+
}
233240
}
234241

235-
if (eventObject.customColor) {
236-
eventComponent.color = getClosestCSS3ColorNameForHex(eventObject.customColor)
242+
for (const attendee of sourceEventComponent.getAttendeeIterator()) {
243+
attendee.participationStatus = 'NEEDS-ACTION'
244+
attendee.rsvp = true
237245
}
238246
}
239247

src/store/calendarObjectInstance.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1560,7 +1560,7 @@ export default defineStore('calendarObjectInstance', {
15601560
calendarId: this.calendarObject?.calendarId ?? null,
15611561
})
15621562
const eventComponent = getObjectAtRecurrenceId(calendarObject, startDate.jsDate)
1563-
copyCalendarObjectInstanceIntoEventComponent(oldCalendarObjectInstance, eventComponent, true)
1563+
copyCalendarObjectInstanceIntoEventComponent(oldCalendarObjectInstance, eventComponent)
15641564
const calendarObjectInstance = mapEventComponentToEventObject(eventComponent)
15651565

15661566
await this.setCalendarObjectInstanceForNewEvent({
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
BEGIN:VCALENDAR
2+
VERSION:2.0
3+
PRODID:-//Tests//
4+
CALSCALE:GREGORIAN
5+
BEGIN:VEVENT
6+
CREATED:20160809T163629Z
7+
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
8+
DTEND:20160816T100000
9+
DTSTART:20160816T090000
10+
DTSTAMP:20160809T163632Z
11+
SEQUENCE:0
12+
END:VEVENT
13+
END:VCALENDAR

0 commit comments

Comments
 (0)