Skip to content

Commit

Permalink
Replace CalDAV availability component with component lib
Browse files Browse the repository at this point in the history
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
  • Loading branch information
ChristophWurst committed Feb 11, 2022
1 parent fbf260f commit d2666b5
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 224 deletions.
117 changes: 9 additions & 108 deletions apps/dav/src/service/CalendarService.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getClient } from '../dav/client'
import ICAL from 'ical.js'
import logger from './logger'
import { parseXML } from 'webdav/dist/node/tools/dav'
import { getZoneString } from 'icalzone'
import { v4 as uuidv4 } from 'uuid'

import {
slotsToVavailability,
vavailabilityToSlots,
} from '@nextcloud/calendar-availability-vue'

/**
*
Expand Down Expand Up @@ -67,44 +69,7 @@ export async function findScheduleInboxAvailability() {
return undefined
}

const parsedIcal = ICAL.parse(availability)

const vcalendarComp = new ICAL.Component(parsedIcal)
const vavailabilityComp = vcalendarComp.getFirstSubcomponent('vavailability')

let timezoneId
const timezoneComp = vcalendarComp.getFirstSubcomponent('vtimezone')
if (timezoneComp) {
timezoneId = timezoneComp.getFirstProperty('tzid').getFirstValue()
}

const availableComps = vavailabilityComp.getAllSubcomponents('available')
// Combine all AVAILABLE blocks into a week of slots
const slots = getEmptySlots()
availableComps.forEach((availableComp) => {
const start = availableComp.getFirstProperty('dtstart').getFirstValue().toJSDate()
const end = availableComp.getFirstProperty('dtend').getFirstValue().toJSDate()
const rrule = availableComp.getFirstProperty('rrule')

if (rrule.getFirstValue().freq !== 'WEEKLY') {
logger.warn('rrule not supported', {
rrule: rrule.toICALString(),
})
return
}

rrule.getFirstValue().getComponent('BYDAY').forEach(day => {
slots[day].push({
start,
end,
})
})
})

return {
slots,
timezoneId,
}
return vavailabilityToSlots(availability)
}

/**
Expand All @@ -117,74 +82,10 @@ export async function saveScheduleInboxAvailability(slots, timezoneId) {
day: dayId,
})))]

const vcalendarComp = new ICAL.Component('vcalendar')
vcalendarComp.addPropertyWithValue('prodid', 'Nextcloud DAV app')

// Store time zone info
// If possible we use the info from a time zone database
const predefinedTimezoneIcal = getZoneString(timezoneId)
if (predefinedTimezoneIcal) {
const timezoneComp = new ICAL.Component(ICAL.parse(predefinedTimezoneIcal))
vcalendarComp.addSubcomponent(timezoneComp)
} else {
// Fall back to a simple markup
const timezoneComp = new ICAL.Component('vtimezone')
timezoneComp.addPropertyWithValue('tzid', timezoneId)
vcalendarComp.addSubcomponent(timezoneComp)
}

// Store availability info
const vavailabilityComp = new ICAL.Component('vavailability')

// Deduplicate by start and end time
const deduplicated = all.reduce((acc, slot) => {
const key = [
slot.start.getHours(),
slot.start.getMinutes(),
slot.end.getHours(),
slot.end.getMinutes(),
].join('-')

return {
...acc,
[key]: [...(acc[key] ?? []), slot],
}
}, {})

// Create an AVAILABILITY component for every recurring slot
Object.keys(deduplicated).map(key => {
const slots = deduplicated[key]
const start = slots[0].start
const end = slots[0].end
// Combine days but make them also unique
const days = slots.map(slot => slot.day).filter((day, index, self) => self.indexOf(day) === index)

const availableComp = new ICAL.Component('available')

// Define DTSTART and DTEND
const startTimeProp = availableComp.addPropertyWithValue('dtstart', ICAL.Time.fromJSDate(start, false))
startTimeProp.setParameter('tzid', timezoneId)
const endTimeProp = availableComp.addPropertyWithValue('dtend', ICAL.Time.fromJSDate(end, false))
endTimeProp.setParameter('tzid', timezoneId)

// Add mandatory UID
availableComp.addPropertyWithValue('uid', uuidv4())

// TODO: add optional summary

// Define RRULE
availableComp.addPropertyWithValue('rrule', {
freq: 'WEEKLY',
byday: days,
})

return availableComp
}).map(vavailabilityComp.addSubcomponent.bind(vavailabilityComp))
const vavailability = slotsToVavailability(all, timezoneId)

vcalendarComp.addSubcomponent(vavailabilityComp)
logger.debug('New availability ical created', {
asObject: vcalendarComp,
asString: vcalendarComp.toString(),
vavailability,
})

const client = getClient('calendars')
Expand All @@ -194,7 +95,7 @@ export async function saveScheduleInboxAvailability(slots, timezoneId) {
<x0:propertyupdate xmlns:x0="DAV:">
<x0:set>
<x0:prop>
<x1:calendar-availability xmlns:x1="urn:ietf:params:xml:ns:caldav">${vcalendarComp.toString()}</x1:calendar-availability>
<x1:calendar-availability xmlns:x1="urn:ietf:params:xml:ns:caldav">${vavailability}</x1:calendar-availability>
</x0:prop>
</x0:set>
</x0:propertyupdate>`,
Expand Down
140 changes: 28 additions & 112 deletions apps/dav/src/views/Availability.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,19 @@
<TimezonePicker v-model="timezone" />
</span>
</div>
<div class="grid-table">
<template v-for="day in daysOfTheWeek">
<div :key="`day-label-${day.id}`" class="label-weekday">
{{ day.displayName }}
</div>
<div :key="`day-slots-${day.id}`" class="availability-slots">
<div class="availability-slot-group">
<template v-for="(slot, idx) in day.slots">
<div :key="`slot-${day.id}-${idx}`" class="availability-slot">
<DatetimePicker v-model="slot.start"
type="time"
class="start-date"
format="H:mm" />
<span class="to-text">
{{ $t('dav', 'to') }}
</span>
<DatetimePicker v-model="slot.end"
type="time"
class="end-date"
format="H:mm" />
<button :key="`slot-${day.id}-${idx}-btn`"
class="icon-delete delete-slot button"
:title="$t('dav', 'Delete slot')"
@click="deleteSlot(day, idx)" />
</div>
</template>
</div>
<span v-if="day.slots.length === 0"
class="empty-content">
{{ $t('dav', 'No working hours set') }}
</span>
</div>
<button :key="`add-slot-${day.id}`"
:disabled="loading"
class="icon-add add-another button"
:title="$t('dav', 'Add slot')"
@click="addSlot(day)" />
</template>
</div>
<CalendarAvailability :slots.sync="slots"
:loading="loading"
:l10n-to="$t('dav', 'to')"
:l10n-delete-slot="$t('dav', 'Delete slot')"
:l10n-empty-day="$t('dav', 'No working hours set')"
:l10n-add-slot="$t('dav', 'Add slot')"
:l10n-monday="$t('dav', 'Monday')"
:l10n-tuesday="$t('dav', 'Tuesday')"
:l10n-wednesday="$t('dav', 'Wednesday')"
:l10n-thursday="$t('dav', 'Thursday')"
:l10n-friday="$t('dav', 'Friday')"
:l10n-saturday="$t('dav', 'Saturday')"
:l10n-sunday="$t('dav', 'Sunday')" />
<button :disabled="loading || saving"
class="button primary"
@click="save">
Expand All @@ -60,83 +34,46 @@
</template>

<script>
import DatetimePicker from '@nextcloud/vue/dist/Components/DatetimePicker'
import { CalendarAvailability } from '@nextcloud/calendar-availability-vue'
import {
findScheduleInboxAvailability,
getEmptySlots,
saveScheduleInboxAvailability,
} from '../service/CalendarService'
import { getFirstDay } from '@nextcloud/l10n'
import jstz from 'jstimezonedetect'
import TimezonePicker from '@nextcloud/vue/dist/Components/TimezonePicker'
export default {
name: 'Availability',
components: {
DatetimePicker,
CalendarAvailability,
TimezonePicker,
},
data() {
// Try to determine the current timezone, and fall back to UTC otherwise
const defaultTimezone = jstz.determine()
const defaultTimezoneId = defaultTimezone ? defaultTimezone.name() : 'UTC'
const moToSa = [
{
id: 'MO',
displayName: this.$t('dav', 'Monday'),
slots: [],
},
{
id: 'TU',
displayName: this.$t('dav', 'Tuesday'),
slots: [],
},
{
id: 'WE',
displayName: this.$t('dav', 'Wednesday'),
slots: [],
},
{
id: 'TH',
displayName: this.$t('dav', 'Thursday'),
slots: [],
},
{
id: 'FR',
displayName: this.$t('dav', 'Friday'),
slots: [],
},
{
id: 'SA',
displayName: this.$t('dav', 'Saturday'),
slots: [],
},
]
const sunday = {
id: 'SU',
displayName: this.$t('dav', 'Sunday'),
slots: [],
}
const daysOfTheWeek = getFirstDay() === 1 ? [...moToSa, sunday] : [sunday, ...moToSa]
return {
loading: true,
saving: false,
timezone: defaultTimezoneId,
daysOfTheWeek,
slots: getEmptySlots(),
}
},
async mounted() {
try {
const { slots, timezoneId } = await findScheduleInboxAvailability()
if (slots) {
this.daysOfTheWeek.forEach(day => {
day.slots.push(...slots[day.id])
})
}
if (timezoneId) {
this.timezone = timezoneId
const slotData = await findScheduleInboxAvailability()
if (!slotData) {
console.info('no availability is set')
this.slots = getEmptySlots()
} else {
const { slots, timezoneId } = slotData
this.slots = slots
if (timezoneId) {
this.timezone = timezoneId
}
console.info('availability loaded', this.slots, this.timezoneId)
}
console.info('availability loaded', this.daysOfTheWeek)
} catch (e) {
console.error('could not load existing availability', e)
Expand All @@ -146,32 +83,11 @@ export default {
}
},
methods: {
addSlot(day) {
const start = new Date()
start.setHours(9)
start.setMinutes(0)
start.setSeconds(0)
const end = new Date()
end.setHours(17)
end.setMinutes(0)
end.setSeconds(0)
day.slots.push({
start,
end,
})
},
deleteSlot(day, idx) {
day.slots.splice(idx, 1)
},
async save() {
try {
this.saving = true
const slots = getEmptySlots()
this.daysOfTheWeek.forEach(day => {
day.slots.forEach(slot => slots[day.id].push(slot))
})
await saveScheduleInboxAvailability(slots, this.timezone)
await saveScheduleInboxAvailability(this.slots, this.timezone)
// TODO: show a nice toast
} catch (e) {
Expand Down
Loading

0 comments on commit d2666b5

Please sign in to comment.