mirror of https://github.com/nextcloud/calendar
Cleanup models and add full unit test coverage
Signed-off-by: Georg Ehrke <developer@georgehrke.com>
This commit is contained in:
parent
53db79f8aa
commit
6f5966a2b4
|
@ -152,7 +152,8 @@
|
|||
"/node_modules/(?!calendar-js).+\\.js$"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"./tests/javascript/jest.setup.js"
|
||||
"./tests/javascript/jest.setup.js",
|
||||
"./tests/assets/loadAsset.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;VALUE=DATE-TIME:20200306T083000Z
|
||||
END:VALARM
|
|
@ -0,0 +1,4 @@
|
|||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;RELATED=START:P1DT9H
|
||||
END:VALARM
|
|
@ -0,0 +1,4 @@
|
|||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;RELATED=START:PT9H
|
||||
END:VALARM
|
|
@ -0,0 +1,4 @@
|
|||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;RELATED=START:-PT15H
|
||||
END:VALARM
|
|
@ -0,0 +1,4 @@
|
|||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;RELATED=END:-PT15H
|
||||
END:VALARM
|
|
@ -0,0 +1,4 @@
|
|||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;RELATED=START:-P6DT15H
|
||||
END:VALARM
|
|
@ -0,0 +1 @@
|
|||
ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:jsmith@example.com
|
|
@ -0,0 +1 @@
|
|||
ATTENDEE;CUTYPE=GROUP:mailto:ietf-calsch@example.org
|
|
@ -0,0 +1 @@
|
|||
ATTENDEE;PARTSTAT=DECLINED:mailto:jsmith@example.com
|
|
@ -0,0 +1 @@
|
|||
ATTENDEE;ROLE=CHAIR:mailto:mrbig@example.com
|
|
@ -0,0 +1 @@
|
|||
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;DELEGATED-FROM="mailto:iamboss@example.com";CN=Henry Cabot:mailto:hcabot@example.com
|
|
@ -0,0 +1 @@
|
|||
ATTENDEE;ROLE=NON-PARTICIPANT;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:hcabot@example.com";CN=The Big Cheese:mailto:iamboss@example.com
|
|
@ -0,0 +1 @@
|
|||
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Jane Doe:mailto:jdoe@example.com
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;UNTIL=20201122T001122Z;COUNT=5
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;COUNT=42
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;UNTIL=20201122T001122Z
|
|
@ -0,0 +1 @@
|
|||
FREQ=SECONDLY
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;BYMONTHDAY=1,2,3,30,31
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;BYMONTHDAY=-1,2,-3,30,31,-31
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;BYMONTHDAY=1,2,3,30,31;BYDAY=MO;BYSETPOS=-1
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;BYDAY=MO;BYSETPOS=3
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;BYDAY=MO,TU,WE;BYSETPOS=3
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;BYDAY=MO;BYSETPOS=-3
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1,2,3
|
|
@ -0,0 +1 @@
|
|||
FREQ=YEARLY
|
|
@ -0,0 +1 @@
|
|||
FREQ=YEARLY;BYMONTH=1,2,3
|
|
@ -0,0 +1 @@
|
|||
FREQ=MINUTELY
|
|
@ -0,0 +1 @@
|
|||
FREQ=YEARLY;BYMONTH=1,2,3,0
|
|
@ -0,0 +1 @@
|
|||
FREQ=YEARLY;BYDAY=MO;BYSETPOS=3
|
|
@ -0,0 +1 @@
|
|||
FREQ=YEARLY;BYDAY=MO,TU,WE;BYSETPOS=3
|
|
@ -0,0 +1 @@
|
|||
FREQ=YEARLY;BYDAY=MO;BYSETPOS=-3
|
|
@ -0,0 +1 @@
|
|||
FREQ=YEARLY;BYDAY=MO;BYSETPOS=1,2,3
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;BYDAY=MO,TU,WE
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;BYDAY=3MO
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;BYDAY=-3MO
|
|
@ -0,0 +1 @@
|
|||
FREQ=MONTHLY;BYDAY=MO
|
|
@ -0,0 +1 @@
|
|||
FREQ=YEARLY;BYDAY=MO,TU,WE
|
|
@ -0,0 +1 @@
|
|||
FREQ=HOURLY
|
|
@ -0,0 +1 @@
|
|||
FREQ=YEARLY;BYDAY=3MO
|
|
@ -0,0 +1 @@
|
|||
FREQ=YEARLY;BYDAY=-3MO
|
|
@ -0,0 +1 @@
|
|||
FREQ=YEARLY;BYDAY=MO
|
|
@ -0,0 +1 @@
|
|||
FREQ=DAILY;INTERVAL=5
|
|
@ -0,0 +1 @@
|
|||
FREQ=DAILY;INTERVAL=42;BYMONTH=1
|
|
@ -0,0 +1 @@
|
|||
FREQ=WEEKLY
|
|
@ -0,0 +1 @@
|
|||
FREQ=WEEKLY;BYDAY=MO,TU,WE
|
|
@ -0,0 +1 @@
|
|||
FREQ=WEEKLY;BYDAY=MO,2TU,-3WE
|
|
@ -0,0 +1 @@
|
|||
FREQ=WEEKLY;BYDAY=MO,TU,WE;BYMONTH=1,2
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Tests//
|
||||
CALSCALE:GREGORIAN
|
||||
END:VCALENDAR
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue