aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/src')
-rw-r--r--apps/dav/src/components/AbsenceForm.vue274
-rw-r--r--apps/dav/src/components/AvailabilityForm.vue223
-rw-r--r--apps/dav/src/components/ExampleContactSettings.vue172
-rw-r--r--apps/dav/src/components/ExampleContentDownloadButton.vue57
-rw-r--r--apps/dav/src/components/ExampleEventSettings.vue217
-rw-r--r--apps/dav/src/dav/client.js54
-rw-r--r--apps/dav/src/service/CalendarService.js28
-rw-r--r--apps/dav/src/service/ExampleEventService.js43
-rw-r--r--apps/dav/src/service/PreferenceService.js24
-rw-r--r--apps/dav/src/service/logger.js22
-rw-r--r--apps/dav/src/settings-example-content.js18
-rw-r--r--apps/dav/src/settings-personal-availability.js6
-rw-r--r--apps/dav/src/settings.js12
-rw-r--r--apps/dav/src/utils/date.js17
-rw-r--r--apps/dav/src/views/Availability.vue214
-rw-r--r--apps/dav/src/views/CalDavSettings.spec.js55
-rw-r--r--apps/dav/src/views/CalDavSettings.vue25
-rw-r--r--apps/dav/src/views/ExampleContentSettingsSection.vue38
-rw-r--r--apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap485
-rw-r--r--apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap.license2
20 files changed, 1442 insertions, 544 deletions
diff --git a/apps/dav/src/components/AbsenceForm.vue b/apps/dav/src/components/AbsenceForm.vue
new file mode 100644
index 00000000000..5350c04a565
--- /dev/null
+++ b/apps/dav/src/components/AbsenceForm.vue
@@ -0,0 +1,274 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <form class="absence" @submit.prevent="saveForm">
+ <div class="absence__dates">
+ <NcDateTimePickerNative id="absence-first-day"
+ v-model="firstDay"
+ :label="$t('dav', 'First day')"
+ class="absence__dates__picker"
+ :required="true" />
+ <NcDateTimePickerNative id="absence-last-day"
+ v-model="lastDay"
+ :label="$t('dav', 'Last day (inclusive)')"
+ class="absence__dates__picker"
+ :required="true" />
+ </div>
+ <label for="replacement-search-input">{{ $t('dav', 'Out of office replacement (optional)') }}</label>
+ <NcSelect ref="select"
+ v-model="replacementUser"
+ input-id="replacement-search-input"
+ :loading="searchLoading"
+ :placeholder="$t('dav', 'Name of the replacement')"
+ :clear-search-on-blur="() => false"
+ :user-select="true"
+ :options="options"
+ @search="asyncFind">
+ <template #no-options="{ search }">
+ {{ search ?$t('dav', 'No results.') : $t('dav', 'Start typing.') }}
+ </template>
+ </NcSelect>
+ <NcTextField :value.sync="status" :label="$t('dav', 'Short absence status')" :required="true" />
+ <NcTextArea :value.sync="message" :label="$t('dav', 'Long absence Message')" :required="true" />
+
+ <div class="absence__buttons">
+ <NcButton :disabled="loading || !valid"
+ type="primary"
+ native-type="submit">
+ {{ $t('dav', 'Save') }}
+ </NcButton>
+ <NcButton :disabled="loading || !valid"
+ type="error"
+ @click="clearAbsence">
+ {{ $t('dav', 'Disable absence') }}
+ </NcButton>
+ </div>
+ </form>
+</template>
+
+<script>
+import { getCurrentUser } from '@nextcloud/auth'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import { loadState } from '@nextcloud/initial-state'
+import { generateOcsUrl } from '@nextcloud/router'
+import { ShareType } from '@nextcloud/sharing'
+import { formatDateAsYMD } from '../utils/date.js'
+import axios from '@nextcloud/axios'
+import debounce from 'debounce'
+import logger from '../service/logger.js'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+import NcTextArea from '@nextcloud/vue/components/NcTextArea'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
+import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative'
+
+export default {
+ name: 'AbsenceForm',
+ components: {
+ NcButton,
+ NcTextField,
+ NcTextArea,
+ NcDateTimePickerNative,
+ NcSelect,
+ },
+ data() {
+ const { firstDay, lastDay, status, message, replacementUserId, replacementUserDisplayName } = loadState('dav', 'absence', {})
+ return {
+ loading: false,
+ status: status ?? '',
+ message: message ?? '',
+ firstDay: firstDay ? new Date(firstDay) : new Date(),
+ lastDay: lastDay ? new Date(lastDay) : null,
+ replacementUserId,
+ replacementUser: replacementUserId ? { user: replacementUserId, displayName: replacementUserDisplayName } : null,
+ searchLoading: false,
+ options: [],
+ }
+ },
+ computed: {
+ /**
+ * @return {boolean}
+ */
+ valid() {
+ // Translate the two date objects to midnight for an accurate comparison
+ const firstDay = new Date(this.firstDay?.getTime())
+ const lastDay = new Date(this.lastDay?.getTime())
+ firstDay?.setHours(0, 0, 0, 0)
+ lastDay?.setHours(0, 0, 0, 0)
+
+ return !!this.firstDay
+ && !!this.lastDay
+ && !!this.status
+ && !!this.message
+ && lastDay >= firstDay
+ },
+ },
+ methods: {
+ resetForm() {
+ this.status = ''
+ this.message = ''
+ this.firstDay = new Date()
+ this.lastDay = null
+ },
+
+ /**
+ * Format shares for the multiselect options
+ *
+ * @param {object} result select entry item
+ * @return {object}
+ */
+ formatForMultiselect(result) {
+ return {
+ user: result.uuid || result.value.shareWith,
+ displayName: result.name || result.label,
+ subtitle: result.dsc | '',
+ }
+ },
+
+ async asyncFind(query) {
+ this.searchLoading = true
+ await this.debounceGetSuggestions(query.trim())
+ },
+ /**
+ * Get suggestions
+ *
+ * @param {string} search the search query
+ */
+ async getSuggestions(search) {
+
+ const shareType = [
+ ShareType.User,
+ ]
+
+ let request = null
+ try {
+ request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1/sharees'), {
+ params: {
+ format: 'json',
+ itemType: 'file',
+ search,
+ shareType,
+ },
+ })
+ } catch (error) {
+ console.error('Error fetching suggestions', error)
+ return
+ }
+
+ const data = request.data.ocs.data
+ const exact = request.data.ocs.data.exact
+ data.exact = [] // removing exact from general results
+ const rawExactSuggestions = exact.users
+ const rawSuggestions = data.users
+ console.info('rawExactSuggestions', rawExactSuggestions)
+ console.info('rawSuggestions', rawSuggestions)
+ // remove invalid data and format to user-select layout
+ const exactSuggestions = rawExactSuggestions
+ .map(share => this.formatForMultiselect(share))
+ const suggestions = rawSuggestions
+ .map(share => this.formatForMultiselect(share))
+
+ const allSuggestions = exactSuggestions.concat(suggestions)
+
+ // Count occurrences of display names in order to provide a distinguishable description if needed
+ const nameCounts = allSuggestions.reduce((nameCounts, result) => {
+ if (!result.displayName) {
+ return nameCounts
+ }
+ if (!nameCounts[result.displayName]) {
+ nameCounts[result.displayName] = 0
+ }
+ nameCounts[result.displayName]++
+ return nameCounts
+ }, {})
+
+ this.options = allSuggestions.map(item => {
+ // Make sure that items with duplicate displayName get the shareWith applied as a description
+ if (nameCounts[item.displayName] > 1 && !item.desc) {
+ return { ...item, desc: item.shareWithDisplayNameUnique }
+ }
+ return item
+ })
+
+ this.searchLoading = false
+ console.info('suggestions', this.options)
+ },
+
+ /**
+ * Debounce getSuggestions
+ *
+ * @param {...*} args the arguments
+ */
+ debounceGetSuggestions: debounce(function(...args) {
+ this.getSuggestions(...args)
+ }, 300),
+
+ async saveForm() {
+ if (!this.valid) {
+ return
+ }
+
+ this.loading = true
+ try {
+ await axios.post(generateOcsUrl('/apps/dav/api/v1/outOfOffice/{userId}', { userId: getCurrentUser().uid }), {
+ firstDay: formatDateAsYMD(this.firstDay),
+ lastDay: formatDateAsYMD(this.lastDay),
+ status: this.status,
+ message: this.message,
+ replacementUserId: this.replacementUser?.user ?? null,
+ })
+ showSuccess(this.$t('dav', 'Absence saved'))
+ } catch (error) {
+ showError(this.$t('dav', 'Failed to save your absence settings'))
+ logger.error('Could not save absence', { error })
+ } finally {
+ this.loading = false
+ }
+ },
+ async clearAbsence() {
+ this.loading = true
+ try {
+ await axios.delete(generateOcsUrl('/apps/dav/api/v1/outOfOffice/{userId}', { userId: getCurrentUser().uid }))
+ this.resetForm()
+ showSuccess(this.$t('dav', 'Absence cleared'))
+ } catch (error) {
+ showError(this.$t('dav', 'Failed to clear your absence settings'))
+ logger.error('Could not clear absence', { error })
+ } finally {
+ this.loading = false
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.absence {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+
+ &__dates {
+ display: flex;
+ gap: 10px;
+ width: 100%;
+
+ &__picker {
+ flex: 1 auto;
+
+ :deep(.native-datetime-picker--input) {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ &__buttons {
+ display: flex;
+ gap: 5px;
+ }
+}
+</style>
diff --git a/apps/dav/src/components/AvailabilityForm.vue b/apps/dav/src/components/AvailabilityForm.vue
new file mode 100644
index 00000000000..d53c092be9d
--- /dev/null
+++ b/apps/dav/src/components/AvailabilityForm.vue
@@ -0,0 +1,223 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <div>
+ <div class="time-zone">
+ <label :for="`vs${timeZonePickerId}__combobox`" class="time-zone__heading">
+ {{ $t('dav', 'Time zone:') }}
+ </label>
+ <span class="time-zone-text">
+ <NcTimezonePicker v-model="timezone" :uid="timeZonePickerId" />
+ </span>
+ </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-week-day-list-label="$t('dav', 'Weekdays')"
+ :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')"
+ :l10n-start-picker-label="(dayName) => $t('dav', 'Pick a start time for {dayName}', { dayName })"
+ :l10n-end-picker-label="(dayName) => $t('dav', 'Pick a end time for {dayName}', { dayName })" />
+
+ <NcCheckboxRadioSwitch :checked.sync="automated">
+ {{ $t('dav', 'Automatically set user status to "Do not disturb" outside of availability to mute all notifications.') }}
+ </NcCheckboxRadioSwitch>
+
+ <NcButton :disabled="loading || saving"
+ type="primary"
+ @click="save">
+ {{ $t('dav', 'Save') }}
+ </NcButton>
+ </div>
+</template>
+
+<script>
+import { CalendarAvailability } from '@nextcloud/calendar-availability-vue'
+import { loadState } from '@nextcloud/initial-state'
+import {
+ showError,
+ showSuccess,
+} from '@nextcloud/dialogs'
+import {
+ findScheduleInboxAvailability,
+ getEmptySlots,
+ saveScheduleInboxAvailability,
+} from '../service/CalendarService.js'
+import {
+ enableUserStatusAutomation,
+ disableUserStatusAutomation,
+} from '../service/PreferenceService.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcTimezonePicker from '@nextcloud/vue/components/NcTimezonePicker'
+
+export default {
+ name: 'AvailabilityForm',
+ components: {
+ NcButton,
+ NcCheckboxRadioSwitch,
+ CalendarAvailability,
+ NcTimezonePicker,
+ },
+ data() {
+ // Try to determine the current timezone, and fall back to UTC otherwise
+ const defaultTimezoneId = (new Intl.DateTimeFormat())?.resolvedOptions()?.timeZone ?? 'UTC'
+
+ return {
+ loading: true,
+ saving: false,
+ timezone: defaultTimezoneId,
+ slots: getEmptySlots(),
+ automated: loadState('dav', 'user_status_automation') === 'yes',
+ }
+ },
+ computed: {
+ timeZonePickerId() {
+ return `tz-${(Math.random() + 1).toString(36).substring(7)}`
+ },
+ },
+ async mounted() {
+ try {
+ 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)
+ }
+ } catch (e) {
+ console.error('could not load existing availability', e)
+
+ showError(t('dav', 'Failed to load availability'))
+ } finally {
+ this.loading = false
+ }
+ },
+ methods: {
+ async save() {
+ try {
+ this.saving = true
+
+ await saveScheduleInboxAvailability(this.slots, this.timezone)
+ if (this.automated) {
+ await enableUserStatusAutomation()
+ } else {
+ await disableUserStatusAutomation()
+ }
+
+ showSuccess(t('dav', 'Saved availability'))
+ } catch (e) {
+ console.error('could not save availability', e)
+
+ showError(t('dav', 'Failed to save availability'))
+ } finally {
+ this.saving = false
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.availability-day) {
+ padding: 0 10px 0 10px;
+ position: absolute;
+}
+
+:deep(.availability-slots) {
+ display: flex;
+ white-space: normal;
+}
+
+:deep(.availability-slot) {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+:deep(.availability-slot-group) {
+ display: flex;
+ flex-direction: column;
+}
+
+:deep(.mx-input-wrapper) {
+ width: 85px;
+}
+
+:deep(.mx-datepicker) {
+ width: 97px;
+}
+
+:deep(.multiselect) {
+ border: 1px solid var(--color-border-dark);
+ width: 120px;
+}
+
+.time-zone {
+ padding-block: 32px 12px;
+ padding-inline: 0 12px;
+ display: flex;
+ flex-wrap: wrap;
+
+ &__heading {
+ margin-inline-end: calc(var(--default-grid-baseline) * 2);
+ line-height: var(--default-clickable-area);
+ font-weight: bold;
+ }
+}
+
+.grid-table {
+ display: grid;
+ margin-bottom: 32px;
+ grid-column-gap: 24px;
+ grid-row-gap: 6px;
+ grid-template-columns: min-content auto min-content;
+ max-width: 500px;
+}
+
+.button {
+ align-self: flex-end;
+}
+
+:deep(.label-weekday) {
+ position: relative;
+ display: inline-flex;
+ padding-top: 4px;
+ align-self: center;
+}
+
+:deep(.delete-slot) {
+ padding-bottom: unset;
+}
+
+:deep(.add-another) {
+ align-self: center;
+}
+
+.to-text {
+ padding-inline-end: 12px;
+}
+
+.empty-content {
+ color: var(--color-text-lighter);
+ margin-block-start: var(--default-grid-baseline);
+ align-self: center;
+}
+</style>
diff --git a/apps/dav/src/components/ExampleContactSettings.vue b/apps/dav/src/components/ExampleContactSettings.vue
new file mode 100644
index 00000000000..cdfdc130189
--- /dev/null
+++ b/apps/dav/src/components/ExampleContactSettings.vue
@@ -0,0 +1,172 @@
+<!--
+ - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <div class="example-contact-settings">
+ <NcCheckboxRadioSwitch :checked="enableDefaultContact"
+ type="switch"
+ @update:model-value="updateEnableDefaultContact">
+ {{ $t('dav', "Add example contact to user's address book when they first log in") }}
+ </NcCheckboxRadioSwitch>
+ <div v-if="enableDefaultContact" class="example-contact-settings__buttons">
+ <ExampleContentDownloadButton :href="downloadUrl">
+ <template #icon>
+ <IconAccount :size="20" />
+ </template>
+ example_contact.vcf
+ </ExampleContentDownloadButton>
+ <NcButton type="secondary"
+ @click="toggleModal">
+ <template #icon>
+ <IconUpload :size="20" />
+ </template>
+ {{ $t('dav', 'Import contact') }}
+ </NcButton>
+ <NcButton v-if="hasCustomDefaultContact"
+ type="tertiary"
+ @click="resetContact">
+ <template #icon>
+ <IconRestore :size="20" />
+ </template>
+ {{ $t('dav', 'Reset to default') }}
+ </NcButton>
+ </div>
+ <NcDialog :open.sync="isModalOpen"
+ :name="$t('dav', 'Import contacts')"
+ :buttons="buttons">
+ <div>
+ <p>{{ $t('dav', 'Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?') }}</p>
+ </div>
+ </NcDialog>
+ <input id="example-contact-import"
+ ref="exampleContactImportInput"
+ :disabled="loading"
+ type="file"
+ accept=".vcf"
+ class="hidden-visually"
+ @change="processFile">
+ </div>
+</template>
+<script>
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+import { loadState } from '@nextcloud/initial-state'
+import { NcDialog, NcButton, NcCheckboxRadioSwitch } from '@nextcloud/vue'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import IconUpload from 'vue-material-design-icons/Upload.vue'
+import IconRestore from 'vue-material-design-icons/Restore.vue'
+import IconAccount from 'vue-material-design-icons/Account.vue'
+import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
+import IconCheck from '@mdi/svg/svg/check.svg?raw'
+import logger from '../service/logger.js'
+import ExampleContentDownloadButton from './ExampleContentDownloadButton.vue'
+
+const enableDefaultContact = loadState('dav', 'enableDefaultContact')
+const hasCustomDefaultContact = loadState('dav', 'hasCustomDefaultContact')
+
+export default {
+ name: 'ExampleContactSettings',
+ components: {
+ NcDialog,
+ NcButton,
+ NcCheckboxRadioSwitch,
+ IconUpload,
+ IconRestore,
+ IconAccount,
+ ExampleContentDownloadButton,
+ },
+ data() {
+ return {
+ enableDefaultContact,
+ hasCustomDefaultContact,
+ isModalOpen: false,
+ loading: false,
+ buttons: [
+ {
+ label: this.$t('dav', 'Cancel'),
+ icon: IconCancel,
+ callback: () => { this.isModalOpen = false },
+ },
+ {
+ label: this.$t('dav', 'Import'),
+ type: 'primary',
+ icon: IconCheck,
+ callback: () => { this.clickImportInput() },
+ },
+ ],
+ }
+ },
+ computed: {
+ downloadUrl() {
+ return generateUrl('/apps/dav/api/defaultcontact/contact')
+ },
+ },
+ methods: {
+ updateEnableDefaultContact() {
+ axios.put(generateUrl('apps/dav/api/defaultcontact/config'), {
+ allow: !this.enableDefaultContact,
+ }).then(() => {
+ this.enableDefaultContact = !this.enableDefaultContact
+ }).catch(() => {
+ showError(this.$t('dav', 'Error while saving settings'))
+ })
+ },
+ toggleModal() {
+ this.isModalOpen = !this.isModalOpen
+ },
+ clickImportInput() {
+ this.$refs.exampleContactImportInput.click()
+ },
+ resetContact() {
+ this.loading = true
+ axios.put(generateUrl('/apps/dav/api/defaultcontact/contact'))
+ .then(() => {
+ this.hasCustomDefaultContact = false
+ showSuccess(this.$t('dav', 'Contact reset successfully'))
+ })
+ .catch((error) => {
+ logger.error('Error importing contact:', { error })
+ showError(this.$t('dav', 'Error while resetting contact'))
+ })
+ .finally(() => {
+ this.loading = false
+ })
+ },
+ processFile(event) {
+ this.loading = true
+
+ const file = event.target.files[0]
+ const reader = new FileReader()
+
+ reader.onload = async () => {
+ this.isModalOpen = false
+ try {
+ await axios.put(generateUrl('/apps/dav/api/defaultcontact/contact'), { contactData: reader.result })
+ this.hasCustomDefaultContact = true
+ showSuccess(this.$t('dav', 'Contact imported successfully'))
+ } catch (error) {
+ logger.error('Error importing contact:', { error })
+ showError(this.$t('dav', 'Error while importing contact'))
+ } finally {
+ this.loading = false
+ event.target.value = ''
+ }
+ }
+ reader.readAsText(file)
+ },
+ },
+}
+</script>
+<style lang="scss" scoped>
+.example-contact-settings {
+ margin-block-start: 2rem;
+
+ &__buttons {
+ display: flex;
+ gap: calc(var(--default-grid-baseline) * 2);
+ margin-top: calc(var(--default-grid-baseline) * 2);
+ }
+}
+</style>
diff --git a/apps/dav/src/components/ExampleContentDownloadButton.vue b/apps/dav/src/components/ExampleContentDownloadButton.vue
new file mode 100644
index 00000000000..6ee13e057bd
--- /dev/null
+++ b/apps/dav/src/components/ExampleContentDownloadButton.vue
@@ -0,0 +1,57 @@
+<!--
+ - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+
+<template>
+ <NcButton type="tertiary" :href="href">
+ <template #icon>
+ <slot name="icon" />
+ </template>
+ <div class="download-button">
+ <span class="download-button__label">
+ <slot name="default" />
+ </span>
+ <IconDownload class="download-button__icon"
+ :size="20" />
+ </div>
+ </NcButton>
+</template>
+
+<script>
+import { NcButton } from '@nextcloud/vue'
+import IconDownload from 'vue-material-design-icons/Download.vue'
+
+export default {
+ name: 'ExampleContentDownloadButton',
+ components: {
+ NcButton,
+ IconDownload,
+ },
+ props: {
+ href: {
+ type: String,
+ required: true,
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.download-button {
+ display: flex;
+ max-width: 200px;
+
+ &__label {
+ font-weight: initial;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+
+ &__icon {
+ margin-top: 2px;
+ margin-inline-start: var(--default-grid-baseline);
+ }
+}
+</style>
diff --git a/apps/dav/src/components/ExampleEventSettings.vue b/apps/dav/src/components/ExampleEventSettings.vue
new file mode 100644
index 00000000000..5d2053def50
--- /dev/null
+++ b/apps/dav/src/components/ExampleEventSettings.vue
@@ -0,0 +1,217 @@
+<!--
+ - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+
+<template>
+ <div class="example-event-settings">
+ <NcCheckboxRadioSwitch :checked="createExampleEvent"
+ :disabled="savingConfig"
+ type="switch"
+ @update:model-value="updateCreateExampleEvent">
+ {{ t('dav', "Add example event to user's calendar when they first log in") }}
+ </NcCheckboxRadioSwitch>
+ <div v-if="createExampleEvent"
+ class="example-event-settings__buttons">
+ <ExampleContentDownloadButton :href="downloadUrl">
+ <template #icon>
+ <IconCalendarBlank :size="20" />
+ </template>
+ example_event.ics
+ </ExampleContentDownloadButton>
+ <NcButton type="secondary"
+ @click="showImportModal = true">
+ <template #icon>
+ <IconUpload :size="20" />
+ </template>
+ {{ t('dav', 'Import calendar event') }}
+ </NcButton>
+ <NcButton v-if="hasCustomEvent"
+ type="tertiary"
+ :disabled="deleting"
+ @click="deleteCustomEvent">
+ <template #icon>
+ <IconRestore :size="20" />
+ </template>
+ {{ t('dav', 'Reset to default') }}
+ </NcButton>
+ </div>
+ <NcDialog :open.sync="showImportModal"
+ :name="t('dav', 'Import calendar event')">
+ <div class="import-event-modal">
+ <p>
+ {{ t('dav', 'Uploading a new event will overwrite the existing one.') }}
+ </p>
+ <input ref="event-file"
+ :disabled="uploading"
+ type="file"
+ accept=".ics,text/calendar"
+ class="import-event-modal__file-picker"
+ @change="selectFile">
+ <div class="import-event-modal__buttons">
+ <NcButton :disabled="uploading || !selectedFile"
+ type="primary"
+ @click="uploadCustomEvent()">
+ <template #icon>
+ <IconUpload :size="20" />
+ </template>
+ {{ t('dav', 'Upload event') }}
+ </NcButton>
+ </div>
+ </div>
+ </NcDialog>
+ </div>
+</template>
+
+<script>
+import { NcButton, NcCheckboxRadioSwitch, NcDialog } from '@nextcloud/vue'
+import { loadState } from '@nextcloud/initial-state'
+import IconCalendarBlank from 'vue-material-design-icons/CalendarBlank.vue'
+import IconUpload from 'vue-material-design-icons/Upload.vue'
+import IconRestore from 'vue-material-design-icons/Restore.vue'
+import * as ExampleEventService from '../service/ExampleEventService.js'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import logger from '../service/logger.js'
+import { generateUrl } from '@nextcloud/router'
+import ExampleContentDownloadButton from './ExampleContentDownloadButton.vue'
+
+export default {
+ name: 'ExampleEventSettings',
+ components: {
+ NcButton,
+ NcCheckboxRadioSwitch,
+ NcDialog,
+ IconCalendarBlank,
+ IconUpload,
+ IconRestore,
+ ExampleContentDownloadButton,
+ },
+ data() {
+ return {
+ createExampleEvent: loadState('dav', 'create_example_event', false),
+ hasCustomEvent: loadState('dav', 'has_custom_example_event', false),
+ showImportModal: false,
+ uploading: false,
+ deleting: false,
+ savingConfig: false,
+ selectedFile: undefined,
+ }
+ },
+ computed: {
+ downloadUrl() {
+ return generateUrl('/apps/dav/api/exampleEvent/event')
+ },
+ },
+ methods: {
+ selectFile() {
+ this.selectedFile = this.$refs['event-file']?.files[0]
+ },
+ async updateCreateExampleEvent() {
+ this.savingConfig = true
+
+ const enable = !this.createExampleEvent
+ try {
+ await ExampleEventService.setCreateExampleEvent(enable)
+ } catch (error) {
+ showError(t('dav', 'Failed to save example event creation setting'))
+ logger.error('Failed to save example event creation setting', {
+ error,
+ enable,
+ })
+ } finally {
+ this.savingConfig = false
+ }
+
+ this.createExampleEvent = enable
+ },
+ uploadCustomEvent() {
+ if (!this.selectedFile) {
+ return
+ }
+
+ this.uploading = true
+
+ const reader = new FileReader()
+ reader.addEventListener('load', async () => {
+ const ics = reader.result
+
+ try {
+ await ExampleEventService.uploadExampleEvent(ics)
+ } catch (error) {
+ showError(t('dav', 'Failed to upload the example event'))
+ logger.error('Failed to upload example ICS', {
+ error,
+ ics,
+ })
+ return
+ } finally {
+ this.uploading = false
+ }
+
+ showSuccess(t('dav', 'Custom example event was saved successfully'))
+ this.showImportModal = false
+ this.hasCustomEvent = true
+ })
+ reader.readAsText(this.selectedFile)
+ },
+ async deleteCustomEvent() {
+ this.deleting = true
+
+ try {
+ await ExampleEventService.deleteExampleEvent()
+ } catch (error) {
+ showError(t('dav', 'Failed to delete the custom example event'))
+ logger.error('Failed to delete the custom example event', {
+ error,
+ })
+ return
+ } finally {
+ this.deleting = false
+ }
+
+ showSuccess(t('dav', 'Custom example event was deleted successfully'))
+ this.hasCustomEvent = false
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.example-event-settings {
+ margin-block: 2rem;
+
+ &__buttons {
+ display: flex;
+ gap: calc(var(--default-grid-baseline) * 2);
+ margin-top: calc(var(--default-grid-baseline) * 2);
+
+ &__download-link {
+ display: flex;
+ max-width: 100px;
+
+ &__label {
+ text-decoration: underline;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+ }
+ }
+}
+
+.import-event-modal {
+ display: flex;
+ flex-direction: column;
+ gap: calc(var(--default-grid-baseline) * 2);
+ padding: calc(var(--default-grid-baseline) * 2);
+
+ &__file-picker {
+ width: 100%;
+ }
+
+ &__buttons {
+ display: flex;
+ justify-content: flex-end;
+ }
+}
+</style>
diff --git a/apps/dav/src/dav/client.js b/apps/dav/src/dav/client.js
index ff858e0492c..d286f6f48d6 100644
--- a/apps/dav/src/dav/client.js
+++ b/apps/dav/src/dav/client.js
@@ -1,39 +1,31 @@
-/*
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import * as webdav from 'webdav'
-import axios from '@nextcloud/axios'
-import memoize from 'lodash/fp/memoize'
+import { createClient } from 'webdav'
+import memoize from 'lodash/fp/memoize.js'
import { generateRemoteUrl } from '@nextcloud/router'
-import { getCurrentUser } from '@nextcloud/auth'
+import { getCurrentUser, getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth'
export const getClient = memoize((service) => {
- // Add this so the server knows it is an request from the browser
- axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
+ // init webdav client
+ const remote = generateRemoteUrl(`dav/${service}/${getCurrentUser().uid}`)
+ const client = createClient(remote)
- // force our axios
- const patcher = webdav.getPatcher()
- patcher.patch('request', axios)
+ // set CSRF token header
+ const setHeaders = (token) => {
+ client.setHeaders({
+ // Add this so the server knows it is an request from the browser
+ 'X-Requested-With': 'XMLHttpRequest',
+ // Inject user auth
+ requesttoken: token ?? '',
+ })
+ }
- return webdav.createClient(
- generateRemoteUrl(`dav/${service}/${getCurrentUser().uid}`)
- )
+ // refresh headers when request token changes
+ onRequestTokenUpdate(setHeaders)
+ setHeaders(getRequestToken())
+
+ return client
})
diff --git a/apps/dav/src/service/CalendarService.js b/apps/dav/src/service/CalendarService.js
index 2b416d6b670..93b36b8e74f 100644
--- a/apps/dav/src/service/CalendarService.js
+++ b/apps/dav/src/service/CalendarService.js
@@ -1,26 +1,10 @@
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { getClient } from '../dav/client'
-import logger from './logger'
-import { parseXML } from 'webdav/dist/node/tools/dav'
+import { getClient } from '../dav/client.js'
+import logger from './logger.js'
+import { parseXML } from 'webdav'
import {
slotsToVavailability,
@@ -58,7 +42,7 @@ export async function findScheduleInboxAvailability() {
</x0:propfind>`,
})
- const xml = await parseXML(response.data)
+ const xml = await parseXML(await response.text())
if (!xml) {
return undefined
diff --git a/apps/dav/src/service/ExampleEventService.js b/apps/dav/src/service/ExampleEventService.js
new file mode 100644
index 00000000000..a39e3641bd9
--- /dev/null
+++ b/apps/dav/src/service/ExampleEventService.js
@@ -0,0 +1,43 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { generateUrl } from '@nextcloud/router'
+import axios from '@nextcloud/axios'
+
+/**
+ * Configure the creation of example events on a user's first login.
+ *
+ * @param {boolean} enable Whether to enable or disable the feature.
+ * @return {Promise<void>}
+ */
+export async function setCreateExampleEvent(enable) {
+ const url = generateUrl('/apps/dav/api/exampleEvent/enable')
+ await axios.post(url, {
+ enable,
+ })
+}
+
+/**
+ * Upload a custom example event.
+ *
+ * @param {string} ics The ICS data of the event.
+ * @return {Promise<void>}
+ */
+export async function uploadExampleEvent(ics) {
+ const url = generateUrl('/apps/dav/api/exampleEvent/event')
+ await axios.post(url, {
+ ics,
+ })
+}
+
+/**
+ * Delete a previously uploaded custom example event.
+ *
+ * @return {Promise<void>}
+ */
+export async function deleteExampleEvent() {
+ const url = generateUrl('/apps/dav/api/exampleEvent/event')
+ await axios.delete(url)
+}
diff --git a/apps/dav/src/service/PreferenceService.js b/apps/dav/src/service/PreferenceService.js
index 6b8d29029b5..39b2c067c61 100644
--- a/apps/dav/src/service/PreferenceService.js
+++ b/apps/dav/src/service/PreferenceService.js
@@ -1,22 +1,6 @@
/**
- * @copyright 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import axios from '@nextcloud/axios'
@@ -33,7 +17,7 @@ export async function enableUserStatusAutomation() {
}),
{
configValue: 'yes',
- }
+ },
)
}
@@ -45,6 +29,6 @@ export async function disableUserStatusAutomation() {
generateOcsUrl('/apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', {
appId: 'dav',
configKey: 'user_status_automation',
- })
+ }),
)
}
diff --git a/apps/dav/src/service/logger.js b/apps/dav/src/service/logger.js
index dd6ec9163a6..cb7f1a95103 100644
--- a/apps/dav/src/service/logger.js
+++ b/apps/dav/src/service/logger.js
@@ -1,22 +1,6 @@
-/*
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getLoggerBuilder } from '@nextcloud/logger'
diff --git a/apps/dav/src/settings-example-content.js b/apps/dav/src/settings-example-content.js
new file mode 100644
index 00000000000..fdeba642a67
--- /dev/null
+++ b/apps/dav/src/settings-example-content.js
@@ -0,0 +1,18 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import Vue from 'vue'
+import { translate } from '@nextcloud/l10n'
+import ExampleContentSettingsSection from './views/ExampleContentSettingsSection.vue'
+
+Vue.mixin({
+ methods: {
+ t: translate,
+ $t: translate,
+ },
+})
+
+const View = Vue.extend(ExampleContentSettingsSection);
+
+(new View({})).$mount('#settings-example-content')
diff --git a/apps/dav/src/settings-personal-availability.js b/apps/dav/src/settings-personal-availability.js
index b0d6b19aa8a..b24144b81f0 100644
--- a/apps/dav/src/settings-personal-availability.js
+++ b/apps/dav/src/settings-personal-availability.js
@@ -1,6 +1,10 @@
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
import Vue from 'vue'
import { translate } from '@nextcloud/l10n'
-import Availability from './views/Availability'
+import Availability from './views/Availability.vue'
Vue.prototype.$t = translate
diff --git a/apps/dav/src/settings.js b/apps/dav/src/settings.js
index 6744f22ad23..c69a8b03614 100644
--- a/apps/dav/src/settings.js
+++ b/apps/dav/src/settings.js
@@ -1,7 +1,11 @@
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
import Vue from 'vue'
import { loadState } from '@nextcloud/initial-state'
import { translate } from '@nextcloud/l10n'
-import CalDavSettings from './views/CalDavSettings'
+import CalDavSettings from './views/CalDavSettings.vue'
Vue.prototype.$t = translate
@@ -13,12 +17,12 @@ const CalDavSettingsView = new View({
sendInvitations: loadState('dav', 'sendInvitations'),
generateBirthdayCalendar: loadState(
'dav',
- 'generateBirthdayCalendar'
+ 'generateBirthdayCalendar',
),
sendEventReminders: loadState('dav', 'sendEventReminders'),
- sendEventRemindersToSharedGroupMembers: loadState(
+ sendEventRemindersToSharedUsers: loadState(
'dav',
- 'sendEventRemindersToSharedGroupMembers'
+ 'sendEventRemindersToSharedUsers',
),
sendEventRemindersPush: loadState('dav', 'sendEventRemindersPush'),
}
diff --git a/apps/dav/src/utils/date.js b/apps/dav/src/utils/date.js
new file mode 100644
index 00000000000..de1d65e310d
--- /dev/null
+++ b/apps/dav/src/utils/date.js
@@ -0,0 +1,17 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/**
+ * Format a date as 'YYYY-MM-DD'.
+ *
+ * @param {Date} date A date instance to format.
+ * @return {string} 'YYYY-MM-DD'
+ */
+export function formatDateAsYMD(date) {
+ const year = date.getFullYear()
+ const month = (date.getMonth() + 1).toString().padStart(2, '0')
+ const day = date.getDate().toString().padStart(2, '0')
+ return `${year}-${month}-${day}`
+}
diff --git a/apps/dav/src/views/Availability.vue b/apps/dav/src/views/Availability.vue
index e0128a59e0a..1922f5b706e 100644
--- a/apps/dav/src/views/Availability.vue
+++ b/apps/dav/src/views/Availability.vue
@@ -1,208 +1,40 @@
+<!--
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <NcSettingsSection :title="$t('dav', 'Availability')"
- :description="$t('dav', 'If you configure your working hours, other users will see when you are out of office when they book a meeting.')">
- <div class="time-zone">
- <strong>
- {{ $t('dav', 'Time zone:') }}
- </strong>
- <span class="time-zone-text">
- <NcTimezonePicker v-model="timezone" />
- </span>
- </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')" />
-
- <NcCheckboxRadioSwitch :checked.sync="automated">
- {{ $t('dav', 'Automatically set user status to "Do not disturb" outside of availability to mute all notifications.') }}
- </NcCheckboxRadioSwitch>
-
- <NcButton :disabled="loading || saving"
- type="primary"
- @click="save">
- {{ $t('dav', 'Save') }}
- </NcButton>
- </NcSettingsSection>
+ <div>
+ <NcSettingsSection id="availability"
+ :name="$t('dav', 'Availability')"
+ :description="$t('dav', 'If you configure your working hours, other people will see when you are out of office when they book a meeting.')">
+ <AvailabilityForm />
+ </NcSettingsSection>
+ <NcSettingsSection v-if="!hideAbsenceSettings"
+ id="absence"
+ :name="$t('dav', 'Absence')"
+ :description="$t('dav', 'Configure your next absence period.')">
+ <AbsenceForm />
+ </NcSettingsSection>
+ </div>
</template>
<script>
-import { CalendarAvailability } from '@nextcloud/calendar-availability-vue'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
+import AbsenceForm from '../components/AbsenceForm.vue'
+import AvailabilityForm from '../components/AvailabilityForm.vue'
import { loadState } from '@nextcloud/initial-state'
-import {
- showError,
- showSuccess,
-} from '@nextcloud/dialogs'
-import {
- findScheduleInboxAvailability,
- getEmptySlots,
- saveScheduleInboxAvailability,
-} from '../service/CalendarService'
-import {
- enableUserStatusAutomation,
- disableUserStatusAutomation,
-} from '../service/PreferenceService'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch'
-import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection'
-import NcTimezonePicker from '@nextcloud/vue/dist/Components/NcTimezonePicker'
export default {
name: 'Availability',
components: {
- NcButton,
- NcCheckboxRadioSwitch,
- CalendarAvailability,
NcSettingsSection,
- NcTimezonePicker,
+ AbsenceForm,
+ AvailabilityForm,
},
data() {
- // Try to determine the current timezone, and fall back to UTC otherwise
- const defaultTimezoneId = (new Intl.DateTimeFormat())?.resolvedOptions()?.timeZone ?? 'UTC'
-
return {
- loading: true,
- saving: false,
- timezone: defaultTimezoneId,
- slots: getEmptySlots(),
- automated: loadState('dav', 'user_status_automation') === 'yes',
+ hideAbsenceSettings: loadState('dav', 'hide_absence_settings', true),
}
},
- async mounted() {
- try {
- 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)
- }
- } catch (e) {
- console.error('could not load existing availability', e)
-
- showError(t('dav', 'Failed to load availability'))
- } finally {
- this.loading = false
- }
- },
- methods: {
- async save() {
- try {
- this.saving = true
-
- await saveScheduleInboxAvailability(this.slots, this.timezone)
- if (this.automated) {
- await enableUserStatusAutomation()
- } else {
- await disableUserStatusAutomation()
- }
-
- showSuccess(t('dav', 'Saved availability'))
- } catch (e) {
- console.error('could not save availability', e)
-
- showError(t('dav', 'Failed to save availability'))
- } finally {
- this.saving = false
- }
- },
- },
}
</script>
-
-<style lang="scss" scoped>
-.availability-day {
- padding: 0 10px 0 10px;
- position: absolute;
-}
-.availability-slots {
- display: flex;
- white-space: nowrap;
-}
-.availability-slot {
- display: flex;
- flex-direction: row;
- align-items: center;
-}
-.availability-slot-group {
- display: flex;
- flex-direction: column;
-}
-::v-deep .mx-input-wrapper {
- width: 85px;
-}
-::v-deep .mx-datepicker {
- width: 97px;
-}
-::v-deep .multiselect {
- border: 1px solid var(--color-border-dark);
- width: 120px;
-}
-.time-zone {
- padding: 32px 12px 12px 0;
-}
-.grid-table {
- display: grid;
- margin-bottom: 32px;
- grid-column-gap: 24px;
- grid-row-gap: 6px;
- grid-template-columns: min-content min-content min-content;
-}
-.button {
- align-self: flex-end;
-}
-.label-weekday {
- position: relative;
- display: inline-flex;
- padding-top: 4px;
-}
-.delete-slot {
- background-color: transparent;
- border: none;
- padding-bottom: 12px;
- opacity: .5;
- &:hover {
- opacity: 1;
- }
-}
-
-.add-another {
- background-color: transparent;
- border: none;
- opacity: .5;
- display: inline-flex;
- padding: 0;
- margin: 0;
- margin-bottom: 3px;
-
- &:hover {
- opacity: 1;
- }
-}
-.to-text {
- padding-right: 12px;
-}
-.time-zone-text{
- padding-left: 22px;
-}
-.empty-content {
- color: var(--color-text-lighter);
- margin-top: 4px;
-}
-
-</style>
diff --git a/apps/dav/src/views/CalDavSettings.spec.js b/apps/dav/src/views/CalDavSettings.spec.js
index 5c81c6259a1..7a4345b3ddf 100644
--- a/apps/dav/src/views/CalDavSettings.spec.js
+++ b/apps/dav/src/views/CalDavSettings.spec.js
@@ -1,38 +1,35 @@
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
import { render } from '@testing-library/vue'
-import CalDavSettings from './CalDavSettings'
-// eslint-disable-next-line no-unused-vars
-import { generateUrl } from '@nextcloud/router'
+import { beforeEach, describe, expect, test, vi } from 'vitest'
-jest.mock('@nextcloud/axios')
-jest.mock('@nextcloud/router', () => {
+import CalDavSettings from './CalDavSettings.vue'
+
+vi.mock('@nextcloud/axios')
+vi.mock('@nextcloud/router', () => {
return {
generateUrl(url) {
return url
},
}
})
-jest.mock('@nextcloud/initial-state', () => {
+vi.mock('@nextcloud/initial-state', () => {
return {
- loadState: jest.fn(() => 'https://docs.nextcloud.com/server/23/go.php?to=user-sync-calendars'),
+ loadState: vi.fn(() => 'https://docs.nextcloud.com/server/23/go.php?to=user-sync-calendars'),
}
})
describe('CalDavSettings', () => {
- const originalOC = global.OC
- const originalOCP = global.OCP
-
beforeEach(() => {
- global.OC = { requestToken: 'secret' }
- global.OCP = {
+ window.OC = { requestToken: 'secret' }
+ window.OCP = {
AppConfig: {
- setValue: jest.fn(),
+ setValue: vi.fn(),
},
}
})
- afterAll(() => {
- global.OC = originalOC
- global.OCP = originalOCP
- })
test('interactions', async () => {
const TLUtils = render(
@@ -43,34 +40,34 @@ describe('CalDavSettings', () => {
sendInvitations: true,
generateBirthdayCalendar: true,
sendEventReminders: true,
- sendEventRemindersToSharedGroupMembers: true,
+ sendEventRemindersToSharedUsers: true,
sendEventRemindersPush: true,
}
},
},
Vue => {
- Vue.prototype.$t = jest.fn((app, text) => text)
- }
+ Vue.prototype.$t = vi.fn((app, text) => text)
+ },
)
expect(TLUtils.container).toMatchSnapshot()
const sendInvitations = TLUtils.getByLabelText(
- 'Send invitations to attendees'
+ 'Send invitations to attendees',
)
expect(sendInvitations).toBeChecked()
const generateBirthdayCalendar = TLUtils.getByLabelText(
- 'Automatically generate a birthday calendar'
+ 'Automatically generate a birthday calendar',
)
expect(generateBirthdayCalendar).toBeChecked()
const sendEventReminders = TLUtils.getByLabelText(
- 'Send notifications for events'
+ 'Send notifications for events',
)
expect(sendEventReminders).toBeChecked()
- const sendEventRemindersToSharedGroupMembers = TLUtils.getByLabelText(
- 'Send reminder notifications to calendar sharees as well'
+ const sendEventRemindersToSharedUsers = TLUtils.getByLabelText(
+ 'Send reminder notifications to calendar sharees as well',
)
- expect(sendEventRemindersToSharedGroupMembers).toBeChecked()
+ expect(sendEventRemindersToSharedUsers).toBeChecked()
const sendEventRemindersPush = TLUtils.getByLabelText(
- 'Enable notifications for events via push'
+ 'Enable notifications for events via push',
)
expect(sendEventRemindersPush).toBeChecked()
@@ -114,7 +111,7 @@ describe('CalDavSettings', () => {
'no'
)
- expect(sendEventRemindersToSharedGroupMembers).toBeDisabled()
+ expect(sendEventRemindersToSharedUsers).toBeDisabled()
expect(sendEventRemindersPush).toBeDisabled()
OCP.AppConfig.setValue.mockClear()
@@ -126,7 +123,7 @@ describe('CalDavSettings', () => {
'yes'
)
- expect(sendEventRemindersToSharedGroupMembers).toBeEnabled()
+ expect(sendEventRemindersToSharedUsers).toBeEnabled()
expect(sendEventRemindersPush).toBeEnabled()
*/
})
diff --git a/apps/dav/src/views/CalDavSettings.vue b/apps/dav/src/views/CalDavSettings.vue
index 776f32bff36..6be67cf93ff 100644
--- a/apps/dav/src/views/CalDavSettings.vue
+++ b/apps/dav/src/views/CalDavSettings.vue
@@ -1,5 +1,9 @@
+<!--
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <NcSettingsSection :title="$t('dav', 'Calendar server')"
+ <NcSettingsSection :name="$t('dav', 'Calendar server')"
:doc-url="userSyncCalendarsDocUrl">
<!-- Can use v-html as:
- $t passes the translated string through DOMPurify.sanitize,
@@ -51,7 +55,7 @@
</p>
<p class="indented">
<NcCheckboxRadioSwitch id="caldavSendEventRemindersToSharedGroupMembers"
- :checked.sync="sendEventRemindersToSharedGroupMembers"
+ :checked.sync="sendEventRemindersToSharedUsers"
type="switch"
:disabled="!sendEventReminders">
{{ $t('dav', 'Send reminder notifications to calendar sharees as well' ) }}
@@ -75,8 +79,8 @@
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
-import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
const userSyncCalendarsDocUrl = loadState('dav', 'userSyncCalendarsDocUrl', '#')
@@ -124,17 +128,17 @@ export default {
OCP.AppConfig.setValue(
'dav',
'sendInvitations',
- value ? 'yes' : 'no'
+ value ? 'yes' : 'no',
)
},
sendEventReminders(value) {
OCP.AppConfig.setValue('dav', 'sendEventReminders', value ? 'yes' : 'no')
},
- sendEventRemindersToSharedGroupMembers(value) {
+ sendEventRemindersToSharedUsers(value) {
OCP.AppConfig.setValue(
'dav',
- 'sendEventRemindersToSharedGroupMembers',
- value ? 'yes' : 'no'
+ 'sendEventRemindersToSharedUsers',
+ value ? 'yes' : 'no',
)
},
sendEventRemindersPush(value) {
@@ -146,12 +150,13 @@ export default {
<style scoped>
.indented {
- padding-left: 28px;
+ padding-inline-start: 28px;
}
/** Use deep selector to affect v-html */
- * >>> a {
+ * :deep(a) {
text-decoration: underline;
}
+
.settings-hint {
margin-top: -.2em;
margin-bottom: 1em;
diff --git a/apps/dav/src/views/ExampleContentSettingsSection.vue b/apps/dav/src/views/ExampleContentSettingsSection.vue
new file mode 100644
index 00000000000..3ee2d9e8648
--- /dev/null
+++ b/apps/dav/src/views/ExampleContentSettingsSection.vue
@@ -0,0 +1,38 @@
+<!--
+ - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <NcSettingsSection id="example-content"
+ :name="$t('dav', 'Example content')"
+ class="example-content-setting"
+ :description="$t('dav', 'Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content.')">
+ <ExampleContactSettings v-if="hasContactsApp" />
+ <ExampleEventSettings v-if="hasCalendarApp" />
+ </NcSettingsSection>
+</template>
+
+<script>
+import { loadState } from '@nextcloud/initial-state'
+import { NcSettingsSection } from '@nextcloud/vue'
+import ExampleEventSettings from '../components/ExampleEventSettings.vue'
+import ExampleContactSettings from '../components/ExampleContactSettings.vue'
+
+export default {
+ name: 'ExampleContentSettingsSection',
+ components: {
+ NcSettingsSection,
+ ExampleContactSettings,
+ ExampleEventSettings,
+ },
+ computed: {
+ hasContactsApp() {
+ return loadState('dav', 'contactsEnabled')
+ },
+ hasCalendarApp() {
+ return loadState('dav', 'calendarEnabled')
+ },
+ },
+}
+</script>
diff --git a/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap b/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap
index 5f4f0d1407d..fdbe09f5b5e 100644
--- a/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap
+++ b/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap
@@ -1,30 +1,30 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[`CalDavSettings interactions 1`] = `
+exports[`CalDavSettings > interactions 1`] = `
<div>
<div
class="settings-section settings-section--limit-width"
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
<h2
- class="settings-section__title"
- data-v-97259c4c=""
+ class="settings-section__name"
+ data-v-6f6953b5=""
>
-
- Calendar server
-
+ Calendar server
<a
aria-label="External documentation for Calendar server"
class="settings-section__info"
- data-v-97259c4c=""
+ data-v-6f6953b5=""
href="https://docs.nextcloud.com/server/23/go.php?to=user-sync-calendars"
- role="note"
+ rel="noreferrer nofollow"
+ target="_blank"
title="External documentation for Calendar server"
>
<span
aria-hidden="true"
class="material-design-icon help-circle-icon"
- data-v-97259c4c=""
+ data-v-6f6953b5=""
role="img"
>
<svg
@@ -43,12 +43,11 @@ exports[`CalDavSettings interactions 1`] = `
</span>
</a>
</h2>
-
<!---->
-
<p
class="settings-hint"
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
Also install the
<a
@@ -67,57 +66,69 @@ exports[`CalDavSettings interactions 1`] = `
</a>
.
</p>
-
<p
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
<span
class="checkbox-radio-switch checkbox-radio-switch-switch checkbox-radio-switch--checked"
- data-v-133c8c94=""
- data-v-97259c4c=""
- style="--icon-size: 36px;"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ data-v-f275cf53=""
+ style="--icon-size: 36px; --icon-height: 16px;"
>
- <label
- class="checkbox-radio-switch__label"
- data-v-133c8c94=""
- for="caldavSendInvitations"
+ <input
+ aria-labelledby="caldavSendInvitations-label"
+ class="checkbox-radio-switch__input"
+ data-v-f275cf53=""
+ id="caldavSendInvitations"
+ type="checkbox"
+ value=""
+ />
+ <span
+ class="checkbox-content checkbox-radio-switch__content checkbox-content-switch checkbox-content--has-text"
+ data-v-3714b019=""
+ data-v-f275cf53=""
+ id="caldavSendInvitations-label"
>
- <input
- class="checkbox-radio-switch__input"
- data-v-133c8c94=""
- id="caldavSendInvitations"
- type="checkbox"
- value=""
- />
-
<span
aria-hidden="true"
- class="material-design-icon toggle-switch-icon checkbox-radio-switch__icon"
- data-v-133c8c94=""
- role="img"
+ class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon"
+ data-v-3714b019=""
+ inert="inert"
>
- <svg
- class="material-design-icon__svg"
- fill="currentColor"
- height="36"
- viewBox="0 0 24 24"
- width="36"
+ <span
+ aria-hidden="true"
+ class="material-design-icon toggle-switch-icon"
+ data-v-3714b019=""
+ role="img"
>
- <path
- d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ <svg
+ class="material-design-icon__svg"
+ fill="currentColor"
+ height="36"
+ viewBox="0 0 24 24"
+ width="36"
>
- <!---->
- </path>
- </svg>
+ <path
+ d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ >
+ <!---->
+ </path>
+ </svg>
+ </span>
</span>
-
- Send invitations to attendees
-
- </label>
+ <span
+ class="checkbox-content__text checkbox-radio-switch__text"
+ data-v-3714b019=""
+ >
+ Send invitations to attendees
+ </span>
+ </span>
</span>
-
<em
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
Please make sure to properly set up
<a
@@ -128,126 +139,146 @@ exports[`CalDavSettings interactions 1`] = `
.
</em>
</p>
-
<p
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
<span
class="checkbox-radio-switch checkbox checkbox-radio-switch-switch checkbox-radio-switch--checked"
- data-v-133c8c94=""
- data-v-97259c4c=""
- style="--icon-size: 36px;"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ data-v-f275cf53=""
+ style="--icon-size: 36px; --icon-height: 16px;"
>
- <label
- class="checkbox-radio-switch__label"
- data-v-133c8c94=""
- for="caldavGenerateBirthdayCalendar"
+ <input
+ aria-labelledby="caldavGenerateBirthdayCalendar-label"
+ class="checkbox-radio-switch__input"
+ data-v-f275cf53=""
+ id="caldavGenerateBirthdayCalendar"
+ type="checkbox"
+ value=""
+ />
+ <span
+ class="checkbox-content checkbox-radio-switch__content checkbox-content-switch checkbox-content--has-text"
+ data-v-3714b019=""
+ data-v-f275cf53=""
+ id="caldavGenerateBirthdayCalendar-label"
>
- <input
- class="checkbox-radio-switch__input"
- data-v-133c8c94=""
- id="caldavGenerateBirthdayCalendar"
- type="checkbox"
- value=""
- />
-
<span
aria-hidden="true"
- class="material-design-icon toggle-switch-icon checkbox-radio-switch__icon"
- data-v-133c8c94=""
- role="img"
+ class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon"
+ data-v-3714b019=""
+ inert="inert"
>
- <svg
- class="material-design-icon__svg"
- fill="currentColor"
- height="36"
- viewBox="0 0 24 24"
- width="36"
+ <span
+ aria-hidden="true"
+ class="material-design-icon toggle-switch-icon"
+ data-v-3714b019=""
+ role="img"
>
- <path
- d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ <svg
+ class="material-design-icon__svg"
+ fill="currentColor"
+ height="36"
+ viewBox="0 0 24 24"
+ width="36"
>
- <!---->
- </path>
- </svg>
+ <path
+ d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ >
+ <!---->
+ </path>
+ </svg>
+ </span>
</span>
-
- Automatically generate a birthday calendar
-
- </label>
+ <span
+ class="checkbox-content__text checkbox-radio-switch__text"
+ data-v-3714b019=""
+ >
+ Automatically generate a birthday calendar
+ </span>
+ </span>
</span>
-
<em
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
-
- Birthday calendars will be generated by a background job.
-
+ Birthday calendars will be generated by a background job.
</em>
-
<br
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
/>
-
<em
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
-
- Hence they will not be available immediately after enabling but will show up after some time.
-
+ Hence they will not be available immediately after enabling but will show up after some time.
</em>
</p>
-
<p
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
<span
class="checkbox-radio-switch checkbox-radio-switch-switch checkbox-radio-switch--checked"
- data-v-133c8c94=""
- data-v-97259c4c=""
- style="--icon-size: 36px;"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ data-v-f275cf53=""
+ style="--icon-size: 36px; --icon-height: 16px;"
>
- <label
- class="checkbox-radio-switch__label"
- data-v-133c8c94=""
- for="caldavSendEventReminders"
+ <input
+ aria-labelledby="caldavSendEventReminders-label"
+ class="checkbox-radio-switch__input"
+ data-v-f275cf53=""
+ id="caldavSendEventReminders"
+ type="checkbox"
+ value=""
+ />
+ <span
+ class="checkbox-content checkbox-radio-switch__content checkbox-content-switch checkbox-content--has-text"
+ data-v-3714b019=""
+ data-v-f275cf53=""
+ id="caldavSendEventReminders-label"
>
- <input
- class="checkbox-radio-switch__input"
- data-v-133c8c94=""
- id="caldavSendEventReminders"
- type="checkbox"
- value=""
- />
-
<span
aria-hidden="true"
- class="material-design-icon toggle-switch-icon checkbox-radio-switch__icon"
- data-v-133c8c94=""
- role="img"
+ class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon"
+ data-v-3714b019=""
+ inert="inert"
>
- <svg
- class="material-design-icon__svg"
- fill="currentColor"
- height="36"
- viewBox="0 0 24 24"
- width="36"
+ <span
+ aria-hidden="true"
+ class="material-design-icon toggle-switch-icon"
+ data-v-3714b019=""
+ role="img"
>
- <path
- d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ <svg
+ class="material-design-icon__svg"
+ fill="currentColor"
+ height="36"
+ viewBox="0 0 24 24"
+ width="36"
>
- <!---->
- </path>
- </svg>
+ <path
+ d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ >
+ <!---->
+ </path>
+ </svg>
+ </span>
+ </span>
+ <span
+ class="checkbox-content__text checkbox-radio-switch__text"
+ data-v-3714b019=""
+ >
+ Send notifications for events
</span>
-
- Send notifications for events
-
- </label>
+ </span>
</span>
-
<em
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
Please make sure to properly set up
<a
@@ -257,125 +288,145 @@ exports[`CalDavSettings interactions 1`] = `
</a>
.
</em>
-
<br
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
/>
-
<em
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
-
- Notifications are sent via background jobs, so these must occur often enough.
-
+ Notifications are sent via background jobs, so these must occur often enough.
</em>
</p>
-
<p
class="indented"
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
<span
class="checkbox-radio-switch checkbox-radio-switch-switch checkbox-radio-switch--checked"
- data-v-133c8c94=""
- data-v-97259c4c=""
- style="--icon-size: 36px;"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ data-v-f275cf53=""
+ style="--icon-size: 36px; --icon-height: 16px;"
>
- <label
- class="checkbox-radio-switch__label"
- data-v-133c8c94=""
- for="caldavSendEventRemindersToSharedGroupMembers"
+ <input
+ aria-labelledby="caldavSendEventRemindersToSharedGroupMembers-label"
+ class="checkbox-radio-switch__input"
+ data-v-f275cf53=""
+ id="caldavSendEventRemindersToSharedGroupMembers"
+ type="checkbox"
+ value=""
+ />
+ <span
+ class="checkbox-content checkbox-radio-switch__content checkbox-content-switch checkbox-content--has-text"
+ data-v-3714b019=""
+ data-v-f275cf53=""
+ id="caldavSendEventRemindersToSharedGroupMembers-label"
>
- <input
- class="checkbox-radio-switch__input"
- data-v-133c8c94=""
- id="caldavSendEventRemindersToSharedGroupMembers"
- type="checkbox"
- value=""
- />
-
<span
aria-hidden="true"
- class="material-design-icon toggle-switch-icon checkbox-radio-switch__icon"
- data-v-133c8c94=""
- role="img"
+ class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon"
+ data-v-3714b019=""
+ inert="inert"
>
- <svg
- class="material-design-icon__svg"
- fill="currentColor"
- height="36"
- viewBox="0 0 24 24"
- width="36"
+ <span
+ aria-hidden="true"
+ class="material-design-icon toggle-switch-icon"
+ data-v-3714b019=""
+ role="img"
>
- <path
- d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ <svg
+ class="material-design-icon__svg"
+ fill="currentColor"
+ height="36"
+ viewBox="0 0 24 24"
+ width="36"
>
- <!---->
- </path>
- </svg>
+ <path
+ d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ >
+ <!---->
+ </path>
+ </svg>
+ </span>
+ </span>
+ <span
+ class="checkbox-content__text checkbox-radio-switch__text"
+ data-v-3714b019=""
+ >
+ Send reminder notifications to calendar sharees as well
</span>
-
- Send reminder notifications to calendar sharees as well
-
- </label>
+ </span>
</span>
-
<em
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
-
- Reminders are always sent to organizers and attendees.
-
+ Reminders are always sent to organizers and attendees.
</em>
</p>
-
<p
class="indented"
- data-v-97259c4c=""
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
<span
class="checkbox-radio-switch checkbox-radio-switch-switch checkbox-radio-switch--checked"
- data-v-133c8c94=""
- data-v-97259c4c=""
- style="--icon-size: 36px;"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ data-v-f275cf53=""
+ style="--icon-size: 36px; --icon-height: 16px;"
>
- <label
- class="checkbox-radio-switch__label"
- data-v-133c8c94=""
- for="caldavSendEventRemindersPush"
+ <input
+ aria-labelledby="caldavSendEventRemindersPush-label"
+ class="checkbox-radio-switch__input"
+ data-v-f275cf53=""
+ id="caldavSendEventRemindersPush"
+ type="checkbox"
+ value=""
+ />
+ <span
+ class="checkbox-content checkbox-radio-switch__content checkbox-content-switch checkbox-content--has-text"
+ data-v-3714b019=""
+ data-v-f275cf53=""
+ id="caldavSendEventRemindersPush-label"
>
- <input
- class="checkbox-radio-switch__input"
- data-v-133c8c94=""
- id="caldavSendEventRemindersPush"
- type="checkbox"
- value=""
- />
-
<span
aria-hidden="true"
- class="material-design-icon toggle-switch-icon checkbox-radio-switch__icon"
- data-v-133c8c94=""
- role="img"
+ class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon"
+ data-v-3714b019=""
+ inert="inert"
>
- <svg
- class="material-design-icon__svg"
- fill="currentColor"
- height="36"
- viewBox="0 0 24 24"
- width="36"
+ <span
+ aria-hidden="true"
+ class="material-design-icon toggle-switch-icon"
+ data-v-3714b019=""
+ role="img"
>
- <path
- d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ <svg
+ class="material-design-icon__svg"
+ fill="currentColor"
+ height="36"
+ viewBox="0 0 24 24"
+ width="36"
>
- <!---->
- </path>
- </svg>
+ <path
+ d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ >
+ <!---->
+ </path>
+ </svg>
+ </span>
+ </span>
+ <span
+ class="checkbox-content__text checkbox-radio-switch__text"
+ data-v-3714b019=""
+ >
+ Enable notifications for events via push
</span>
-
- Enable notifications for events via push
-
- </label>
+ </span>
</span>
</p>
</div>
diff --git a/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap.license b/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap.license
new file mode 100644
index 00000000000..b8f52265f1f
--- /dev/null
+++ b/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file