aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/src
diff options
context:
space:
mode:
authorRichard Steinmetz <richard@steinmetz.cloud>2023-10-04 12:31:46 +0200
committerRichard Steinmetz <richard@steinmetz.cloud>2023-11-07 17:27:10 +0100
commit425e770c0447e5dc5e812f52163c0bbc96badbe7 (patch)
tree54a95ffcdff06d0e017ce4fe3fc3e8fcdb36e312 /apps/dav/src
parent3e6642ab0b4d0a95ef4ebff8553ae93a6b9e6d00 (diff)
downloadnextcloud-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.vue160
-rw-r--r--apps/dav/src/components/AvailabilityForm.vue205
-rw-r--r--apps/dav/src/utils/date.js34
-rw-r--r--apps/dav/src/views/Availability.vue209
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>