diff options
Diffstat (limited to 'apps/dav/src/components')
-rw-r--r-- | apps/dav/src/components/AbsenceForm.vue | 274 | ||||
-rw-r--r-- | apps/dav/src/components/AvailabilityForm.vue | 223 | ||||
-rw-r--r-- | apps/dav/src/components/ExampleContactSettings.vue | 172 | ||||
-rw-r--r-- | apps/dav/src/components/ExampleContentDownloadButton.vue | 57 | ||||
-rw-r--r-- | apps/dav/src/components/ExampleEventSettings.vue | 217 |
5 files changed, 943 insertions, 0 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> |