diff options
Diffstat (limited to 'apps/dav/src')
-rw-r--r-- | apps/dav/src/components/ExampleContactSettings.vue (renamed from apps/dav/src/views/ExampleContactSettings.vue) | 104 | ||||
-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/service/ExampleEventService.js | 43 | ||||
-rw-r--r-- | apps/dav/src/settings-example-content.js | 11 | ||||
-rw-r--r-- | apps/dav/src/views/ExampleContentSettingsSection.vue | 38 |
6 files changed, 421 insertions, 49 deletions
diff --git a/apps/dav/src/views/ExampleContactSettings.vue b/apps/dav/src/components/ExampleContactSettings.vue index d4d1c7d31d0..cdfdc130189 100644 --- a/apps/dav/src/views/ExampleContactSettings.vue +++ b/apps/dav/src/components/ExampleContactSettings.vue @@ -4,35 +4,34 @@ --> <template> - <NcSettingsSection id="exmaple-content" - :name="$t('dav', 'Example Content')" - class="example-content-setting" - :description="$t('dav', 'Set example content to be created on new user first login.')"> - <div class="example-content-setting__contacts"> - <input id="enable-default-contact" - v-model="enableDefaultContact" - type="checkbox" - class="checkbox" - @change="updateEnableDefaultContact"> - <label for="enable-default-contact"> {{ $t('dav',"Default contact is added to the user's own address book on user's first login.") }} </label> - <div v-if="enableDefaultContact" class="example-content-setting__contacts__buttons"> - <NcButton type="primary" - class="example-content-setting__contacts__buttons__button" - @click="toggleModal"> - <template #icon> - <IconUpload :size="20" /> - </template> - {{ $t('dav', 'Import contact') }} - </NcButton> - <NcButton type="secondary" - class="example-content-setting__contacts__buttons__button" - @click="resetContact"> - <template #icon> - <IconRestore :size="20" /> - </template> - {{ $t('dav', 'Reset to default contact') }} - </NcButton> - </div> + <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')" @@ -48,33 +47,40 @@ accept=".vcf" class="hidden-visually" @change="processFile"> - </NcSettingsSection> + </div> </template> <script> import axios from '@nextcloud/axios' import { generateUrl } from '@nextcloud/router' import { loadState } from '@nextcloud/initial-state' -import { NcDialog, NcButton, NcSettingsSection } from '@nextcloud/vue' +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') === 'yes' +const enableDefaultContact = loadState('dav', 'enableDefaultContact') +const hasCustomDefaultContact = loadState('dav', 'hasCustomDefaultContact') export default { name: 'ExampleContactSettings', components: { NcDialog, NcButton, - NcSettingsSection, + NcCheckboxRadioSwitch, IconUpload, IconRestore, + IconAccount, + ExampleContentDownloadButton, }, data() { return { enableDefaultContact, + hasCustomDefaultContact, isModalOpen: false, loading: false, buttons: [ @@ -92,12 +98,18 @@ export default { ], } }, + computed: { + downloadUrl() { + return generateUrl('/apps/dav/api/defaultcontact/contact') + }, + }, methods: { updateEnableDefaultContact() { axios.put(generateUrl('apps/dav/api/defaultcontact/config'), { - allow: this.enableDefaultContact ? 'yes' : 'no', - }).catch(() => { + allow: !this.enableDefaultContact, + }).then(() => { this.enableDefaultContact = !this.enableDefaultContact + }).catch(() => { showError(this.$t('dav', 'Error while saving settings')) }) }, @@ -111,10 +123,11 @@ export default { this.loading = true axios.put(generateUrl('/apps/dav/api/defaultcontact/contact')) .then(() => { + this.hasCustomDefaultContact = false showSuccess(this.$t('dav', 'Contact reset successfully')) }) .catch((error) => { - console.error('Error importing contact:', error) + logger.error('Error importing contact:', { error }) showError(this.$t('dav', 'Error while resetting contact')) }) .finally(() => { @@ -131,9 +144,10 @@ export default { 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) { - console.error('Error importing contact:', error) + logger.error('Error importing contact:', { error }) showError(this.$t('dav', 'Error while importing contact')) } finally { this.loading = false @@ -146,15 +160,13 @@ export default { } </script> <style lang="scss" scoped> -.example-content-setting{ - &__contacts{ - &__buttons{ - margin-top: 1rem; - display: flex; - &__button{ - margin-inline-end: 5px; - } - } +.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..d3ee793eddc --- /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..c7a90b71a4a --- /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/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/settings-example-content.js b/apps/dav/src/settings-example-content.js index f1374027bf3..ca0291ace4f 100644 --- a/apps/dav/src/settings-example-content.js +++ b/apps/dav/src/settings-example-content.js @@ -4,10 +4,15 @@ */ import Vue from 'vue' import { translate } from '@nextcloud/l10n' -import ExampleContactSettings from './views/ExampleContactSettings.vue' +import ExampleContentSettingsSection from './views/ExampleContentSettingsSection.vue' -Vue.prototype.$t = translate +Vue.mixin({ + methods: { + t: translate, + $t: translate, + } +}) -const View = Vue.extend(ExampleContactSettings); +const View = Vue.extend(ExampleContentSettingsSection); (new View({})).$mount('#settings-example-content') diff --git a/apps/dav/src/views/ExampleContentSettingsSection.vue b/apps/dav/src/views/ExampleContentSettingsSection.vue new file mode 100644 index 00000000000..5e65a1ba3b4 --- /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> |