Cleanup models and add full unit test coverage

Signed-off-by: Georg Ehrke <developer@georgehrke.com>
This commit is contained in:
Georg Ehrke 2020-04-01 09:39:35 +02:00
parent 53db79f8aa
commit 6f5966a2b4
No known key found for this signature in database
GPG Key ID: 9D98FD9380A1CB43
113 changed files with 6213 additions and 1177 deletions

View File

@ -152,7 +152,8 @@
"/node_modules/(?!calendar-js).+\\.js$"
],
"setupFilesAfterEnv": [
"./tests/javascript/jest.setup.js"
"./tests/javascript/jest.setup.js",
"./tests/assets/loadAsset.js"
]
}
}

View File

@ -22,6 +22,7 @@
import { getDurationValueFromFullCalendarDuration } from '../fullcalendar/duration'
import getTimezoneManager from '../services/timezoneDataProviderService'
import logger from '../utils/logger.js'
import { getObjectAtRecurrenceId } from '../utils/calendarObject.js'
/**
* Returns a function to drop an event at a different position
@ -60,7 +61,7 @@ export default function(store, fcAPI) {
return
}
const eventComponent = calendarObject.getObjectAtRecurrenceId(recurrenceIdDate)
const eventComponent = getObjectAtRecurrenceId(calendarObject, recurrenceIdDate)
if (!eventComponent) {
console.debug('Recurrence-id not found')
revert()
@ -71,7 +72,9 @@ export default function(store, fcAPI) {
// shiftByDuration may throw exceptions in certain cases
eventComponent.shiftByDuration(deltaDuration, event.allDay, timezone, defaultAllDayDuration, defaultTimedDuration)
} catch (error) {
calendarObject.resetToDav()
store.commit('resetCalendarObjectToDav', {
calendarObject,
})
console.debug(error)
revert()
return
@ -86,7 +89,9 @@ export default function(store, fcAPI) {
calendarObject,
})
} catch (error) {
calendarObject.resetToDav()
store.commit('resetCalendarObjectToDav', {
calendarObject,
})
console.debug(error)
revert()
}

View File

@ -20,6 +20,7 @@
*
*/
import { getDurationValueFromFullCalendarDuration } from './duration'
import { getObjectAtRecurrenceId } from '../utils/calendarObject.js'
/**
* Returns a function to resize an event
@ -50,7 +51,7 @@ export default function(store) {
return
}
const eventComponent = calendarObject.getObjectAtRecurrenceId(recurrenceIdDate)
const eventComponent = getObjectAtRecurrenceId(calendarObject, recurrenceIdDate)
if (!eventComponent) {
console.debug('Recurrence-id not found')
revert()
@ -73,7 +74,9 @@ export default function(store) {
calendarObject,
})
} catch (error) {
calendarObject.resetToDav()
store.commit('resetCalendarObjectToDav', {
calendarObject,
})
console.debug(error)
revert()
}

View File

@ -27,6 +27,7 @@ import {
getHexForColorName,
} from '../utils/color.js'
import logger from '../utils/logger.js'
import { getAllObjectsInTimeRange } from '../utils/calendarObject.js'
/**
* convert an array of calendar-objects to events
@ -43,7 +44,7 @@ export function eventSourceFunction(calendarObjects, calendar, start, end, timez
for (const calendarObject of calendarObjects) {
let allObjectsInTimeRange
try {
allObjectsInTimeRange = calendarObject.getAllObjectsInTimeRange(start, end)
allObjectsInTimeRange = getAllObjectsInTimeRange(calendarObject, start, end)
} catch (error) {
logger.error(error.message)
continue

View File

@ -300,7 +300,7 @@ export default {
return false
}
return this.calendarObject.existsOnServer()
return this.calendarObject.existsOnServer
},
/**
* Returns whether or not the user is allowed to create recurrence exceptions for this event
@ -336,7 +336,7 @@ export default {
return false
}
return this.calendarObject.existsOnServer()
return this.calendarObject.existsOnServer
},
/**
* Returns the download url as a string or null if event is loading or does not exist on the server (yet)
@ -385,7 +385,7 @@ export default {
// override the internally stored calendarId. If we did not do this,
// it would create the event in the default calendar first and move it
// to the desired calendar as a second step.
if (this.calendarObject && !this.calendarObject.existsOnServer()) {
if (this.calendarObject && !this.calendarObject.existsOnServer) {
this.calendarObject.calendarId = selectedCalendar.id
}
},
@ -423,7 +423,10 @@ export default {
return
}
await this.$store.dispatch('resetCalendarObjectInstance')
this.$store.commit('resetCalendarObjectToDav', {
calendarObject: this.calendarObject,
})
this.requiresActionOnRouteLeave = false
this.closeEditor()
},

119
src/models/alarm.js Normal file
View File

@ -0,0 +1,119 @@
/**
* @copyright Copyright (c) 2020 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import {
getAmountAndUnitForTimedEvents,
getAmountHoursMinutesAndUnitForAllDayEvents,
} from '../utils/alarms.js'
import { getDateFromDateTimeValue } from '../utils/date.js'
/**
* Creates a complete alarm object based on given props
*
* @param {Object} props The alarm properties already provided
* @returns {Object}
*/
const getDefaultAlarmObject = (props = {}) => Object.assign({}, {
// The calendar-js alarm component
alarmComponent: null,
// Type of alarm: DISPLAY, EMAIL, AUDIO
type: null,
// Whether or not the alarm is relative
isRelative: false,
// Date object of an absolute alarm (if it's absolute, it must be DATE-TIME)
absoluteDate: null,
// Whether or not the relative alarm is before the event,
relativeIsBefore: null,
// Whether or not the alarm is relative to the event's start
relativeIsRelatedToStart: null,
// TIMED EVENTS:
// Unit (seconds, minutes, hours, ...) if this alarm is inside a timed event
relativeUnitTimed: null,
// The amount of unit if this alarm is inside a timed event
relativeAmountTimed: null,
// ALL-DAY EVENTS:
// Unit (seconds, minutes, hours, ...) if this alarm is inside an all-day event
relativeUnitAllDay: null,
// The amount of unit if this alarm is inside a all-day event
relativeAmountAllDay: null,
// The hours to display alarm for in an all-day event (e.g. 1 day before at 9:00 am)
relativeHoursAllDay: null,
// The minutes to display alarm for in an all-day event (e.g. 1 day before at 9:30 am)
relativeMinutesAllDay: null,
// The total amount of seconds for a relative alarm
relativeTrigger: null,
}, props)
/**
* Map an alarm component to our alarm object
*
* @param {AlarmComponent} alarmComponent The calendar-js alarm-component to turn into an alarm object
* @returns {Object}
*/
const mapAlarmComponentToAlarmObject = (alarmComponent) => {
if (alarmComponent.trigger.isRelative()) {
const relativeIsBefore = alarmComponent.trigger.value.isNegative
const relativeIsRelatedToStart = alarmComponent.trigger.related === 'START'
const {
amount: relativeAmountTimed,
unit: relativeUnitTimed,
} = getAmountAndUnitForTimedEvents(alarmComponent.trigger.value.totalSeconds)
const {
unit: relativeUnitAllDay,
amount: relativeAmountAllDay,
hours: relativeHoursAllDay,
minutes: relativeMinutesAllDay,
} = getAmountHoursMinutesAndUnitForAllDayEvents(alarmComponent.trigger.value.totalSeconds)
const relativeTrigger = alarmComponent.trigger.value.totalSeconds
return getDefaultAlarmObject({
alarmComponent,
type: alarmComponent.action,
isRelative: alarmComponent.trigger.isRelative(),
relativeIsBefore,
relativeIsRelatedToStart,
relativeUnitTimed,
relativeAmountTimed,
relativeUnitAllDay,
relativeAmountAllDay,
relativeHoursAllDay,
relativeMinutesAllDay,
relativeTrigger,
})
} else {
const absoluteDate = getDateFromDateTimeValue(alarmComponent.trigger.value)
return getDefaultAlarmObject({
alarmComponent,
type: alarmComponent.action,
isRelative: alarmComponent.trigger.isRelative(),
absoluteDate,
})
}
}
export {
getDefaultAlarmObject,
mapAlarmComponentToAlarmObject,
}

70
src/models/attendee.js Normal file
View File

@ -0,0 +1,70 @@
/**
* @copyright Copyright (c) 2020 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/**
* Creates a complete attendee object based on given props
*
* TODO:
* - we should eventually support delegatedFrom and delegatedTo
*
* @param {Object} props The attendee properties already provided
* @returns {Object}
*/
const getDefaultAttendeeObject = (props = {}) => Object.assign({}, {
// The calendar-js attendee property
attendeeProperty: null,
// The display-name of the attendee
commonName: null,
// The calendar-user-type of the attendee
calendarUserType: 'INDIVIDUAL',
// The participation status of the attendee
participationStatus: 'NEEDS-ACTION',
// The role of the attendee
role: 'REQ-PARTICIPANT',
// The RSVP for the attendee
rsvp: false,
// The uri of the attendee
uri: null,
}, props)
/**
* Maps a calendar-js attendee property to our attendee object
*
* @param {AttendeeProperty} attendeeProperty The calendar-js attendeeProperty to turn into a attendee object
* @returns {Object}
*/
const mapAttendeePropertyToAttendeeObject = (attendeeProperty) => {
return getDefaultAttendeeObject({
attendeeProperty,
commonName: attendeeProperty.commonName,
calendarUserType: attendeeProperty.userType,
participationStatus: attendeeProperty.participationStatus,
role: attendeeProperty.role,
rsvp: attendeeProperty.rsvp,
uri: attendeeProperty.email,
})
}
export {
getDefaultAttendeeObject,
mapAttendeePropertyToAttendeeObject,
}

View File

