aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/settings/src/components/PersonalInfo/EmailSection/Email.vue')
-rw-r--r--apps/settings/src/components/PersonalInfo/EmailSection/Email.vue420
1 files changed, 420 insertions, 0 deletions
diff --git a/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue b/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue
new file mode 100644
index 00000000000..6a6baef8817
--- /dev/null
+++ b/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue
@@ -0,0 +1,420 @@
+<!--
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <div>
+ <div class="email" :class="{ 'email--additional': !primary }">
+ <div v-if="!primary" class="email__label-container">
+ <label :for="inputIdWithDefault">{{ inputPlaceholder }}</label>
+ <FederationControl v-if="!federationDisabled && !primary"
+ :readable="propertyReadable"
+ :additional="true"
+ :additional-value="email"
+ :disabled="federationDisabled"
+ :handle-additional-scope-change="saveAdditionalEmailScope"
+ :scope.sync="localScope"
+ @update:scope="onScopeChange" />
+ </div>
+ <div class="email__input-container">
+ <NcTextField :id="inputIdWithDefault"
+ ref="email"
+ class="email__input"
+ autocapitalize="none"
+ autocomplete="email"
+ :error="hasError || !!helperText"
+ :helper-text="helperTextWithNonConfirmed"
+ label-outside
+ :placeholder="inputPlaceholder"
+ spellcheck="false"
+ :success="isSuccess"
+ type="email"
+ :value.sync="emailAddress" />
+
+ <div class="email__actions">
+ <NcActions :aria-label="actionsLabel">
+ <NcActionButton v-if="!primary || !isNotificationEmail"
+ close-after-click
+ :disabled="!isConfirmedAddress"
+ @click="setNotificationMail">
+ <template #icon>
+ <NcIconSvgWrapper v-if="isNotificationEmail" :path="mdiStar" />
+ <NcIconSvgWrapper v-else :path="mdiStarOutline" />
+ </template>
+ {{ setNotificationMailLabel }}
+ </NcActionButton>
+ <NcActionButton close-after-click
+ :disabled="deleteDisabled"
+ @click="deleteEmail">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiTrashCanOutline" />
+ </template>
+ {{ deleteEmailLabel }}
+ </NcActionButton>
+ </NcActions>
+ </div>
+ </div>
+ </div>
+
+ <em v-if="isNotificationEmail">
+ {{ t('settings', 'Primary email for password reset and notifications') }}
+ </em>
+ </div>
+</template>
+
+<script>
+import NcActions from '@nextcloud/vue/components/NcActions'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+
+import debounce from 'debounce'
+
+import { mdiArrowLeft, mdiLockOutline, mdiStar, mdiStarOutline, mdiTrashCanOutline } from '@mdi/js'
+
+import FederationControl from '../shared/FederationControl.vue'
+import { handleError } from '../../../utils/handlers.ts'
+
+import { ACCOUNT_PROPERTY_READABLE_ENUM, VERIFICATION_ENUM } from '../../../constants/AccountPropertyConstants.js'
+import {
+ removeAdditionalEmail,
+ saveAdditionalEmail,
+ saveAdditionalEmailScope,
+ saveNotificationEmail,
+ savePrimaryEmail,
+ updateAdditionalEmail,
+} from '../../../service/PersonalInfo/EmailService.js'
+import { validateEmail } from '../../../utils/validate.js'
+
+export default {
+ name: 'Email',
+
+ components: {
+ NcActions,
+ NcActionButton,
+ NcIconSvgWrapper,
+ NcTextField,
+ FederationControl,
+ },
+
+ props: {
+ email: {
+ type: String,
+ required: true,
+ },
+ index: {
+ type: Number,
+ default: 0,
+ },
+ primary: {
+ type: Boolean,
+ default: false,
+ },
+ scope: {
+ type: String,
+ required: true,
+ },
+ activeNotificationEmail: {
+ type: String,
+ default: '',
+ },
+ localVerificationState: {
+ type: Number,
+ default: VERIFICATION_ENUM.NOT_VERIFIED,
+ },
+ inputId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ setup() {
+ return {
+ mdiArrowLeft,
+ mdiLockOutline,
+ mdiStar,
+ mdiStarOutline,
+ mdiTrashCanOutline,
+ saveAdditionalEmailScope,
+ }
+ },
+
+ data() {
+ return {
+ hasError: false,
+ helperText: null,
+ initialEmail: this.email,
+ isSuccess: false,
+ localScope: this.scope,
+ propertyReadable: ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL,
+ showFederationSettings: false,
+ }
+ },
+
+ computed: {
+ actionsLabel() {
+ if (this.primary) {
+ return t('settings', 'Email options')
+ } else {
+ return t('settings', 'Options for additional email address {index}', { index: this.index + 1 })
+ }
+ },
+
+ deleteDisabled() {
+ if (this.primary) {
+ // Disable for empty primary email as there is nothing to delete
+ // OR when initialEmail (reflects server state) and email (current input) are not the same
+ return this.email === '' || this.initialEmail !== this.email
+ } else if (this.initialEmail !== '') {
+ return this.initialEmail !== this.email
+ }
+ return false
+ },
+
+ deleteEmailLabel() {
+ if (this.primary) {
+ return t('settings', 'Remove primary email')
+ }
+ return t('settings', 'Delete email')
+ },
+
+ isConfirmedAddress() {
+ return this.primary || this.localVerificationState === VERIFICATION_ENUM.VERIFIED
+ },
+
+ isNotConfirmedHelperText() {
+ if (!this.isConfirmedAddress) {
+ return t('settings', 'This address is not confirmed')
+ }
+ return ''
+ },
+
+ helperTextWithNonConfirmed() {
+ if (this.helperText || this.hasError || this.isSuccess) {
+ return this.helperText || ''
+ }
+ return this.isNotConfirmedHelperText
+ },
+
+ setNotificationMailLabel() {
+ if (this.isNotificationEmail) {
+ return t('settings', 'Unset as primary email')
+ }
+ return t('settings', 'Set as primary email')
+ },
+
+ federationDisabled() {
+ return !this.initialEmail
+ },
+
+ inputIdWithDefault() {
+ return this.inputId || `account-property-email--${this.index}`
+ },
+
+ inputPlaceholder() {
+ // Primary email has implicit linked <label>
+ return !this.primary ? t('settings', 'Additional email address {index}', { index: this.index + 1 }) : undefined
+ },
+
+ isNotificationEmail() {
+ return (this.email && this.email === this.activeNotificationEmail)
+ || (this.primary && this.activeNotificationEmail === '')
+ },
+
+ emailAddress: {
+ get() {
+ return this.email
+ },
+ set(value) {
+ this.$emit('update:email', value)
+ this.debounceEmailChange(value.trim())
+ },
+ },
+ },
+
+ mounted() {
+ if (!this.primary && this.initialEmail === '') {
+ // $nextTick is needed here, otherwise it may not always work
+ // https://stackoverflow.com/questions/51922767/autofocus-input-on-mount-vue-ios/63485725#63485725
+ this.$nextTick(() => this.$refs.email?.focus())
+ }
+ },
+
+ methods: {
+ debounceEmailChange: debounce(async function(email) {
+ // TODO: provide method to get native input in NcTextField
+ this.helperText = this.$refs.email.$refs.inputField.$refs.input.validationMessage || null
+ if (this.helperText !== null) {
+ return
+ }
+ if (validateEmail(email) || email === '') {
+ if (this.primary) {
+ await this.updatePrimaryEmail(email)
+ } else {
+ if (email) {
+ if (this.initialEmail === '') {
+ await this.addAdditionalEmail(email)
+ } else {
+ await this.updateAdditionalEmail(email)
+ }
+ }
+ }
+ }
+ }, 1000),
+
+ async deleteEmail() {
+ if (this.primary) {
+ this.$emit('update:email', '')
+ await this.updatePrimaryEmail('')
+ } else {
+ await this.deleteAdditionalEmail()
+ }
+ },
+
+ async updatePrimaryEmail(email) {
+ try {
+ const responseData = await savePrimaryEmail(email)
+ this.handleResponse({
+ email,
+ status: responseData.ocs?.meta?.status,
+ })
+ } catch (e) {
+ if (email === '') {
+ this.handleResponse({
+ errorMessage: t('settings', 'Unable to delete primary email address'),
+ error: e,
+ })
+ } else {
+ this.handleResponse({
+ errorMessage: t('settings', 'Unable to update primary email address'),
+ error: e,
+ })
+ }
+ }
+ },
+
+ async addAdditionalEmail(email) {
+ try {
+ const responseData = await saveAdditionalEmail(email)
+ this.handleResponse({
+ email,
+ status: responseData.ocs?.meta?.status,
+ })
+ } catch (e) {
+ this.handleResponse({
+ errorMessage: t('settings', 'Unable to add additional email address'),
+ error: e,
+ })
+ }
+ },
+
+ async setNotificationMail() {
+ try {
+ const newNotificationMailValue = (this.primary || this.isNotificationEmail) ? '' : this.initialEmail
+ const responseData = await saveNotificationEmail(newNotificationMailValue)
+ this.handleResponse({
+ notificationEmail: newNotificationMailValue,
+ status: responseData.ocs?.meta?.status,
+ })
+ } catch (e) {
+ this.handleResponse({
+ errorMessage: 'Unable to choose this email for notifications',
+ error: e,
+ })
+ }
+ },
+
+ async updateAdditionalEmail(email) {
+ try {
+ const responseData = await updateAdditionalEmail(this.initialEmail, email)
+ this.handleResponse({
+ email,
+ status: responseData.ocs?.meta?.status,
+ })
+ } catch (e) {
+ this.handleResponse({
+ errorMessage: t('settings', 'Unable to update additional email address'),
+ error: e,
+ })
+ }
+ },
+
+ async deleteAdditionalEmail() {
+ try {
+ const responseData = await removeAdditionalEmail(this.initialEmail)
+ this.handleDeleteAdditionalEmail(responseData.ocs?.meta?.status)
+ } catch (e) {
+ this.handleResponse({
+ errorMessage: t('settings', 'Unable to delete additional email address'),
+ error: e,
+ })
+ }
+ },
+
+ handleDeleteAdditionalEmail(status) {
+ if (status === 'ok') {
+ this.$emit('delete-additional-email')
+ if (this.isNotificationEmail) {
+ this.$emit('update:notification-email', '')
+ }
+ } else {
+ this.handleResponse({
+ errorMessage: t('settings', 'Unable to delete additional email address'),
+ })
+ }
+ },
+
+ handleResponse({ email, notificationEmail, status, errorMessage, error }) {
+ if (status === 'ok') {
+ // Ensure that local state reflects server state
+ if (email) {
+ this.initialEmail = email
+ } else if (notificationEmail !== undefined) {
+ this.$emit('update:notification-email', notificationEmail)
+ }
+ this.isSuccess = true
+ setTimeout(() => { this.isSuccess = false }, 2000)
+ } else {
+ handleError(error, errorMessage)
+ this.hasError = true
+ setTimeout(() => { this.hasError = false }, 2000)
+ }
+ },
+
+ onScopeChange(scope) {
+ this.$emit('update:scope', scope)
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.email {
+ &__label-container {
+ height: var(--default-clickable-area);
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: calc(var(--default-grid-baseline) * 2);
+ }
+
+ &__input-container {
+ position: relative;
+ }
+
+ &__input {
+ // TODO: provide a way to hide status icon or combine it with trailing button in NcInputField
+ :deep(.input-field__icon--trailing) {
+ display: none;
+ }
+ }
+
+ &__actions {
+ position: absolute;
+ inset-block-start: 0;
+ inset-inline-end: 0;
+ }
+}
+</style>