diff options
Diffstat (limited to 'apps/dav/src/components/AbsenceForm.vue')
-rw-r--r-- | apps/dav/src/components/AbsenceForm.vue | 162 |
1 files changed, 130 insertions, 32 deletions
diff --git a/apps/dav/src/components/AbsenceForm.vue b/apps/dav/src/components/AbsenceForm.vue index f43fb889b9e..5350c04a565 100644 --- a/apps/dav/src/components/AbsenceForm.vue +++ b/apps/dav/src/components/AbsenceForm.vue @@ -1,24 +1,7 @@ <!-- - - @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud> - - - - @author Richard Steinmetz <richard@steinmetz.cloud> - - - - @license AGPL-3.0-or-later - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU General Public License as published by - - the Free Software Foundation, either version 3 of the License, or - - (at your option) any later version. - - - - This program is distributed in the hope that it will be useful, - - but WITHOUT ANY WARRANTY; without even the implied warranty of - - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - - GNU General Public License for more details. - - - - You should have received a copy of the GNU General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> + - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> <form class="absence" @submit.prevent="saveForm"> @@ -34,6 +17,20 @@ 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" /> @@ -53,19 +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 NcDateTimePickerNative from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js' -import { generateOcsUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' -import axios from '@nextcloud/axios' -import { formatDateAsYMD } from '../utils/date.js' -import { loadState } from '@nextcloud/initial-state' 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: { @@ -73,16 +73,20 @@ export default { NcTextField, NcTextArea, NcDateTimePickerNative, + NcSelect, }, data() { - const { firstDay, lastDay, status, message } = 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, + replacementUser: replacementUserId ? { user: replacementUserId, displayName: replacementUserDisplayName } : null, + searchLoading: false, + options: [], } }, computed: { @@ -110,6 +114,99 @@ export default { 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 @@ -122,6 +219,7 @@ export default { lastDay: formatDateAsYMD(this.lastDay), status: this.status, message: this.message, + replacementUserId: this.replacementUser?.user ?? null, }) showSuccess(this.$t('dav', 'Absence saved')) } catch (error) { @@ -162,7 +260,7 @@ export default { &__picker { flex: 1 auto; - ::v-deep .native-datetime-picker--input { + :deep(.native-datetime-picker--input) { margin-bottom: 0; } } |