diff options
author | Richard Steinmetz <richard@steinmetz.cloud> | 2023-10-04 12:31:46 +0200 |
---|---|---|
committer | Richard Steinmetz <richard@steinmetz.cloud> | 2023-11-07 17:27:10 +0100 |
commit | 425e770c0447e5dc5e812f52163c0bbc96badbe7 (patch) | |
tree | 54a95ffcdff06d0e017ce4fe3fc3e8fcdb36e312 /apps/dav/src | |
parent | 3e6642ab0b4d0a95ef4ebff8553ae93a6b9e6d00 (diff) | |
download | nextcloud-server-425e770c0447e5dc5e812f52163c0bbc96badbe7.tar.gz nextcloud-server-425e770c0447e5dc5e812f52163c0bbc96badbe7.zip |
feat(dav): implement personal absence settings
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
Diffstat (limited to 'apps/dav/src')
-rw-r--r-- | apps/dav/src/components/AbsenceForm.vue | 160 | ||||
-rw-r--r-- | apps/dav/src/components/AvailabilityForm.vue | 205 | ||||
-rw-r--r-- | apps/dav/src/utils/date.js | 34 | ||||
-rw-r--r-- | apps/dav/src/views/Availability.vue | 209 |
4 files changed, 416 insertions, 192 deletions
diff --git a/apps/dav/src/components/AbsenceForm.vue b/apps/dav/src/components/AbsenceForm.vue new file mode 100644 index 00000000000..0462c22a53d --- /dev/null +++ b/apps/dav/src/components/AbsenceForm.vue @@ -0,0 +1,160 @@ +<!-- + - @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/>. + - + --> + +<template> + <div class="absence"> + <div class="absence__dates"> + <NcDateTimePickerNative id="absence-first-day" + v-model="firstDay" + :label="$t('dav', 'First day')" + class="absence__dates__picker" /> + <NcDateTimePickerNative id="absence-last-day" + v-model="lastDay" + :label="$t('dav', 'Last day (inclusive)')" + class="absence__dates__picker" /> + </div> + <NcTextField :value.sync="status" :label="$t('dav', 'Short absence status')" /> + <NcTextArea :value.sync="message" :label="$t('dav', 'Long absence Message')" /> + + <div class="absence__buttons"> + <NcButton :disabled="loading || !valid" + type="primary" + @click="saveForm"> + {{ $t('dav', 'Save') }} + </NcButton> + <NcButton :disabled="loading || !valid" + type="error" + @click="clearAbsence"> + {{ $t('dav', 'Disable absence') }} + </NcButton> + </div> + </div> +</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 { generateUrl } from '@nextcloud/router' +import axios from '@nextcloud/axios' +import { formatDateAsYMD } from '../utils/date.js' +import { loadState } from '@nextcloud/initial-state' +import { showError } from '@nextcloud/dialogs' + +export default { + name: 'AbsenceForm', + components: { + NcButton, + NcTextField, + NcTextArea, + NcDateTimePickerNative, + }, + data() { + const { firstDay, lastDay, status, message } = loadState('dav', 'absence', {}) + + return { + loading: false, + status: status ?? '', + message: message ?? '', + firstDay: firstDay ? new Date(firstDay) : new Date(), + lastDay: lastDay ? new Date(lastDay) : null, + } + }, + computed: { + /** + * @return {boolean} + */ + valid() { + return !!this.firstDay + && !!this.lastDay + && !!this.status + && this.lastDay > this.firstDay + }, + }, + methods: { + resetForm() { + this.status = '' + this.message = '' + this.firstDay = new Date() + this.lastDay = null + }, + async saveForm() { + if (!this.valid) { + return + } + + this.loading = true + try { + await axios.post(generateUrl('/apps/dav/settings/absence'), { + firstDay: formatDateAsYMD(this.firstDay), + lastDay: formatDateAsYMD(this.lastDay), + status: this.status, + message: this.message, + }) + } catch (error) { + showError(this.$t('dav', 'Failed to save your absence settings')) + } finally { + this.loading = false + } + }, + async clearAbsence() { + this.loading = true + try { + await axios.delete(generateUrl('/apps/dav/settings/absence')) + this.resetForm() + } catch (error) { + showError(this.$t('dav', 'Failed to clear your absence settings')) + } 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; + + ::v-deep .native-datetime-picker--input { + margin-bottom: 0; + } + } + } + + &__buttons { + display: flex; + gap: 5px; + } +} +</style> diff --git a/apps/dav/src/components/AvailabilityForm.vue b/apps/dav/src/components/AvailabilityForm.vue new file mode 100644 index 00000000000..27bd71c6ca4 --- /dev/null +++ b/apps/dav/src/components/AvailabilityForm.vue @@ -0,0 +1,205 @@ +<template> + <div> + <div class="time-zone"> + <label :for="`vs${timeZonePickerId}__combobox`" class="time-zone__heading"> + {{ $t('dav', 'Time zone:') }} + </label> + <span class="time-zone-text"> + <NcTimezonePicker v-model="timezone" :uid="timeZonePickerId" /> + </span> + </div> + + <CalendarAvailability :slots.sync="slots" + :loading="loading" + :l10n-to="$t('dav', 'to')" + :l10n-delete-slot="$t('dav', 'Delete slot')" + :l10n-empty-day="$t('dav', 'No working hours set')" + :l10n-add-slot="$t('dav', 'Add slot')" + :l10n-monday="$t('dav', 'Monday')" + :l10n-tuesday="$t('dav', 'Tuesday')" + :l10n-wednesday="$t('dav', 'Wednesday')" + :l10n-thursday="$t('dav', 'Thursday')" + :l10n-friday="$t('dav', 'Friday')" + :l10n-saturday="$t('dav', 'Saturday')" + :l10n-sunday="$t('dav', 'Sunday')" /> + + <NcCheckboxRadioSwitch :checked.sync="automated"> + {{ $t('dav', 'Automatically set user status to "Do not disturb" outside of availability to mute all notifications.') }} + </NcCheckboxRadioSwitch> + + <NcButton :disabled="loading || saving" + type="primary" + @click="save"> + {{ $t('dav', 'Save') }} + </NcButton> + </div> +</template> + +<script> +import { CalendarAvailability } from '@nextcloud/calendar-availability-vue' +import { loadState } from '@nextcloud/initial-state' +import { + showError, + showSuccess, +} from '@nextcloud/dialogs' +import { + findScheduleInboxAvailability, + getEmptySlots, + saveScheduleInboxAvailability, +} from '../service/CalendarService.js' +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' + +export default { + name: 'AvailabilityForm', + components: { + NcButton, + NcCheckboxRadioSwitch, + CalendarAvailability, + NcTimezonePicker, + }, + data() { + // Try to determine the current timezone, and fall back to UTC otherwise + const defaultTimezoneId = (new Intl.DateTimeFormat())?.resolvedOptions()?.timeZone ?? 'UTC' + + return { + loading: true, + saving: false, + timezone: defaultTimezoneId, + slots: getEmptySlots(), + automated: loadState('dav', 'user_status_automation') === 'yes', + } + }, + computed: { + timeZonePickerId() { + return `tz-${(Math.random() + 1).toString(36).substring(7)}` + }, + }, + async mounted() { + try { + const slotData = await findScheduleInboxAvailability() + if (!slotData) { + console.info('no availability is set') + this.slots = getEmptySlots() + } else { + const { slots, timezoneId } = slotData + this.slots = slots + if (timezoneId) { + this.timezone = timezoneId + } + console.info('availability loaded', this.slots, this.timezoneId) + } + } catch (e) { + console.error('could not load existing availability', e) + + showError(t('dav', 'Failed to load availability')) + } finally { + this.loading = false + } + }, + methods: { + async save() { + try { + this.saving = true + + await saveScheduleInboxAvailability(this.slots, this.timezone) + if (this.automated) { + await enableUserStatusAutomation() + } else { + await disableUserStatusAutomation() + } + + showSuccess(t('dav', 'Saved availability')) + } catch (e) { + console.error('could not save availability', e) + + showError(t('dav', 'Failed to save availability')) + } finally { + this.saving = false + } + }, + }, +} +</script> + +<style lang="scss" scoped> +:deep(.availability-day) { + 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; + + &__heading { + margin-right: calc(var(--default-grid-baseline) * 2); + line-height: var(--default-clickable-area); + font-weight: bold; + } +} +.grid-table { + display: grid; + margin-bottom: 32px; + grid-column-gap: 24px; + grid-row-gap: 6px; + grid-template-columns: min-content auto min-content; + max-width: 500px; +} +.button { + align-self: flex-end; +} +:deep(.label-weekday) { + position: relative; + display: inline-flex; + padding-top: 4px; + align-self: center; +} + +:deep(.delete-slot) { + padding-bottom: unset; +} + +:deep(.add-another) { + align-self: center; +} + +.to-text { + padding-right: 12px; +} + +.empty-content { + color: var(--color-text-lighter); + margin-top: 4px; + align-self: center; +} +</style> diff --git a/apps/dav/src/utils/date.js b/apps/dav/src/utils/date.js new file mode 100644 index 00000000000..2ba5ba926f6 --- /dev/null +++ b/apps/dav/src/utils/date.js @@ -0,0 +1,34 @@ +/** + * @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/>. + * + */ + +/** + * Format a date as 'YYYY-MM-DD'. + * + * @param {Date} date A date instance to format. + * @return {string} 'YYYY-MM-DD' + */ +export function formatDateAsYMD(date) { + const year = date.getUTCFullYear() + const month = (date.getUTCMonth() + 1).toString().padStart(2, '0') + const day = date.getUTCDate().toString().padStart(2, '0') + return `${year}-${month}-${day}` +} diff --git a/apps/dav/src/views/Availability.vue b/apps/dav/src/views/Availability.vue index 3b42df1e9df..62778839142 100644 --- a/apps/dav/src/views/Availability.vue +++ b/apps/dav/src/views/Availability.vue @@ -1,209 +1,34 @@ <template> - <NcSettingsSection :name="$t('dav', 'Availability')" - :description="$t('dav', 'If you configure your working hours, other users will see when you are out of office when they book a meeting.')"> - <div class="time-zone"> - <label :for="`vs${timeZonePickerId}__combobox`" class="time-zone__heading"> - {{ $t('dav', 'Time zone:') }} - </label> - <span class="time-zone-text"> - <NcTimezonePicker v-model="timezone" :uid="timeZonePickerId" /> - </span> - </div> - - <CalendarAvailability :slots.sync="slots" - :loading="loading" - :l10n-to="$t('dav', 'to')" - :l10n-delete-slot="$t('dav', 'Delete slot')" - :l10n-empty-day="$t('dav', 'No working hours set')" - :l10n-add-slot="$t('dav', 'Add slot')" - :l10n-monday="$t('dav', 'Monday')" - :l10n-tuesday="$t('dav', 'Tuesday')" - :l10n-wednesday="$t('dav', 'Wednesday')" - :l10n-thursday="$t('dav', 'Thursday')" - :l10n-friday="$t('dav', 'Friday')" - :l10n-saturday="$t('dav', 'Saturday')" - :l10n-sunday="$t('dav', 'Sunday')" /> - - <NcCheckboxRadioSwitch :checked.sync="automated"> - {{ $t('dav', 'Automatically set user status to "Do not disturb" outside of availability to mute all notifications.') }} - </NcCheckboxRadioSwitch> - - <NcButton :disabled="loading || saving" - type="primary" - @click="save"> - {{ $t('dav', 'Save') }} - </NcButton> - </NcSettingsSection> + <div> + <NcSettingsSection :name="$t('dav', 'Availability')" + :description="$t('dav', 'If you configure your working hours, other users will see when you are out of office when they book a meeting.')"> + <AvailabilityForm /> + </NcSettingsSection> + <NcSettingsSection v-if="!hideAbsenceSettings" + :name="$t('dav', 'Absence')" + :description="$t('dav', 'Configure your next absence period.')"> + <AbsenceForm /> + </NcSettingsSection> + </div> </template> <script> -import { CalendarAvailability } from '@nextcloud/calendar-availability-vue' -import { loadState } from '@nextcloud/initial-state' -import { - showError, - showSuccess, -} from '@nextcloud/dialogs' -import { - findScheduleInboxAvailability, - getEmptySlots, - saveScheduleInboxAvailability, -} from '../service/CalendarService.js' -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 NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js' -import NcTimezonePicker from '@nextcloud/vue/dist/Components/NcTimezonePicker.js' +import AbsenceForm from '../components/AbsenceForm.vue' +import AvailabilityForm from '../components/AvailabilityForm.vue' +import { loadState } from '@nextcloud/initial-state' export default { name: 'Availability', components: { - NcButton, - NcCheckboxRadioSwitch, - CalendarAvailability, NcSettingsSection, - NcTimezonePicker, + AbsenceForm, + AvailabilityForm, }, data() { - // Try to determine the current timezone, and fall back to UTC otherwise - const defaultTimezoneId = (new Intl.DateTimeFormat())?.resolvedOptions()?.timeZone ?? 'UTC' - return { - loading: true, - saving: false, - timezone: defaultTimezoneId, - slots: getEmptySlots(), - automated: loadState('dav', 'user_status_automation') === 'yes', - } - }, - computed: { - timeZonePickerId() { - return `tz-${(Math.random() + 1).toString(36).substring(7)}` - }, - }, - async mounted() { - try { - const slotData = await findScheduleInboxAvailability() - if (!slotData) { - console.info('no availability is set') - this.slots = getEmptySlots() - } else { - const { slots, timezoneId } = slotData - this.slots = slots - if (timezoneId) { - this.timezone = timezoneId - } - console.info('availability loaded', this.slots, this.timezoneId) - } - } catch (e) { - console.error('could not load existing availability', e) - - showError(t('dav', 'Failed to load availability')) - } finally { - this.loading = false + hideAbsenceSettings: loadState('dav', 'hide_absence_settings', true), } }, - methods: { - async save() { - try { - this.saving = true - - await saveScheduleInboxAvailability(this.slots, this.timezone) - if (this.automated) { - await enableUserStatusAutomation() - } else { - await disableUserStatusAutomation() - } - - showSuccess(t('dav', 'Saved availability')) - } catch (e) { - console.error('could not save availability', e) - - showError(t('dav', 'Failed to save availability')) - } finally { - this.saving = false - } - }, - }, } </script> - -<style lang="scss" scoped> -:deep(.availability-day) { - 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; - - &__heading { - margin-right: calc(var(--default-grid-baseline) * 2); - line-height: var(--default-clickable-area); - font-weight: bold; - } -} -.grid-table { - display: grid; - margin-bottom: 32px; - grid-column-gap: 24px; - grid-row-gap: 6px; - grid-template-columns: min-content auto min-content; - max-width: 500px; -} -.button { - align-self: flex-end; -} -:deep(.label-weekday) { - position: relative; - display: inline-flex; - padding-top: 4px; - align-self: center; -} - -:deep(.delete-slot) { - padding-bottom: unset; -} - -:deep(.add-another) { - align-self: center; -} - -.to-text { - padding-right: 12px; -} - -.empty-content { - color: var(--color-text-lighter); - margin-top: 4px; - align-self: center; -} - -</style> |