mirror of
https://github.com/nextcloud/server.git
synced 2026-05-28 04:32:30 -04:00
Merge pull request #28028 from nextcloud/feat/27869/full-name
This commit is contained in:
commit
78a5768888
36 changed files with 947 additions and 371 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -145,14 +145,17 @@ class PersonalInfo implements ISettings {
|
|||
'groups' => $this->getGroups($user),
|
||||
] + $messageParameters + $languageParameters + $localeParameters;
|
||||
|
||||
$emails = $this->getEmails($account);
|
||||
$personalInfoParameters = [
|
||||
'displayNames' => $this->getDisplayNames($account),
|
||||
'emails' => $this->getEmails($account),
|
||||
];
|
||||
|
||||
$accountParameters = [
|
||||
'displayNameChangeSupported' => $user->canChangeDisplayName(),
|
||||
'lookupServerUploadEnabled' => $lookupServerUploadEnabled,
|
||||
];
|
||||
|
||||
$this->initialStateService->provideInitialState('emails', $emails);
|
||||
$this->initialStateService->provideInitialState('personalInfoParameters', $personalInfoParameters);
|
||||
$this->initialStateService->provideInitialState('accountParameters', $accountParameters);
|
||||
|
||||
return new TemplateResponse('settings', 'settings/personal/personal.info', $parameters, '');
|
||||
|
|
@ -196,6 +199,29 @@ class PersonalInfo implements ISettings {
|
|||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the primary display name in an
|
||||
* associative array
|
||||
*
|
||||
* NOTE may be extended to provide additional display names (i.e. aliases) in the future
|
||||
*
|
||||
* @param IAccount $account
|
||||
* @return array
|
||||
*/
|
||||
private function getDisplayNames(IAccount $account): array {
|
||||
$primaryDisplayName = [
|
||||
'value' => $account->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getValue(),
|
||||
'scope' => $account->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getScope(),
|
||||
'verified' => $account->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getVerified(),
|
||||
];
|
||||
|
||||
$displayNames = [
|
||||
'primaryDisplayName' => $primaryDisplayName,
|
||||
];
|
||||
|
||||
return $displayNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the primary email and additional emails in an
|
||||
* associative array
|
||||
|
|
|
|||
|
|
@ -0,0 +1,179 @@
|
|||
<!--
|
||||
- @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'
|
||||
import { validateDisplayName } from '../../../utils/validate'
|
||||
|
||||
// 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)
|
||||
this.debounceDisplayNameChange(e.target.value.trim())
|
||||
},
|
||||
|
||||
debounceDisplayNameChange: debounce(async function(displayName) {
|
||||
if (validateDisplayName(displayName)) {
|
||||
await this.updatePrimaryDisplayName(displayName)
|
||||
}
|
||||
}, 500),
|
||||
|
||||
async updatePrimaryDisplayName(displayName) {
|
||||
try {
|
||||
const responseData = await savePrimaryDisplayName(displayName)
|
||||
this.handleResponse({
|
||||
displayName,
|
||||
status: responseData.ocs?.meta?.status,
|
||||
})
|
||||
} catch (e) {
|
||||
this.handleResponse({
|
||||
errorMessage: 'Unable to update full name',
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleResponse({ displayName, status, errorMessage, error }) {
|
||||
if (status === 'ok') {
|
||||
// Ensure that local initialDiplayName state reflects server state
|
||||
this.initialDisplayName = 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)
|
||||
}
|
||||
},
|
||||
|
||||
onScopeChange(scope) {
|
||||
this.$emit('update:scope', scope)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.displayname {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
grid-area: 1 / 1;
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
margin: 3px 3px 3px 0;
|
||||
padding: 7px 6px;
|
||||
cursor: text;
|
||||
font-family: var(--font-face);
|
||||
border: 1px solid var(--color-border-dark);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-main-background);
|
||||
color: var(--color-main-text);
|
||||
}
|
||||
|
||||
.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,90 @@
|
|||
<!--
|
||||
- @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>
|
||||
<section>
|
||||
<HeaderBar
|
||||
:account-property="accountProperty"
|
||||
label-for="displayname"
|
||||
:is-editable="displayNameChangeSupported"
|
||||
:is-valid-section="isValidSection"
|
||||
:handle-scope-change="savePrimaryDisplayNameScope"
|
||||
:scope.sync="primaryDisplayName.scope" />
|
||||
|
||||
<template v-if="displayNameChangeSupported">
|
||||
<DisplayName
|
||||
:display-name.sync="primaryDisplayName.value"
|
||||
:scope.sync="primaryDisplayName.scope" />
|
||||
</template>
|
||||
|
||||
<span v-else>
|
||||
{{ primaryDisplayName.value || t('settings', 'No full name set') }}
|
||||
</span>
|
||||
</section>
|
||||
</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'
|
||||
import { validateDisplayName } from '../../../utils/validate'
|
||||
|
||||
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,
|
||||
primaryDisplayName,
|
||||
savePrimaryDisplayNameScope,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isValidSection() {
|
||||
return validateDisplayName(this.primaryDisplayName.value)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
section {
|
||||
padding: 10px 10px;
|
||||
|
||||
&::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,27 +34,32 @@
|
|||
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">
|
||||
<ActionButton
|
||||
:aria-label="deleteEmailLabel"
|
||||
:close-after-click="true"
|
||||
:disabled="deleteDisabled"
|
||||
icon="icon-delete"
|
||||
@click.stop.prevent="deleteEmail">
|
||||
{{ deleteEmailLabel }}
|
||||
|
|
@ -75,8 +80,11 @@ 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'
|
||||
import { validateEmail } from '../../../utils/validate'
|
||||
|
||||
export default {
|
||||
name: 'Email',
|
||||
|
|
@ -92,30 +100,54 @@ 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: {
|
||||
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')
|
||||
},
|
||||
|
||||
federationDisabled() {
|
||||
return !this.initialEmail
|
||||
},
|
||||
|
||||
inputName() {
|
||||
if (this.primary) {
|
||||
return 'email'
|
||||
|
|
@ -129,28 +161,11 @@ export default {
|
|||
}
|
||||
return t('settings', 'Additional email address {index}', { index: this.index + 1 })
|
||||
},
|
||||
|
||||
federationDisabled() {
|
||||
return !this.initialEmail
|
||||
},
|
||||
|
||||
deleteDisabled() {
|
||||
if (this.primary) {
|
||||
return this.email === ''
|
||||
}
|
||||
return this.email !== '' && !this.isValid()
|
||||
},
|
||||
|
||||
deleteEmailLabel() {
|
||||
if (this.primary) {
|
||||
return t('settings', 'Remove primary email')
|
||||
}
|
||||
return t('settings', 'Delete email')
|
||||
},
|
||||
},
|
||||
|
||||
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())
|
||||
}
|
||||
},
|
||||
|
|
@ -158,20 +173,19 @@ export default {
|
|||
methods: {
|
||||
onEmailChange(e) {
|
||||
this.$emit('update:email', e.target.value)
|
||||
// $nextTick() ensures that references to this.email further down the chain give the correct non-outdated value
|
||||
this.$nextTick(() => this.debounceEmailChange())
|
||||
this.debounceEmailChange(e.target.value.trim())
|
||||
},
|
||||
|
||||
debounceEmailChange: debounce(async function() {
|
||||
if (this.$refs.email?.checkValidity() || this.email === '') {
|
||||
debounceEmailChange: debounce(async function(email) {
|
||||
if (validateEmail(email) || email === '') {
|
||||
if (this.primary) {
|
||||
await this.updatePrimaryEmail()
|
||||
await this.updatePrimaryEmail(email)
|
||||
} else {
|
||||
if (this.email) {
|
||||
if (email) {
|
||||
if (this.initialEmail === '') {
|
||||
await this.addAdditionalEmail()
|
||||
await this.addAdditionalEmail(email)
|
||||
} else {
|
||||
await this.updateAdditionalEmail()
|
||||
await this.updateAdditionalEmail(email)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -181,40 +195,61 @@ export default {
|
|||
async deleteEmail() {
|
||||
if (this.primary) {
|
||||
this.$emit('update:email', '')
|
||||
this.$nextTick(async() => await this.updatePrimaryEmail())
|
||||
await this.updatePrimaryEmail('')
|
||||
} else {
|
||||
await this.deleteAdditionalEmail()
|
||||
}
|
||||
},
|
||||
|
||||
async updatePrimaryEmail() {
|
||||
async updatePrimaryEmail(email) {
|
||||
try {
|
||||
const responseData = await savePrimaryEmail(this.email)
|
||||
this.handleResponse(responseData.ocs?.meta?.status)
|
||||
const responseData = await savePrimaryEmail(email)
|
||||
this.handleResponse({
|
||||
email,
|
||||
status: responseData.ocs?.meta?.status,
|
||||
})
|
||||
} catch (e) {
|
||||
if (this.email === '') {
|
||||
this.handleResponse('error', 'Unable to delete primary email address', e)
|
||||
if (email === '') {
|
||||
this.handleResponse({
|
||||
errorMessage: 'Unable to delete primary email address',
|
||||
error: e,
|
||||
})
|
||||
} else {
|
||||
this.handleResponse('error', 'Unable to update primary email address', e)
|
||||
this.handleResponse({
|
||||
errorMessage: 'Unable to update primary email address',
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async addAdditionalEmail() {
|
||||
async addAdditionalEmail(email) {
|
||||
try {
|
||||
const responseData = await saveAdditionalEmail(this.email)
|
||||
this.handleResponse(responseData.ocs?.meta?.status)
|
||||
const responseData = await saveAdditionalEmail(email)
|
||||
this.handleResponse({
|
||||
email,
|
||||
status: responseData.ocs?.meta?.status,
|
||||
})
|
||||
} catch (e) {
|
||||
this.handleResponse('error', 'Unable to add additional email address', e)
|
||||
this.handleResponse({
|
||||
errorMessage: 'Unable to add additional email address',
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async updateAdditionalEmail() {
|
||||
async updateAdditionalEmail(email) {
|
||||
try {
|
||||
const responseData = await updateAdditionalEmail(this.initialEmail, this.email)
|
||||
this.handleResponse(responseData.ocs?.meta?.status)
|
||||
const responseData = await updateAdditionalEmail(this.initialEmail, email)
|
||||
this.handleResponse({
|
||||
email,
|
||||
status: responseData.ocs?.meta?.status,
|
||||
})
|
||||
} catch (e) {
|
||||
this.handleResponse('error', 'Unable to update additional email address', e)
|
||||
this.handleResponse({
|
||||
errorMessage: 'Unable to update additional email address',
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -223,26 +258,27 @@ export default {
|
|||
const responseData = await removeAdditionalEmail(this.initialEmail)
|
||||
this.handleDeleteAdditionalEmail(responseData.ocs?.meta?.status)
|
||||
} catch (e) {
|
||||
this.handleResponse('error', 'Unable to delete additional email address', e)
|
||||
this.handleResponse({
|
||||
errorMessage: 'Unable to delete additional email address',
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
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', {})
|
||||
this.handleResponse({
|
||||
errorMessage: 'Unable to delete additional email address',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleResponse(status, errorMessage, error) {
|
||||
handleResponse({ email, status, errorMessage, error }) {
|
||||
if (status === 'ok') {
|
||||
// Ensure that local initialEmail state reflects server state
|
||||
this.initialEmail = this.email
|
||||
this.initialEmail = email
|
||||
this.showCheckmarkIcon = true
|
||||
setTimeout(() => { this.showCheckmarkIcon = false }, 2000)
|
||||
} else {
|
||||
|
|
@ -261,15 +297,25 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.email-container {
|
||||
.email {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
|
||||
input[type=email] {
|
||||
input {
|
||||
grid-area: 1 / 1;
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
margin: 3px 3px 3px 0;
|
||||
padding: 7px 6px;
|
||||
cursor: text;
|
||||
font-family: var(--font-face);
|
||||
border: 1px solid var(--color-border-dark);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-main-background);
|
||||
color: var(--color-main-text);
|
||||
}
|
||||
|
||||
.email-actions-container {
|
||||
.email__actions-container {
|
||||
grid-area: 1 / 1;
|
||||
justify-self: flex-end;
|
||||
height: 30px;
|
||||
|
|
@ -278,7 +324,7 @@ export default {
|
|||
gap: 0 2px;
|
||||
margin-right: 5px;
|
||||
|
||||
.actions-email {
|
||||
.email__actions {
|
||||
opacity: 0.4 !important;
|
||||
|
||||
&:hover {
|
||||
|
|
@ -293,17 +339,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 +352,11 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-active {
|
||||
transition: opacity 200ms ease-out;
|
||||
}
|
||||
|
|
@ -324,9 +364,4 @@ export default {
|
|||
.fade-leave-active {
|
||||
transition: opacity 300ms ease-out;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -20,15 +20,16 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<form
|
||||
ref="form"
|
||||
class="section"
|
||||
@submit.stop.prevent="() => {}">
|
||||
<section>
|
||||
<HeaderBar
|
||||
:can-edit-emails="displayNameChangeSupported"
|
||||
:is-valid-form="isValidForm"
|
||||
:account-property="accountProperty"
|
||||
label-for="email"
|
||||
:handle-scope-change="savePrimaryEmailScope"
|
||||
:is-editable="displayNameChangeSupported"
|
||||
:is-multi-value-supported="true"
|
||||
:is-valid-section="isValidSection"
|
||||
:scope.sync="primaryEmail.scope"
|
||||
@addAdditionalEmail="onAddAdditionalEmail" />
|
||||
@add-additional="onAddAdditionalEmail" />
|
||||
|
||||
<template v-if="displayNameChangeSupported">
|
||||
<Email
|
||||
|
|
@ -42,26 +43,27 @@
|
|||
:scope.sync="additionalEmail.scope"
|
||||
:email.sync="additionalEmail.value"
|
||||
@update:email="onUpdateEmail"
|
||||
@deleteAdditionalEmail="onDeleteAdditionalEmail(index)" />
|
||||
@delete-additional-email="onDeleteAdditionalEmail(index)" />
|
||||
</template>
|
||||
|
||||
<span v-else>
|
||||
{{ primaryEmail.value || t('settings', 'No email address set') }}
|
||||
</span>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<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'
|
||||
import { validateEmail } from '../../../utils/validate'
|
||||
|
||||
const { emails: { additionalEmails, primaryEmail } } = loadState('settings', 'personalInfoParameters', {})
|
||||
const { displayNameChangeSupported } = loadState('settings', 'accountParameters', {})
|
||||
|
||||
export default {
|
||||
|
|
@ -74,14 +76,27 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL,
|
||||
additionalEmails,
|
||||
displayNameChangeSupported,
|
||||
primaryEmail,
|
||||
isValidForm: true,
|
||||
savePrimaryEmailScope,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
firstAdditionalEmail() {
|
||||
if (this.additionalEmails.length) {
|
||||
return this.additionalEmails[0].value
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
isValidSection() {
|
||||
return validateEmail(this.primaryEmail.value)
|
||||
&& this.additionalEmails.map(({ value }) => value).every(validateEmail)
|
||||
},
|
||||
|
||||
primaryEmailValue: {
|
||||
get() {
|
||||
return this.primaryEmail.value
|
||||
|
|
@ -90,41 +105,25 @@ export default {
|
|||
this.primaryEmail.value = value
|
||||
},
|
||||
},
|
||||
|
||||
firstAdditionalEmail() {
|
||||
if (this.additionalEmails.length) {
|
||||
return this.additionalEmails[0].value
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$nextTick(() => this.updateFormValidity())
|
||||
},
|
||||
|
||||
methods: {
|
||||
onAddAdditionalEmail() {
|
||||
if (this.$refs.form?.checkValidity()) {
|
||||
if (this.isValidSection) {
|
||||
this.additionalEmails.push({ value: '', scope: DEFAULT_ADDITIONAL_EMAIL_SCOPE })
|
||||
this.$nextTick(() => this.updateFormValidity())
|
||||
}
|
||||
},
|
||||
|
||||
onDeleteAdditionalEmail(index) {
|
||||
this.$delete(this.additionalEmails, index)
|
||||
this.$nextTick(() => this.updateFormValidity())
|
||||
},
|
||||
|
||||
async onUpdateEmail() {
|
||||
this.$nextTick(() => this.updateFormValidity())
|
||||
|
||||
if (this.primaryEmailValue === '' && this.firstAdditionalEmail) {
|
||||
const deletedEmail = this.firstAdditionalEmail
|
||||
await this.deleteFirstAdditionalEmail()
|
||||
this.primaryEmailValue = deletedEmail
|
||||
await this.updatePrimaryEmail()
|
||||
this.$nextTick(() => this.updateFormValidity())
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -160,17 +159,15 @@ export default {
|
|||
this.logger.error(errorMessage, error)
|
||||
}
|
||||
},
|
||||
|
||||
updateFormValidity() {
|
||||
this.isValidForm = this.$refs.form?.checkValidity()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
form::v-deep button {
|
||||
&:disabled {
|
||||
section {
|
||||
padding: 10px 10px;
|
||||
|
||||
&::v-deep button:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.toLocaleLowerCase(),
|
||||
initialScope: this.scope,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
federationScopes() {
|
||||
return Object.values(SCOPE_PROPERTY_ENUM).filter(({ name }) => !this.unsupportedScopes.includes(name))
|
||||
ariaLabel() {
|
||||
return t('settings', 'Change privacy level of {accountProperty}', { accountProperty: this.accountPropertyLowerCase })
|
||||
},
|
||||
|
||||
unsupportedScopes() {
|
||||
if (!lookupServerUploadEnabled) {
|
||||
federationScopes() {
|
||||
return Object.values(SCOPE_PROPERTY_ENUM).filter(({ name }) => this.supportedScopes.includes(name))
|
||||
},
|
||||
|
||||
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]
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -112,39 +122,49 @@ export default {
|
|||
async changeScope(scope) {
|
||||
this.$emit('update:scope', scope)
|
||||
|
||||
this.$nextTick(async() => {
|
||||
if (this.primary) {
|
||||
await this.updatePrimaryEmailScope()
|
||||
} else {
|
||||
await this.updateAdditionalEmailScope()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async updatePrimaryEmailScope() {
|
||||
try {
|
||||
const responseData = await savePrimaryEmailScope(this.scope)
|
||||
this.handleResponse(responseData.ocs?.meta?.status)
|
||||
} catch (e) {
|
||||
this.handleResponse('error', 'Unable to update federation scope of the primary email', e)
|
||||
if (!this.additional) {
|
||||
await this.updatePrimaryScope(scope)
|
||||
} else {
|
||||
await this.updateAdditionalScope(scope)
|
||||
}
|
||||
},
|
||||
|
||||
async updateAdditionalEmailScope() {
|
||||
async updatePrimaryScope(scope) {
|
||||
try {
|
||||
const responseData = await saveAdditionalEmailScope(this.email, this.scope)
|
||||
this.handleResponse(responseData.ocs?.meta?.status)
|
||||
const responseData = await this.handleScopeChange(scope)
|
||||
this.handleResponse({
|
||||
scope,
|
||||
status: responseData.ocs?.meta?.status,
|
||||
})
|
||||
} catch (e) {
|
||||
this.handleResponse('error', 'Unable to update federation scope of additional email', e)
|
||||
this.handleResponse({
|
||||
errorMessage: t('settings', 'Unable to update federation scope of the primary {accountProperty}', { accountProperty: this.accountPropertyLowerCase }),
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleResponse(status, errorMessage, error) {
|
||||
async updateAdditionalScope(scope) {
|
||||
try {
|
||||
const responseData = await this.handleScopeChange(this.additionalValue, scope)
|
||||
this.handleResponse({
|
||||
scope,
|
||||
status: responseData.ocs?.meta?.status,
|
||||
})
|
||||
} catch (e) {
|
||||
this.handleResponse({
|
||||
errorMessage: t('settings', 'Unable to update federation scope of additional {accountProperty}', { accountProperty: this.accountPropertyLowerCase }),
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleResponse({ scope, status, errorMessage, error }) {
|
||||
if (status === 'ok') {
|
||||
this.initialScope = this.scope
|
||||
this.initialScope = scope
|
||||
} else {
|
||||
this.$emit('update:scope', this.initialScope)
|
||||
showError(t('settings', errorMessage))
|
||||
showError(errorMessage)
|
||||
this.logger.error(errorMessage, error)
|
||||
}
|
||||
},
|
||||
|
|
@ -153,7 +173,8 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions-federation {
|
||||
.federation-actions,
|
||||
.federation-actions--additional {
|
||||
opacity: 0.4 !important;
|
||||
|
||||
&:hover {
|
||||
|
|
@ -161,12 +182,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 +202,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,43 +21,66 @@
|
|||
|
||||
<template>
|
||||
<h3>
|
||||
<label for="email">
|
||||
{{ t('settings', 'Email') }}
|
||||
<label :for="labelFor">
|
||||
<!-- Already translated as required by prop validator -->
|
||||
{{ 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="!isValidSection"
|
||||
@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,
|
||||
required: true,
|
||||
},
|
||||
isMultiValueSupported: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isValidSection: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isValidForm: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
labelFor: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
scope: {
|
||||
type: String,
|
||||
|
|
@ -72,8 +95,8 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
addAdditionalEmail() {
|
||||
this.$emit('addAdditionalEmail')
|
||||
onAddAdditional() {
|
||||
this.$emit('add-additional')
|
||||
},
|
||||
|
||||
onScopeChange(scope) {
|
||||
|
|
@ -84,6 +107,18 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h3 {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
margin: 12px 0 0 0;
|
||||
font-size: 16px;
|
||||
color: var(--color-text-light);
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.federation-control {
|
||||
margin: -12px 0 0 8px;
|
||||
}
|
||||
|
|
@ -24,50 +24,73 @@
|
|||
* SYNC to be kept in sync with lib/public/Accounts/IAccountManager.php
|
||||
*/
|
||||
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
/** Enum of account properties */
|
||||
export const ACCOUNT_PROPERTY_ENUM = Object.freeze({
|
||||
ADDRESS: 'address',
|
||||
AVATAR: 'avatar',
|
||||
DISPLAYNAME: 'displayname',
|
||||
PHONE: 'phone',
|
||||
EMAIL: 'email',
|
||||
WEBSITE: 'website',
|
||||
ADDRESS: 'address',
|
||||
TWITTER: 'twitter',
|
||||
EMAIL_COLLECTION: 'additional_mail',
|
||||
PHONE: 'phone',
|
||||
TWITTER: 'twitter',
|
||||
WEBSITE: 'website',
|
||||
})
|
||||
|
||||
/** Enum of account properties to human readable account property names */
|
||||
export const ACCOUNT_PROPERTY_READABLE_ENUM = Object.freeze({
|
||||
ADDRESS: t('settings', 'Address'),
|
||||
AVATAR: t('settings', 'Avatar'),
|
||||
DISPLAYNAME: t('settings', 'Full name'),
|
||||
EMAIL: t('settings', 'Email'),
|
||||
EMAIL_COLLECTION: t('settings', 'Additional email'),
|
||||
PHONE: t('settings', 'Phone number'),
|
||||
TWITTER: t('settings', 'Twitter'),
|
||||
WEBSITE: t('settings', 'Website'),
|
||||
})
|
||||
|
||||
/** Enum of scopes */
|
||||
export const SCOPE_ENUM = Object.freeze({
|
||||
PRIVATE: 'v2-private',
|
||||
LOCAL: 'v2-local',
|
||||
PRIVATE: 'v2-private',
|
||||
FEDERATED: 'v2-federated',
|
||||
PUBLISHED: 'v2-published',
|
||||
})
|
||||
|
||||
/** Enum of readable account properties to supported scopes */
|
||||
export const PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM = Object.freeze({
|
||||
[ACCOUNT_PROPERTY_READABLE_ENUM.ADDRESS]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
|
||||
[ACCOUNT_PROPERTY_READABLE_ENUM.AVATAR]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
|
||||
[ACCOUNT_PROPERTY_READABLE_ENUM.DISPLAYNAME]: [SCOPE_ENUM.LOCAL],
|
||||
[ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL]: [SCOPE_ENUM.LOCAL],
|
||||
[ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL_COLLECTION]: [SCOPE_ENUM.LOCAL],
|
||||
[ACCOUNT_PROPERTY_READABLE_ENUM.PHONE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
|
||||
[ACCOUNT_PROPERTY_READABLE_ENUM.TWITTER]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
|
||||
[ACCOUNT_PROPERTY_READABLE_ENUM.WEBSITE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
|
||||
})
|
||||
|
||||
/** Scope suffix */
|
||||
export const SCOPE_SUFFIX = 'Scope'
|
||||
|
||||
/** Default additional email scope */
|
||||
export const DEFAULT_ADDITIONAL_EMAIL_SCOPE = SCOPE_ENUM.LOCAL
|
||||
|
||||
/**
|
||||
* Enum of scope names to properties
|
||||
*
|
||||
* *Used for federation control*
|
||||
*/
|
||||
export const SCOPE_PROPERTY_ENUM = Object.freeze({
|
||||
[SCOPE_ENUM.PRIVATE]: {
|
||||
name: SCOPE_ENUM.PRIVATE,
|
||||
displayName: t('settings', 'Private'),
|
||||
tooltip: t('settings', 'Only visible to people matched via phone number integration through Talk on mobile'),
|
||||
iconClass: 'icon-phone',
|
||||
},
|
||||
[SCOPE_ENUM.LOCAL]: {
|
||||
name: SCOPE_ENUM.LOCAL,
|
||||
displayName: t('settings', 'Local'),
|
||||
tooltip: t('settings', 'Only visible to people on this instance and guests'),
|
||||
iconClass: 'icon-password',
|
||||
},
|
||||
[SCOPE_ENUM.PRIVATE]: {
|
||||
name: SCOPE_ENUM.PRIVATE,
|
||||
displayName: t('settings', 'Private'),
|
||||
tooltip: t('settings', 'Only visible to people matched via phone number integration through Talk on mobile'),
|
||||
iconClass: 'icon-phone',
|
||||
},
|
||||
[SCOPE_ENUM.FEDERATED]: {
|
||||
name: SCOPE_ENUM.FEDERATED,
|
||||
displayName: t('settings', 'Federated'),
|
||||
|
|
@ -81,3 +104,14 @@ export const SCOPE_PROPERTY_ENUM = Object.freeze({
|
|||
iconClass: 'icon-link',
|
||||
},
|
||||
})
|
||||
|
||||
/** Default additional email scope */
|
||||
export const DEFAULT_ADDITIONAL_EMAIL_SCOPE = SCOPE_ENUM.LOCAL
|
||||
|
||||
/**
|
||||
* Email validation regex
|
||||
*
|
||||
* *Sourced from https://github.com/mpyw/FILTER_VALIDATE_EMAIL.js/blob/71e62ca48841d2246a1b531e7e84f5a01f15e615/src/regexp/ascii.ts*
|
||||
*/
|
||||
// eslint-disable-next-line no-control-regex
|
||||
export const VALIDATE_EMAIL_REGEX = /^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-+[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-+[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/i
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
68
apps/settings/src/service/PersonalInfo/DisplayNameService.js
Normal file
68
apps/settings/src/service/PersonalInfo/DisplayNameService.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import confirmPassword from '@nextcloud/password-confirmation'
|
||||
|
||||
import { ACCOUNT_PROPERTY_ENUM, SCOPE_SUFFIX } from '../../constants/AccountPropertyConstants'
|
||||
|
||||
/**
|
||||
* Save the primary display name of the user
|
||||
*
|
||||
* @param {string} displayName the primary display name
|
||||
* @returns {object}
|
||||
*/
|
||||
export const savePrimaryDisplayName = async(displayName) => {
|
||||
const userId = getCurrentUser().uid
|
||||
const url = generateOcsUrl('cloud/users/{userId}', { userId })
|
||||
|
||||
await confirmPassword()
|
||||
|
||||
const res = await axios.put(url, {
|
||||
key: ACCOUNT_PROPERTY_ENUM.DISPLAYNAME,
|
||||
value: displayName,
|
||||
})
|
||||
|
||||
return res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the federation scope for the primary display name of the user
|
||||
*
|
||||
* @param {string} scope the federation scope
|
||||
* @returns {object}
|
||||
*/
|
||||
export const savePrimaryDisplayNameScope = async(scope) => {
|
||||
const userId = getCurrentUser().uid
|
||||
const url = generateOcsUrl('cloud/users/{userId}', { userId })
|
||||
|
||||
await confirmPassword()
|
||||
|
||||
const res = await axios.put(url, {
|
||||
key: `${ACCOUNT_PROPERTY_ENUM.DISPLAYNAME}${SCOPE_SUFFIX}`,
|
||||
value: scope,
|
||||
})
|
||||
|
||||
return res.data
|
||||
}
|
||||
|
|
@ -25,13 +25,13 @@ import { getCurrentUser } from '@nextcloud/auth'
|
|||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import confirmPassword from '@nextcloud/password-confirmation'
|
||||
|
||||
import { ACCOUNT_PROPERTY_ENUM, SCOPE_SUFFIX } from '../constants/AccountPropertyConstants'
|
||||
import { ACCOUNT_PROPERTY_ENUM, SCOPE_SUFFIX } from '../../constants/AccountPropertyConstants'
|
||||
|
||||
/**
|
||||
* Save the primary email of the user
|
||||
*
|
||||
* @param {string} email the primary email
|
||||
* @returns {Object}
|
||||
* @returns {object}
|
||||
*/
|
||||
export const savePrimaryEmail = async(email) => {
|
||||
const userId = getCurrentUser().uid
|
||||
|
|
@ -53,7 +53,7 @@ export const savePrimaryEmail = async(email) => {
|
|||
* *Will be appended to the user's additional emails*
|
||||
*
|
||||
* @param {string} email the additional email
|
||||
* @returns {Object}
|
||||
* @returns {object}
|
||||
*/
|
||||
export const saveAdditionalEmail = async(email) => {
|
||||
const userId = getCurrentUser().uid
|
||||
|
|
@ -73,7 +73,7 @@ export const saveAdditionalEmail = async(email) => {
|
|||
* Remove an additional email of the user
|
||||
*
|
||||
* @param {string} email the additional email
|
||||
* @returns {Object}
|
||||
* @returns {object}
|
||||
*/
|
||||
export const removeAdditionalEmail = async(email) => {
|
||||
const userId = getCurrentUser().uid
|
||||
|
|
@ -94,7 +94,7 @@ export const removeAdditionalEmail = async(email) => {
|
|||
*
|
||||
* @param {string} prevEmail the additional email to be updated
|
||||
* @param {string} newEmail the new additional email
|
||||
* @returns {Object}
|
||||
* @returns {object}
|
||||
*/
|
||||
export const updateAdditionalEmail = async(prevEmail, newEmail) => {
|
||||
const userId = getCurrentUser().uid
|
||||
|
|
@ -114,7 +114,7 @@ export const updateAdditionalEmail = async(prevEmail, newEmail) => {
|
|||
* Save the federation scope for the primary email of the user
|
||||
*
|
||||
* @param {string} scope the federation scope
|
||||
* @returns {Object}
|
||||
* @returns {object}
|
||||
*/
|
||||
export const savePrimaryEmailScope = async(scope) => {
|
||||
const userId = getCurrentUser().uid
|
||||
|
|
@ -135,7 +135,7 @@ export const savePrimaryEmailScope = async(scope) => {
|
|||
*
|
||||
* @param {string} email the additional email
|
||||
* @param {string} scope the federation scope
|
||||
* @returns {Object}
|
||||
* @returns {object}
|
||||
*/
|
||||
export const saveAdditionalEmailScope = async(email, scope) => {
|
||||
const userId = getCurrentUser().uid
|
||||
51
apps/settings/src/utils/validate.js
Normal file
51
apps/settings/src/utils/validate.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { VALIDATE_EMAIL_REGEX } from '../constants/AccountPropertyConstants'
|
||||
|
||||
/**
|
||||
* Validate the display name input
|
||||
*
|
||||
* @param {string} input the input
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function validateDisplayName(input) {
|
||||
return input !== ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the email input
|
||||
*
|
||||
* *Compliant with PHP core FILTER_VALIDATE_EMAIL validator*
|
||||
*
|
||||
* *Reference implementation https://github.com/mpyw/FILTER_VALIDATE_EMAIL.js/blob/71e62ca48841d2246a1b531e7e84f5a01f15e615/src/index.ts*
|
||||
*
|
||||
* @param {string} input the input
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function validateEmail(input) {
|
||||
return typeof input === 'string'
|
||||
&& VALIDATE_EMAIL_REGEX.test(input)
|
||||
&& input.slice(-1) !== '\n'
|
||||
&& input.length <= 320
|
||||
&& encodeURIComponent(input).replace(/%../g, 'x').length <= 320
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue