diff options
Diffstat (limited to 'apps/dav/src')
-rw-r--r-- | apps/dav/src/components/AbsenceForm.vue | 42 | ||||
-rw-r--r-- | apps/dav/src/components/AvailabilityForm.vue | 29 | ||||
-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 | ||||
-rw-r--r-- | apps/dav/src/dav/client.js | 2 | ||||
-rw-r--r-- | apps/dav/src/service/ExampleEventService.js | 43 | ||||
-rw-r--r-- | apps/dav/src/service/PreferenceService.js | 4 | ||||
-rw-r--r-- | apps/dav/src/settings-example-content.js | 18 | ||||
-rw-r--r-- | apps/dav/src/settings.js | 4 | ||||
-rw-r--r-- | apps/dav/src/views/Availability.vue | 6 | ||||
-rw-r--r-- | apps/dav/src/views/CalDavSettings.spec.js | 39 | ||||
-rw-r--r-- | apps/dav/src/views/CalDavSettings.vue | 13 | ||||
-rw-r--r-- | apps/dav/src/views/ExampleContentSettingsSection.vue | 38 | ||||
-rw-r--r-- | apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap | 189 |
15 files changed, 706 insertions, 167 deletions
diff --git a/apps/dav/src/components/AbsenceForm.vue b/apps/dav/src/components/AbsenceForm.vue index 33f1483a7fb..5350c04a565 100644 --- a/apps/dav/src/components/AbsenceForm.vue +++ b/apps/dav/src/components/AbsenceForm.vue @@ -26,8 +26,7 @@ :clear-search-on-blur="() => false" :user-select="true" :options="options" - @search="asyncFind" - > + @search="asyncFind"> <template #no-options="{ search }"> {{ search ?$t('dav', 'No results.') : $t('dav', 'Start typing.') }} </template> @@ -51,22 +50,22 @@ </template> <script> -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' -import NcTextArea from '@nextcloud/vue/dist/Components/NcTextArea.js' -import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js' -import NcDateTimePickerNative from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js' -import { generateOcsUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' -import debounce from 'debounce' -import axios from '@nextcloud/axios' -import { formatDateAsYMD } from '../utils/date.js' -import { loadState } from '@nextcloud/initial-state' import { showError, showSuccess } from '@nextcloud/dialogs' -import { Type as ShareTypes } from '@nextcloud/sharing' - +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: { @@ -74,17 +73,17 @@ export default { NcTextField, NcTextArea, NcDateTimePickerNative, - NcSelect + NcSelect, }, data() { - const { firstDay, lastDay, status, message ,replacementUserId ,replacementUserDisplayName } = loadState('dav', 'absence', {}) + 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: replacementUserId , + replacementUserId, replacementUser: replacementUserId ? { user: replacementUserId, displayName: replacementUserDisplayName } : null, searchLoading: false, options: [], @@ -126,10 +125,10 @@ export default { return { user: result.uuid || result.value.shareWith, displayName: result.name || result.label, - subtitle: result.dsc | '' + subtitle: result.dsc | '', } }, - + async asyncFind(query) { this.searchLoading = true await this.debounceGetSuggestions(query.trim()) @@ -142,7 +141,7 @@ export default { async getSuggestions(search) { const shareType = [ - ShareTypes.SHARE_TYPE_USER, + ShareType.User, ] let request = null @@ -221,7 +220,6 @@ export default { status: this.status, message: this.message, replacementUserId: this.replacementUser?.user ?? null, - replacementUserDisplayName: this.replacementUser?.displayName ?? null, }) showSuccess(this.$t('dav', 'Absence saved')) } catch (error) { @@ -262,7 +260,7 @@ export default { &__picker { flex: 1 auto; - ::v-deep .native-datetime-picker--input { + :deep(.native-datetime-picker--input) { margin-bottom: 0; } } diff --git a/apps/dav/src/components/AvailabilityForm.vue b/apps/dav/src/components/AvailabilityForm.vue index 307c62b8c35..d53c092be9d 100644 --- a/apps/dav/src/components/AvailabilityForm.vue +++ b/apps/dav/src/components/AvailabilityForm.vue @@ -58,9 +58,9 @@ import { enableUserStatusAutomation, disableUserStatusAutomation, } from '../service/PreferenceService.js' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' -import NcTimezonePicker from '@nextcloud/vue/dist/Components/NcTimezonePicker.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', @@ -139,41 +139,50 @@ export default { 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: 32px 12px 12px 0; - display: flex; - flex-wrap: wrap; + padding-block: 32px 12px; + padding-inline: 0 12px; + display: flex; + flex-wrap: wrap; &__heading { - margin-right: calc(var(--default-grid-baseline) * 2); + 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; @@ -182,9 +191,11 @@ export default { grid-template-columns: min-content auto min-content; max-width: 500px; } + .button { align-self: flex-end; } + :deep(.label-weekday) { position: relative; display: inline-flex; @@ -201,12 +212,12 @@ export default { } .to-text { - padding-right: 12px; + padding-inline-end: 12px; } .empty-content { color: var(--color-text-lighter); - margin-top: 4px; + 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 a4e41114862..d286f6f48d6 100644 --- a/apps/dav/src/dav/client.js +++ b/apps/dav/src/dav/client.js @@ -27,5 +27,5 @@ export const getClient = memoize((service) => { onRequestTokenUpdate(setHeaders) setHeaders(getRequestToken()) - return client; + return client }) 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 f03d53a10cc..39b2c067c61 100644 --- a/apps/dav/src/service/PreferenceService.js +++ b/apps/dav/src/service/PreferenceService.js @@ -17,7 +17,7 @@ export async function enableUserStatusAutomation() { }), { configValue: 'yes', - } + }, ) } @@ -29,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/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.js b/apps/dav/src/settings.js index bb9b7107a25..c69a8b03614 100644 --- a/apps/dav/src/settings.js +++ b/apps/dav/src/settings.js @@ -17,12 +17,12 @@ const CalDavSettingsView = new View({ sendInvitations: loadState('dav', 'sendInvitations'), generateBirthdayCalendar: loadState( 'dav', - 'generateBirthdayCalendar' + 'generateBirthdayCalendar', ), sendEventReminders: loadState('dav', 'sendEventReminders'), sendEventRemindersToSharedUsers: loadState( 'dav', - 'sendEventRemindersToSharedUsers' + 'sendEventRemindersToSharedUsers', ), sendEventRemindersPush: loadState('dav', 'sendEventRemindersPush'), } diff --git a/apps/dav/src/views/Availability.vue b/apps/dav/src/views/Availability.vue index f5415909429..1922f5b706e 100644 --- a/apps/dav/src/views/Availability.vue +++ b/apps/dav/src/views/Availability.vue @@ -4,11 +4,13 @@ --> <template> <div> - <NcSettingsSection :name="$t('dav', 'Availability')" + <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 /> @@ -17,7 +19,7 @@ </template> <script> -import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js' +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' diff --git a/apps/dav/src/views/CalDavSettings.spec.js b/apps/dav/src/views/CalDavSettings.spec.js index e5f18999fe0..7a4345b3ddf 100644 --- a/apps/dav/src/views/CalDavSettings.spec.js +++ b/apps/dav/src/views/CalDavSettings.spec.js @@ -3,40 +3,33 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { render } from '@testing-library/vue' +import { beforeEach, describe, expect, test, vi } from 'vitest' + import CalDavSettings from './CalDavSettings.vue' -// eslint-disable-next-line no-unused-vars -import { generateUrl } from '@nextcloud/router' -jest.mock('@nextcloud/axios') -jest.mock('@nextcloud/router', () => { +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( @@ -53,28 +46,28 @@ describe('CalDavSettings', () => { }, }, 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 sendEventRemindersToSharedUsers = TLUtils.getByLabelText( - 'Send reminder notifications to calendar sharees as well' + 'Send reminder notifications to calendar sharees as well', ) expect(sendEventRemindersToSharedUsers).toBeChecked() const sendEventRemindersPush = TLUtils.getByLabelText( - 'Enable notifications for events via push' + 'Enable notifications for events via push', ) expect(sendEventRemindersPush).toBeChecked() diff --git a/apps/dav/src/views/CalDavSettings.vue b/apps/dav/src/views/CalDavSettings.vue index 162f85743a2..6be67cf93ff 100644 --- a/apps/dav/src/views/CalDavSettings.vue +++ b/apps/dav/src/views/CalDavSettings.vue @@ -79,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.js' -import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' +import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' const userSyncCalendarsDocUrl = loadState('dav', 'userSyncCalendarsDocUrl', '#') @@ -128,7 +128,7 @@ export default { OCP.AppConfig.setValue( 'dav', 'sendInvitations', - value ? 'yes' : 'no' + value ? 'yes' : 'no', ) }, sendEventReminders(value) { @@ -138,7 +138,7 @@ export default { OCP.AppConfig.setValue( 'dav', 'sendEventRemindersToSharedUsers', - value ? 'yes' : 'no' + value ? 'yes' : 'no', ) }, sendEventRemindersPush(value) { @@ -150,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 ed50c779749..fdbe09f5b5e 100644 --- a/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap +++ b/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap @@ -1,20 +1,21 @@ -// 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-251fe753="" + data-v-6b8d4c30="" + data-v-6f6953b5="" > <h2 class="settings-section__name" - data-v-251fe753="" + data-v-6f6953b5="" > Calendar server <a aria-label="External documentation for Calendar server" class="settings-section__info" - data-v-251fe753="" + data-v-6f6953b5="" href="https://docs.nextcloud.com/server/23/go.php?to=user-sync-calendars" rel="noreferrer nofollow" target="_blank" @@ -23,7 +24,7 @@ exports[`CalDavSettings interactions 1`] = ` <span aria-hidden="true" class="material-design-icon help-circle-icon" - data-v-251fe753="" + data-v-6f6953b5="" role="img" > <svg @@ -45,7 +46,8 @@ exports[`CalDavSettings interactions 1`] = ` <!----> <p class="settings-hint" - data-v-251fe753="" + data-v-6b8d4c30="" + data-v-6f6953b5="" > Also install the <a @@ -64,40 +66,41 @@ exports[`CalDavSettings interactions 1`] = ` </a> . </p> - <p - data-v-251fe753="" + data-v-6b8d4c30="" + data-v-6f6953b5="" > <span class="checkbox-radio-switch checkbox-radio-switch-switch checkbox-radio-switch--checked" - data-v-251fe753="" - data-v-919d07b7="" + data-v-6b8d4c30="" + data-v-6f6953b5="" + data-v-f275cf53="" style="--icon-size: 36px; --icon-height: 16px;" > <input aria-labelledby="caldavSendInvitations-label" class="checkbox-radio-switch__input" - data-v-919d07b7="" + 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-02d27370="" - data-v-919d07b7="" + data-v-3714b019="" + data-v-f275cf53="" id="caldavSendInvitations-label" > <span aria-hidden="true" class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon" - data-v-02d27370="" + data-v-3714b019="" inert="inert" > <span aria-hidden="true" class="material-design-icon toggle-switch-icon" - data-v-02d27370="" + data-v-3714b019="" role="img" > <svg @@ -117,17 +120,15 @@ exports[`CalDavSettings interactions 1`] = ` </span> <span class="checkbox-content__text checkbox-radio-switch__text" - data-v-02d27370="" + data-v-3714b019="" > - - Send invitations to attendees - + Send invitations to attendees </span> </span> </span> - <em - data-v-251fe753="" + data-v-6b8d4c30="" + data-v-6f6953b5="" > Please make sure to properly set up <a @@ -138,40 +139,41 @@ exports[`CalDavSettings interactions 1`] = ` . </em> </p> - <p - data-v-251fe753="" + data-v-6b8d4c30="" + data-v-6f6953b5="" > <span class="checkbox-radio-switch checkbox checkbox-radio-switch-switch checkbox-radio-switch--checked" - data-v-251fe753="" - data-v-919d07b7="" + data-v-6b8d4c30="" + data-v-6f6953b5="" + data-v-f275cf53="" style="--icon-size: 36px; --icon-height: 16px;" > <input aria-labelledby="caldavGenerateBirthdayCalendar-label" class="checkbox-radio-switch__input" - data-v-919d07b7="" + 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-02d27370="" - data-v-919d07b7="" + data-v-3714b019="" + data-v-f275cf53="" id="caldavGenerateBirthdayCalendar-label" > <span aria-hidden="true" class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon" - data-v-02d27370="" + data-v-3714b019="" inert="inert" > <span aria-hidden="true" class="material-design-icon toggle-switch-icon" - data-v-02d27370="" + data-v-3714b019="" role="img" > <svg @@ -191,69 +193,64 @@ exports[`CalDavSettings interactions 1`] = ` </span> <span class="checkbox-content__text checkbox-radio-switch__text" - data-v-02d27370="" + data-v-3714b019="" > - - Automatically generate a birthday calendar - + Automatically generate a birthday calendar </span> </span> </span> - <em - data-v-251fe753="" + 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-251fe753="" + data-v-6b8d4c30="" + data-v-6f6953b5="" /> - <em - data-v-251fe753="" + 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-251fe753="" + data-v-6b8d4c30="" + data-v-6f6953b5="" > <span class="checkbox-radio-switch checkbox-radio-switch-switch checkbox-radio-switch--checked" - data-v-251fe753="" - data-v-919d07b7="" + data-v-6b8d4c30="" + data-v-6f6953b5="" + data-v-f275cf53="" style="--icon-size: 36px; --icon-height: 16px;" > <input aria-labelledby="caldavSendEventReminders-label" class="checkbox-radio-switch__input" - data-v-919d07b7="" + 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-02d27370="" - data-v-919d07b7="" + data-v-3714b019="" + data-v-f275cf53="" id="caldavSendEventReminders-label" > <span aria-hidden="true" class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon" - data-v-02d27370="" + data-v-3714b019="" inert="inert" > <span aria-hidden="true" class="material-design-icon toggle-switch-icon" - data-v-02d27370="" + data-v-3714b019="" role="img" > <svg @@ -273,17 +270,15 @@ exports[`CalDavSettings interactions 1`] = ` </span> <span class="checkbox-content__text checkbox-radio-switch__text" - data-v-02d27370="" + data-v-3714b019="" > - - Send notifications for events - + Send notifications for events </span> </span> </span> - <em - data-v-251fe753="" + data-v-6b8d4c30="" + data-v-6f6953b5="" > Please make sure to properly set up <a @@ -293,54 +288,53 @@ exports[`CalDavSettings interactions 1`] = ` </a> . </em> - <br - data-v-251fe753="" + data-v-6b8d4c30="" + data-v-6f6953b5="" /> - <em - data-v-251fe753="" + 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-251fe753="" + data-v-6b8d4c30="" + data-v-6f6953b5="" > <span class="checkbox-radio-switch checkbox-radio-switch-switch checkbox-radio-switch--checked" - data-v-251fe753="" - data-v-919d07b7="" + data-v-6b8d4c30="" + data-v-6f6953b5="" + data-v-f275cf53="" style="--icon-size: 36px; --icon-height: 16px;" > <input aria-labelledby="caldavSendEventRemindersToSharedGroupMembers-label" class="checkbox-radio-switch__input" - data-v-919d07b7="" + 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-02d27370="" - data-v-919d07b7="" + data-v-3714b019="" + data-v-f275cf53="" id="caldavSendEventRemindersToSharedGroupMembers-label" > <span aria-hidden="true" class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon" - data-v-02d27370="" + data-v-3714b019="" inert="inert" > <span aria-hidden="true" class="material-design-icon toggle-switch-icon" - data-v-02d27370="" + data-v-3714b019="" role="img" > <svg @@ -360,58 +354,55 @@ exports[`CalDavSettings interactions 1`] = ` </span> <span class="checkbox-content__text checkbox-radio-switch__text" - data-v-02d27370="" + data-v-3714b019="" > - - Send reminder notifications to calendar sharees as well - + Send reminder notifications to calendar sharees as well </span> </span> </span> - <em - data-v-251fe753="" + 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-251fe753="" + data-v-6b8d4c30="" + data-v-6f6953b5="" > <span class="checkbox-radio-switch checkbox-radio-switch-switch checkbox-radio-switch--checked" - data-v-251fe753="" - data-v-919d07b7="" + data-v-6b8d4c30="" + data-v-6f6953b5="" + data-v-f275cf53="" style="--icon-size: 36px; --icon-height: 16px;" > <input aria-labelledby="caldavSendEventRemindersPush-label" class="checkbox-radio-switch__input" - data-v-919d07b7="" + 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-02d27370="" - data-v-919d07b7="" + data-v-3714b019="" + data-v-f275cf53="" id="caldavSendEventRemindersPush-label" > <span aria-hidden="true" class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon" - data-v-02d27370="" + data-v-3714b019="" inert="inert" > <span aria-hidden="true" class="material-design-icon toggle-switch-icon" - data-v-02d27370="" + data-v-3714b019="" role="img" > <svg @@ -431,11 +422,9 @@ exports[`CalDavSettings interactions 1`] = ` </span> <span class="checkbox-content__text checkbox-radio-switch__text" - data-v-02d27370="" + data-v-3714b019="" > - - Enable notifications for events via push - + Enable notifications for events via push </span> </span> </span> |