- abstract shared components
- rewrite email section

Signed-off-by: Christopher Ng <chrng8@gmail.com>
pull/28028/head
Christopher Ng 2021-07-29 01:23:04 +07:00
parent db182d6517
commit a8ad5a3b6e
9 changed files with 470 additions and 154 deletions

@ -0,0 +1,167 @@
<!--
- @copyright 2021, Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.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/>.
-->
<template>
<div class="displayname">
<input
id="displayname"
ref="displayName"
type="text"
name="displayname"
:placeholder="t('settings', 'Your full name')"
:value="displayName"
autocapitalize="none"
autocomplete="on"
autocorrect="off"
required="true"
@input="onDisplayNameChange">
<div class="displayname__actions-container">
<transition name="fade">
<span v-if="showCheckmarkIcon" class="icon-checkmark" />
<span v-else-if="showErrorIcon" class="icon-error" />
</transition>
</div>
</div>
</template>
<script>
import { showError } from '@nextcloud/dialogs'
import debounce from 'debounce'
import { savePrimaryDisplayName } from '../../../service/PersonalInfo/DisplayNameService'
// TODO Global avatar updating on events (e.g. updating the displayname) is currently being handled by global js, investigate using https://github.com/nextcloud/nextcloud-event-bus for global avatar updating
export default {
name: 'DisplayName',
props: {
displayName: {
type: String,
required: true,
},
scope: {
type: String,
required: true,
},
},
data() {
return {
initialDisplayName: this.displayName,
localScope: this.scope,
showCheckmarkIcon: false,
showErrorIcon: false,
}
},
methods: {
onDisplayNameChange(e) {
this.$emit('update:display-name', e.target.value.trim())
// $nextTick() ensures that references to this.dipslayName further down the chain give the correct non-outdated value
this.$nextTick(() => this.debounceDisplayNameChange())
},
debounceDisplayNameChange: debounce(async function() {
if (this.$refs.displayName?.checkValidity() && this.isValid()) {
await this.updatePrimaryDisplayName()
}
}, 500),
async updatePrimaryDisplayName() {
try {
const responseData = await savePrimaryDisplayName(this.displayName)
this.handleResponse(responseData.ocs?.meta?.status)
} catch (e) {
this.handleResponse('error', 'Unable to update full name', e)
}
},
handleResponse(status, errorMessage, error) {
if (status === 'ok') {
// Ensure that local initialDiplayName state reflects server state
this.initialDisplayName = this.displayName
this.showCheckmarkIcon = true
setTimeout(() => { this.showCheckmarkIcon = false }, 2000)
} else {
showError(t('settings', errorMessage))
this.logger.error(errorMessage, error)
this.showErrorIcon = true
setTimeout(() => { this.showErrorIcon = false }, 2000)
}
},
isValid() {
return this.displayName !== ''
},
onScopeChange(scope) {
this.$emit('update:scope', scope)
},
},
}
</script>
<style lang="scss" scoped>
.displayname {
display: grid;
align-items: center;
input[type=text] {
grid-area: 1 / 1;
}
.displayname__actions-container {
grid-area: 1 / 1;
justify-self: flex-end;
height: 30px;
display: flex;
gap: 0 2px;
margin-right: 5px;
.icon-checkmark,
.icon-error {
height: 30px !important;
min-height: 30px !important;
width: 30px !important;
min-width: 30px !important;
top: 0;
right: 0;
float: none;
}
}
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.fade-enter-active {
transition: opacity 200ms ease-out;
}
.fade-leave-active {
transition: opacity 300ms ease-out;
}
</style>

@ -0,0 +1,100 @@
<!--
- @copyright 2021, Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.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/>.
-->
<template>
<form
ref="form"
class="section"
@submit.stop.prevent="() => {}">
<HeaderBar
:account-property="accountProperty"
label-for="displayname"
:is-editable="displayNameChangeSupported"
:is-valid-form="isValidForm"
:handle-scope-change="savePrimaryDisplayNameScope"
:scope.sync="primaryDisplayName.scope" />
<template v-if="displayNameChangeSupported">
<DisplayName
:scope.sync="primaryDisplayName.scope"
:display-name.sync="primaryDisplayName.value"
@update:display-name="onUpdateDisplayName" />
</template>
<span v-else>
{{ primaryDisplayName.value || t('settings', 'No full name set') }}
</span>
</form>
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import DisplayName from './DisplayName'
import HeaderBar from '../shared/HeaderBar'
import { ACCOUNT_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants'
import { savePrimaryDisplayNameScope } from '../../../service/PersonalInfo/DisplayNameService'
const { displayNames: { primaryDisplayName } } = loadState('settings', 'personalInfoParameters', {})
const { displayNameChangeSupported } = loadState('settings', 'accountParameters', {})
export default {
name: 'DisplayNameSection',
components: {
DisplayName,
HeaderBar,
},
data() {
return {
accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.DISPLAYNAME,
displayNameChangeSupported,
isValidForm: true,
primaryDisplayName,
savePrimaryDisplayNameScope,
}
},
mounted() {
this.$nextTick(() => this.updateFormValidity())
},
methods: {
onUpdateDisplayName() {
this.$nextTick(() => this.updateFormValidity())
},
updateFormValidity() {
this.isValidForm = this.$refs.form?.checkValidity()
},
},
}
</script>
<style lang="scss" scoped>
form::v-deep button {
&:disabled {
cursor: default;
}
}
</style>

@ -21,7 +21,7 @@
<template>
<div>
<div class="email-container">
<div class="email">
<input
ref="email"
type="email"
@ -34,21 +34,25 @@
required="true"
@input="onEmailChange">
<div class="email-actions-container">
<div class="email__actions-container">
<transition name="fade">
<span v-if="showCheckmarkIcon" class="icon-checkmark" />
<span v-else-if="showErrorIcon" class="icon-error" />
</transition>
<FederationControl v-if="!primary"
class="federation-control"
:disabled="federationDisabled"
:email="email"
:scope.sync="localScope"
@update:scope="onScopeChange" />
<template v-if="!primary">
<FederationControl
:account-property="accountProperty"
:additional="true"
:additional-value="email"
:disabled="federationDisabled"
:handle-scope-change="saveAdditionalEmailScope"
:scope.sync="localScope"
@update:scope="onScopeChange" />
</template>
<Actions
class="actions-email"
class="email__actions"
:aria-label="t('settings', 'Email options')"
:disabled="deleteDisabled"
:force-menu="true">
@ -75,8 +79,10 @@ import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import { showError } from '@nextcloud/dialogs'
import debounce from 'debounce'
import FederationControl from './FederationControl'
import { savePrimaryEmail, saveAdditionalEmail, updateAdditionalEmail, removeAdditionalEmail } from '../../../service/PersonalInfoService'
import FederationControl from '../shared/FederationControl'
import { ACCOUNT_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants'
import { savePrimaryEmail, saveAdditionalEmail, saveAdditionalEmailScope, updateAdditionalEmail, removeAdditionalEmail } from '../../../service/PersonalInfo/EmailService'
export default {
name: 'Email',
@ -92,60 +98,62 @@ export default {
type: String,
required: true,
},
scope: {
type: String,
required: true,
index: {
type: Number,
default: 0,
},
primary: {
type: Boolean,
default: false,
},
index: {
type: Number,
default: 0,
scope: {
type: String,
required: true,
},
},
data() {
return {
accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL,
initialEmail: this.email,
localScope: this.scope,
saveAdditionalEmailScope,
showCheckmarkIcon: false,
showErrorIcon: false,
}
},
computed: {
inputName() {
deleteDisabled() {
if (this.primary) {
return 'email'
return this.email === ''
}
return 'additionalEmail[]'
return this.email !== '' && !this.isValid()
},
inputPlaceholder() {
deleteEmailLabel() {
if (this.primary) {
return t('settings', 'Your email address')
return t('settings', 'Remove primary email')
}
return t('settings', 'Additional email address {index}', { index: this.index + 1 })
return t('settings', 'Delete email')
},
federationDisabled() {
return !this.initialEmail
},
deleteDisabled() {
inputName() {
if (this.primary) {
return this.email === ''
return 'email'
}
return this.email !== '' && !this.isValid()
return 'additionalEmail[]'
},
deleteEmailLabel() {
inputPlaceholder() {
if (this.primary) {
return t('settings', 'Remove primary email')
return t('settings', 'Your email address')
}
return t('settings', 'Delete email')
return t('settings', 'Additional email address {index}', { index: this.index + 1 })
},
},
@ -157,7 +165,7 @@ export default {
methods: {
onEmailChange(e) {
this.$emit('update:email', e.target.value)
this.$emit('update:email', e.target.value.trim())
// $nextTick() ensures that references to this.email further down the chain give the correct non-outdated value
this.$nextTick(() => this.debounceEmailChange())
},
@ -227,13 +235,9 @@ export default {
}
},
isValid() {
return /^\S+$/.test(this.email)
},
handleDeleteAdditionalEmail(status) {
if (status === 'ok') {
this.$emit('deleteAdditionalEmail')
this.$emit('delete-additional-email')
} else {
this.handleResponse('error', 'Unable to delete additional email address', {})
}
@ -253,6 +257,10 @@ export default {
}
},
isValid() {
return /^\S+$/.test(this.email)
},
onScopeChange(scope) {
this.$emit('update:scope', scope)
},
@ -261,7 +269,7 @@ export default {
</script>
<style lang="scss" scoped>
.email-container {
.email {
display: grid;
align-items: center;
@ -269,7 +277,7 @@ export default {
grid-area: 1 / 1;
}
.email-actions-container {
.email__actions-container {
grid-area: 1 / 1;
justify-self: flex-end;
height: 30px;
@ -278,7 +286,7 @@ export default {
gap: 0 2px;
margin-right: 5px;
.actions-email {
.email__actions {
opacity: 0.4 !important;
&:hover {
@ -293,17 +301,6 @@ export default {
}
}
.federation-control {
&::v-deep button {
// TODO remove this hack
padding-bottom: 7px;
height: 30px !important;
min-height: 30px !important;
width: 30px !important;
min-width: 30px !important;
}
}
.icon-checkmark,
.icon-error {
height: 30px !important;
@ -317,6 +314,11 @@ export default {
}
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.fade-enter-active {
transition: opacity 200ms ease-out;
}
@ -324,9 +326,4 @@ export default {
.fade-leave-active {
transition: opacity 300ms ease-out;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

@ -25,10 +25,14 @@
class="section"
@submit.stop.prevent="() => {}">
<HeaderBar
:can-edit-emails="displayNameChangeSupported"
:account-property="accountProperty"
label-for="email"
:handle-scope-change="savePrimaryEmailScope"
:is-editable="displayNameChangeSupported"
:is-multi-value-supported="true"
:is-valid-form="isValidForm"
:scope.sync="primaryEmail.scope"
@addAdditionalEmail="onAddAdditionalEmail" />
@add-additional="onAddAdditionalEmail" />
<template v-if="displayNameChangeSupported">
<Email
@ -42,7 +46,7 @@
:scope.sync="additionalEmail.scope"
:email.sync="additionalEmail.value"
@update:email="onUpdateEmail"
@deleteAdditionalEmail="onDeleteAdditionalEmail(index)" />
@delete-additional-email="onDeleteAdditionalEmail(index)" />
</template>
<span v-else>
@ -54,14 +58,14 @@
<script>
import { loadState } from '@nextcloud/initial-state'
import { showError } from '@nextcloud/dialogs'
import '@nextcloud/dialogs/styles/toast.scss'
import HeaderBar from './HeaderBar'
import Email from './Email'
import { savePrimaryEmail, removeAdditionalEmail } from '../../../service/PersonalInfoService'
import { DEFAULT_ADDITIONAL_EMAIL_SCOPE } from '../../../constants/AccountPropertyConstants'
import HeaderBar from '../shared/HeaderBar'
const { additionalEmails, primaryEmail } = loadState('settings', 'emails', {})
import { ACCOUNT_PROPERTY_READABLE_ENUM, DEFAULT_ADDITIONAL_EMAIL_SCOPE } from '../../../constants/AccountPropertyConstants'
import { savePrimaryEmail, savePrimaryEmailScope, removeAdditionalEmail } from '../../../service/PersonalInfo/EmailService'
const { emails: { additionalEmails, primaryEmail } } = loadState('settings', 'personalInfoParameters', {})
const { displayNameChangeSupported } = loadState('settings', 'accountParameters', {})
export default {
@ -74,10 +78,12 @@ export default {
data() {
return {
accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL,
additionalEmails,
displayNameChangeSupported,
primaryEmail,
isValidForm: true,
primaryEmail,
savePrimaryEmailScope,
}
},

@ -54,8 +54,8 @@ export default {
border: none;
background-color: transparent;
&:hover {
background-color: rgba(127, 127, 127, .15);
.icon {
margin-right: 8px;
}
&:enabled {
@ -66,13 +66,13 @@ export default {
}
}
&:hover {
background-color: rgba(127, 127, 127, .15);
}
&:enabled:hover {
background-color: rgba(127, 127, 127, .25);
opacity: 0.8 !important;
}
.icon {
margin-right: 8px;
}
}
</style>

@ -21,15 +21,15 @@
<template>
<Actions
class="actions-federation"
:aria-label="t('settings', 'Change privacy level of email')"
:class="{ 'federation-actions': !additional, 'federation-actions--additional': additional }"
:aria-label="ariaLabel"
:default-icon="scopeIcon"
:disabled="disabled">
<ActionButton v-for="federationScope in federationScopes"
:key="federationScope.name"
class="forced-action"
:class="{ 'forced-active': scope === federationScope.name }"
:aria-label="federationScope.tooltip"
class="federation-actions__btn"
:class="{ 'federation-actions__btn--active': scope === federationScope.name }"
:close-after-click="true"
:icon="federationScope.iconClass"
:title="federationScope.displayName"
@ -45,14 +45,10 @@ import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import { loadState } from '@nextcloud/initial-state'
import { showError } from '@nextcloud/dialogs'
import { SCOPE_ENUM, SCOPE_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants'
import { savePrimaryEmailScope, saveAdditionalEmailScope } from '../../../service/PersonalInfoService'
import { ACCOUNT_PROPERTY_READABLE_ENUM, PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM, SCOPE_ENUM, SCOPE_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants'
const { lookupServerUploadEnabled } = loadState('settings', 'accountParameters', {})
// TODO hardcoded for email, should abstract this for other sections
const excludedScopes = [SCOPE_ENUM.PRIVATE]
export default {
name: 'FederationControl',
@ -62,49 +58,63 @@ export default {
},
props: {
primary: {
accountProperty: {
type: String,
required: true,
validator: (value) => Object.values(ACCOUNT_PROPERTY_READABLE_ENUM).includes(value),
},
additional: {
type: Boolean,
default: false,
},
email: {
additionalValue: {
type: String,
default: '',
},
scope: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
handleScopeChange: {
type: Function,
required: true,
},
scope: {
type: String,
required: true,
},
},
data() {
return {
accountPropertyLowerCase: this.accountProperty.toLowerCase(),
initialScope: this.scope,
}
},
computed: {
ariaLabel() {
return t('settings', 'Change privacy level of {accountProperty}', { accountProperty: this.accountPropertyLowerCase })
},
federationScopes() {
return Object.values(SCOPE_PROPERTY_ENUM).filter(({ name }) => !this.unsupportedScopes.includes(name))
return Object.values(SCOPE_PROPERTY_ENUM).filter(({ name }) => this.supportedScopes.includes(name))
},
unsupportedScopes() {
if (!lookupServerUploadEnabled) {
scopeIcon() {
return SCOPE_PROPERTY_ENUM[this.scope].iconClass
},
supportedScopes() {
if (lookupServerUploadEnabled) {
return [
...excludedScopes,
...PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM[this.accountProperty],
SCOPE_ENUM.FEDERATED,
SCOPE_ENUM.PUBLISHED,
]
}
return excludedScopes
},
scopeIcon() {
return SCOPE_PROPERTY_ENUM[this.scope].iconClass
return PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM[this.accountProperty]
},
},
@ -113,29 +123,45 @@ export default {
this.$emit('update:scope', scope)
this.$nextTick(async() => {
if (this.primary) {
await this.updatePrimaryEmailScope()
if (!this.additional) {
await this.updatePrimaryScope()
} else {
await this.updateAdditionalEmailScope()
await this.updateAdditionalScope()
}
})
},
async updatePrimaryEmailScope() {
async updatePrimaryScope() {
try {
const responseData = await savePrimaryEmailScope(this.scope)
const responseData = await this.handleScopeChange(this.scope)
this.handleResponse(responseData.ocs?.meta?.status)
} catch (e) {
this.handleResponse('error', 'Unable to update federation scope of the primary email', e)
this.handleResponse(
'error',
t(
'settings',
'Unable to update federation scope of the primary {accountProperty}',
{ accountProperty: this.accountPropertyLowerCase }
),
e,
)
}
},
async updateAdditionalEmailScope() {
async updateAdditionalScope() {
try {
const responseData = await saveAdditionalEmailScope(this.email, this.scope)
const responseData = await this.handleScopeChange(this.additionalValue, this.scope)
this.handleResponse(responseData.ocs?.meta?.status)
} catch (e) {
this.handleResponse('error', 'Unable to update federation scope of additional email', e)
this.handleResponse(
'error',
t(
'settings',
'Unable to update federation scope of additional {accountProperty}',
{ accountProperty: this.accountPropertyLowerCase }
),
e,
)
}
},
@ -144,7 +170,7 @@ export default {
this.initialScope = this.scope
} else {
this.$emit('update:scope', this.initialScope)
showError(t('settings', errorMessage))
showError(errorMessage)
this.logger.error(errorMessage, error)
}
},
@ -153,7 +179,8 @@ export default {
</script>
<style lang="scss" scoped>
.actions-federation {
.federation-actions,
.federation-actions--additional {
opacity: 0.4 !important;
&:hover {
@ -161,12 +188,18 @@ export default {
}
}
.forced-active {
background-color: var(--color-primary-light) !important;
box-shadow: inset 2px 0 var(--color-primary) !important;
.federation-actions--additional {
&::v-deep button {
// TODO remove this hack
padding-bottom: 7px;
height: 30px !important;
min-height: 30px !important;
width: 30px !important;
min-width: 30px !important;
}
}
.forced-action {
.federation-actions__btn {
&::v-deep p {
width: 150px !important;
padding: 8px 0 !important;
@ -175,4 +208,9 @@ export default {
line-height: 1.5em !important;
}
}
.federation-actions__btn--active {
background-color: var(--color-primary-light) !important;
box-shadow: inset 2px 0 var(--color-primary) !important;
}
</style>

@ -21,44 +21,66 @@
<template>
<h3>
<label for="email">
{{ t('settings', 'Email') }}
<label :for="labelFor">
{{ t('settings', accountProperty) }}
</label>
<FederationControl
class="federation-control"
:primary="true"
:account-property="accountProperty"
:handle-scope-change="handleScopeChange"
:scope.sync="localScope"
@update:scope="onScopeChange" />
<AddButton v-if="canEditEmails"
class="add-button"
:disabled="!isValidForm"
@click.stop.prevent="addAdditionalEmail" />
<template v-if="isEditable && isMultiValueSupported">
<AddButton
class="add-button"
:disabled="!isValidForm"
@click.stop.prevent="onAddAdditional" />
</template>
</h3>
</template>
<script>
import FederationControl from './FederationControl'
import AddButton from './AddButton'
import FederationControl from './FederationControl'
import { ACCOUNT_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants'
export default {
name: 'HeaderBar',
components: {
FederationControl,
AddButton,
FederationControl,
},
props: {
canEditEmails: {
accountProperty: {
type: String,
required: true,
validator: (value) => Object.values(ACCOUNT_PROPERTY_READABLE_ENUM).includes(value),
},
handleScopeChange: {
type: Function,
required: true,
},
isEditable: {
type: Boolean,
default: true,
required: true,
},
isMultiValueSupported: {
type: Boolean,
default: false,
},
isValidForm: {
type: Boolean,
default: true,
},
labelFor: {
type: String,
required: true,
},
scope: {
type: String,
required: true,
@ -72,8 +94,8 @@ export default {
},
methods: {
addAdditionalEmail() {
this.$emit('addAdditionalEmail')
onAddAdditional() {
this.$emit('add-additional')
},
onScopeChange(scope) {

@ -21,18 +21,29 @@
*/
import Vue from 'vue'
import { getRequestToken } from '@nextcloud/auth'
import { translate as t } from '@nextcloud/l10n'
import '@nextcloud/dialogs/styles/toast.scss'
import logger from './logger'
import DisplayNameSection from './components/PersonalInfo/DisplayNameSection/DisplayNameSection'
import EmailSection from './components/PersonalInfo/EmailSection/EmailSection'
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(OC.requestToken)
__webpack_nonce__ = btoa(getRequestToken())
Vue.prototype.t = t
Vue.prototype.logger = logger
const View = Vue.extend(EmailSection)
export default new View({
el: '#vue-emailsection',
Vue.mixin({
props: {
logger,
},
methods: {
t,
},
})
const DisplayNameView = Vue.extend(DisplayNameSection)
const EmailView = Vue.extend(EmailSection)
new DisplayNameView().$mount('#vue-displaynamesection')
new EmailView().$mount('#vue-emailsection')

@ -100,32 +100,7 @@ script('settings', [
<div class="personal-settings-container">
<div class="personal-settings-setting-box">
<form id="displaynameform" class="section">
<h3>
<label for="displayname"><?php p($l->t('Full name')); ?></label>
<a href="#" class="federation-menu" aria-label="<?php p($l->t('Change privacy level of full name')); ?>">
<span class="icon-federation-menu icon-password">
<span class="icon-triangle-s"></span>
</span>
</a>
</h3>
<input type="text" id="displayname" name="displayname"
<?php if (!$_['displayNameChangeSupported']) {
print_unescaped('class="hidden"');
} ?>
value="<?php p($_['displayName']) ?>"
autocomplete="on" autocapitalize="none" autocorrect="off" />
<?php if (!$_['displayNameChangeSupported']) { ?>
<span><?php if (isset($_['displayName']) && !empty($_['displayName'])) {
p($_['displayName']);
} else {
p($l->t('No display name set'));
} ?></span>
<?php } ?>
<span class="icon-checkmark hidden"></span>
<span class="icon-error hidden" ></span>
<input type="hidden" id="displaynamescope" value="<?php p($_['displayNameScope']) ?>">
</form>
<div id="vue-displaynamesection" class="section"></div>
</div>
<div class="personal-settings-setting-box">
<div id="vue-emailsection" class="section"></div>