Replace CalDAV availability component with component lib

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
pull/29796/head
Christoph Wurst 3 years ago
parent 5ee0fb3acb
commit daf1b5f6a3
No known key found for this signature in database
GPG Key ID: CC42AC2A7F0E56D8

@ -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'
/**
*
@ -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)
}
/**
@ -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')
@ -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>`,

@ -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">
@ -60,19 +34,18 @@
</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() {
@ -80,63 +53,27 @@ export default {
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)
@ -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) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

28
package-lock.json generated

@ -12,6 +12,7 @@
"@chenfengyuan/vue-qrcode": "^1.0.2",
"@nextcloud/auth": "^1.3.0",
"@nextcloud/axios": "^1.9.0",
"@nextcloud/calendar-availability-vue": "^0.3.0",
"@nextcloud/capabilities": "^1.0.4",
"@nextcloud/dialogs": "^3.1.2",
"@nextcloud/event-bus": "^2.1.1",
@ -42,7 +43,6 @@
"escape-html": "^1.0.3",
"handlebars": "^4.7.7",
"ical.js": "^1.4.0",
"icalzone": "^0.0.1",
"jquery": "~3.6",
"jquery-migrate": "~3.3",
"jquery-ui": "^1.13.1",
@ -64,7 +64,6 @@
"strengthify": "github:nextcloud/strengthify#0.5.9",
"underscore": "1.12.0",
"url-search-params-polyfill": "^8.1.1",
"uuid": "^8.3.2",
"v-click-outside": "^3.1.2",
"v-tooltip": "^2.1.3",
"vue": "^2.6.14",
@ -2725,6 +2724,21 @@
"integrity": "sha512-kC42RQW5rZjZZsRaEjVlIQpp6aW/yxm+zZdETnrRQnUzcPwBgF4wO4makfGT63Ckd+LkgUW+geesPiPRqxFVew==",
"dev": true
},
"node_modules/@nextcloud/calendar-availability-vue": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@nextcloud/calendar-availability-vue/-/calendar-availability-vue-0.3.0.tgz",
"integrity": "sha512-O6v3Eq5ofT6jfORqN7BmDoP5jzhLWWHgCDNx+esH+iwhxdVoWti9M6YjMf1ZRvetReanxepLOblEeT9rBBi7bw==",
"dependencies": {
"ical.js": "^1.4.0",
"icalzone": "^0.0.1",
"uuid": "^8.3.2"
},
"peerDependencies": {
"@nextcloud/l10n": "^1.4",
"@nextcloud/vue": "^4.2||^5.0",
"vue": "^2.6"
}
},
"node_modules/@nextcloud/calendar-js": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@nextcloud/calendar-js/-/calendar-js-3.0.0.tgz",
@ -21651,6 +21665,16 @@
"integrity": "sha512-kC42RQW5rZjZZsRaEjVlIQpp6aW/yxm+zZdETnrRQnUzcPwBgF4wO4makfGT63Ckd+LkgUW+geesPiPRqxFVew==",
"dev": true
},
"@nextcloud/calendar-availability-vue": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@nextcloud/calendar-availability-vue/-/calendar-availability-vue-0.3.0.tgz",
"integrity": "sha512-O6v3Eq5ofT6jfORqN7BmDoP5jzhLWWHgCDNx+esH+iwhxdVoWti9M6YjMf1ZRvetReanxepLOblEeT9rBBi7bw==",
"requires": {
"ical.js": "^1.4.0",
"icalzone": "^0.0.1",
"uuid": "^8.3.2"
}
},
"@nextcloud/calendar-js": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@nextcloud/calendar-js/-/calendar-js-3.0.0.tgz",

@ -29,6 +29,7 @@
"@chenfengyuan/vue-qrcode": "^1.0.2",
"@nextcloud/auth": "^1.3.0",
"@nextcloud/axios": "^1.9.0",
"@nextcloud/calendar-availability-vue": "^0.3.0",
"@nextcloud/capabilities": "^1.0.4",
"@nextcloud/dialogs": "^3.1.2",
"@nextcloud/event-bus": "^2.1.1",
@ -59,7 +60,6 @@
"escape-html": "^1.0.3",
"handlebars": "^4.7.7",
"ical.js": "^1.4.0",
"icalzone": "^0.0.1",
"jquery": "~3.6",
"jquery-migrate": "~3.3",
"jquery-ui": "^1.13.1",
@ -81,7 +81,6 @@
"strengthify": "github:nextcloud/strengthify#0.5.9",
"underscore": "1.12.0",
"url-search-params-polyfill": "^8.1.1",
"uuid": "^8.3.2",
"v-click-outside": "^3.1.2",
"v-tooltip": "^2.1.3",
"vue": "^2.6.14",

@ -142,6 +142,11 @@ module.exports = {
new webpack.ProvidePlugin({
// Provide jQuery to jquery plugins as some are loaded before $ is exposed globally.
jQuery: 'jquery',
// Shim ICAL to prevent using the global object (window.ICAL).
// The library ical.js heavily depends on instanceof checks which will
// break if two separate versions of the library are used (e.g. bundled one
// and global one).
ICAL: 'ical.js',
}),
],
resolve: {

Loading…
Cancel
Save