aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/src/components/AbsenceForm.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/src/components/AbsenceForm.vue')
-rw-r--r--apps/dav/src/components/AbsenceForm.vue274
1 files changed, 274 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>