@ -20,6 +20,7 @@
*
*/
import { detectColor, uidToHexColor } from '../utils/color.js'
import { mapDavShareeToCalendarShareObject } from './calendarShare.js'
/**
* Creates a complete calendar-object based on given props
@ -27,7 +28,7 @@ import { detectColor, uidToHexColor } from '../utils/color.js'
* @param {Object} props Calendar-props already provided
* @returns {Object}
*/
export const getDefaultCalendarObject = (props = {}) => Object.assign({}, {
const getDefaultCalendarObject = (props = {}) => Object.assign({}, {
// Id of the calendar
id: '',
// Visible display name
@ -79,7 +80,7 @@ export const getDefaultCalendarObject = (props = {}) => Object.assign({}, {
* @param {Object=} currentUserPrincipal The principal model of the current user principal
* @returns {Object}
*/
export function mapDavCollectionToCalendar(calendar, currentUserPrincipal) {
const mapDavCollectionToCalendar = (calendar, currentUserPrincipal) => {
const id = btoa(calendar.url)
const displayName = calendar.displayname || getCalendarUriFromUrl(calendar.url)
@ -134,11 +135,11 @@ export function mapDavCollectionToCalendar(calendar, currentUserPrincipal) {
continue
}
shares.push(mapDavShareeToSharee(share))
shares.push(mapDavShareeToCalendarShareObject(share))
}
}
return {
return getDefaultCalendarObject({
id,
displayName,
color,
@ -157,43 +158,7 @@ export function mapDavCollectionToCalendar(calendar, currentUserPrincipal) {
shares,
timezone,
dav: calendar,
}
}
/**
* Map a dav collection to our calendar object model
*
* @param {Object} sharee The sharee object from the cdav library shares
* @returns {Object}
*/
export function mapDavShareeToSharee(sharee) {
// sharee.href might contain non-latin characters, so let's uri encode it first
const id = btoa(encodeURI(sharee.href))
let displayName
if (sharee['common-name']) {
displayName = sharee['common-name']
} else if (sharee.href.startsWith('principal:principals/groups/')) {
displayName = sharee.href.substr(28)
} else if (sharee.href.startsWith('principal:principals/users/')) {
displayName = sharee.href.substr(27)
} else {
displayName = sharee.href
}
const writeable = sharee.access[0].endsWith('read-write')
const isGroup = sharee.href.indexOf('principal:principals/groups/') === 0
const isCircle = sharee.href.indexOf('principal:principals/circles/') === 0
const uri = sharee.href
return {
id,
displayName,
writeable,
isGroup,
isCircle,
uri,
}
})
}
/**
@ -209,3 +174,8 @@ function getCalendarUriFromUrl(url) {
return url.substring(url.lastIndexOf('/') + 1)
}
export {
getDefaultCalendarObject,
mapDavCollectionToCalendar,
}

View File

@ -19,218 +19,112 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { getParserManager } from 'calendar-js'
import DateTimeValue from 'calendar-js/src/values/dateTimeValue'
import CalendarComponent from 'calendar-js/src/components/calendarComponent'
import {
COMPONENT_NAME_EVENT,
COMPONENT_NAME_JOURNAL,
COMPONENT_NAME_VTODO,
} from './consts.js'
/**
* This model represents exactly
* Creates a complete calendar-object-object based on given props
*
* TODO: this should not be a class, but a simple object
* TODO: all methods should be converted to vuex commits
* @param {Object} props Calendar-object-props already provided
* @returns {Object}
*/
export default class CalendarObject {
const getDefaultCalendarObjectObject = (props = {}) => Object.assign({}, {
// Id of this calendar-object
id: null,
// Id of the associated calendar
calendarId: null,
// The cdav-library object storing the calendar-object
dav: null,
// The parsed calendar-js object
calendarComponent: null,
// The uid of the calendar-object
uid: null,
// The uri of the calendar-object
uri: null,
// The type of calendar-object
objectType: null,
// Whether or not the calendar-object is an event
isEvent: false,
// Whether or not the calendar-object is a journal
isJournal: false,
// Whether or not the calendar-object is a task
isTodo: false,
// Whether or not the calendar-object exists on the server
existsOnServer: false,
}, props)
/**
* Constructor of calendar-object
*
* @param {String|CalendarComponent} calendarData The raw unparsed calendar-data
* @param {String} calendarId Id of the calendar this calendar-object belongs to
* @param {VObject} dav The dav object
*/
constructor(calendarData, calendarId, dav = null) {
/**
* Id of the calendar this calendar-object is part of
*
* @type {String}
*/
this.calendarId = calendarId
/**
* Maps a calendar-object from c-dav to our calendar-object object
*
* @param {VObject} dav The c-dav VObject
* @param {String} calendarId The calendar-id this object is associated with
* @returns {Object}
*/
const mapCDavObjectToCalendarObject = (dav, calendarId) => {
const parserManager = getParserManager()
const parser = parserManager.getParserForFileType('text/calendar')
/**
* Whether or not there has been a conflict with the server version
*
* @type {boolean}
*/
this.conflict = false
/**
* The dav-object associated with this calendar-object
*
* @type {Object}|null
*/
this.dav = dav
/**
* parsed calendar-js object
* @type {CalendarComponent}
*/
this.vcalendar = null
this.resetToDav(calendarData)
// This should not be the case, but let's just be on the safe side
if (typeof dav.data !== 'string' || dav.data.trim() === '') {
throw new Error('Empty calendar object')
}
/**
* ID of the calendar-object
*
* @returns {string}
*/
get id() {
if (this.dav) {
return btoa(this.dav.url)
}
return 'new'
parser.parse(dav.data)
const calendarComponentIterator = parser.getItemIterator()
const calendarComponent = calendarComponentIterator.next().value
if (!calendarComponent) {
throw new Error('Empty calendar object')
}
/**
* @returns {string}
*/
getId() {
if (this.dav) {
return btoa(this.dav.url)
}
return 'new'
}
/**
* UID of the calendar-object
*
* @returns {null|String}
*/
get uid() {
const iterator = this.vcalendar.getVObjectIterator()
const firstVObject = iterator.next().value
if (firstVObject) {
return firstVObject.uid
}
return null
}
/**
* Type of the calendar-object
*
* @returns {null|String}
*/
get objectType() {
const iterator = this.vcalendar.getVObjectIterator()
const firstVObject = iterator.next().value
if (firstVObject) {
return firstVObject.name
}
return null
}
/**
* Whether or not this calendar-object is an event
*
* @returns {boolean}
*/
isEvent() {
return this.objectType === 'vevent'
}
/**
* Whether or not this calendar-object is a task
*
* @returns {boolean}
*/
isTodo() {
return this.objectType === 'vtodo'
}
/**
* Get all recurrence-items in given range
*
* @param {Date} start Begin of time-range
* @param {Date} end End of time-range
* @returns {Array}
*/
getAllObjectsInTimeRange(start, end) {
const iterator = this.vcalendar.getVObjectIterator()
const firstVObject = iterator.next().value
if (!firstVObject) {
return []
}
const s = DateTimeValue.fromJSDate(start, true)
const e = DateTimeValue.fromJSDate(end, true)
return firstVObject.recurrenceManager.getAllOccurrencesBetween(s, e)
}
/**
* Get recurrence-item closest to the given recurrence-Id
* This is either the one next in the future or if none exist,
* the one closest in the past
*
* @param {Date} closeTo The time to get the
* @returns {AbstractRecurringComponent|null}
*/
getClosestRecurrence(closeTo) {
const iterator = this.vcalendar.getVObjectIterator()
const firstVObject = iterator.next().value
if (!firstVObject) {
return null
}
const d = DateTimeValue.fromJSDate(closeTo, true)
return firstVObject.recurrenceManager.getClosestOccurrence(d)
}
/**
* Get recurrence-item at exactly a given recurrence-Id
*
* @param {Date} recurrenceId RecurrenceId to retrieve
* @returns {AbstractRecurringComponent|null}
*/
getObjectAtRecurrenceId(recurrenceId) {
const iterator = this.vcalendar.getVObjectIterator()
const firstVObject = iterator.next().value
if (!firstVObject) {
return null
}
const d = DateTimeValue.fromJSDate(recurrenceId, true)
return firstVObject.recurrenceManager.getOccurrenceAtExactly(d)
}
/**
* resets the inter vcalendar to the dav data
*
* @param {CalendarComponent|String} data Data to reset to
*/
resetToDav(data = null) {
if (data instanceof CalendarComponent) {
this.vcalendar = data
return
}
if (data === null && this.dav === null) {
return
}
const parserManager = getParserManager()
const parser = parserManager.getParserForFileType('text/calendar')
const calendarData = data || this.dav.data
parser.parse(calendarData)
const itemIterator = parser.getItemIterator()
const firstVCalendar = itemIterator.next().value
if (firstVCalendar) {
this.vcalendar = firstVCalendar
}
}
/**
* Whether or not this objects exists on the server
*
* @returns {boolean}
*/
existsOnServer() {
return !!this.dav
}
const vObjectIterator = calendarComponent.getVObjectIterator()
const firstVObject = vObjectIterator.next().value
return getDefaultCalendarObjectObject({
id: btoa(dav.url),
calendarId,
dav,
calendarComponent,
uid: firstVObject.uid,
uri: dav.url,
objectType: firstVObject.name,
isEvent: firstVObject.name === COMPONENT_NAME_EVENT,
isJournal: firstVObject.name === COMPONENT_NAME_JOURNAL,
isTodo: firstVObject.name === COMPONENT_NAME_VTODO,
existsOnServer: true,
})
}
/**
* Maps a calendar-component from calendar-js to our calendar-object object
*
* @param {CalendarComponent} calendarComponent The calendarComponent to create the calendarObject from
* @param {String=} calendarId The associated calendar if applicable
* @returns {Object}
*/
const mapCalendarJsToCalendarObject = (calendarComponent, calendarId = null) => {
const vObjectIterator = calendarComponent.getVObjectIterator()
const firstVObject = vObjectIterator.next().value
if (!firstVObject) {
throw new Error('Calendar object without vobjects')
}
return getDefaultCalendarObjectObject({
calendarId,
calendarComponent,
uid: firstVObject.uid,
objectType: firstVObject.name,
isEvent: firstVObject.name === COMPONENT_NAME_EVENT,
isJournal: firstVObject.name === COMPONENT_NAME_JOURNAL,
isTodo: firstVObject.name === COMPONENT_NAME_VTODO,
})
}
export {
getDefaultCalendarObjectObject,
mapCDavObjectToCalendarObject,
mapCalendarJsToCalendarObject,
}

View File

@ -1,677 +0,0 @@
/**
* @copyright Copyright (c) 2019 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { getDateFromDateTimeValue } from '../utils/date.js'
import DurationValue from 'calendar-js/src/values/durationValue.js'
import { getWeekDayFromDate } from '../utils/recurrence.js'
import { getAmountAndUnitForTimedEvents, getAmountHoursMinutesAndUnitForAllDayEvents } from '../utils/alarms.js'
import { getHexForColorName } from '../utils/color.js'
/**
* Creates a complete calendar-object-instance-object based on given props
*
* @param {Object} props The props already provided
* @returns {Object}
*/
export const getDefaultCalendarObjectInstanceObject = (props = {}) => Object.assign({}, {
// Title of the event
title: null,
// Start date of the event
startDate: null,
// Timezone of the start date
startTimezoneId: null,
// End date of the event
endDate: null,
// Timezone of the end date
endTimezoneId: null,
// Indicator whether or not event is all-day
isAllDay: false,
// Location that the event takes places in
location: null,
// description of the event
description: null,
// Access class of the event (PUBLIC, PRIVATE, CONFIDENTIAL)
accessClass: null,
// Status of the event (CONFIRMED, TENTATIVE, CANCELLED)
status: null,
// Whether or not to block this event in Free-Busy reports (TRANSPARENT, OPAQUE)
timeTransparency: null,
// The recurrence rule of this event. We only support one recurrence-rule
recurrenceRule: {
frequency: 'NONE',
interval: 1,
count: null,
until: null,
byDay: [],
byMonth: [],
byMonthDay: [],
bySetPosition: null,
isUnsupported: false,
recurrenceRuleValue: null,
},
// Attendees of this event
attendees: [],
// Organizer of the event
organizer: {
// name of the organizer
name: null,
// email of the organizer
uri: null,
},
// Alarm of the event
alarms: [],
// Custom color of the event
customColor: null,
// Categories
categories: [],
// Whether or not the user is allowed to toggle the all-day checkbox
canModifyAllDay: true,
// The real event-component coming from calendar-js
eventComponent: null,
}, props)
/**
* Map an EventComponent from calendar-js to our calendar object instance object
*
* @param {EventComponent} eventComponent The EventComponent object to map to an object
* @returns {{color: *, canModifyAllDay: *, timeTransparency: *, description: *, location: *, eventComponent: *, title: *, accessClass: *, status: *}}
*/
export const mapEventComponentToCalendarObjectInstanceObject = (eventComponent) => {
const calendarObjectInstanceObject = {
title: eventComponent.title,
location: eventComponent.location,
description: eventComponent.description,
accessClass: eventComponent.accessClass,
status: eventComponent.status,
timeTransparency: eventComponent.timeTransparency,
color: eventComponent.color,
canModifyAllDay: eventComponent.canModifyAllDay(),
eventComponent,
}
// The end date of an event is non-inclusive. This is rather intuitive for timed-events, but very unintuitive for all-day events.
// That's why, when an events is from 2019-10-03 to 2019-10-04, we will show 2019-10-03 to 2019-10-03 in the editor.
calendarObjectInstanceObject.isAllDay = eventComponent.isAllDay()
calendarObjectInstanceObject.startDate = getDateFromDateTimeValue(eventComponent.startDate)
calendarObjectInstanceObject.startTimezoneId = eventComponent.startDate.timezoneId
if (eventComponent.isAllDay()) {
const endDate = eventComponent.endDate.clone()
endDate.addDuration(DurationValue.fromSeconds(-1 * 60 * 60 * 24))
calendarObjectInstanceObject.endDate = getDateFromDateTimeValue(endDate)
} else {
calendarObjectInstanceObject.endDate = getDateFromDateTimeValue(eventComponent.endDate)
}
calendarObjectInstanceObject.endTimezoneId = eventComponent.endDate.timezoneId
calendarObjectInstanceObject.categories = getCategoriesFromEventComponent(eventComponent)
calendarObjectInstanceObject.organizer = getOrganizerFromEventComponent(eventComponent)
calendarObjectInstanceObject.recurrenceRule = getRecurrenceRuleFromEventComponent(eventComponent)
calendarObjectInstanceObject.hasMultipleRecurrenceRules
= Array.from(eventComponent.getPropertyIterator('RRULE')).length > 1
calendarObjectInstanceObject.attendees = getAttendeesFromEventComponent(eventComponent)
calendarObjectInstanceObject.alarms = getAlarmsFromEventComponent(eventComponent)
if (eventComponent.hasProperty('COLOR')) {
const hexColor = getHexForColorName(eventComponent.getFirstPropertyFirstValue('COLOR'))
if (hexColor !== null) {
calendarObjectInstanceObject.customColor = hexColor
}
}
return calendarObjectInstanceObject
}
/**
* Gets the organizer from the event component
*
* @param {EventComponent} eventComponent The event-component representing the instance
* @returns {null|{commonName: *, uri: *}}
*/
function getOrganizerFromEventComponent(eventComponent) {
if (eventComponent.organizer) {
const organizerProperty = eventComponent.getFirstProperty('ORGANIZER')
return {
commonName: organizerProperty.commonName,
uri: organizerProperty.email,
attendeeProperty: organizerProperty,
}
}
return null
}
/**
* Gets all categories (without a language tag) from the event component
*
* @param {EventComponent} eventComponent The event-component representing the instance
* @returns {String[]}
*/
function getCategoriesFromEventComponent(eventComponent) {
return Array.from(eventComponent.getCategoryIterator())
}
/**
* Gets the first recurrence rule from the event component
*
* @param {EventComponent} eventComponent The event-component representing the instance
* @returns {{byMonth: [], frequency: null, count: null, byDay: [], interval: number, until: null, bySetPosition: null, byMonthDay: []}|{byMonth: *, frequency: *, count: *, byDay: *, interval: *, until: *, bySetPosition: *, byMonthDay: *}}
*/
function getRecurrenceRuleFromEventComponent(eventComponent) {
/** @type {RecurValue} */
const recurrenceRule = eventComponent.getFirstPropertyFirstValue('RRULE')
if (recurrenceRule) {
const component = {
frequency: recurrenceRule.frequency,
interval: parseInt(recurrenceRule.interval, 10) || 1,
count: recurrenceRule.count,
until: null,
byDay: [],
byMonth: [],
byMonthDay: [],
bySetPosition: null,
isUnsupported: false,
recurrenceRuleValue: recurrenceRule,
}
if (recurrenceRule.until) {
component.until = recurrenceRule.until.jsDate
}
switch (component.frequency) {
case 'DAILY':
getRecurrenceComponentFromDailyRule(recurrenceRule, component)
break
case 'WEEKLY':
getRecurrenceComponentFromWeeklyRule(recurrenceRule, component, eventComponent)
break
case 'MONTHLY':
getRecurrenceComponentFromMonthlyRule(recurrenceRule, component, eventComponent)
break
case 'YEARLY':
getRecurrenceComponentFromYearlyRule(recurrenceRule, component, eventComponent)
break
default:
component.isUnsupported = true
break
}
return component
}
return {
frequency: 'NONE',
interval: 1,
count: null,
until: null,
byDay: [],
byMonth: [],
byMonthDay: [],
bySetPosition: null,
isUnsupported: false,
recurrenceRuleValue: null,
}
}
/**
* Checks if the recurrence-rule contains any of the given components
*
* @param {RecurValue} recurrenceRule The recurrence-rule value to check for the given components
* @param {String[]} components List of components to check for
* @returns {Boolean}
*/
function containsRecurrenceComponent(recurrenceRule, components) {
for (const component of components) {
const componentValue = recurrenceRule.getComponent(component)
if (componentValue.length > 0) {
return true
}
}
return false
}
/**
* Gets all attendees from the event component
*
* @param {EventComponent} eventComponent The event-component representing the instance
* @returns {[]}
*/
function getAttendeesFromEventComponent(eventComponent) {
const attendees = []
for (const attendee of eventComponent.getAttendeeIterator()) {
attendees.push({
commonName: attendee.commonName,
participationStatus: attendee.participationStatus,
role: attendee.role,
rsvp: attendee.rsvp,
uri: attendee.email,
attendeeProperty: attendee,
})
}
return attendees
}
/**
* Get all alarms from the event Component
*
* @param {EventComponent} eventComponent The event-component representing the instance
* @returns {[]}
*/
function getAlarmsFromEventComponent(eventComponent) {
const alarms = []
for (const alarm of eventComponent.getAlarmIterator()) {
alarms.push(getAlarmFromAlarmComponent(alarm))
}
return alarms
}
/**
* Get all numbers between start and end as strings
*
* @param {Number} start Lower end of range
* @param {Number} end Upper end of range
* @returns {string[]}
*/
function getRangeAsStrings(start, end) {
return Array
.apply(null, Array((end - start) + 1))
.map((_, n) => n + start)
.map((s) => s.toString())
}
/**
* Extracts the recurrence component from a daily recurrence rule
*
* @param {RecurValue} recurrenceRule The RecurValue to extract data from
* @param {Object} recurrenceComponent The recurrence component to write data into
*/
function getRecurrenceComponentFromDailyRule(recurrenceRule, recurrenceComponent) {
/**
* # Daily
*
* The Nextcloud-editor does not support any BY-parts for the daily rule, hence
* we will mark any DAILY rule with BY-parts as unsupported.
*/
const forbiddenComponents = [
'BYSECOND',
'BYMINUTE',
'BYHOUR',
'BYDAY',
'BYMONTHDAY',
'BYYEARDAY',
'BYWEEKNO',
'BYMONTH',
'BYSETPOS',
]
if (containsRecurrenceComponent(recurrenceRule, forbiddenComponents)) {
recurrenceComponent.isUnsupported = true
}
}
/**
* Extracts the recurrence component from a weekly recurrence rule
*
* @param {RecurValue} recurrenceRule The RecurValue to extract data from
* @param {Object} recurrenceComponent The recurrence component to write data into
* @param {EventComponent} eventComponent The event component needed for default values
*/
function getRecurrenceComponentFromWeeklyRule(recurrenceRule, recurrenceComponent, eventComponent) {
/**
* # Weekly
*
* The Nextcloud-editor only supports BYDAY in order to expand the weekly rule.
* It does not support other BY-parts like BYSETPOS or BYMONTH
*
* As defined by RFC 5545, the individual BYDAY components may not be preceded
* by a positive or negative integer.
*/
const forbiddenComponents = [
'BYSECOND',
'BYMINUTE',
'BYHOUR',
'BYMONTHDAY',
'BYYEARDAY',
'BYWEEKNO',
'BYMONTH',
'BYSETPOS',
]
if (containsRecurrenceComponent(recurrenceRule, forbiddenComponents)) {
recurrenceComponent.isUnsupported = true
}
recurrenceComponent.byDay = recurrenceRule.getComponent('BYDAY')
.filter((weekDay) => ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'].includes(weekDay))
// If the BYDAY is empty, add the day that the event occurs in
// E.g. if the event is on a Wednesday, automatically set BYDAY:WE
if (recurrenceComponent.byDay.length === 0) {
recurrenceComponent.byDay.push(getWeekDayFromDate(eventComponent.startDate.jsDate))
}
}
/**
* Extracts the recurrence component from a monthly recurrence rule
*
* @param {RecurValue} recurrenceRule The RecurValue to extract data from
* @param {Object} recurrenceComponent The recurrence component to write data into
* @param {EventComponent} eventComponent The event component needed for default values
*/
function getRecurrenceComponentFromMonthlyRule(recurrenceRule, recurrenceComponent, eventComponent) {
/**
* # Monthly
*
* The Nextcloud-editor only supports BYMONTHDAY, BYDAY, BYSETPOS in order to expand the monthly rule.
* It supports either BYMONTHDAY or the combination of BYDAY and BYSETPOS. They have to be used exclusively
* and cannot be combined.
*
* It does not support other BY-parts like BYMONTH
*
* For monthly recurrence-rules, BYDAY components are allowed to be preceded by positive or negative integers.
* The Nextcloud-editor supports at most one BYDAY component with an integer.
* If it's presented with such a BYDAY component, it will internally be converted to BYDAY without integer and BYSETPOS.
* e.g.
* BYDAY=3WE => BYDAY=WE,BYSETPOS=3
*
* BYSETPOS is limited to -2, -1, 1, 2, 3, 4, 5
* Other values are not supported
*
* BYDAY is limited to "MO", "TU", "WE", "TH", "FR", "SA", "SU",
* "MO,TU,WE,TH,FR,SA,SU", "MO,TU,WE,TH,FR", "SA,SU"
*
* BYMONYHDAY is limited to "1", "2", ..., "31"
*/
const forbiddenComponents = [
'BYSECOND',
'BYMINUTE',
'BYHOUR',
'BYYEARDAY',
'BYWEEKNO',
'BYMONTH',
]
if (containsRecurrenceComponent(recurrenceRule, forbiddenComponents)) {
recurrenceComponent.isUnsupported = true
}
if (containsRecurrenceComponent(recurrenceRule, ['BYMONYHDAY'])) {
if (containsRecurrenceComponent(recurrenceRule, ['BYDAY', 'BYSETPOS'])) {
recurrenceComponent.isUnsupported = true
}
const allowedValues = getRangeAsStrings(1, 31)
const byMonthDayComponent = recurrenceRule.getComponent('BYMONYHDAY')
recurrenceComponent.byMonthDay = byMonthDayComponent.filter((day) =>
allowedValues.includes(day))
if (byMonthDayComponent.length !== recurrenceComponent.byMonthDay.length) {
recurrenceComponent.isUnsupported = true
}
// TODO: the following is duplicate code, the same as in the yearly function.
} else if (containsRecurrenceComponent(recurrenceRule, ['BYDAY']) && containsRecurrenceComponent(recurrenceRule, ['BYSETPOS'])) {
if (isAllowedByDay(recurrenceRule.getComponent('BYDAY'))) {
recurrenceComponent.byDay = recurrenceRule.getComponent('BYDAY')
} else {
recurrenceComponent.byDay = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']
recurrenceComponent.isUnsupported = true
}
const setPositionArray = recurrenceRule.getComponent('BYSETPOS')
if (setPositionArray.length === 1 && isAllowedBySetPos(setPositionArray[0])) {
recurrenceComponent.bySetPosition = setPositionArray[0]
} else {
recurrenceComponent.bySetPosition = 1
recurrenceComponent.isUnsupported = true
}
} else if (containsRecurrenceComponent(recurrenceRule, ['BYDAY'])) {
const byDayArray = recurrenceRule.getComponent('BYDAY')
if (byDayArray.length > 1) {
recurrenceComponent.byMonthDay.push(eventComponent.startDate.day.toString())
recurrenceComponent.isUnsupported = true
} else {
const firstElement = byDayArray[0]
const match = /^(-?\d)([A-Z]){2}$/.exec(firstElement)
if (match) {
const bySetPosition = match[1]
const byDay = match[2]
if (isAllowedBySetPos(bySetPosition)) {
recurrenceComponent.byDay = [byDay]
recurrenceComponent.bySetPosition = bySetPosition
} else {
recurrenceComponent.byDay = [byDay]
recurrenceComponent.bySetPosition = 1
recurrenceComponent.isUnsupported = true
}
} else {
recurrenceComponent.byMonthDay.push(eventComponent.startDate.day.toString())
recurrenceComponent.isUnsupported = true
}
}
} else {
// If none of the previous rules are present, automatically set a BYMONTHDAY
recurrenceComponent.byMonthDay.push(eventComponent.startDate.day.toString())
}
}
/**
* Extracts the recurrence component from a yearly recurrence rule
*
* @param {RecurValue} recurrenceRule The RecurValue to extract data from
* @param {Object} recurrenceComponent The recurrence component to write data into
* @param {EventComponent} eventComponent The event component needed for default values
*/
function getRecurrenceComponentFromYearlyRule(recurrenceRule, recurrenceComponent, eventComponent) {
/**
* # YEARLY
*
* The Nextcloud-editor only supports BYMONTH, BYDAY, BYSETPOS in order to expand the monthly rule.
*
*
*
*
*
*
* BYSETPOS is limited to -2, -1, 1, 2, 3, 4, 5
* Other values are not supported
*
* BYDAY is limited to "MO", "TU", "WE", "TH", "FR", "SA", "SU",
* "MO,TU,WE,TH,FR,SA,SU", "MO,TU,WE,TH,FR", "SA,SU"
*/
const forbiddenComponents = [
'BYSECOND',
'BYMINUTE',
'BYHOUR',
'BYMONTHDAY',
'BYYEARDAY',
'BYWEEKNO',
]
if (containsRecurrenceComponent(recurrenceRule, forbiddenComponents)) {
recurrenceComponent.isUnsupported = true
}
if (containsRecurrenceComponent(recurrenceRule, ['BYMONTH'])) {
recurrenceComponent.byMonth = recurrenceRule.getComponent('BYMONTH')
} else {
recurrenceComponent.byMonth.push(eventComponent.startDate.month.toString())
}
// TODO: the following is duplicate code, the same as in the month function.
if (containsRecurrenceComponent(recurrenceRule, ['BYDAY']) && containsRecurrenceComponent(recurrenceRule, ['BYSETPOS'])) {
if (isAllowedByDay(recurrenceRule.getComponent('BYDAY'))) {
recurrenceComponent.byDay = recurrenceRule.getComponent('BYDAY')
} else {
recurrenceComponent.byDay = ['MO', 'TU', 'W E', 'TH', 'FR', 'SA', 'SU']
recurrenceComponent.isUnsupported = true
}
const setPositionArray = recurrenceRule.getComponent('BYSETPOS')
if (setPositionArray.length === 1 && isAllowedBySetPos(setPositionArray[0])) {
recurrenceComponent.bySetPosition = setPositionArray[0]
} else {
recurrenceComponent.bySetPosition = 1
recurrenceComponent.isUnsupported = true
}
} else if (containsRecurrenceComponent(recurrenceRule, ['BYDAY'])) {
const byDayArray = recurrenceRule.getComponent('BYDAY')
if (byDayArray.length > 1) {
recurrenceComponent.byMonthDay.push(eventComponent.startDate.day.toString())
recurrenceComponent.isUnsupported = true
} else {
const firstElement = byDayArray[0]
const match = /^(-?\d)([A-Z]){2}$/.exec(firstElement)
if (match) {
const bySetPosition = match[1]
const byDay = match[2]
if (isAllowedBySetPos(bySetPosition)) {
recurrenceComponent.byDay = [byDay]
recurrenceComponent.bySetPosition = bySetPosition
} else {
recurrenceComponent.byDay = [byDay]
recurrenceComponent.bySetPosition = 1
recurrenceComponent.isUnsupported = true
}
} else {
recurrenceComponent.byMonthDay.push(eventComponent.startDate.day.toString())
recurrenceComponent.isUnsupported = true
}
}
}
}
/**
* Checks if the given parameter is a supported BYDAY value
*
* @param {String[]} byDay The byDay component to check
* @returns {Boolean}
*/
function isAllowedByDay(byDay) {
const allowedByDay = [
'MO',
'TU',
'WE',
'TH',
'FR',
'SA',
'SU',
'FR,MO,SA,SU,TH,TU,WE',
'FR,MO,TH,TU,WE',
'SA,SU',
]
return allowedByDay.includes(byDay.slice().sort().join(','))
}
/**
* Checks if the given parameter is a supported BYSETPOS value
*
* @param {String} bySetPos The bySetPos component to check
* @returns {Boolean}
*/
function isAllowedBySetPos(bySetPos) {
const allowedBySetPos = [
'-2',
'-1',
'1',
'2',
'3',
'4',
'5',
]
return allowedBySetPos.includes(bySetPos.toString())
}
/**
*
* @param {Object} alarm The alarm to set / update
* @param {AlarmComponent} alarmComponent The alarm component to read from
*/
export function updateAlarmFromAlarmComponent(alarm, alarmComponent) {
alarm.type = alarmComponent.action
alarm.isRelative = alarmComponent.trigger.isRelative()
alarm.absoluteDate = null
alarm.absoluteTimezoneId = null
alarm.relativeIsBefore = null
alarm.relativeIsRelatedToStart = null
alarm.relativeUnitTimed = null
alarm.relativeAmountTimed = null
alarm.relativeUnitAllDay = null
alarm.relativeAmountAllDay = null
alarm.relativeHoursAllDay = null
alarm.relativeMinutesAllDay = null
alarm.relativeTrigger = null
alarm.alarmComponent = alarmComponent
if (alarm.isRelative) {
alarm.relativeIsBefore = alarmComponent.trigger.value.isNegative
alarm.relativeIsRelatedToStart = alarmComponent.trigger.related === 'START'
const timedData = getAmountAndUnitForTimedEvents(alarmComponent.trigger.value.totalSeconds)
alarm.relativeAmountTimed = timedData.amount
alarm.relativeUnitTimed = timedData.unit
const allDayData = getAmountHoursMinutesAndUnitForAllDayEvents(alarmComponent.trigger.value.totalSeconds)
alarm.relativeUnitAllDay = allDayData.unit
alarm.relativeAmountAllDay = allDayData.amount
alarm.relativeHoursAllDay = allDayData.hours
alarm.relativeMinutesAllDay = allDayData.minutes
alarm.relativeTrigger = alarmComponent.trigger.value.totalSeconds
} else {
alarm.absoluteDate = getDateFromDateTimeValue(alarmComponent.trigger.value)
alarm.absoluteTimezoneId = alarmComponent.trigger.value.timezoneId
}
}
/**
*
* @param {AlarmComponent} alarmComponent The alarm component to read from
* @returns {Object}
*/
export function getAlarmFromAlarmComponent(alarmComponent) {
const alarmObject = {}
updateAlarmFromAlarmComponent(alarmObject, alarmComponent)
return alarmObject
}

View File

@ -0,0 +1,92 @@
/**
* @copyright Copyright (c) 2020 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import {
PRINCIPAL_PREFIX_CIRCLE,
PRINCIPAL_PREFIX_GROUP,
PRINCIPAL_PREFIX_USER,
} from './consts.js'
/**
* Creates a complete calendar-share-object based on given props
*
* @param {Object} props Calendar-share-props already provided
* @returns {Object}
*/
const getDefaultCalendarShareObject = (props = {}) => Object.assign({}, {
// Unique identifier
id: null,
// Displayname of the sharee
displayName: null,
// Whether or not share is writable
writeable: false,
// Whether or not sharee is an individual user
isUser: false,
// Whether or not sharee is an admin-defined group
isGroup: false,
// Whether or not sharee is a user-defined group
isCircle: false,
// Uri necessary for deleting / updating share
uri: null,
}, props)
/**
* Map a dav collection to our calendar object model
*
* @param {Object} sharee The sharee object from the cdav library shares
* @returns {Object}
*/
const mapDavShareeToCalendarShareObject = (sharee) => {
// sharee.href might contain non-latin characters, so let's uri encode it first
const id = btoa(encodeURI(sharee.href))
let displayName
if (sharee['common-name'] && sharee['common-name'].trim() !== '') {
displayName = sharee['common-name']
} else if (sharee.href.startsWith(PRINCIPAL_PREFIX_GROUP)) {
displayName = sharee.href.substr(28)
} else if (sharee.href.startsWith(PRINCIPAL_PREFIX_USER)) {
displayName = sharee.href.substr(27)
} else {
displayName = sharee.href
}
const writeable = sharee.access[0].endsWith('read-write')
const isUser = sharee.href.startsWith(PRINCIPAL_PREFIX_USER)
const isGroup = sharee.href.startsWith(PRINCIPAL_PREFIX_GROUP)
const isCircle = sharee.href.startsWith(PRINCIPAL_PREFIX_CIRCLE)
const uri = sharee.href
return getDefaultCalendarShareObject({
id,
displayName,
writeable,
isUser,
isGroup,
isCircle,
uri,
})
}
export {
getDefaultCalendarShareObject,
mapDavShareeToCalendarShareObject,
}

58
src/models/consts.js Normal file
View File

@ -0,0 +1,58 @@
/**
* @copyright Copyright (c) 2020 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
const COMPONENT_NAME_EVENT = 'VEVENT'
const COMPONENT_NAME_JOURNAL = 'VJOURNAL'
const COMPONENT_NAME_VTODO = 'VTODO'
const ITIP_MESSAGE_ADD = 'ADD'
const ITIP_MESSAGE_CANCEL = 'CANCEL'
const ITIP_MESSAGE_COUNTER = 'COUNTER'
const ITIP_MESSAGE_DECLINECOUNTER = 'DECLINECOUNTER'
const ITIP_MESSAGE_PUBLISH = 'PUBLISH'
const ITIP_MESSAGE_REFRESH = 'REFRESH'
const ITIP_MESSAGE_REPLY = 'REPLY'
const ITIP_MESSAGE_REQUEST = 'REQUEST'
const PRINCIPAL_PREFIX_USER = 'principal:principals/users/'
const PRINCIPAL_PREFIX_GROUP = 'principal:principals/groups/'
const PRINCIPAL_PREFIX_CIRCLE = 'principal:principals/circles/'
const PRINCIPAL_PREFIX_CALENDAR_RESOURCE = 'principal:principals/calendar-resources/'
const PRINCIPAL_PREFIX_CALENDAR_ROOM = 'principal:principals/calendar-rooms/'
export {
COMPONENT_NAME_EVENT,
COMPONENT_NAME_JOURNAL,
COMPONENT_NAME_VTODO,
ITIP_MESSAGE_ADD,
ITIP_MESSAGE_CANCEL,
ITIP_MESSAGE_COUNTER,
ITIP_MESSAGE_DECLINECOUNTER,
ITIP_MESSAGE_PUBLISH,
ITIP_MESSAGE_REFRESH,
ITIP_MESSAGE_REPLY,
ITIP_MESSAGE_REQUEST,
PRINCIPAL_PREFIX_USER,
PRINCIPAL_PREFIX_GROUP,
PRINCIPAL_PREFIX_CIRCLE,
PRINCIPAL_PREFIX_CALENDAR_RESOURCE,
PRINCIPAL_PREFIX_CALENDAR_ROOM,
}

View File

@ -26,9 +26,9 @@
* @param {Object} props Contacts-props already provided
* @returns {Object}
*/
export const getDefaultContactsObject = (props = {}) => Object.assign({}, {
const getDefaultContactsObject = (props = {}) => Object.assign({}, {
// The name of the contact
name: '',
name: null,
// Calendar-user-type of the contact
calendarUserType: 'INDIVIDUAL',
// Whether or not this is a user
@ -51,3 +51,7 @@ export const getDefaultContactsObject = (props = {}) => Object.assign({}, {
// Timezone of the user
timezoneId: null,
}, props)
export {
getDefaultContactsObject,
}

185
src/models/event.js Normal file
View File

@ -0,0 +1,185 @@
/**
* @copyright Copyright (c) 2020 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { getDateFromDateTimeValue } from '../utils/date.js'
import DurationValue from 'calendar-js/src/values/durationValue.js'
import { getHexForColorName } from '../utils/color.js'
import { mapAlarmComponentToAlarmObject } from './alarm.js'
import { mapAttendeePropertyToAttendeeObject } from './attendee.js'
import {
getDefaultRecurrenceRuleObject,
mapRecurrenceRuleValueToRecurrenceRuleObject,
} from './recurrenceRule.js'
/**
* Creates a complete calendar-object-instance-object based on given props
*
* @param {Object} props The props already provided
* @returns {Object}
*/
const getDefaultEventObject = (props = {}) => Object.assign({}, {
// The real event-component coming from calendar-js
eventComponent: null,
// Title of the event
title: null,
// Start date of the event
startDate: null,
// Timezone of the start date
startTimezoneId: null,
// End date of the event
endDate: null,
// Timezone of the end date
endTimezoneId: null,
// Indicator whether or not event is all-day
isAllDay: false,
// Whether or not the user is allowed to toggle the all-day checkbox
canModifyAllDay: true,
// Location that the event takes places in
location: null,
// description of the event
description: null,
// Access class of the event (PUBLIC, PRIVATE, CONFIDENTIAL)
accessClass: null,
// Status of the event (CONFIRMED, TENTATIVE, CANCELLED)
status: null,
// Whether or not to block this event in Free-Busy reports (TRANSPARENT, OPAQUE)
timeTransparency: null,
// The recurrence rule of this event. We only support one recurrence-rule
recurrenceRule: getDefaultRecurrenceRuleObject(),
// Whether or not this event has multiple recurrence-rules
hasMultipleRRules: false,
// Whether or not this is the master item
isMasterItem: false,
// Whether or not this is a recurrence-exception
isRecurrenceException: false,
// Whether or not the applied modifications require to update this and all future
forceThisAndAllFuture: false,
// Whether or not it's possible to create a recurrence-exception for this event
canCreateRecurrenceException: false,
// Attendees of this event
attendees: [],
// Organizer of the event
organizer: null,
// Alarm of the event
alarms: [],
// Custom color of the event
customColor: null,
// Categories
categories: [],
}, props)
/**
*
* @param {EventComponent} eventComponent The calendar-js eventComponent
* @returns {Object}
*/
const mapEventComponentToEventObject = (eventComponent) => {
const eventObject = getDefaultEventObject({
eventComponent,
title: eventComponent.title,
isAllDay: eventComponent.isAllDay(),
canModifyAllDay: eventComponent.canModifyAllDay(),
location: eventComponent.location,
description: eventComponent.description,
accessClass: eventComponent.accessClass,
status: eventComponent.status,
timeTransparency: eventComponent.timeTransparency,
categories: Array.from(eventComponent.getCategoryIterator()),
isMasterItem: eventComponent.isMasterItem(),
isRecurrenceException: eventComponent.isRecurrenceException(),
canCreateRecurrenceException: eventComponent.canCreateRecurrenceExceptions(),
})
/**
* According to RFC5545, DTEND is exclusive. This is rather intuitive for timed-events
* but rather unintuitive for all-day events
*
* That's why, when an event is all-day from 2019-10-03 to 2019-10-04,
* it will be displayed as 2019-10-03 to 2019-10-03 in the editor.
*/
eventObject.startDate = getDateFromDateTimeValue(eventComponent.startDate)
eventObject.startTimezoneId = eventComponent.startDate.timezoneId
if (eventComponent.isAllDay()) {
const endDate = eventComponent.endDate.clone()
endDate.addDuration(DurationValue.fromSeconds(-1 * 60 * 60 * 24))
eventObject.endDate = getDateFromDateTimeValue(endDate)
} else {
eventObject.endDate = getDateFromDateTimeValue(eventComponent.endDate)
}
eventObject.endTimezoneId = eventComponent.endDate.timezoneId
/**
* Extract organizer if there is any
*/
if (eventComponent.organizer) {
const organizerProperty = eventComponent.getFirstProperty('ORGANIZER')
eventObject.organizer = {
commonName: organizerProperty.commonName,
uri: organizerProperty.email,
attendeeProperty: organizerProperty,
}
}
/**
* Extract alarms
*/
for (const alarm of eventComponent.getAlarmIterator()) {
eventObject.alarms.push(mapAlarmComponentToAlarmObject(alarm))
}
/**
* Extract attendees
*/
for (const attendee of eventComponent.getAttendeeIterator()) {
eventObject.attendees.push(mapAttendeePropertyToAttendeeObject(attendee))
}
/**
* Extract recurrence-rule
*/
const recurrenceRuleIterator = eventComponent.getPropertyIterator('RRULE')
const recurrenceRuleFirstIteration = recurrenceRuleIterator.next()
const firstRecurrenceRule = recurrenceRuleFirstIteration.value
if (firstRecurrenceRule) {
eventObject.recurrenceRule = mapRecurrenceRuleValueToRecurrenceRuleObject(firstRecurrenceRule.getFirstValue(), eventComponent.startDate)
eventObject.hasMultipleRRules = !recurrenceRuleIterator.next().done
}
/**
* Convert the CSS 3 color name to a hex color
*/
if (eventComponent.hasProperty('COLOR')) {
const hexColor = getHexForColorName(eventComponent.getFirstPropertyFirstValue('COLOR'))
if (hexColor !== null) {
eventObject.customColor = hexColor
}
}
return eventObject
}
export {
getDefaultEventObject,
mapEventComponentToEventObject,
}

View File

@ -20,46 +20,108 @@
*
*/
import {
PRINCIPAL_PREFIX_CALENDAR_RESOURCE,
PRINCIPAL_PREFIX_CALENDAR_ROOM,
PRINCIPAL_PREFIX_CIRCLE,
PRINCIPAL_PREFIX_GROUP,
PRINCIPAL_PREFIX_USER,
} from './consts.js'
/**
* Creates a complete principal-object based on given props
*
* @param {Object} props Principal-props already provided
* @returns {any}
*/
export const getDefaultPrincipalObject = (props) => Object.assign({}, {
const getDefaultPrincipalObject = (props) => Object.assign({}, {
// Id of the principal
id: '',
id: null,
// Calendar-user-type. This can be INDIVIDUAL, GROUP, RESOURCE or ROOM
calendarUserType: '',
calendarUserType: 'INDIVIDUAL',
// E-Mail address of principal used for scheduling
emailAddress: '',
emailAddress: null,
// The principals display-name
displayname: '',
// TODO: this should be renamed to displayName
displayname: null,
// principalScheme
principalScheme: '',
principalScheme: null,
// The internal user-id in case it is of type INDIVIDUAL and a user
userId: '',
// TODO: userId is deprecrated, use principalId instead
userId: null,
// url to the DAV-principal-resource
url: '',
url: null,
// The cdav-library object
dav: false,
dav: null,
// Whether or not this principal represents a circle
isCircle: false,
// Whether or not this principal represents a user
isUser: false,
// Whether or not this principal represents a group
isGroup: false,
// Whether or not this principal represents a calendar-resource
isCalendarResource: false,
// Whether or not this principal represents a calendar-room
isCalendarRoom: false,
// The id of the principal without prefix. e.g. userId / groupId / etc.
principalId: null,
}, props)
/**
* converts a dav principal into a vuex object
*
* @param {Object} principal cdav-library Principal object
* @returns {{emailAddress: *, displayname: *, dav: *, id: *, calendarUserType: *, userId: *, url: *}}
* @param {Object} dav cdav-library Principal object
* @returns {Object}
*/
export function mapDavToPrincipal(principal) {
return {
id: btoa(principal.url),
calendarUserType: principal.calendarUserType,
principalScheme: principal.principalScheme,
emailAddress: principal.email,
displayname: principal.displayname,
userId: principal.userId,
url: principal.principalUrl,
dav: principal,
const mapDavToPrincipal = (dav) => {
const id = btoa(encodeURI(dav.url))
const calendarUserType = dav.calendarUserType
const principalScheme = dav.principalScheme
const emailAddress = dav.email
const displayname = dav.displayname
const isUser = dav.principalScheme.startsWith(PRINCIPAL_PREFIX_USER)
const isGroup = dav.principalScheme.startsWith(PRINCIPAL_PREFIX_GROUP)
const isCircle = dav.principalScheme.startsWith(PRINCIPAL_PREFIX_CIRCLE)
const isCalendarResource = dav.principalScheme.startsWith(PRINCIPAL_PREFIX_CALENDAR_RESOURCE)
const isCalendarRoom = dav.principalScheme.startsWith(PRINCIPAL_PREFIX_CALENDAR_ROOM)
let principalId = null
if (isUser) {
principalId = dav.principalScheme.substring(PRINCIPAL_PREFIX_USER.length)
} else if (isGroup) {
principalId = dav.principalScheme.substring(PRINCIPAL_PREFIX_GROUP.length)
} else if (isCircle) {
principalId = dav.principalScheme.substring(PRINCIPAL_PREFIX_CIRCLE.length)
} else if (isCalendarResource) {
principalId = dav.principalScheme.substring(PRINCIPAL_PREFIX_CALENDAR_RESOURCE.length)
} else if (isCalendarRoom) {
principalId = dav.principalScheme.substring(PRINCIPAL_PREFIX_CALENDAR_ROOM.length)
}
const url = dav.principalUrl
const userId = dav.userId
return getDefaultPrincipalObject({
id,
calendarUserType,
principalScheme,
emailAddress,
displayname,
url,
dav,
isUser,
isGroup,
isCircle,
isCalendarResource,
isCalendarRoom,
principalId,
userId,
})
}
export {
getDefaultPrincipalObject,
mapDavToPrincipal,
}

View File

@ -0,0 +1,503 @@
/**
* @copyright Copyright (c) 2020 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { getWeekDayFromDate } from '../utils/recurrence.js'
import { getDateFromDateTimeValue } from '../utils/date.js'
/**
* Creates a complete recurrence-rule-object based on given props
*
* @param {Object} props Recurrence-rule-object-props already provided
* @returns {Object}
*/
const getDefaultRecurrenceRuleObject = (props = {}) => Object.assign({}, {
// The calendar-js recurrence-rule value
recurrenceRuleValue: null,
// The frequency of the recurrence-rule (DAILY, WEEKLY, ...)
frequency: 'NONE',
// The interval of the recurrence-rule, must be a positive integer
interval: 1,
// Positive integer if recurrence-rule limited by count, null otherwise
count: null,
// Date if recurrence-rule limited by date, null otherwise
// We do not store a timezone here, since we only care about the date part
until: null,
// List of byDay components to limit/expand the recurrence-rule
byDay: [],
// List of byMonth components to limit/expand the recurrence-rule
byMonth: [],
// List of byMonthDay components to limit/expand the recurrence-rule
byMonthDay: [],
// A position to limit the recurrence-rule (e.g. -1 for last Friday)
bySetPosition: null,
// Whether or not the rule is not supported for editing
isUnsupported: false,
}, props)
/**
* Maps a calendar-js recurrence-rule-value to an recurrence-rule-object
*
* @param {RecurValue} recurrenceRuleValue The calendar-js recurrence rule value
* @param {DateTimeValue} baseDate The base-date used to fill unset values
* @returns {Object}
*/
const mapRecurrenceRuleValueToRecurrenceRuleObject = (recurrenceRuleValue, baseDate) => {
switch (recurrenceRuleValue.frequency) {
case 'DAILY':
return mapDailyRuleValueToRecurrenceRuleObject(recurrenceRuleValue)
case 'WEEKLY':
return mapWeeklyRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)
case 'MONTHLY':
return mapMonthlyRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)
case 'YEARLY':
return mapYearlyRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)
default: // SECONDLY, MINUTELY, HOURLY
return getDefaultRecurrenceRuleObjectForRecurrenceValue(recurrenceRuleValue, {
isUnsupported: true,
})
}
}
const FORBIDDEN_BY_PARTS_DAILY = [
'BYSECOND',
'BYMINUTE',
'BYHOUR',
'BYDAY',
'BYMONTHDAY',
'BYYEARDAY',
'BYWEEKNO',
'BYMONTH',
'BYSETPOS',
]
const FORBIDDEN_BY_PARTS_WEEKLY = [
'BYSECOND',
'BYMINUTE',
'BYHOUR',
'BYMONTHDAY',
'BYYEARDAY',
'BYWEEKNO',
'BYMONTH',
'BYSETPOS',
]
const FORBIDDEN_BY_PARTS_MONTHLY = [
'BYSECOND',
'BYMINUTE',
'BYHOUR',
'BYYEARDAY',
'BYWEEKNO',
'BYMONTH',
]
const FORBIDDEN_BY_PARTS_YEARLY = [
'BYSECOND',
'BYMINUTE',
'BYHOUR',
'BYMONTHDAY',
'BYYEARDAY',
'BYWEEKNO',
]
const SUPPORTED_BY_DAY_WEEKLY = [
'SU',
'MO',
'TU',
'WE',
'TH',
'FR',
'SA',
]
/**
* Get all numbers between start and end as strings
*
* @param {Number} start Lower end of range
* @param {Number} end Upper end of range
* @returns {string[]}
*/
const getRangeAsStrings = (start, end) => {
return Array
.apply(null, Array((end - start) + 1))
.map((_, n) => n + start)
.map((s) => s.toString())
}
const SUPPORTED_BY_MONTHDAY_MONTHLY = getRangeAsStrings(1, 31)
const SUPPORTED_BY_MONTH_YEARLY = getRangeAsStrings(1, 12)
/**
* Maps a daily calendar-js recurrence-rule-value to an recurrence-rule-object
*
* @param {RecurValue} recurrenceRuleValue The calendar-js recurrence rule value
* @returns {Object}
*/
const mapDailyRuleValueToRecurrenceRuleObject = (recurrenceRuleValue) => {
/**
* We only support DAILY rules without any by-parts in the editor.
* If the recurrence-rule contains any by-parts, mark it as unsupported.
*/
const isUnsupported = containsRecurrenceComponent(recurrenceRuleValue, FORBIDDEN_BY_PARTS_DAILY)
return getDefaultRecurrenceRuleObjectForRecurrenceValue(recurrenceRuleValue, {
isUnsupported,
})
}
/**
* Maps a weekly calendar-js recurrence-rule-value to an recurrence-rule-object
*
* @param {RecurValue} recurrenceRuleValue The calendar-js recurrence rule value
* @param {DateTimeValue} baseDate The base-date used to fill unset values
* @returns {Object}
*/
const mapWeeklyRuleValueToRecurrenceRuleObject = (recurrenceRuleValue, baseDate) => {
/**
* For WEEKLY recurrences, our editor only allows BYDAY
*
* As defined in RFC5545 3.3.10. Recurrence Rule:
* > Each BYDAY value can also be preceded by a positive (+n) or
* > negative (-n) integer. If present, this indicates the nth
* > occurrence of a specific day within the MONTHLY or YEARLY "RRULE".
*
* RFC 5545 specifies other components, which can be used along WEEKLY.
* Among them are BYMONTH and BYSETPOS. We don't support those.
*/
const containsUnsupportedByParts = containsRecurrenceComponent(recurrenceRuleValue, FORBIDDEN_BY_PARTS_WEEKLY)
const containsInvalidByDayPart = recurrenceRuleValue.getComponent('BYDAY')
.some((weekday) => !SUPPORTED_BY_DAY_WEEKLY.includes(weekday))
const isUnsupported = containsUnsupportedByParts || containsInvalidByDayPart
const byDay = recurrenceRuleValue.getComponent('BYDAY')
.filter((weekday) => SUPPORTED_BY_DAY_WEEKLY.includes(weekday))
// If the BYDAY is empty, add the day that the event occurs in
// E.g. if the event is on a Wednesday, automatically set BYDAY:WE
if (byDay.length === 0) {
byDay.push(getWeekDayFromDate(baseDate.jsDate))
}
return getDefaultRecurrenceRuleObjectForRecurrenceValue(recurrenceRuleValue, {
byDay,
isUnsupported,
})
}
/**
* Maps a monthly calendar-js recurrence-rule-value to an recurrence-rule-object
*
* @param {RecurValue} recurrenceRuleValue The calendar-js recurrence rule value
* @param {DateTimeValue} baseDate The base-date used to fill unset values
* @returns {Object}
*/
const mapMonthlyRuleValueToRecurrenceRuleObject = (recurrenceRuleValue, baseDate) => {
/**
* We only supports BYMONTHDAY, BYDAY, BYSETPOS in order to expand the monthly rule.
* It supports either BYMONTHDAY or the combination of BYDAY and BYSETPOS. They have to be used exclusively
* and cannot be combined.
*
* We do not support other BY-parts like BYMONTH
*
* For monthly recurrence-rules, BYDAY components are allowed to be preceded by positive or negative integers.
* The Nextcloud-editor supports at most one BYDAY component with an integer.
* If it's presented with such a BYDAY component, it will internally be converted to BYDAY without integer and BYSETPOS.
* e.g.
* BYDAY=3WE => BYDAY=WE,BYSETPOS=3
*
* BYSETPOS is limited to -2, -1, 1, 2, 3, 4, 5
* Other values are not supported
*
* BYDAY is limited to "MO", "TU", "WE", "TH", "FR", "SA", "SU",
* "MO,TU,WE,TH,FR,SA,SU", "MO,TU,WE,TH,FR", "SA,SU"
*
* BYMONTHDAY is limited to "1", "2", ..., "31"
*/
let isUnsupported = containsRecurrenceComponent(recurrenceRuleValue, FORBIDDEN_BY_PARTS_MONTHLY)
let byDay = []
let bySetPosition = null
let byMonthDay = []
// This handles the first case, where we have a BYMONTHDAY rule
if (containsRecurrenceComponent(recurrenceRuleValue, ['BYMONTHDAY'])) {
// verify there is no BYDAY or BYSETPOS at the same time
if (containsRecurrenceComponent(recurrenceRuleValue, ['BYDAY', 'BYSETPOS'])) {
isUnsupported = true
}
const containsInvalidByMonthDay = recurrenceRuleValue.getComponent('BYMONTHDAY')
.some((monthDay) => !SUPPORTED_BY_MONTHDAY_MONTHLY.includes(monthDay.toString()))
isUnsupported = isUnsupported || containsInvalidByMonthDay
byMonthDay = recurrenceRuleValue.getComponent('BYMONTHDAY')
.filter((monthDay) => SUPPORTED_BY_MONTHDAY_MONTHLY.includes(monthDay.toString()))
.map((monthDay) => monthDay.toString())
// This handles cases where we have both BYDAY and BYSETPOS
} else if (containsRecurrenceComponent(recurrenceRuleValue, ['BYDAY']) && containsRecurrenceComponent(recurrenceRuleValue, ['BYSETPOS'])) {
if (isAllowedByDay(recurrenceRuleValue.getComponent('BYDAY'))) {
byDay = recurrenceRuleValue.getComponent('BYDAY')
} else {
byDay = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']
isUnsupported = true
}
const setPositionArray = recurrenceRuleValue.getComponent('BYSETPOS')
if (setPositionArray.length === 1 && isAllowedBySetPos(setPositionArray[0])) {
bySetPosition = setPositionArray[0]
} else {
bySetPosition = 1
isUnsupported = true
}
// This handles cases where we only have a BYDAY
} else if (containsRecurrenceComponent(recurrenceRuleValue, ['BYDAY'])) {
const byDayArray = recurrenceRuleValue.getComponent('BYDAY')
if (byDayArray.length > 1) {
byMonthDay.push(baseDate.day.toString())
isUnsupported = true
} else {
const firstElement = byDayArray[0]
const match = /^(-?\d)([A-Z]{2})$/.exec(firstElement)
if (match) {
const matchedBySetPosition = match[1]
const matchedByDay = match[2]
if (isAllowedBySetPos(matchedBySetPosition)) {
byDay = [matchedByDay]
bySetPosition = parseInt(matchedBySetPosition, 10)
} else {
byDay = [matchedByDay]
bySetPosition = 1
isUnsupported = true
}
} else {
byMonthDay.push(baseDate.day.toString())
isUnsupported = true
}
}
// This is a fallback where we just default BYMONTHDAY to the start date of the event
} else {
byMonthDay.push(baseDate.day.toString())
}
return getDefaultRecurrenceRuleObjectForRecurrenceValue(recurrenceRuleValue, {
byDay,
bySetPosition,
byMonthDay,
isUnsupported,
})
}
/**
* Maps a yearly calendar-js recurrence-rule-value to an recurrence-rule-object
*
* @param {RecurValue} recurrenceRuleValue The calendar-js recurrence rule value
* @param {DateTimeValue} baseDate The base-date used to fill unset values
* @returns {Object}
*/
const mapYearlyRuleValueToRecurrenceRuleObject = (recurrenceRuleValue, baseDate) => {
/**
* We only supports BYMONTH, BYDAY, BYSETPOS in order to expand the yearly rule.
* It supports a combination of them.
*
* We do not support other BY-parts.
*
* For yearly recurrence-rules, BYDAY components are allowed to be preceded by positive or negative integers.
* The Nextcloud-editor supports at most one BYDAY component with an integer.
* If it's presented with such a BYDAY component, it will internally be converted to BYDAY without integer and BYSETPOS.
* e.g.
* BYDAY=3WE => BYDAY=WE,BYSETPOS=3
*
* BYSETPOS is limited to -2, -1, 1, 2, 3, 4, 5
* Other values are not supported
*
* BYDAY is limited to "MO", "TU", "WE", "TH", "FR", "SA", "SU",
* "MO,TU,WE,TH,FR,SA,SU", "MO,TU,WE,TH,FR", "SA,SU"
*/
let isUnsupported = containsRecurrenceComponent(recurrenceRuleValue, FORBIDDEN_BY_PARTS_YEARLY)
let byDay = []
let bySetPosition = null
let byMonth = []
if (containsRecurrenceComponent(recurrenceRuleValue, ['BYMONTH'])) {
const containsInvalidByMonthDay = recurrenceRuleValue.getComponent('BYMONTH')
.some((month) => !SUPPORTED_BY_MONTH_YEARLY.includes(month.toString()))
isUnsupported = isUnsupported || containsInvalidByMonthDay
byMonth = recurrenceRuleValue.getComponent('BYMONTH')
.filter((monthDay) => SUPPORTED_BY_MONTH_YEARLY.includes(monthDay.toString()))
.map((month) => month.toString())
} else {
byMonth.push(baseDate.month.toString())
}
if (containsRecurrenceComponent(recurrenceRuleValue, ['BYDAY']) && containsRecurrenceComponent(recurrenceRuleValue, ['BYSETPOS'])) {
if (isAllowedByDay(recurrenceRuleValue.getComponent('BYDAY'))) {
byDay = recurrenceRuleValue.getComponent('BYDAY')
} else {
byDay = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']
isUnsupported = true
}
const setPositionArray = recurrenceRuleValue.getComponent('BYSETPOS')
if (setPositionArray.length === 1 && isAllowedBySetPos(setPositionArray[0])) {
bySetPosition = setPositionArray[0]
} else {
bySetPosition = 1
isUnsupported = true
}
} else if (containsRecurrenceComponent(recurrenceRuleValue, ['BYDAY'])) {
const byDayArray = recurrenceRuleValue.getComponent('BYDAY')
if (byDayArray.length > 1) {
isUnsupported = true
} else {
const firstElement = byDayArray[0]
const match = /^(-?\d)([A-Z]{2})$/.exec(firstElement)
if (match) {
const matchedBySetPosition = match[1]
const matchedByDay = match[2]
if (isAllowedBySetPos(matchedBySetPosition)) {
byDay = [matchedByDay]
bySetPosition = parseInt(matchedBySetPosition, 10)
} else {
byDay = [matchedByDay]
bySetPosition = 1
isUnsupported = true
}
} else {
isUnsupported = true
}
}
}
return getDefaultRecurrenceRuleObjectForRecurrenceValue(recurrenceRuleValue, {
byDay,
bySetPosition,
byMonth,
isUnsupported,
})
}
/**
* Checks if the given parameter is a supported BYDAY value
*
* @param {String[]} byDay The byDay component to check
* @returns {Boolean}
*/
const isAllowedByDay = (byDay) => {
return [
'MO',
'TU',
'WE',
'TH',
'FR',
'SA',
'SU',
'FR,MO,SA,SU,TH,TU,WE',
'FR,MO,TH,TU,WE',
'SA,SU',
].includes(byDay.slice().sort().join(','))
}
/**
* Checks if the given parameter is a supported BYSETPOS value
*
* @param {String} bySetPos The bySetPos component to check
* @returns {Boolean}
*/
const isAllowedBySetPos = (bySetPos) => {
return [
'-2',
'-1',
'1',
'2',
'3',
'4',
'5',
].includes(bySetPos.toString())
}
/**
* Checks if the recurrence-rule contains any of the given components
*
* @param {RecurValue} recurrenceRule The recurrence-rule value to check for the given components
* @param {String[]} components List of components to check for
* @returns {Boolean}
*/
const containsRecurrenceComponent = (recurrenceRule, components) => {
for (const component of components) {
const componentValue = recurrenceRule.getComponent(component)
if (componentValue.length > 0) {
return true
}
}
return false
}
/**
* Returns a full recurrence-rule-object with default values derived from recurrenceRuleValue
* and additional props
*
* @param {RecurValue} recurrenceRuleValue The recurrence-rule value to get default values from
* @param {Object} props The properties to provide on top of default one
* @returns {Object}
*/
const getDefaultRecurrenceRuleObjectForRecurrenceValue = (recurrenceRuleValue, props) => {
const isUnsupported = recurrenceRuleValue.count !== null && recurrenceRuleValue.until !== null
let isUnsupportedProps = {}
if (isUnsupported) {
isUnsupportedProps = {
isUnsupported,
}
}
return getDefaultRecurrenceRuleObject(Object.assign({}, {
recurrenceRuleValue,
frequency: recurrenceRuleValue.frequency,
interval: parseInt(recurrenceRuleValue.interval, 10) || 1,
count: recurrenceRuleValue.count,
until: recurrenceRuleValue.until
? getDateFromDateTimeValue(recurrenceRuleValue.until)
: null,
}, props, isUnsupportedProps))
}
export {
getDefaultRecurrenceRuleObject,
mapRecurrenceRuleValueToRecurrenceRuleObject,
}

View File

@ -27,7 +27,7 @@ import { getDefaultCategories } from '../defaults/defaultCategories.js'
*
* @returns {{color: {readableName: *, icon: string, multiple: boolean, info: *}, timeTransparency: {readableName: *, defaultValue: string, icon: string, multiple: boolean, options: *[], info: *}, description: {readableName: *, icon: string, placeholder: *, defaultNumberOfRows: number}, location: {readableName: *, icon: string, placeholder: *}, categories: {readableName: *, icon: string, multiple: boolean, options: *, tagPlaceholder: *, placeholder: *, info: *}, accessClass: {readableName: *, defaultValue: string, icon: string, options: *[], multiple: boolean, info: *}, status: {readableName: *, defaultValue: string, icon: string, options: *[], multiple: boolean, info: *}}}
*/
export function getRFCProperties() {
const getRFCProperties = () => {
return {
/**
* https://tools.ietf.org/html/rfc5545#section-3.8.1.3
@ -114,4 +114,6 @@ export function getRFCProperties() {
}
}
export default getRFCProperties
export {
getRFCProperties,
}

View File

@ -0,0 +1,187 @@
/**
* @copyright Copyright (c) 2020 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { getParserManager } from 'calendar-js'
import { getDateFromDateTimeValue } from '../utils/date.js'
import {
ITIP_MESSAGE_ADD,
ITIP_MESSAGE_CANCEL,
ITIP_MESSAGE_COUNTER,
ITIP_MESSAGE_DECLINECOUNTER,
ITIP_MESSAGE_PUBLISH,
ITIP_MESSAGE_REFRESH,
ITIP_MESSAGE_REPLY,
ITIP_MESSAGE_REQUEST,
} from './consts.js'
/**
* Creates a complete scheduling-object-object based on given props
*
* @param {Object} props Scheduling-object-props already provided
* @returns {Object}
*/
const getDefaultSchedulingObject = (props = {}) => Object.assign({}, {
// Id of the scheduling-object
id: null,
// The cdav-library object storing the scheduling-object
dav: null,
// The parsed calendar-js object
calendarComponent: null,
// The uid of the scheduling-object
uid: null,
// Recurrence-id of the scheduling-object
recurrenceId: null,
// The uri of the scheduling-object
uri: null,
// The scheduling method
method: null,
// Whether or not the method is PUBLISH
isPublish: false,
// Whether or not the method is REQUEST
isRequest: false,
// Whether or not the method is REPLY
isReply: false,
// Whether or not the method is ADD
isAdd: false,
// Whether or not the method is CANCEL
isCancel: false,
// Whether or not the method is REFRESH
isRefresh: false,
// Whether or not the method is COUNTER
isCounter: false,
// Whether or not the method is DECLINECOUNTER
isDeclineCounter: false,
// Whether or not the scheduling-object exists on the server
existsOnServer: false,
}, props)
/**
* Maps a calendar-object from c-dav to our calendar-object object
*
* @param {VObject} dav The c-dav VObject
* @returns {Object}
*/
const mapCDavObjectToSchedulingObject = (dav) => {
const parserManager = getParserManager()
const parser = parserManager.getParserForFileType('text/calendar', {
preserveMethod: true,
processFreeBusy: true,
})
// This should not be the case, but let's just be on the safe side
if (typeof dav.data !== 'string' || dav.data.trim() === '') {
throw new Error('Empty scheduling object')
}
parser.parse(dav.data)
const calendarComponentIterator = parser.getItemIterator()
const calendarComponent = calendarComponentIterator.next().value
if (!calendarComponent) {
throw new Error('Empty scheduling object')
}
const firstVObject = getFirstObjectFromCalendarComponent(calendarComponent)
let recurrenceId = null
if (firstVObject.recurrenceId) {
recurrenceId = getDateFromDateTimeValue(firstVObject.recurrenceId)
}
if (!calendarComponent.method) {
throw new Error('Scheduling-object does not have method')
}
return getDefaultSchedulingObject({
id: btoa(dav.url),
dav,
calendarComponent,
uid: firstVObject.uid,
uri: dav.url,
recurrenceId,
method: calendarComponent.method,
isPublish: calendarComponent.method === ITIP_MESSAGE_PUBLISH,
isRequest: calendarComponent.method === ITIP_MESSAGE_REQUEST,
isReply: calendarComponent.method === ITIP_MESSAGE_REPLY,
isAdd: calendarComponent.method === ITIP_MESSAGE_ADD,
isCancel: calendarComponent.method === ITIP_MESSAGE_CANCEL,
isRefresh: calendarComponent.method === ITIP_MESSAGE_REFRESH,
isCounter: calendarComponent.method === ITIP_MESSAGE_COUNTER,
isDeclineCounter: calendarComponent.method === ITIP_MESSAGE_DECLINECOUNTER,
existsOnServer: true,
})
}
/**
* Maps a calendar-component from calendar-js to our calendar-object object
*
* @param {CalendarComponent} calendarComponent The calendarComponent to create the calendarObject from
* @returns {Object}
*/
const mapCalendarJsToSchedulingObject = (calendarComponent) => {
const firstVObject = getFirstObjectFromCalendarComponent(calendarComponent)
let recurrenceId = null
if (firstVObject.recurrenceId) {
recurrenceId = getDateFromDateTimeValue(firstVObject.recurrenceId)
}
if (!calendarComponent.method) {
throw new Error('Scheduling-object does not have method')
}
return getDefaultSchedulingObject({
calendarComponent,
uid: firstVObject.uid,
recurrenceId,
method: calendarComponent.method,
isPublish: calendarComponent.method === ITIP_MESSAGE_PUBLISH,
isRequest: calendarComponent.method === ITIP_MESSAGE_REQUEST,
isReply: calendarComponent.method === ITIP_MESSAGE_REPLY,
isAdd: calendarComponent.method === ITIP_MESSAGE_ADD,
isCancel: calendarComponent.method === ITIP_MESSAGE_CANCEL,
isRefresh: calendarComponent.method === ITIP_MESSAGE_REFRESH,
isCounter: calendarComponent.method === ITIP_MESSAGE_COUNTER,
isDeclineCounter: calendarComponent.method === ITIP_MESSAGE_DECLINECOUNTER,
})
}
/**
* Extracts the first object from the calendar-component
*
* @param {CalendarComponent} calendarComponent The calendar-component
* @returns {any} First VEvent / VJournal / VTodo / VFreeBusy
*/
const getFirstObjectFromCalendarComponent = (calendarComponent) => {
const vObjectIterator = calendarComponent.getVObjectIterator()
const firstVObject = vObjectIterator.next().value
if (firstVObject) {
return firstVObject
}
const vFreeBusyIterator = calendarComponent.getFreebusyIterator()
return vFreeBusyIterator.next().value
}
export {
getDefaultSchedulingObject,
mapCDavObjectToSchedulingObject,
mapCalendarJsToSchedulingObject,
}

View File

@ -31,9 +31,9 @@ import RecurValue from 'calendar-js/src/values/recurValue.js'
import Property from 'calendar-js/src/properties/property.js'
import { getBySetPositionAndBySetFromDate, getWeekDayFromDate } from '../utils/recurrence.js'
import {
getAlarmFromAlarmComponent,
getDefaultCalendarObjectInstanceObject, mapEventComponentToCalendarObjectInstanceObject,
} from '../models/calendarObjectInstance.js'
getDefaultEventObject,
mapEventComponentToEventObject,
} from '../models/event.js'
import {
getAmountAndUnitForTimedEvents,
getAmountHoursMinutesAndUnitForAllDayEvents,
@ -43,6 +43,8 @@ import {
getClosestCSS3ColorNameForHex,
getHexForColorName,
} from '../utils/color.js'
import { mapAlarmComponentToAlarmObject } from '../models/alarm.js'
import { getObjectAtRecurrenceId } from '../utils/calendarObject.js'
const state = {
isNew: null,
@ -970,7 +972,7 @@ const mutations = {
removeRecurrenceRuleFromCalendarObjectInstance(state, { calendarObjectInstance, recurrenceRule }) {
if (recurrenceRule.recurrenceRuleValue) {
calendarObjectInstance.eventComponent.deleteAllProperties('RRULE')
Vue.set(calendarObjectInstance, 'recurrenceRule', getDefaultCalendarObjectInstanceObject().recurrenceRule)
Vue.set(calendarObjectInstance, 'recurrenceRule', getDefaultEventObject().recurrenceRule)
console.debug(calendarObjectInstance)
console.debug(recurrenceRule)
@ -1300,7 +1302,7 @@ const mutations = {
const duration = DurationValue.fromSeconds(totalSeconds)
const alarmComponent = eventComponent.addRelativeAlarm(type, duration)
const alarmObject = getAlarmFromAlarmComponent(alarmComponent)
const alarmObject = mapAlarmComponentToAlarmObject(alarmComponent)
calendarObjectInstance.alarms.push(alarmObject)
@ -1349,9 +1351,15 @@ const actions = {
*/
async resolveClosestRecurrenceIdForCalendarObject({ state, dispatch, commit }, { objectId, closeToDate }) {
const calendarObject = await dispatch('getEventByObjectId', { objectId })
const eventComponent = calendarObject.getClosestRecurrence(closeToDate)
const iterator = calendarObject.calendarComponent.getVObjectIterator()
const firstVObject = iterator.next().value
return eventComponent.getReferenceRecurrenceId().unixTime
const d = DateTimeValue.fromJSDate(closeToDate, true)
return firstVObject
.recurrenceManager
.getClosestOccurrence(d)
.getReferenceRecurrenceId()
.unixTime
},
/**
@ -1380,12 +1388,12 @@ const actions = {
const recurrenceIdDate = new Date(recurrenceId * 1000)
const calendarObject = await dispatch('getEventByObjectId', { objectId })
const eventComponent = calendarObject.getObjectAtRecurrenceId(recurrenceIdDate)
const eventComponent = getObjectAtRecurrenceId(calendarObject, recurrenceIdDate)
if (eventComponent === null) {
throw new Error('Not a valid recurrence-id')
}
const calendarObjectInstance = mapEventComponentToCalendarObjectInstanceObject(eventComponent)
const calendarObjectInstance = mapEventComponentToEventObject(eventComponent)
commit('setCalendarObjectInstanceForExistingEvent', {
calendarObject,
calendarObjectInstance,
@ -1423,8 +1431,8 @@ const actions = {
const calendarObject = await dispatch('createNewEvent', { start, end, isAllDay, timezoneId })
const startDate = new Date(start * 1000)
const eventComponent = calendarObject.getObjectAtRecurrenceId(startDate)
const calendarObjectInstance = mapEventComponentToCalendarObjectInstanceObject(eventComponent)
const eventComponent = getObjectAtRecurrenceId(calendarObject, startDate)
const calendarObjectInstance = mapEventComponentToEventObject(eventComponent)
commit('setCalendarObjectInstanceForNewEvent', {
calendarObject,
@ -1539,22 +1547,6 @@ const actions = {
}
},
/**
* Resets a calendar-object-instance to it's original data and
* removes all data from the calendar-object-instance store
*
* @param {Object} vuex The vuex destructuring object
* @param {Object} vuex.state The Vuex state
* @param {Function} vuex.dispatch The Vuex dispatch function
* @param {Function} vuex.commit The Vuex commit function
* @returns {Promise<void>}
*/
async resetCalendarObjectInstance({ state, commit }) {
if (state.calendarObject) {
state.calendarObject.resetToDav()
}
},
/**
*
* @param {Object} data The destructuring object for Vuex

View File

@ -22,10 +22,14 @@
*
*/
import Vue from 'vue'
import CalendarObject from '../models/calendarObject'
import { mapCalendarJsToCalendarObject } from '../models/calendarObject'
import logger from '../utils/logger.js'
import DateTimeValue from 'calendar-js/src/values/dateTimeValue'
import { createEvent, getTimezoneManager } from 'calendar-js'
import {
createEvent,
getParserManager,
getTimezoneManager,
} from 'calendar-js'
const state = {
calendarObjects: {},
@ -43,12 +47,8 @@ const mutations = {
*/
appendCalendarObjects(state, { calendarObjects = [] }) {
for (const calendarObject of calendarObjects) {
if (!state.calendarObjects[calendarObject.getId()]) {
if (calendarObject instanceof CalendarObject) {
Vue.set(state.calendarObjects, calendarObject.getId(), calendarObject)
} else {
logger.error('Invalid calendarObject object')
}
if (!state.calendarObjects[calendarObject.id]) {
Vue.set(state.calendarObjects, calendarObject.id, calendarObject)
}
}
},
@ -61,12 +61,49 @@ const mutations = {
* @param {Object} data.calendarObject Calendar-object to add
*/
appendCalendarObject(state, { calendarObject }) {
if (!state.calendarObjects[calendarObject.getId()]) {
if (calendarObject instanceof CalendarObject) {
Vue.set(state.calendarObjects, calendarObject.getId(), calendarObject)
} else {
logger.error('Invalid calendarObject object')
}
if (!state.calendarObjects[calendarObject.id]) {
Vue.set(state.calendarObjects, calendarObject.id, calendarObject)
}
},
/**
* Updates a calendar-object id
*
* @param {Object} state The store data
* @param {Object} data The destructuring object
* @param {Object} data.calendarObject Calendar-object to update
*/
updateCalendarObjectId(state, { calendarObject }) {
if (calendarObject.dav === null) {
calendarObject.id = null
} else {
calendarObject.id = btoa(calendarObject.dav.url)
}
},
/**
* Resets a calendar-object to it's original server state
*
* @param {Object} state The store data
* @param {Object} data The destructuring object
* @param {Object} data.calendarObject Calendar-object to reset
*/
resetCalendarObjectToDav(state, { calendarObject }) {
calendarObject = state.calendarObjects[calendarObject.id]
// If this object does not exist on the server yet, there is nothing to do
if (!calendarObject || !calendarObject.existsOnServer) {
return
}
const parserManager = getParserManager()
const parser = parserManager.getParserForFileType('text/calendar')
parser.parse(calendarObject.dav.data)
const itemIterator = parser.getItemIterator()
const firstVCalendar = itemIterator.next().value
if (firstVCalendar) {
calendarObject.calendarComponent = firstVCalendar
}
},
@ -78,7 +115,7 @@ const mutations = {
* @param {Object} data.calendarObject Calendar-object to delete
*/
deleteCalendarObject(state, { calendarObject }) {
Vue.delete(state.calendarObjects, calendarObject.getId())
Vue.delete(state.calendarObjects, calendarObject.id)
},
/**
@ -114,7 +151,7 @@ const actions = {
* @returns {Promise<void>}
*/
async moveCalendarObject(context, { calendarObject, newCalendarId }) {
if (!calendarObject.existsOnServer()) {
if (!calendarObject.existsOnServer) {
return
}
@ -172,8 +209,8 @@ const actions = {
* @returns {Promise<void>}
*/
async updateCalendarObject(context, { calendarObject }) {
if (calendarObject.existsOnServer()) {
calendarObject.dav.data = calendarObject.vcalendar.toICS()
if (calendarObject.existsOnServer) {
calendarObject.dav.data = calendarObject.calendarComponent.toICS()
await calendarObject.dav.update()
context.commit('addCalendarObjectIdToAllTimeRangesOfCalendar', {
@ -188,7 +225,9 @@ const actions = {
}
const calendar = context.getters.getCalendarById(calendarObject.calendarId)
calendarObject.dav = await calendar.dav.createVObject(calendarObject.vcalendar.toICS())
calendarObject.dav = await calendar.dav.createVObject(calendarObject.calendarComponent.toICS())
calendarObject.existsOnServer = true
context.commit('updateCalendarObjectId', { calendarObject })
context.commit('appendCalendarObject', { calendarObject })
context.commit('addCalendarObjectToCalendar', {
@ -215,8 +254,10 @@ const actions = {
*/
async createCalendarObjectFromFork(context, { eventComponent, calendarId }) {
const calendar = context.getters.getCalendarById(calendarId)
const calendarObject = new CalendarObject(eventComponent.root, calendar.id)
calendarObject.dav = await calendar.dav.createVObject(calendarObject.vcalendar.toICS())
const calendarObject = mapCalendarJsToCalendarObject(eventComponent.root, calendar.id)
calendarObject.dav = await calendar.dav.createVObject(calendarObject.calendarComponent.toICS())
calendarObject.existsOnServer = true
context.commit('updateCalendarObjectId', { calendarObject })
context.commit('appendCalendarObject', { calendarObject })
context.commit('addCalendarObjectToCalendar', {
@ -243,7 +284,7 @@ const actions = {
async deleteCalendarObject(context, { calendarObject }) {
// If this calendar-object was not created on the server yet,
// no need to send requests to the server
if (calendarObject.existsOnServer()) {
if (calendarObject.existsOnServer) {
await calendarObject.dav.delete()
}
@ -296,7 +337,7 @@ const actions = {
}
const firstCalendar = context.getters.sortedCalendars[0].id
return Promise.resolve(new CalendarObject(calendar, firstCalendar))
return Promise.resolve(mapCalendarJsToCalendarObject(calendar, firstCalendar))
},
/**

View File

@ -25,7 +25,7 @@
*/
import Vue from 'vue'
import client from '../services/caldavService.js'
import CalendarObject from '../models/calendarObject'
import { mapCDavObjectToCalendarObject } from '../models/calendarObject'
import { dateFactory, getUnixTimestampFromDate } from '../utils/date.js'
import { getDefaultCalendarObject, mapDavCollectionToCalendar } from '../models/calendar'
import pLimit from 'p-limit'
@ -706,7 +706,7 @@ const actions = {
const calendarObjects = []
const calendarObjectIds = []
for (const r of response) {
const calendarObject = new CalendarObject(r.data, calendar.id, r)
const calendarObject = mapCDavObjectToCalendarObject(r, calendar.id)
calendarObjects.push(calendarObject)
calendarObjectIds.push(calendarObject.id)
}
@ -751,7 +751,7 @@ const actions = {
const calendar = context.state.calendarsById[calendarId]
const vObject = await calendar.dav.find(objectFileName)
const calendarObject = new CalendarObject(vObject.data, calendar.id, vObject)
const calendarObject = mapCDavObjectToCalendarObject(vObject, calendar.id)
context.commit('appendCalendarObject', { calendarObject })
context.commit('addCalendarObjectToCalendar', {
calendar: {
@ -831,7 +831,7 @@ const actions = {
return
}
const calendarObject = new CalendarObject(davObject.data, calendarId, davObject)
const calendarObject = mapCDavObjectToCalendarObject(davObject, calendarId)
context.commit('appendCalendarObject', { calendarObject })
context.commit('addCalendarObjectToCalendar', {
calendar,

View File

@ -21,11 +21,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/* eslint-disable import/first */
import Vue from 'vue'
Vue.config.devtools = true
import Vuex from 'vuex'
import calendarObjectInstance from './calendarObjectInstance'
import calendarObjects from './calendarObjects'
import calendars from './calendars.js'

View File

@ -0,0 +1,65 @@
/**
* @copyright Copyright (c) 2020 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import DateTimeValue from 'calendar-js/src/values/dateTimeValue.js'
/**
* Get all recurrence-items in given range
*
* @param {Object} calendarObject Calendar-object model
* @param {Date} start Begin of time-range
* @param {Date} end End of time-range
* @returns {Array}
*/
const getAllObjectsInTimeRange = (calendarObject, start, end) => {
const iterator = calendarObject.calendarComponent.getVObjectIterator()
const firstVObject = iterator.next().value
if (!firstVObject) {
return []
}
const s = DateTimeValue.fromJSDate(start, true)
const e = DateTimeValue.fromJSDate(end, true)
return firstVObject.recurrenceManager.getAllOccurrencesBetween(s, e)
}
/**
* Get recurrence-item at exactly a given recurrence-Id
*
* @param {Object} calendarObject Calendar-object model
* @param {Date} recurrenceId RecurrenceId to retrieve
* @returns {AbstractRecurringComponent|null}
*/
const getObjectAtRecurrenceId = (calendarObject, recurrenceId) => {
const iterator = calendarObject.calendarComponent.getVObjectIterator()
const firstVObject = iterator.next().value
if (!firstVObject) {
return null
}
const d = DateTimeValue.fromJSDate(recurrenceId, true)
return firstVObject.recurrenceManager.getOccurrenceAtExactly(d)
}
export {
getAllObjectsInTimeRange,
getObjectAtRecurrenceId,
}

View File

@ -0,0 +1,4 @@
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;VALUE=DATE-TIME:20200306T083000Z
END:VALARM

View File

@ -0,0 +1,4 @@
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:P1DT9H
END:VALARM

View File

@ -0,0 +1,4 @@
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:PT9H
END:VALARM

View File

@ -0,0 +1,4 @@
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:-PT15H
END:VALARM

View File

@ -0,0 +1,4 @@
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=END:-PT15H
END:VALARM

View File

@ -0,0 +1,4 @@
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:-P6DT15H
END:VALARM

View File

@ -0,0 +1 @@
ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:jsmith@example.com

View File

@ -0,0 +1 @@
ATTENDEE;CUTYPE=GROUP:mailto:ietf-calsch@example.org

View File

@ -0,0 +1 @@
ATTENDEE;PARTSTAT=DECLINED:mailto:jsmith@example.com

View File

@ -0,0 +1 @@
ATTENDEE;ROLE=CHAIR:mailto:mrbig@example.com

View File

@ -0,0 +1 @@
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;DELEGATED-FROM="mailto:iamboss@example.com";CN=Henry Cabot:mailto:hcabot@example.com

View File

@ -0,0 +1 @@
ATTENDEE;ROLE=NON-PARTICIPANT;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:hcabot@example.com";CN=The Big Cheese:mailto:iamboss@example.com

View File

@ -0,0 +1 @@
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Jane Doe:mailto:jdoe@example.com

View File

@ -0,0 +1 @@
FREQ=MONTHLY;UNTIL=20201122T001122Z;COUNT=5

View File

@ -0,0 +1 @@
FREQ=MONTHLY;COUNT=42

View File

@ -0,0 +1 @@
FREQ=MONTHLY;UNTIL=20201122T001122Z

View File

@ -0,0 +1 @@
FREQ=SECONDLY

View File

@ -0,0 +1 @@
FREQ=MONTHLY

View File

@ -0,0 +1 @@
FREQ=MONTHLY;BYMONTHDAY=1,2,3,30,31

View File

@ -0,0 +1 @@
FREQ=MONTHLY;BYMONTHDAY=-1,2,-3,30,31,-31

View File

@ -0,0 +1 @@
FREQ=MONTHLY;BYMONTHDAY=1,2,3,30,31;BYDAY=MO;BYSETPOS=-1

View File

@ -0,0 +1 @@
FREQ=MONTHLY;BYDAY=MO;BYSETPOS=3

View File

@ -0,0 +1 @@
FREQ=MONTHLY;BYDAY=MO,TU,WE;BYSETPOS=3

View File

@ -0,0 +1 @@
FREQ=MONTHLY;BYDAY=MO;BYSETPOS=-3

View File

@ -0,0 +1 @@
FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1,2,3

View File

@ -0,0 +1 @@
FREQ=YEARLY

View File

@ -0,0 +1 @@
FREQ=YEARLY;BYMONTH=1,2,3

View File

@ -0,0 +1 @@
FREQ=MINUTELY

View File

@ -0,0 +1 @@
FREQ=YEARLY;BYMONTH=1,2,3,0

View File

@ -0,0 +1 @@
FREQ=YEARLY;BYDAY=MO;BYSETPOS=3

View File

@ -0,0 +1 @@
FREQ=YEARLY;BYDAY=MO,TU,WE;BYSETPOS=3

View File

@ -0,0 +1 @@
FREQ=YEARLY;BYDAY=MO;BYSETPOS=-3

View File

@ -0,0 +1 @@
FREQ=YEARLY;BYDAY=MO;BYSETPOS=1,2,3

View File

@ -0,0 +1 @@
FREQ=MONTHLY;BYDAY=MO,TU,WE

View File

@ -0,0 +1 @@
FREQ=MONTHLY;BYDAY=3MO

View File

@ -0,0 +1 @@
FREQ=MONTHLY;BYDAY=-3MO

View File

@ -0,0 +1 @@
FREQ=MONTHLY;BYDAY=MO

View File

@ -0,0 +1 @@
FREQ=YEARLY;BYDAY=MO,TU,WE

View File

@ -0,0 +1 @@
FREQ=HOURLY

View File

@ -0,0 +1 @@
FREQ=YEARLY;BYDAY=3MO

View File

@ -0,0 +1 @@
FREQ=YEARLY;BYDAY=-3MO

View File

@ -0,0 +1 @@
FREQ=YEARLY;BYDAY=MO

View File

@ -0,0 +1 @@
FREQ=DAILY;INTERVAL=5

View File

@ -0,0 +1 @@
FREQ=DAILY;INTERVAL=42;BYMONTH=1

View File

@ -0,0 +1 @@
FREQ=WEEKLY

View File

@ -0,0 +1 @@
FREQ=WEEKLY;BYDAY=MO,TU,WE

View File

@ -0,0 +1 @@
FREQ=WEEKLY;BYDAY=MO,2TU,-3WE

View File

@ -0,0 +1 @@
FREQ=WEEKLY;BYDAY=MO,TU,WE;BYMONTH=1,2

View File

@ -0,0 +1,18 @@
BEGIN:VCALENDAR
METHOD:ADD
PRODID:-//Example/ExampleCalendarClient//EN
VERSION:2.0
BEGIN:VEVENT
UID:123456789@example.com
SEQUENCE:2
ORGANIZER:mailto:a@example.com
ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com
ATTENDEE;RSVP=TRUE:mailto:b@example.com
SUMMARY:Review Accounts
DTSTART:19980315T180000Z
DTEND:19980315T200000Z
DTSTAMP:19980307T193000Z
LOCATION:Conference Room A
STATUS:CONFIRMED
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,16 @@
BEGIN:VCALENDAR
METHOD:CANCEL
PRODID:-//Example/ExampleCalendarClient//EN
VERSION:2.0
BEGIN:VEVENT
UID:guid-1@example.com
ORGANIZER:mailto:a@example.com
ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com
ATTENDEE:mailto:b@example.com
ATTENDEE:mailto:c@example.com
ATTENDEE:mailto:d@example.com
DTSTAMP:19970721T103000Z
STATUS:CANCELLED
SEQUENCE:3
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,23 @@
BEGIN:VCALENDAR
METHOD:COUNTER
PRODID:-//Example/ExampleCalendarClient//EN
VERSION:2.0
BEGIN:VEVENT
UID:guid-1@example.com
RECURRENCE-ID:19970715T210000Z
SEQUENCE:4
ORGANIZER:mailto:a@example.com
ATTENDEE;ROLE=CHAIR;RSVP=TRUE:mailto:a@example.com
ATTENDEE;RSVP=TRUE:mailto:b@example.com
ATTENDEE;RSVP=TRUE:mailto:c@example.com
ATTENDEE;RSVP=TRUE:mailto:d@example.com
DESCRIPTION:IETF-C&S Conference Call
CLASS:PUBLIC
SUMMARY:IETF Calendaring Working Group Meeting
DTSTART:19970715T220000Z
DTEND:19970715T230000Z
LOCATION:Conference Call
COMMENT:May we bump this by an hour? I have a conflict
DTSTAMP:19970629T094000Z
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,13 @@
BEGIN:VCALENDAR
PRODID:-//Example/ExampleCalendarClient//EN
METHOD:DECLINECOUNTER
VERSION:2.0
BEGIN:VEVENT
ORGANIZER:mailto:a@example.com
ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL:mailto:b@example.com
COMMENT:Sorry, I cannot change this meeting time
UID:calsrv.example.com-873970198738777@example.com
SEQUENCE:0
DTSTAMP:19970614T190000Z
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,14 @@
BEGIN:VCALENDAR
PRODID:-//Example/ExampleCalendarClient//EN
METHOD:REPLY
VERSION:2.0
BEGIN:VFREEBUSY
ORGANIZER:mailto:a@example.com
ATTENDEE:mailto:b@example.com
DTSTART:19970701T080000Z
DTEND:19970701T200000Z
UID:calsrv.example.com-873970198738777@example.com
FREEBUSY:19970701T090000Z/PT1H,19970701T140000Z/PT30M
DTSTAMP:19970613T190030Z
END:VFREEBUSY
END:VCALENDAR

View File

@ -0,0 +1,15 @@
BEGIN:VCALENDAR
PRODID:-//Example/ExampleCalendarClient//EN
METHOD:REQUEST
VERSION:2.0
BEGIN:VFREEBUSY
ORGANIZER:mailto:a@example.com
ATTENDEE;ROLE=CHAIR:mailto:a@example.com
ATTENDEE:mailto:b@example.com
ATTENDEE:mailto:c@example.com
DTSTAMP:19970613T190000Z
DTSTART:19970701T080000Z
DTEND:19970701T200000
UID:calsrv.example.com-873970198738777@example.com
END:VFREEBUSY
END:VCALENDAR

View File

@ -0,0 +1,18 @@
BEGIN:VCALENDAR
PRODID:-//Example/ExampleCalendarClient//EN
VERSION:2.0
METHOD:PUBLISH
BEGIN:VFREEBUSY
DTSTAMP:19980101T124100Z
ORGANIZER:mailto:a@example.com
DTSTART:19980101T124200Z
DTEND:19980108T124200Z
FREEBUSY:19980101T180000Z/19980101T190000Z
FREEBUSY:19980103T020000Z/19980103T050000Z
FREEBUSY:19980107T020000Z/19980107T050000Z
FREEBUSY:19980113T000000Z/19980113T010000Z
FREEBUSY:19980115T190000Z/19980115T200000Z
FREEBUSY:19980115T220000Z/19980115T230000Z
FREEBUSY:19980116T013000Z/19980116T043000Z
END:VFREEBUSY
END:VCALENDAR

View File

@ -0,0 +1,14 @@
BEGIN:VCALENDAR
PRODID:-//Example/ExampleCalendarClient//EN
METHOD:REFRESH
VERSION:2.0
BEGIN:VEVENT
ORGANIZER:mailto:a@example.com
ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com
ATTENDEE:mailto:b@example.com
ATTENDEE:mailto:c@example.com
ATTENDEE:mailto:d@example.com
UID:guid-1-12345@example.com
DTSTAMP:19970603T094000
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,13 @@
BEGIN:VCALENDAR
PRODID:-//Example/ExampleCalendarClient//EN
METHOD:REPLY
VERSION:2.0
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED:mailto:b@example.com
ORGANIZER:mailto:a@example.com
UID:calsrv.example.com-873970198738777@example.com
SEQUENCE:0
REQUEST-STATUS:2.0;Success
DTSTAMP:19970612T190000Z
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,19 @@
BEGIN:VCALENDAR
METHOD:REQUEST
PRODID:-//Example/ExampleCalendarClient//EN
VERSION:2.0
BEGIN:VEVENT
UID:123456789@example.com
SEQUENCE:1
RECURRENCE-ID:19980311T180000Z
ORGANIZER:mailto:a@example.com
ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com
ATTENDEE;RSVP=TRUE:mailto:b@example.com
SUMMARY:Review Accounts
DTSTART:19980311T160000Z
DTEND:19980311T180000Z
DTSTAMP:19980306T193000Z
LOCATION:The Small conference room
STATUS:CONFIRMED
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,5 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tests//
CALSCALE:GREGORIAN
END:VCALENDAR

View File

@ -0,0 +1,40 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tests//
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
DTSTART:19810329T020000
TZNAME:GMT+2
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
DTSTART:19961027T030000
TZNAME:GMT+1
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
DTEND;TZID=Europe/Berlin:20160816T100000
TRANSP:OPAQUE
SUMMARY:Test Europe Berlin
DTSTART;TZID=Europe/Berlin:20160816T090000
DTSTAMP:20160809T163632Z
SEQUENCE:0
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:P1DT9H
END:VALARM
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;VALUE=DATE-TIME:20200306T083000Z
END:VALARM
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,15 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20161004T144433Z
UID:85560E76-1B0D-47E1-A735-21625767FCA4
DTEND;VALUE=DATE:20161008
TRANSP:TRANSPARENT
DTSTART;VALUE=DATE:20161005
DTSTAMP:20161004T144437Z
SUMMARY:allday event
SEQUENCE:0
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,37 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tests//
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
DTSTART:19810329T020000
TZNAME:GMT+2
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
DTSTART:19961027T030000
TZNAME:GMT+1
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
DTEND;TZID=Europe/Berlin:20160816T100000
TRANSP:OPAQUE
SUMMARY:Test Europe Berlin
DTSTART;TZID=Europe/Berlin:20160816T090000
DTSTAMP:20160809T163632Z
SEQUENCE:0
ORGANIZER;CN=John Smith:mailto:jsmith@example.com
ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:jsmith@example.com
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Henry Cabot:mailto:hcabot@example.com
ATTENDEE;ROLE=REQ-PARTICIPANT;DELEGATED-FROM="mailto:bob@example.com";PARTSTAT=ACCEPTED;CN=Jane Doe:mailto:jdoe@example.com
ATTENDEE;ROLE=NON-PARTICIPANT;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:hcabot@example.com";CN=The Big Cheese:mailto:iamboss@example.com
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,33 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tests//
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
DTSTART:19810329T020000
TZNAME:GMT+2
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
DTSTART:19961027T030000
TZNAME:GMT+1
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
DTEND;TZID=Europe/Berlin:20160816T100000
TRANSP:OPAQUE
SUMMARY:Test Europe Berlin
DTSTART;TZID=Europe/Berlin:20160816T090000
DTSTAMP:20160809T163632Z
SEQUENCE:0
CATEGORIES:BUSINESS,HUMAN RESOURCES
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,33 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tests//
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
DTSTART:19810329T020000
TZNAME:GMT+2
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
DTSTART:19961027T030000
TZNAME:GMT+1
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
DTEND;TZID=Europe/Berlin:20160816T100000
TRANSP:OPAQUE
SUMMARY:Test Europe Berlin
DTSTART;TZID=Europe/Berlin:20160816T090000
DTSTAMP:20160809T163632Z
SEQUENCE:0
COLOR:turquoise
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,32 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tests//
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
DTSTART:19810329T020000
TZNAME:GMT+2
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
DTSTART:19961027T030000
TZNAME:GMT+1
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
DTEND:20160816T100000
TRANSP:OPAQUE
SUMMARY:Test Europe Berlin
DTSTART:20160816T090000
DTSTAMP:20160809T163632Z
SEQUENCE:0
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,34 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tests//
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
DTSTART:19810329T020000
TZNAME:GMT+2
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
DTSTART:19961027T030000
TZNAME:GMT+1
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
DTEND;TZID=Europe/Berlin:20160816T100000
TRANSP:OPAQUE
SUMMARY:Test Europe Berlin
DTSTART;TZID=Europe/Berlin:20160816T090000
DTSTAMP:20160809T163632Z
SEQUENCE:0
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU
RRULE:FREQ=DAILY;BYMONTH=1
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,16 @@
BEGIN:VCALENDAR
PRODID:-//IDN nextcloud.com//Calendar app 2.0.2//EN
CALSCALE:GREGORIAN
VERSION:2.0
BEGIN:VEVENT
CREATED:20200401T142357Z
DTSTAMP:20200401T142406Z
LAST-MODIFIED:20200401T142406Z
SEQUENCE:2
UID:4f7a5e63-6ae5-43da-b949-7bad490882c5
DTSTART;VALUE=DATE:20200401
DTEND;VALUE=DATE:20200402
RRULE:FREQ=WEEKLY;BYDAY=WE
SUMMARY:Weekly test
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,85 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tests//
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
DTSTART:19810329T020000
TZNAME:GMT+2
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
DTSTART:19961027T030000
TZNAME:GMT+1
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20160809T163629Z
DTSTAMP:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
SUMMARY:TEST
RRULE:FREQ=WEEKLY
DTSTART;TZID=Europe/Berlin:20200301T150000
DTEND;TZID=Europe/Berlin:20200301T160000
END:VEVENT
BEGIN:VEVENT
CREATED:20160809T163629Z
DTSTAMP:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
SUMMARY:TEST EX 1
RECURRENCE-ID;TZID=Europe/Berlin:20200308T150000
DTSTART;TZID=Europe/Berlin:20200401T150000
DTEND;TZID=Europe/Berlin:20200401T160000
END:VEVENT
BEGIN:VEVENT
CREATED:20160809T163629Z
DTSTAMP:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
SUMMARY:TEST EX 2
RECURRENCE-ID;TZID=Europe/Berlin:20200315T150000
DTSTART;TZID=Europe/Berlin:20201101T150000
DTEND;TZID=Europe/Berlin:20201101T160000
END:VEVENT
BEGIN:VEVENT
CREATED:20160809T163629Z
DTSTAMP:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
SUMMARY:TEST EX 3
RECURRENCE-ID;TZID=Europe/Berlin:20200405T150000
DTSTART;TZID=Europe/Berlin:20200406T150000
DTEND;TZID=Europe/Berlin:20200406T160000
END:VEVENT
BEGIN:VEVENT
CREATED:20160809T163629Z
DTSTAMP:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
SUMMARY:TEST EX 4
RECURRENCE-ID;TZID=Europe/Berlin:20200412T150000
DTSTART;TZID=Europe/Berlin:20201201T150000
DTEND;TZID=Europe/Berlin:20201201T160000
END:VEVENT
BEGIN:VEVENT
CREATED:20160809T163629Z
DTSTAMP:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
SUMMARY:TEST EX 5
RECURRENCE-ID;TZID=Europe/Berlin:20200426T150000
DTSTART;TZID=Europe/Berlin:20200410T150000
DTEND;TZID=Europe/Berlin:20200410T160000
END:VEVENT
BEGIN:VEVENT
CREATED:20160809T163629Z
DTSTAMP:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
SUMMARY:INVALID RECURRENCE-ID
RECURRENCE-ID;TZID=Europe/Berlin:20200427T150000
DTSTART;TZID=Europe/Berlin:20200420T150000
DTEND;TZID=Europe/Berlin:20200420T160000
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,32 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tests//
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
DTSTART:19810329T020000
TZNAME:GMT+2
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
DTSTART:19961027T030000
TZNAME:GMT+1
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
DTEND;TZID=Europe/Berlin:20160816T100000
TRANSP:OPAQUE
SUMMARY:Test Europe Berlin
DTSTART;TZID=Europe/Berlin:20160816T090000
DTSTAMP:20160809T163632Z
SEQUENCE:0
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,32 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tests//
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
DTSTART:19810329T020000
TZNAME:GMT+2
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
DTSTART:19961027T030000
TZNAME:GMT+1
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20160809T163629Z
UID:0AD16F58-01B3-463B-A215-FD09FC729A02
DTEND:20160816T100000Z
TRANSP:OPAQUE
SUMMARY:Test Europe Berlin
DTSTART:20160816T090000Z
DTSTAMP:20160809T163632Z
SEQUENCE:0
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,20 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tests//
CALSCALE:GREGORIAN
BEGIN:VJOURNAL
UID:19970901T130000Z-123405@example.com
DTSTAMP:19970901T130000Z
DTSTART;VALUE=DATE:19970317
SUMMARY:Staff meeting minutes
DESCRIPTION:1. Staff meeting: Participants include Joe\,
Lisa\, and Bob. Aurora project plans were reviewed.
There is currently no budget reserves for this project.
Lisa will escalate to management. Next meeting on Tuesday.\n
2. Telephone Conference: ABC Corp. sales representative
called to discuss new printer. Promised to get us a demo by
Friday.\n3. Henry Miller (Handsoff Insurance): Car was
totaled by tree. Is looking into a loaner car. 555-2323
(tel).
END:VJOURNAL
END:VCALENDAR

View File

@ -0,0 +1,14 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tests//
CALSCALE:GREGORIAN
BEGIN:VTODO
UID:20070313T123432Z-456553@example.com
DTSTAMP:20070313T123432Z
DUE;VALUE=DATE:20070501
SUMMARY:Submit Quebec Income Tax Return for 2006
CLASS:CONFIDENTIAL
CATEGORIES:FAMILY,FINANCE
STATUS:NEEDS-ACTION
END:VTODO
END:VCALENDAR

View File

@ -0,0 +1,22 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tests//
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
DTSTART:19810329T020000
TZNAME:GMT+2
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
DTSTART:19961027T030000
TZNAME:GMT+1
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
END:VCALENDAR

94
tests/assets/loadAsset.js Normal file
View File

@ -0,0 +1,94 @@
/**
* @copyright Copyright (c) 2020 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import ICAL from 'ical.js'
import AlarmComponent from 'calendar-js/src/components/nested/alarmComponent.js'
import AttendeeProperty from "calendar-js/src/properties/attendeeProperty.js";
import RecurValue from "calendar-js/src/values/recurValue.js";
import {getParserManager} from "calendar-js";
const fs = require('fs')
/**
* global helper function to load an ics asset by name
*
* @param {string} assetName
* @returns {string}
*/
global.loadICS = (assetName) => {
return fs.readFileSync('tests/assets/ics/' + assetName + '.ics', 'UTF8')
}
/**
* Loads an AlarmComponent from an asset
*
* @param {String} assetName Name of the asset
* @returns {AlarmComponent}
*/
global.getAlarmComponentFromAsset = (assetName) => {
const ics = loadICS(assetName)
const jCal = ICAL.parse(ics.trim())
const iCalComp = new ICAL.Component(jCal)
return AlarmComponent.fromICALJs(iCalComp)
}
/**
* Loads an AttendeeProperty from an asset
*
* @param {String} assetName Name of the asset
* @returns {AttendeeProperty}
*/
global.getAttendeePropertyFromAsset = (assetName) => {
const ics = loadICS(assetName)
const iCalProp = ICAL.Property.fromString(ics.trim())
return AttendeeProperty.fromICALJs(iCalProp)
}
/**
* Loads a RecurValue from an asset
*
* @param {String} assetName Name of the asset
* @returns {RecurValue}
*/
global.getRecurValueFromAsset = (assetName) => {
const ics = loadICS(assetName)
const iCalValue = ICAL.Recur.fromString(ics.trim())
return RecurValue.fromICALJs(iCalValue)
}
/**
* Loads an eventComponent from an asset
*
* @param {String} assetName Name of the asset
* @param {DateTimeValue} recurrenceId RecurrenceId of instance
*/
global.getEventComponentFromAsset = (assetName, recurrenceId) => {
const ics = loadICS(assetName)
const parser = getParserManager().getParserForFileType('text/calendar')
parser.parse(ics)
const calendarComponent = parser.getAllItems()[0]
const firstVObject = Array.from(calendarComponent.getVObjectIterator())[0]
return firstVObject.recurrenceManager.getOccurrenceAtExactly(recurrenceId)
}

View File

@ -22,15 +22,18 @@
import eventDrop from "../../../../src/fullcalendar/eventDrop.js";
import { getDurationValueFromFullCalendarDuration} from "../../../../src/fullcalendar/duration.js";
import getTimezoneManager from '../../../../src/services/timezoneDataProviderService.js'
import {getObjectAtRecurrenceId} from "../../../../src/utils/calendarObject.js";
jest.mock("../../../../src/fullcalendar/duration.js")
jest.mock('../../../../src/services/timezoneDataProviderService.js')
jest.mock("../../../../src/utils/calendarObject.js")
describe('fullcalendar/eventDrop test suite', () => {
beforeEach(() => {
getDurationValueFromFullCalendarDuration.mockClear()
getTimezoneManager.mockClear()
getObjectAtRecurrenceId.mockClear()
})
it('should properly drop a non-recurring event', async () => {
@ -74,12 +77,14 @@ describe('fullcalendar/eventDrop test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId
store.dispatch.mockResolvedValueOnce() // updateCalendarObject
store.dispatch
.mockResolvedValueOnce(calendarObject) // getEventByObjectId
.mockResolvedValueOnce() // updateCalendarObject
const eventDropFunction = eventDrop(store, fcAPI)
await eventDropFunction({ event, delta, revert })
@ -104,7 +109,6 @@ describe('fullcalendar/eventDrop test suite', () => {
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(1)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0)
expect(revert).toHaveBeenCalledTimes(0)
})
@ -149,13 +153,15 @@ describe('fullcalendar/eventDrop test suite', () => {
canCreateRecurrenceExceptions: jest.fn().mockReturnValue(false),
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId
store.dispatch.mockResolvedValueOnce() // updateCalendarObject
.mockResolvedValueOnce() // updateCalendarObject
const eventDropFunction = eventDrop(store, fcAPI)
await eventDropFunction({ event, delta, revert })
@ -184,7 +190,6 @@ describe('fullcalendar/eventDrop test suite', () => {
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(1)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0)
expect(revert).toHaveBeenCalledTimes(0)
})
@ -229,9 +234,10 @@ describe('fullcalendar/eventDrop test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId
store.dispatch.mockResolvedValueOnce() // updateCalendarObject
@ -260,7 +266,6 @@ describe('fullcalendar/eventDrop test suite', () => {
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(1)
expect(eventComponent.createRecurrenceException).toHaveBeenNthCalledWith(1)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0)
expect(revert).toHaveBeenCalledTimes(0)
})
@ -305,9 +310,10 @@ describe('fullcalendar/eventDrop test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId
store.dispatch.mockResolvedValueOnce() // updateCalendarObject
@ -331,7 +337,6 @@ describe('fullcalendar/eventDrop test suite', () => {
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0)
expect(revert).toHaveBeenCalledTimes(1)
})
@ -376,9 +381,10 @@ describe('fullcalendar/eventDrop test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId
store.dispatch.mockResolvedValueOnce() // updateCalendarObject
@ -402,7 +408,6 @@ describe('fullcalendar/eventDrop test suite', () => {
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0)
expect(revert).toHaveBeenCalledTimes(1)
})
@ -447,9 +452,10 @@ describe('fullcalendar/eventDrop test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId
store.dispatch.mockResolvedValueOnce() // updateCalendarObject
@ -473,7 +479,6 @@ describe('fullcalendar/eventDrop test suite', () => {
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0)
expect(revert).toHaveBeenCalledTimes(1)
})
@ -518,9 +523,10 @@ describe('fullcalendar/eventDrop test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch.mockRejectedValueOnce({ message: 'error message' }) // getEventByObjectId
@ -544,7 +550,6 @@ describe('fullcalendar/eventDrop test suite', () => {
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0)
expect(revert).toHaveBeenCalledTimes(1)
})
@ -584,9 +589,10 @@ describe('fullcalendar/eventDrop test suite', () => {
})
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(null),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(null)
store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId
store.dispatch.mockResolvedValueOnce() // updateCalendarObject
@ -607,7 +613,6 @@ describe('fullcalendar/eventDrop test suite', () => {
expect(store.dispatch).toHaveBeenCalledTimes(1)
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'getEventByObjectId', { objectId: 'object123' })
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0)
expect(revert).toHaveBeenCalledTimes(1)
})
@ -654,13 +659,16 @@ describe('fullcalendar/eventDrop test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId
store.dispatch.mockResolvedValueOnce() // updateCalendarObject
store.commit = jest.fn()
const eventDropFunction = eventDrop(store, fcAPI)
await eventDropFunction({ event, delta, revert })
@ -677,13 +685,15 @@ describe('fullcalendar/eventDrop test suite', () => {
expect(store.dispatch).toHaveBeenCalledTimes(1)
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'getEventByObjectId', { objectId: 'object123' })
expect(store.commit).toHaveBeenCalledTimes(1)
expect(store.commit).toHaveBeenNthCalledWith(1, 'resetCalendarObjectToDav', { calendarObject: calendarObject })
expect(eventComponent.shiftByDuration).toHaveBeenCalledTimes(1)
expect(eventComponent.shiftByDuration).toHaveBeenNthCalledWith(1, { calendarJsDurationValue: true, hours: 5 }, false, { calendarJsTimezone: true, tzid: 'America/New_York' }, { calendarJsDurationValue: true, days: 1 }, { calendarJsDurationValue: true, hours: 2 })
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(1)
expect(revert).toHaveBeenCalledTimes(1)
})
@ -728,15 +738,18 @@ describe('fullcalendar/eventDrop test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId
store.dispatch.mockImplementationOnce(() => {
throw new Error()
}) // updateCalendarObject
store.commit = jest.fn()
const eventDropFunction = eventDrop(store, fcAPI)
await eventDropFunction({ event, delta, revert })
@ -754,13 +767,15 @@ describe('fullcalendar/eventDrop test suite', () => {
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'getEventByObjectId', { objectId: 'object123' })
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'updateCalendarObject', { calendarObject })
expect(store.commit).toHaveBeenCalledTimes(1)
expect(store.commit).toHaveBeenNthCalledWith(1, 'resetCalendarObjectToDav', { calendarObject: calendarObject })
expect(eventComponent.shiftByDuration).toHaveBeenCalledTimes(1)
expect(eventComponent.shiftByDuration).toHaveBeenNthCalledWith(1, { calendarJsDurationValue: true, hours: 5 }, false, { calendarJsTimezone: true, tzid: 'America/New_York' }, { calendarJsDurationValue: true, days: 1 }, { calendarJsDurationValue: true, hours: 2 })
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(1)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(1)
expect(revert).toHaveBeenCalledTimes(1)
})
})

View File

@ -22,12 +22,15 @@
import eventResize from "../../../../src/fullcalendar/eventResize.js";
import { getDurationValueFromFullCalendarDuration} from '../../../../src/fullcalendar/duration.js'
import {getObjectAtRecurrenceId} from "../../../../src/utils/calendarObject.js";
jest.mock('../../../../src/fullcalendar/duration.js')
jest.mock("../../../../src/utils/calendarObject.js")
describe('fullcalendar/eventResize test suite', () => {
beforeEach(() => {
getDurationValueFromFullCalendarDuration.mockClear()
getObjectAtRecurrenceId.mockClear()
})
it('should properly resize a non-recurring event', async () => {
@ -57,9 +60,10 @@ describe('fullcalendar/eventResize test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch
.mockResolvedValueOnce(calendarObject) // getEventByObjectId
@ -84,7 +88,6 @@ describe('fullcalendar/eventResize test suite', () => {
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(1)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0)
expect(revert).toHaveBeenCalledTimes(0)
})
@ -115,9 +118,10 @@ describe('fullcalendar/eventResize test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch
.mockResolvedValueOnce(calendarObject) // getEventByObjectId
@ -142,7 +146,6 @@ describe('fullcalendar/eventResize test suite', () => {
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(1)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(1)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0)
expect(revert).toHaveBeenCalledTimes(0)
})
@ -171,9 +174,10 @@ describe('fullcalendar/eventResize test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch
.mockResolvedValueOnce(calendarObject) // getEventByObjectId
@ -194,7 +198,6 @@ describe('fullcalendar/eventResize test suite', () => {
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0)
expect(revert).toHaveBeenCalledTimes(1)
})
@ -225,9 +228,10 @@ describe('fullcalendar/eventResize test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch
.mockImplementationOnce(() => {
@ -251,7 +255,6 @@ describe('fullcalendar/eventResize test suite', () => {
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0)
expect(revert).toHaveBeenCalledTimes(1)
})
@ -282,9 +285,10 @@ describe('fullcalendar/eventResize test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(null),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(null)
store.dispatch
.mockResolvedValueOnce(calendarObject) // getEventByObjectId
@ -306,7 +310,6 @@ describe('fullcalendar/eventResize test suite', () => {
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0)
expect(revert).toHaveBeenCalledTimes(1)
})
@ -337,9 +340,10 @@ describe('fullcalendar/eventResize test suite', () => {
createRecurrenceException: jest.fn(),
}
const calendarObject = {
getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent),
resetToDav: jest.fn()
_isCalendarObject: true,
}
getObjectAtRecurrenceId
.mockReturnValue(eventComponent)
store.dispatch
.mockResolvedValueOnce(calendarObject) // getEventByObjectId
@ -347,6 +351,8 @@ describe('fullcalendar/eventResize test suite', () => {
throw new Error()
}) // updateCalendarObject
store.commit = jest.fn()
const eventResizeFunction = eventResize(store)
await eventResizeFunction({ event, startDelta, endDelta, revert })
@ -358,6 +364,9 @@ describe('fullcalendar/eventResize test suite', () => {
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'getEventByObjectId', { objectId: 'object123' })
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'updateCalendarObject', { calendarObject })
expect(store.commit).toHaveBeenCalledTimes(1)
expect(store.commit).toHaveBeenNthCalledWith(1, 'resetCalendarObjectToDav', { calendarObject: calendarObject })
expect(eventComponent.addDurationToStart).toHaveBeenCalledTimes(1)
expect(eventComponent.addDurationToStart).toHaveBeenNthCalledWith(1, { calendarJsDurationValue: true, hours: 5 })
@ -366,7 +375,6 @@ describe('fullcalendar/eventResize test suite', () => {
expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(1)
expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0)
expect(calendarObject.resetToDav).toHaveBeenCalledTimes(1)
expect(revert).toHaveBeenCalledTimes(1)
})

View File

@ -27,8 +27,10 @@ import {
getHexForColorName,
} from '../../../../src/utils/color.js'
import { translate } from '@nextcloud/l10n'
import {getAllObjectsInTimeRange} from "../../../../src/utils/calendarObject.js";
jest.mock('@nextcloud/l10n')
jest.mock('../../../../src/utils/color.js')
jest.mock("../../../../src/utils/calendarObject.js")
describe('fullcalendar/eventSourceFunction test suite', () => {
@ -36,6 +38,7 @@ describe('fullcalendar/eventSourceFunction test suite', () => {
translate.mockClear()
getHexForColorName.mockClear()
generateTextColorForHex.mockClear()
getAllObjectsInTimeRange.mockClear()
})
it('should provide fc-events', () => {
@ -149,28 +152,26 @@ describe('fullcalendar/eventSourceFunction test suite', () => {
color: 'red',
}]
getAllObjectsInTimeRange
.mockReturnValueOnce(eventComponentSet1)
.mockReturnValueOnce(eventComponentSet2)
.mockImplementationOnce(() => {
throw new Error('Error while getting all objects in time-range')
})
.mockReturnValueOnce(eventComponentSet4)
const calendarObjects = [{
calendarObject: true,
id: '1',
getAllObjectsInTimeRange: jest.fn()
.mockReturnValueOnce(eventComponentSet1),
}, {
calendarObject: true,
id: '2',
getAllObjectsInTimeRange: jest.fn()
.mockReturnValueOnce(eventComponentSet2),
}, {
calendarObject: true,
id: '3',
getAllObjectsInTimeRange: jest.fn()
.mockImplementationOnce(() => {
throw new Error('Error while getting all objects in time-range')
}),
}, {
calendarObject: true,
id: '4',
getAllObjectsInTimeRange: jest.fn()
.mockReturnValueOnce(eventComponentSet4),
}]
const start = new Date(Date.UTC(2019, 0, 1, 0, 0, 0, 0))
const end = new Date(Date.UTC(2020, 0, 31, 59, 59, 59, 999))
@ -304,6 +305,12 @@ describe('fullcalendar/eventSourceFunction test suite', () => {
expect(translate).toHaveBeenNthCalledWith(4, 'calendar', 'Untitled event')
expect(translate).toHaveBeenNthCalledWith(5, 'calendar', 'Untitled event')
expect(getAllObjectsInTimeRange).toHaveBeenCalledTimes(4)
expect(getAllObjectsInTimeRange).toHaveBeenNthCalledWith(1, calendarObjects[0], start, end)
expect(getAllObjectsInTimeRange).toHaveBeenNthCalledWith(2, calendarObjects[1], start, end)
expect(getAllObjectsInTimeRange).toHaveBeenNthCalledWith(3, calendarObjects[2], start, end)
expect(getAllObjectsInTimeRange).toHaveBeenNthCalledWith(4, calendarObjects[3], start, end)
expect(getHexForColorName).toHaveBeenCalledTimes(1)
expect(getHexForColorName).toHaveBeenNthCalledWith(1, 'red')

Some files were not shown because too many files have changed in this diff Show More