diff options
Diffstat (limited to 'apps/user_status/src/components')
-rw-r--r-- | apps/user_status/src/components/ClearAtSelect.vue | 85 | ||||
-rw-r--r-- | apps/user_status/src/components/CustomMessageInput.vue | 106 | ||||
-rw-r--r-- | apps/user_status/src/components/OnlineStatusSelect.vue | 110 | ||||
-rw-r--r-- | apps/user_status/src/components/PredefinedStatus.vue | 128 | ||||
-rw-r--r-- | apps/user_status/src/components/PredefinedStatusesList.vue | 84 | ||||
-rw-r--r-- | apps/user_status/src/components/PreviousStatus.vue | 106 | ||||
-rw-r--r-- | apps/user_status/src/components/SetStatusModal.vue | 391 |
7 files changed, 1010 insertions, 0 deletions
diff --git a/apps/user_status/src/components/ClearAtSelect.vue b/apps/user_status/src/components/ClearAtSelect.vue new file mode 100644 index 00000000000..91b816dc04a --- /dev/null +++ b/apps/user_status/src/components/ClearAtSelect.vue @@ -0,0 +1,85 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <div class="clear-at-select"> + <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" + :value="option" + :clearable="false" + placement="top" + label-outside + @option:selected="select" /> + </div> +</template> + +<script> +import NcSelect from '@nextcloud/vue/components/NcSelect' +import { getAllClearAtOptions } from '../services/clearAtOptionsService.js' +import { clearAtFilter } from '../filters/clearAtFilter.js' + +export default { + name: 'ClearAtSelect', + components: { + NcSelect, + }, + props: { + clearAt: { + type: Object, + default: null, + }, + }, + data() { + return { + options: getAllClearAtOptions(), + } + }, + computed: { + /** + * Returns an object of the currently selected option + * + * @return {object} + */ + option() { + return { + clearAt: this.clearAt, + label: clearAtFilter(this.clearAt), + } + }, + }, + methods: { + /** + * Triggered when the user selects a new option. + * + * @param {object=} option The new selected option + */ + select(option) { + if (!option) { + return + } + + this.$emit('select-clear-at', option.clearAt) + }, + }, +} +</script> + +<style lang="scss" scoped> +.clear-at-select { + display: flex; + gap: calc(2 * var(--default-grid-baseline)); + align-items: center; + margin-block: 0 calc(2 * var(--default-grid-baseline)); + + &__select { + flex-grow: 1; + min-width: 215px; + } +} +</style> diff --git a/apps/user_status/src/components/CustomMessageInput.vue b/apps/user_status/src/components/CustomMessageInput.vue new file mode 100644 index 00000000000..fb129281430 --- /dev/null +++ b/apps/user_status/src/components/CustomMessageInput.vue @@ -0,0 +1,106 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <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, + default: () => '', + }, + disabled: { + type: Boolean, + 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() + }, + + /** + * Notifies the parent component about a changed input + * + * @param {Event} event The Change Event + */ + onChange(event) { + this.$emit('change', event.target.value) + }, + + setIcon(icon) { + this.$emit('select-icon', icon) + }, + }, +} +</script> + +<style lang="scss" scoped> +.custom-input { + display: flex; + align-items: flex-end; + gap: var(--default-grid-baseline); + width: 100%; + + &__container { + width: 100%; + } +} +</style> diff --git a/apps/user_status/src/components/OnlineStatusSelect.vue b/apps/user_status/src/components/OnlineStatusSelect.vue new file mode 100644 index 00000000000..0abcc8d68e6 --- /dev/null +++ b/apps/user_status/src/components/OnlineStatusSelect.vue @@ -0,0 +1,110 @@ +<!-- + - 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="hidden-visually user-status-online-select__input" + type="radio" + name="user-status-online" + @change="onChange"> + <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> + </div> +</template> + +<script> +import NcUserStatusIcon from '@nextcloud/vue/components/NcUserStatusIcon' + +export default { + name: 'OnlineStatusSelect', + + components: { + NcUserStatusIcon, + }, + + props: { + checked: { + type: Boolean, + default: false, + }, + type: { + type: String, + required: true, + }, + label: { + type: String, + required: true, + }, + subline: { + type: String, + default: null, + }, + }, + + computed: { + id() { + return `user-status-online-status-${this.type}` + }, + }, + + methods: { + onChange() { + this.$emit('select', this.type) + }, + }, +} +</script> + +<style lang="scss" scoped> +.user-status-online-select { + &__label { + 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); + + &, & * { + 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:focus-visible + &__label { + outline: 2px solid var(--color-primary-element) !important; + background-color: var(--color-background-dark); + } + + &__subline { + display: block; + color: var(--color-text-lighter); + } +} +</style> diff --git a/apps/user_status/src/components/PredefinedStatus.vue b/apps/user_status/src/components/PredefinedStatus.vue new file mode 100644 index 00000000000..b12892d4add --- /dev/null +++ b/apps/user_status/src/components/PredefinedStatus.vue @@ -0,0 +1,128 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <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.js' + +export default { + name: 'PredefinedStatus', + filters: { + clearAtFilter, + }, + props: { + messageId: { + type: String, + required: true, + }, + icon: { + type: String, + required: true, + }, + message: { + type: String, + required: true, + }, + clearAt: { + type: Object, + required: false, + default: null, + }, + selected: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + id() { + return `user-status-predefined-status-${this.messageId}` + }, + }, + methods: { + /** + * Emits an event when the user clicks the row + */ + select() { + this.$emit('select') + }, + }, +} +</script> + +<style lang="scss" scoped> +.predefined-status { + &__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); + + &, & * { + cursor: pointer; + } + + &:hover { + 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: ' – '; + } + } + } + + &__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 new file mode 100644 index 00000000000..cdf359dce76 --- /dev/null +++ b/apps/user_status/src/components/PredefinedStatusesList.vue @@ -0,0 +1,84 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <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)" /> + </ul> + <div v-else + class="predefined-statuses-list"> + <div class="icon icon-loading-small" /> + </div> +</template> + +<script> +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, + }), + ...mapGetters(['statusesHaveLoaded']), + }, + + watch: { + messageId: { + immediate: true, + handler() { + this.lastSelected = this.messageId + }, + }, + }, + + /** + * Loads all predefined statuses from the server + * when this component is mounted + */ + created() { + this.$store.dispatch('loadAllPredefinedStatuses') + }, + methods: { + /** + * Emits an event when the user selects a status + * + * @param {object} status The selected status + */ + selectStatus(status) { + this.lastSelected = status.id + this.$emit('select-status', status) + }, + }, +} +</script> + +<style lang="scss" scoped> +.predefined-statuses-list { + display: flex; + flex-direction: column; + 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 new file mode 100644 index 00000000000..8624ed19e94 --- /dev/null +++ b/apps/user_status/src/components/SetStatusModal.vue @@ -0,0 +1,391 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <NcModal size="normal" + label-id="user_status-set-dialog" + dark + :set-return-focus="setReturnFocus" + @close="closeModal"> + <div class="set-status-modal"> + <!-- Status selector --> + <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" + :checked="status.type === statusType" + @select="changeStatus" /> + </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> + </NcModal> +</template> + +<script> +import { showError } from '@nextcloud/dialogs' +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', + + components: { + ClearAtSelect, + CustomMessageInput, + NcModal, + OnlineStatusSelect, + PredefinedStatusesList, + 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, + editedMessage: '', + predefinedMessageId: null, + isSavingStatus: false, + statuses: getAllStatusOptions(), + } + }, + + computed: { + 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 + }, + }, + }, + + /** + * Loads the current status when a user opens dialog + */ + mounted() { + this.$store.dispatch('fetchBackupFromServer') + + this.predefinedMessageId = this.$store.state.userStatus.messageId + if (this.$store.state.userStatus.clearAt !== null) { + this.clearAt = { + type: '_time', + time: this.$store.state.userStatus.clearAt, + } + } + }, + methods: { + /** + * Closes the Set Status modal + */ + closeModal() { + this.$emit('close') + }, + /** + * Sets a new icon + * + * @param {string} icon The new icon + */ + setIcon(icon) { + this.predefinedMessageId = null + this.$store.dispatch('setCustomMessage', { + message: this.message, + icon, + clearAt: this.clearAt, + }) + this.$nextTick(() => { + this.$refs.customMessageInput.focus() + }) + }, + /** + * Sets a new message + * + * @param {string} message The new message + */ + setMessage(message) { + this.predefinedMessageId = null + this.editedMessage = message + }, + /** + * Sets a new clearAt value + * + * @param {object} clearAt The new clearAt object + */ + setClearAt(clearAt) { + this.clearAt = clearAt + }, + /** + * Sets new icon/message/clearAt based on a predefined message + * + * @param {object} status The predefined status object + */ + selectPredefinedMessage(status) { + this.predefinedMessageId = status.id + this.clearAt = status.clearAt + this.$store.dispatch('setPredefinedMessage', { + messageId: status.id, + clearAt: status.clearAt, + }) + }, + /** + * Saves the status and closes the + * + * @return {Promise<void>} + */ + async saveStatus() { + if (this.isSavingStatus) { + return + } + + try { + this.isSavingStatus = true + + if (this.predefinedMessageId === null) { + await this.$store.dispatch('setCustomMessage', { + message: this.editedMessage, + icon: this.icon, + clearAt: this.clearAt, + }) + } else { + this.$store.dispatch('setPredefinedMessage', { + messageId: this.predefinedMessageId, + clearAt: this.clearAt, + }) + } + } catch (err) { + showError(this.$t('user_status', 'There was an error saving the status')) + console.debug(err) + this.isSavingStatus = false + return + } + + this.isSavingStatus = false + this.closeModal() + }, + /** + * + * @return {Promise<void>} + */ + async clearStatus() { + try { + this.isSavingStatus = true + + await this.$store.dispatch('clearMessage') + } catch (err) { + showError(this.$t('user_status', 'There was an error clearing the status')) + console.debug(err) + this.isSavingStatus = false + return + } + + 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> + +<style lang="scss" scoped> + +.set-status-modal { + padding: 8px 20px 20px 20px; + + &, & * { + box-sizing: border-box; + } + + &__header { + font-size: 21px; + text-align: center; + 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: 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%; + 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-inline-start:0; + gap: 3px; + } +} + +@media only screen and (max-width: 500px) { + .set-status-modal__online-status { + grid-template-columns: none !important; + } +} + +</style> |