diff options
Diffstat (limited to 'apps/dav/src/components')
-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 |
5 files changed, 486 insertions, 31 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> |