diff options
Diffstat (limited to 'apps/user_status/src/components')
-rw-r--r-- | apps/user_status/src/components/ClearAtSelect.vue | 58 | ||||
-rw-r--r-- | apps/user_status/src/components/CustomMessageInput.vue | 103 | ||||
-rw-r--r-- | apps/user_status/src/components/OnlineStatusSelect.vue | 94 | ||||
-rw-r--r-- | apps/user_status/src/components/PredefinedStatus.vue | 134 | ||||
-rw-r--r-- | apps/user_status/src/components/PredefinedStatusesList.vue | 65 | ||||
-rw-r--r-- | apps/user_status/src/components/PreviousStatus.vue | 106 | ||||
-rw-r--r-- | apps/user_status/src/components/SetStatusModal.vue | 330 |
7 files changed, 554 insertions, 336 deletions
diff --git a/apps/user_status/src/components/ClearAtSelect.vue b/apps/user_status/src/components/ClearAtSelect.vue index 09e0068f87f..91b816dc04a 100644 --- a/apps/user_status/src/components/ClearAtSelect.vue +++ b/apps/user_status/src/components/ClearAtSelect.vue @@ -1,46 +1,33 @@ <!-- - - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> - - @author Georg Ehrke <oc.list@georgehrke.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero 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 Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> <div class="clear-at-select"> - <span class="clear-at-select__label"> - {{ $t('user_status', 'Clear status message after') }} - </span> - <Multiselect label="label" - :value="option" + <label class="clear-at-select__label" for="clearStatus"> + {{ $t('user_status', 'Clear status after') }} + </label> + <NcSelect input-id="clearStatus" + class="clear-at-select__select" :options="options" - open-direction="top" - @select="select" /> + :value="option" + :clearable="false" + placement="top" + label-outside + @option:selected="select" /> </div> </template> <script> -import Multiselect from '@nextcloud/vue/dist/Components/Multiselect' -import { getAllClearAtOptions } from '../services/clearAtOptionsService' -import { clearAtFilter } from '../filters/clearAtFilter' +import NcSelect from '@nextcloud/vue/components/NcSelect' +import { getAllClearAtOptions } from '../services/clearAtOptionsService.js' +import { clearAtFilter } from '../filters/clearAtFilter.js' export default { name: 'ClearAtSelect', components: { - Multiselect, + NcSelect, }, props: { clearAt: { @@ -86,16 +73,13 @@ export default { <style lang="scss" scoped> .clear-at-select { display: flex; - margin-bottom: 10px; + gap: calc(2 * var(--default-grid-baseline)); align-items: center; + margin-block: 0 calc(2 * var(--default-grid-baseline)); - &__label { - margin-right: 10px; - } - - .multiselect { + &__select { flex-grow: 1; - min-width: 130px; + min-width: 215px; } } </style> diff --git a/apps/user_status/src/components/CustomMessageInput.vue b/apps/user_status/src/components/CustomMessageInput.vue index 88956e6871b..fb129281430 100644 --- a/apps/user_status/src/components/CustomMessageInput.vue +++ b/apps/user_status/src/components/CustomMessageInput.vue @@ -1,43 +1,49 @@ <!-- - - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> - - @author Georg Ehrke <oc.list@georgehrke.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero 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 Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> - <form class="custom-input__form" - @submit.prevent> - <input ref="input" - maxlength="80" - :disabled="disabled" - :placeholder="$t('user_status', 'What is your status?')" - type="text" - :value="message" - @change="change" - @keyup="change" - @paste="change" - @keyup.enter="submit"> - </form> + <div class="custom-input" role="group"> + <NcEmojiPicker container=".custom-input" @select="setIcon"> + <NcButton type="tertiary" + :aria-label="t('user_status', 'Emoji for your status message')"> + <template #icon> + {{ visibleIcon }} + </template> + </NcButton> + </NcEmojiPicker> + <div class="custom-input__container"> + <NcTextField ref="input" + maxlength="80" + :disabled="disabled" + :placeholder="t('user_status', 'What is your status?')" + :value="message" + type="text" + :label="t('user_status', 'What is your status?')" + @input="onChange" /> + </div> + </div> </template> <script> +import NcButton from '@nextcloud/vue/components/NcButton' +import NcEmojiPicker from '@nextcloud/vue/components/NcEmojiPicker' +import NcTextField from '@nextcloud/vue/components/NcTextField' + export default { name: 'CustomMessageInput', + + components: { + NcTextField, + NcButton, + NcEmojiPicker, + }, + props: { + icon: { + type: String, + default: '😀', + }, message: { type: String, required: true, @@ -48,6 +54,23 @@ export default { default: false, }, }, + + emits: [ + 'change', + 'select-icon', + ], + + computed: { + /** + * Returns the user-set icon or a smiley in case no icon is set + * + * @return {string} + */ + visibleIcon() { + return this.icon || '😀' + }, + }, + methods: { focus() { this.$refs.input.focus() @@ -58,24 +81,26 @@ export default { * * @param {Event} event The Change Event */ - change(event) { + onChange(event) { this.$emit('change', event.target.value) }, - submit(event) { - this.$emit('submit', event.target.value) + setIcon(icon) { + this.$emit('select-icon', icon) }, }, } </script> <style lang="scss" scoped> -.custom-input__form { - flex-grow: 1; +.custom-input { + display: flex; + align-items: flex-end; + gap: var(--default-grid-baseline); + width: 100%; - input { + &__container { width: 100%; - border-radius: 0 var(--border-radius) var(--border-radius) 0; } } </style> diff --git a/apps/user_status/src/components/OnlineStatusSelect.vue b/apps/user_status/src/components/OnlineStatusSelect.vue index d9ce249ad13..0abcc8d68e6 100644 --- a/apps/user_status/src/components/OnlineStatusSelect.vue +++ b/apps/user_status/src/components/OnlineStatusSelect.vue @@ -1,33 +1,19 @@ <!-- - - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com> - - - - @author John Molakvoæ <skjnldsv@protonmail.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero 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 Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> <div class="user-status-online-select"> <input :id="id" :checked="checked" - class="user-status-online-select__input" + class="hidden-visually user-status-online-select__input" type="radio" name="user-status-online" @change="onChange"> - <label :for="id" :class="icon" class="user-status-online-select__label"> + <label :for="id" class="user-status-online-select__label"> + <NcUserStatusIcon :status="type" + class="user-status-online-select__icon" + aria-hidden="true" /> {{ label }} <em class="user-status-online-select__subline">{{ subline }}</em> </label> @@ -35,18 +21,20 @@ </template> <script> +import NcUserStatusIcon from '@nextcloud/vue/components/NcUserStatusIcon' + export default { name: 'OnlineStatusSelect', + components: { + NcUserStatusIcon, + }, + props: { checked: { type: Boolean, default: false, }, - icon: { - type: String, - required: true, - }, type: { type: String, required: true, @@ -76,41 +64,42 @@ export default { </script> <style lang="scss" scoped> -$icon-size: 24px; -$label-padding: 8px; - .user-status-online-select { - // Inputs are here for keyboard navigation, they are not visually visible - &__input { - position: absolute; - top: auto; - left: -10000px; - overflow: hidden; - width: 1px; - height: 1px; - } - &__label { - display: block; - margin: $label-padding; - padding: $label-padding; - padding-left: $icon-size + $label-padding * 2; - border: 2px solid var(--color-main-background); + box-sizing: inherit; + display: grid; + grid-template-columns: var(--default-clickable-area) 1fr 2fr; + align-items: center; + gap: var(--default-grid-baseline); + min-height: var(--default-clickable-area); + padding: var(--default-grid-baseline); border-radius: var(--border-radius-large); background-color: var(--color-background-hover); - background-position: $label-padding center; - background-size: $icon-size; - span, - & { + &, & * { cursor: pointer; } + + &:hover { + background-color: var(--color-background-dark); + } + } + + &__icon { + flex-shrink: 0; + max-width: 34px; + max-height: 100%; + } + + &__input:checked + &__label { + outline: 2px solid var(--color-main-text); + background-color: var(--color-background-dark); + box-shadow: 0 0 0 4px var(--color-main-background); } - &__input:checked + &__label, - &__input:focus + &__label, - &__label:hover { - border-color: var(--color-primary); + &__input:focus-visible + &__label { + outline: 2px solid var(--color-primary-element) !important; + background-color: var(--color-background-dark); } &__subline { @@ -118,5 +107,4 @@ $label-padding: 8px; color: var(--color-text-lighter); } } - </style> diff --git a/apps/user_status/src/components/PredefinedStatus.vue b/apps/user_status/src/components/PredefinedStatus.vue index f8d5fe26be4..b12892d4add 100644 --- a/apps/user_status/src/components/PredefinedStatus.vue +++ b/apps/user_status/src/components/PredefinedStatus.vue @@ -1,43 +1,31 @@ <!-- - - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> - - @author Georg Ehrke <oc.list@georgehrke.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero 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 Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> - <div class="predefined-status" - tabindex="0" - @keyup.enter="select" - @keyup.space="select" - @click="select"> - <span class="predefined-status__icon"> - {{ icon }} - </span> - <span class="predefined-status__message"> - {{ message }} - </span> - <span class="predefined-status__clear-at"> - {{ clearAt | clearAtFilter }} - </span> - </div> + <li class="predefined-status"> + <input :id="id" + class="hidden-visually predefined-status__input" + type="radio" + name="predefined-status" + :checked="selected" + @change="select"> + <label class="predefined-status__label" :for="id"> + <span aria-hidden="true" class="predefined-status__label--icon"> + {{ icon }} + </span> + <span class="predefined-status__label--message"> + {{ message }} + </span> + <span class="predefined-status__label--clear-at"> + {{ clearAt | clearAtFilter }} + </span> + </label> + </li> </template> <script> -import { clearAtFilter } from '../filters/clearAtFilter' +import { clearAtFilter } from '../filters/clearAtFilter.js' export default { name: 'PredefinedStatus', @@ -62,6 +50,16 @@ export default { required: false, default: null, }, + selected: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + id() { + return `user-status-predefined-status-${this.messageId}` + }, }, methods: { /** @@ -76,35 +74,55 @@ export default { <style lang="scss" scoped> .predefined-status { - display: flex; - flex-wrap: nowrap; - justify-content: flex-start; - flex-basis: 100%; - border-radius: var(--border-radius); - align-items: center; - min-height: 44px; + &__label { + display: flex; + flex-wrap: nowrap; + justify-content: flex-start; + flex-basis: 100%; + border-radius: var(--border-radius); + align-items: center; + min-height: var(--default-clickable-area); + padding-inline: var(--default-grid-baseline); - &:hover, - &:focus { - background-color: var(--color-background-hover); - } + &, & * { + cursor: pointer; + } - &__icon { - flex-basis: 40px; - text-align: center; - } + &:hover { + background-color: var(--color-background-dark); + } - &__message { - font-weight: bold; - padding: 0 6px; - } + &--icon { + flex-basis: var(--default-clickable-area); + text-align: center; + } + + &--message { + font-weight: bold; + padding: 0 6px; + } - &__clear-at { - opacity: .7; + &--clear-at { + color: var(--color-text-maxcontrast); - &::before { - content: ' - '; + &::before { + content: ' – '; + } } } + + &__input:checked + &__label, + &__label:active { + outline: 2px solid var(--color-main-text); + box-shadow: 0 0 0 4px var(--color-main-background); + background-color: var(--color-background-dark); + border-radius: var(--border-radius-large); + } + + &__input:focus-visible + &__label { + outline: 2px solid var(--color-primary-element) !important; + background-color: var(--color-background-dark); + border-radius: var(--border-radius-large); + } } </style> diff --git a/apps/user_status/src/components/PredefinedStatusesList.vue b/apps/user_status/src/components/PredefinedStatusesList.vue index cff03289715..cdf359dce76 100644 --- a/apps/user_status/src/components/PredefinedStatusesList.vue +++ b/apps/user_status/src/components/PredefinedStatusesList.vue @@ -1,35 +1,21 @@ <!-- - - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> - - @author Georg Ehrke <oc.list@georgehrke.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero 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 Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> - <div v-if="hasLoaded" - class="predefined-statuses-list"> + <ul v-if="statusesHaveLoaded" + class="predefined-statuses-list" + :aria-label="t('user_status', 'Predefined statuses')"> <PredefinedStatus v-for="status in predefinedStatuses" :key="status.id" :message-id="status.id" :icon="status.icon" :message="status.message" :clear-at="status.clearAt" + :selected="lastSelected === status.id" @select="selectStatus(status)" /> - </div> + </ul> <div v-else class="predefined-statuses-list"> <div class="icon icon-loading-small" /> @@ -37,32 +23,41 @@ </template> <script> -import PredefinedStatus from './PredefinedStatus' -import { mapState } from 'vuex' +import PredefinedStatus from './PredefinedStatus.vue' +import { mapGetters, mapState } from 'vuex' export default { name: 'PredefinedStatusesList', components: { PredefinedStatus, }, + data() { + return { + lastSelected: null, + } + }, computed: { ...mapState({ predefinedStatuses: state => state.predefinedStatuses.predefinedStatuses, + messageId: state => state.userStatus.messageId, }), - /** - * Indicator whether the predefined statuses have already been loaded - * - * @return {boolean} - */ - hasLoaded() { - return this.predefinedStatuses.length > 0 - }, + ...mapGetters(['statusesHaveLoaded']), }, + + watch: { + messageId: { + immediate: true, + handler() { + this.lastSelected = this.messageId + }, + }, + }, + /** * Loads all predefined statuses from the server * when this component is mounted */ - mounted() { + created() { this.$store.dispatch('loadAllPredefinedStatuses') }, methods: { @@ -72,6 +67,7 @@ export default { * @param {object} status The selected status */ selectStatus(status) { + this.lastSelected = status.id this.$emit('select-status', status) }, }, @@ -82,6 +78,7 @@ export default { .predefined-statuses-list { display: flex; flex-direction: column; - margin-bottom: 10px; + gap: var(--default-grid-baseline); + margin-block: 0 calc(2 * var(--default-grid-baseline)); } </style> diff --git a/apps/user_status/src/components/PreviousStatus.vue b/apps/user_status/src/components/PreviousStatus.vue new file mode 100644 index 00000000000..58d6ebd294b --- /dev/null +++ b/apps/user_status/src/components/PreviousStatus.vue @@ -0,0 +1,106 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div class="predefined-status backup-status" + tabindex="0" + @keyup.enter="select" + @keyup.space="select" + @click="select"> + <span class="predefined-status__icon"> + {{ icon }} + </span> + <span class="predefined-status__message"> + {{ message }} + </span> + <span class="predefined-status__clear-at"> + {{ $t('user_status', 'Previously set') }} + </span> + + <div class="backup-status__reset-button"> + <NcButton @click="select"> + {{ $t('user_status', 'Reset status') }} + </NcButton> + </div> + </div> +</template> + +<script> +import NcButton from '@nextcloud/vue/components/NcButton' + +export default { + name: 'PreviousStatus', + + components: { + NcButton, + }, + + props: { + icon: { + type: [String, null], + required: true, + }, + message: { + type: String, + required: true, + }, + }, + methods: { + /** + * Emits an event when the user clicks the row + */ + select() { + this.$emit('select') + }, + }, +} +</script> + +<style lang="scss" scoped> +.predefined-status { + display: flex; + flex-wrap: nowrap; + justify-content: flex-start; + flex-basis: 100%; + border-radius: var(--border-radius); + align-items: center; + min-height: var(--default-clickable-area); + padding-inline: var(--default-grid-baseline); + + &:hover, + &:focus { + background-color: var(--color-background-hover); + } + + &:active{ + background-color: var(--color-background-dark); + } + + &__icon { + flex-basis: var(--default-clickable-area); + text-align: center; + } + + &__message { + font-weight: bold; + padding: 0 6px; + } + + &__clear-at { + color: var(--color-text-maxcontrast); + + &::before { + content: ' – '; + } + } +} + +.backup-status { + &__reset-button { + justify-content: flex-end; + display: flex; + flex-grow: 1; + } +} +</style> diff --git a/apps/user_status/src/components/SetStatusModal.vue b/apps/user_status/src/components/SetStatusModal.vue index df6858ca6ff..8624ed19e94 100644 --- a/apps/user_status/src/components/SetStatusModal.vue +++ b/apps/user_status/src/components/SetStatusModal.vue @@ -1,34 +1,22 @@ <!-- - - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> - - @author Georg Ehrke <oc.list@georgehrke.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero 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 Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> - <Modal size="normal" - :title="$t('user_status', 'Set status')" + <NcModal size="normal" + label-id="user_status-set-dialog" + dark + :set-return-focus="setReturnFocus" @close="closeModal"> <div class="set-status-modal"> <!-- Status selector --> - <div class="set-status-modal__header"> - <h3>{{ $t('user_status', 'Online status') }}</h3> - </div> - <div class="set-status-modal__online-status"> + <h2 id="user_status-set-dialog" class="set-status-modal__header"> + {{ $t('user_status', 'Online status') }} + </h2> + <div class="set-status-modal__online-status" + role="radiogroup" + :aria-label="$t('user_status', 'Online status')"> <OnlineStatusSelect v-for="status in statuses" :key="status.type" v-bind="status" @@ -36,55 +24,69 @@ @select="changeStatus" /> </div> - <!-- Status message --> - <div class="set-status-modal__header"> - <h3>{{ $t('user_status', 'Status message') }}</h3> - </div> - <div class="set-status-modal__custom-input"> - <EmojiPicker @select="setIcon"> - <button class="custom-input__emoji-button"> - {{ visibleIcon }} - </button> - </EmojiPicker> - <CustomMessageInput ref="customMessageInput" - :message="message" - @change="setMessage" - @submit="saveStatus" /> - </div> - <PredefinedStatusesList @select-status="selectPredefinedMessage" /> - <ClearAtSelect :clear-at="clearAt" - @select-clear-at="setClearAt" /> - <div class="status-buttons"> - <ButtonVue wide="true" - type="tertiary" - :text="$t('user_status', 'Clear status message')" - :disabled="isSavingStatus" - @click="clearStatus"> - {{ $t('user_status', 'Clear status message') }} - </ButtonVue> - <ButtonVue wide="true" - type="primary" - :text="$t('user_status', 'Set status message')" - :disabled="isSavingStatus" - @click="saveStatus"> - {{ $t('user_status', 'Set status message') }} - </ButtonVue> - </div> + <!-- Status message form --> + <form @submit.prevent="saveStatus" @reset="clearStatus"> + <h3 class="set-status-modal__header"> + {{ $t('user_status', 'Status message') }} + </h3> + <div class="set-status-modal__custom-input"> + <CustomMessageInput ref="customMessageInput" + :icon="icon" + :message="editedMessage" + @change="setMessage" + @select-icon="setIcon" /> + <NcButton v-if="messageId === 'vacationing'" + :href="absencePageUrl" + target="_blank" + type="secondary" + :aria-label="$t('user_status', 'Set absence period')"> + {{ $t('user_status', 'Set absence period and replacement') + ' ↗' }} + </NcButton> + </div> + <div v-if="hasBackupStatus" + class="set-status-modal__automation-hint"> + {{ $t('user_status', 'Your status was set automatically') }} + </div> + <PreviousStatus v-if="hasBackupStatus" + :icon="backupIcon" + :message="backupMessage" + @select="revertBackupFromServer" /> + <PredefinedStatusesList @select-status="selectPredefinedMessage" /> + <ClearAtSelect :clear-at="clearAt" + @select-clear-at="setClearAt" /> + <div class="status-buttons"> + <NcButton :wide="true" + type="tertiary" + native-type="reset" + :aria-label="$t('user_status', 'Clear status message')" + :disabled="isSavingStatus"> + {{ $t('user_status', 'Clear status message') }} + </NcButton> + <NcButton :wide="true" + type="primary" + native-type="submit" + :aria-label="$t('user_status', 'Set status message')" + :disabled="isSavingStatus"> + {{ $t('user_status', 'Set status message') }} + </NcButton> + </div> + </form> </div> - </Modal> + </NcModal> </template> <script> import { showError } from '@nextcloud/dialogs' -import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker' -import Modal from '@nextcloud/vue/dist/Components/Modal' -import ButtonVue from '@nextcloud/vue/dist/Components/Button' -import { getAllStatusOptions } from '../services/statusOptionsService' -import OnlineStatusMixin from '../mixins/OnlineStatusMixin' -import PredefinedStatusesList from './PredefinedStatusesList' -import CustomMessageInput from './CustomMessageInput' -import ClearAtSelect from './ClearAtSelect' -import OnlineStatusSelect from './OnlineStatusSelect' +import { generateUrl } from '@nextcloud/router' +import NcModal from '@nextcloud/vue/components/NcModal' +import NcButton from '@nextcloud/vue/components/NcButton' +import { getAllStatusOptions } from '../services/statusOptionsService.js' +import OnlineStatusMixin from '../mixins/OnlineStatusMixin.js' +import PredefinedStatusesList from './PredefinedStatusesList.vue' +import PreviousStatus from './PreviousStatus.vue' +import CustomMessageInput from './CustomMessageInput.vue' +import ClearAtSelect from './ClearAtSelect.vue' +import OnlineStatusSelect from './OnlineStatusSelect.vue' export default { name: 'SetStatusModal', @@ -92,32 +94,93 @@ export default { components: { ClearAtSelect, CustomMessageInput, - EmojiPicker, - Modal, + NcModal, OnlineStatusSelect, PredefinedStatusesList, - ButtonVue, + PreviousStatus, + NcButton, }, mixins: [OnlineStatusMixin], + props: { + /** + * Whether the component should be rendered as a Dashboard Status or a User Menu Entries + * true = Dashboard Status + * false = User Menu Entries + */ + inline: { + type: Boolean, + default: false, + }, + }, + data() { return { clearAt: null, - icon: null, - message: '', - messageId: '', + editedMessage: '', + predefinedMessageId: null, isSavingStatus: false, statuses: getAllStatusOptions(), } }, + computed: { - /** - * Returns the user-set icon or a smiley in case no icon is set - * - * @return {string} - */ - visibleIcon() { - return this.icon || '😀' + messageId() { + return this.$store.state.userStatus.messageId + }, + icon() { + return this.$store.state.userStatus.icon + }, + message() { + return this.$store.state.userStatus.message || '' + }, + hasBackupStatus() { + return this.messageId && (this.backupIcon || this.backupMessage) + }, + backupIcon() { + return this.$store.state.userBackupStatus.icon || '' + }, + backupMessage() { + return this.$store.state.userBackupStatus.message || '' + }, + + absencePageUrl() { + return generateUrl('settings/user/availability#absence') + }, + + resetButtonText() { + if (this.backupIcon && this.backupMessage) { + return this.$t('user_status', 'Reset status to "{icon} {message}"', { + icon: this.backupIcon, + message: this.backupMessage, + }) + } else if (this.backupMessage) { + return this.$t('user_status', 'Reset status to "{message}"', { + message: this.backupMessage, + }) + } else if (this.backupIcon) { + return this.$t('user_status', 'Reset status to "{icon}"', { + icon: this.backupIcon, + }) + } + + return this.$t('user_status', 'Reset status') + }, + + setReturnFocus() { + if (this.inline) { + return undefined + } + return document.querySelector('[aria-controls="header-menu-user-menu"]') ?? undefined + }, + }, + + watch: { + message: { + immediate: true, + handler(newValue) { + this.editedMessage = newValue + }, }, }, @@ -125,10 +188,9 @@ export default { * Loads the current status when a user opens dialog */ mounted() { - this.messageId = this.$store.state.userStatus.messageId - this.icon = this.$store.state.userStatus.icon - this.message = this.$store.state.userStatus.message || '' + this.$store.dispatch('fetchBackupFromServer') + this.predefinedMessageId = this.$store.state.userStatus.messageId if (this.$store.state.userStatus.clearAt !== null) { this.clearAt = { type: '_time', @@ -149,8 +211,12 @@ export default { * @param {string} icon The new icon */ setIcon(icon) { - this.messageId = null - this.icon = icon + this.predefinedMessageId = null + this.$store.dispatch('setCustomMessage', { + message: this.message, + icon, + clearAt: this.clearAt, + }) this.$nextTick(() => { this.$refs.customMessageInput.focus() }) @@ -161,8 +227,8 @@ export default { * @param {string} message The new message */ setMessage(message) { - this.messageId = null - this.message = message + this.predefinedMessageId = null + this.editedMessage = message }, /** * Sets a new clearAt value @@ -178,10 +244,12 @@ export default { * @param {object} status The predefined status object */ selectPredefinedMessage(status) { - this.messageId = status.id + this.predefinedMessageId = status.id this.clearAt = status.clearAt - this.icon = status.icon - this.message = status.message + this.$store.dispatch('setPredefinedMessage', { + messageId: status.id, + clearAt: status.clearAt, + }) }, /** * Saves the status and closes the @@ -196,15 +264,15 @@ export default { try { this.isSavingStatus = true - if (this.messageId !== undefined && this.messageId !== null) { - await this.$store.dispatch('setPredefinedMessage', { - messageId: this.messageId, + if (this.predefinedMessageId === null) { + await this.$store.dispatch('setCustomMessage', { + message: this.editedMessage, + icon: this.icon, clearAt: this.clearAt, }) } else { - await this.$store.dispatch('setCustomMessage', { - message: this.message, - icon: this.icon, + this.$store.dispatch('setPredefinedMessage', { + messageId: this.predefinedMessageId, clearAt: this.clearAt, }) } @@ -235,8 +303,30 @@ export default { } this.isSavingStatus = false + this.predefinedMessageId = null this.closeModal() }, + /** + * + * @return {Promise<void>} + */ + async revertBackupFromServer() { + try { + this.isSavingStatus = true + + await this.$store.dispatch('revertBackupFromServer', { + messageId: this.messageId, + }) + } catch (err) { + showError(this.$t('user_status', 'There was an error reverting the status')) + console.debug(err) + this.isSavingStatus = false + return + } + + this.isSavingStatus = false + this.predefinedMessageId = this.$store.state.userStatus?.messageId + }, }, } </script> @@ -246,38 +336,48 @@ export default { .set-status-modal { padding: 8px 20px 20px 20px; + &, & * { + box-sizing: border-box; + } + &__header { + font-size: 21px; text-align: center; - font-weight: bold; + height: fit-content; + min-height: var(--default-clickable-area); + line-height: var(--default-clickable-area); + overflow-wrap: break-word; + margin-block: 0 calc(2 * var(--default-grid-baseline)); } &__online-status { - display: grid; - // Space between the two sections - margin-bottom: 40px; - grid-template-columns: 1fr 1fr; + display: flex; + flex-direction: column; + gap: calc(2 * var(--default-grid-baseline)); + margin-block: 0 calc(2 * var(--default-grid-baseline)); } &__custom-input { display: flex; + flex-direction: column; + align-items: center; + gap: var(--default-grid-baseline); width: 100%; - margin-bottom: 10px; - - .custom-input__emoji-button { - flex-basis: 40px; - flex-grow: 0; - width: 40px; - height: 34px; - margin-right: 0; - border-right: none; - border-radius: var(--border-radius) 0 0 var(--border-radius); - } + padding-inline-start: var(--default-grid-baseline); + margin-block: 0 calc(2 * var(--default-grid-baseline)); + } + + &__automation-hint { + display: flex; + width: 100%; + margin-block: 0 calc(2 * var(--default-grid-baseline)); + color: var(--color-text-maxcontrast); } .status-buttons { display: flex; padding: 3px; - padding-left:0; + padding-inline-start:0; gap: 3px; } } |