Modularize shared account property components

Signed-off-by: Christopher Ng <chrng8@gmail.com>
This commit is contained in:
Christopher Ng 2022-07-15 04:17:20 +00:00
parent 696a48ae97
commit c5c70daa66
30 changed files with 502 additions and 1201 deletions

View file

@ -135,7 +135,6 @@ class PersonalInfo implements ISettings {
$totalSpace = \OC_Helper::humanFileSize($storageInfo['total']);
}
$languageParameters = $this->getLanguageMap($user);
$localeParameters = $this->getLocales($user);
$messageParameters = $this->getMessageParameters($account);
@ -148,12 +147,6 @@ class PersonalInfo implements ISettings {
'federationEnabled' => $federationEnabled,
'lookupServerUploadEnabled' => $lookupServerUploadEnabled,
'avatarScope' => $account->getProperty(IAccountManager::PROPERTY_AVATAR)->getScope(),
'displayNameChangeSupported' => $user->canChangeDisplayName(),
'displayName' => $account->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getValue(),
'displayNameScope' => $account->getProperty(IAccountManager::PROPERTY_DISPLAYNAME)->getScope(),
'email' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue(),
'emailScope' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getScope(),
'emailVerification' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getVerified(),
'phone' => $account->getProperty(IAccountManager::PROPERTY_PHONE)->getValue(),
'phoneScope' => $account->getProperty(IAccountManager::PROPERTY_PHONE)->getScope(),
'address' => $account->getProperty(IAccountManager::PROPERTY_ADDRESS)->getValue(),
@ -167,7 +160,7 @@ class PersonalInfo implements ISettings {
'groups' => $this->getGroups($user),
'isFairUseOfFreePushService' => $this->isFairUseOfFreePushService(),
'profileEnabledGlobally' => $this->profileManager->isProfileEnabled(),
] + $messageParameters + $languageParameters + $localeParameters;
] + $messageParameters + $localeParameters;
$personalInfoParameters = [
'userId' => $uid,
@ -213,6 +206,7 @@ class PersonalInfo implements ISettings {
*/
private function getProperty(IAccount $account, string $property): array {
$property = [
'name' => $account->getProperty($property)->getName(),
'value' => $account->getProperty($property)->getValue(),
'scope' => $account->getProperty($property)->getScope(),
'verified' => $account->getProperty($property)->getVerified(),
@ -262,6 +256,7 @@ class PersonalInfo implements ISettings {
*/
private function getEmailMap(IAccount $account): array {
$systemEmail = [
'name' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getName(),
'value' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue(),
'scope' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getScope(),
'verified' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getVerified(),
@ -270,6 +265,7 @@ class PersonalInfo implements ISettings {
$additionalEmails = array_map(
function (IAccountProperty $property) {
return [
'name' => $property->getName(),
'value' => $property->getValue(),
'scope' => $property->getScope(),
'verified' => $property->getVerified(),

View file

@ -1,9 +1,9 @@
<!--
- @copyright 2021, Christopher Ng <chrng8@gmail.com>
- @copyright 2022 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @license GNU AGPL version 3 or any later version
- @license AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
@ -12,7 +12,7 @@
-
- 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
- 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
@ -21,23 +21,17 @@
-->
<template>
<section>
<HeaderBar :account-property="accountProperty"
label-for="biography"
:scope.sync="biography.scope" />
<Biography :biography.sync="biography.value"
:scope.sync="biography.scope" />
</section>
<AccountPropertySection v-bind.sync="biography"
:placeholder="t('settings', 'Your biography')"
:multi-line="true" />
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import Biography from './Biography'
import HeaderBar from '../shared/HeaderBar'
import AccountPropertySection from './shared/AccountPropertySection.vue'
import { ACCOUNT_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants'
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
const { biography } = loadState('settings', 'personalInfoParameters', {})
@ -45,25 +39,13 @@ export default {
name: 'BiographySection',
components: {
Biography,
HeaderBar,
AccountPropertySection,
},
data() {
return {
accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.BIOGRAPHY,
biography,
biography: { ...biography, readable: NAME_READABLE_ENUM[biography.name] },
}
},
}
</script>
<style lang="scss" scoped>
section {
padding: 10px 10px;
&::v-deep button:disabled {
cursor: default;
}
}
</style>

View file

@ -1,184 +0,0 @@
<!--
- @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="biography">
<textarea id="biography"
:placeholder="t('settings', 'Your biography')"
:value="biography"
rows="8"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
@input="onBiographyChange" />
<div class="biography__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 { emit } from '@nextcloud/event-bus'
import debounce from 'debounce'
import { ACCOUNT_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService'
import logger from '../../../logger'
export default {
name: 'Biography',
props: {
biography: {
type: String,
required: true,
},
scope: {
type: String,
required: true,
},
},
data() {
return {
initialBiography: this.biography,
localScope: this.scope,
showCheckmarkIcon: false,
showErrorIcon: false,
}
},
methods: {
onBiographyChange(e) {
this.$emit('update:biography', e.target.value)
this.debounceBiographyChange(e.target.value.trim())
},
debounceBiographyChange: debounce(async function(biography) {
await this.updatePrimaryBiography(biography)
}, 500),
async updatePrimaryBiography(biography) {
try {
const responseData = await savePrimaryAccountProperty(ACCOUNT_PROPERTY_ENUM.BIOGRAPHY, biography)
this.handleResponse({
biography,
status: responseData.ocs?.meta?.status,
})
} catch (e) {
this.handleResponse({
errorMessage: t('settings', 'Unable to update biography'),
error: e,
})
}
},
handleResponse({ biography, status, errorMessage, error }) {
if (status === 'ok') {
// Ensure that local state reflects server state
this.initialBiography = biography
emit('settings:biography:updated', biography)
this.showCheckmarkIcon = true
setTimeout(() => { this.showCheckmarkIcon = false }, 2000)
} else {
showError(errorMessage)
logger.error(errorMessage, error)
this.showErrorIcon = true
setTimeout(() => { this.showErrorIcon = false }, 2000)
}
},
onScopeChange(scope) {
this.$emit('update:scope', scope)
},
},
}
</script>
<style lang="scss" scoped>
.biography {
display: grid;
align-items: center;
textarea {
resize: vertical;
grid-area: 1 / 1;
width: 100%;
margin: 3px 3px 3px 0;
padding: 7px 6px;
color: var(--color-main-text);
border: 1px solid var(--color-border-dark);
border-radius: var(--border-radius);
background-color: var(--color-main-background);
font-family: var(--font-face);
cursor: text;
&:hover,
&:focus,
&:active {
border-color: var(--color-primary-element) !important;
outline: none !important;
}
}
.biography__actions-container {
grid-area: 1 / 1;
justify-self: flex-end;
align-self: flex-end;
height: 30px;
display: flex;
gap: 0 2px;
margin-right: 5px;
margin-bottom: 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>

View file

@ -0,0 +1,66 @@
<!--
- @copyright 2022 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @license AGPL-3.0-or-later
-
- 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>
<AccountPropertySection v-bind.sync="displayName"
:placeholder="t('settings', 'Your full name')"
:is-editable="displayNameChangeSupported"
:on-validate="onValidate"
:on-save="onSave" />
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import { emit } from '@nextcloud/event-bus'
import AccountPropertySection from './shared/AccountPropertySection.vue'
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
const { displayName } = loadState('settings', 'personalInfoParameters', {})
const { displayNameChangeSupported } = loadState('settings', 'accountParameters', {})
export default {
name: 'DisplayNameSection',
components: {
AccountPropertySection,
},
data() {
return {
displayName: { ...displayName, readable: NAME_READABLE_ENUM[displayName.name] },
displayNameChangeSupported,
}
},
methods: {
onValidate(value) {
return value !== ''
},
onSave(value) {
emit('settings:display-name:updated', value)
},
}
}
</script>

View file

@ -1,180 +0,0 @@
<!--
- @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"
type="text"
:placeholder="t('settings', 'Your full name')"
:value="displayName"
autocapitalize="none"
autocomplete="on"
autocorrect="off"
@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 { emit } from '@nextcloud/event-bus'
import debounce from 'debounce'
import { ACCOUNT_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService'
import { validateStringInput } from '../../../utils/validate'
import logger from '../../../logger'
// 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 (validateStringInput(displayName)) {
await this.updatePrimaryDisplayName(displayName)
}
}, 500),
async updatePrimaryDisplayName(displayName) {
try {
const responseData = await savePrimaryAccountProperty(ACCOUNT_PROPERTY_ENUM.DISPLAYNAME, displayName)
this.handleResponse({
displayName,
status: responseData.ocs?.meta?.status,
})
} catch (e) {
this.handleResponse({
errorMessage: t('settings', 'Unable to update full name'),
error: e,
})
}
},
handleResponse({ displayName, status, errorMessage, error }) {
if (status === 'ok') {
// Ensure that local state reflects server state
this.initialDisplayName = displayName
emit('settings:display-name:updated', displayName)
this.showCheckmarkIcon = true
setTimeout(() => { this.showCheckmarkIcon = false }, 2000)
} else {
showError(errorMessage)
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;
width: 100%;
height: 34px;
margin: 3px 3px 3px 0;
padding: 7px 6px;
color: var(--color-main-text);
border: 1px solid var(--color-border-dark);
border-radius: var(--border-radius);
background-color: var(--color-main-background);
font-family: var(--font-face);
cursor: 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>

View file

@ -1,86 +0,0 @@
<!--
- @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"
:scope.sync="displayName.scope" />
<template v-if="displayNameChangeSupported">
<DisplayName :display-name.sync="displayName.value"
:scope.sync="displayName.scope" />
</template>
<span v-else>
{{ displayName.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 { validateStringInput } from '../../../utils/validate'
const { displayName } = loadState('settings', 'personalInfoParameters', {})
const { displayNameChangeSupported } = loadState('settings', 'accountParameters', {})
export default {
name: 'DisplayNameSection',
components: {
DisplayName,
HeaderBar,
},
data() {
return {
accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.DISPLAYNAME,
displayNameChangeSupported,
displayName,
}
},
computed: {
isValidSection() {
return validateStringInput(this.displayName.value)
},
},
}
</script>
<style lang="scss" scoped>
section {
padding: 10px 10px;
&::v-deep button:disabled {
cursor: default;
}
}
</style>

View file

@ -40,7 +40,7 @@
</transition>
<template v-if="!primary">
<FederationControl :account-property="accountProperty"
<FederationControl :readable="propertyReadable"
:additional="true"
:additional-value="email"
:disabled="federationDisabled"
@ -85,10 +85,10 @@ import Check from 'vue-material-design-icons/Check'
import { showError } from '@nextcloud/dialogs'
import debounce from 'debounce'
import FederationControl from '../shared/FederationControl'
import logger from '../../../logger'
import FederationControl from '../shared/FederationControl.vue'
import logger from '../../../logger.js'
import { ACCOUNT_PROPERTY_READABLE_ENUM, VERIFICATION_ENUM } from '../../../constants/AccountPropertyConstants'
import { ACCOUNT_PROPERTY_READABLE_ENUM, VERIFICATION_ENUM } from '../../../constants/AccountPropertyConstants.js'
import {
removeAdditionalEmail,
saveAdditionalEmail,
@ -96,8 +96,8 @@ import {
saveNotificationEmail,
savePrimaryEmail,
updateAdditionalEmail,
} from '../../../service/PersonalInfo/EmailService'
import { validateEmail } from '../../../utils/validate'
} from '../../../service/PersonalInfo/EmailService.js'
import { validateEmail } from '../../../utils/validate.js'
export default {
name: 'Email',
@ -139,7 +139,7 @@ export default {
data() {
return {
accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL,
propertyReadable: ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL,
initialEmail: this.email,
localScope: this.scope,
saveAdditionalEmailScope,

View file

@ -22,8 +22,8 @@
<template>
<section>
<HeaderBar :account-property="accountProperty"
label-for="email"
<HeaderBar :input-id="inputId"
:readable="primaryEmail.readable"
:handle-scope-change="savePrimaryEmailScope"
:is-editable="true"
:is-multi-value-supported="true"
@ -65,13 +65,13 @@
import { loadState } from '@nextcloud/initial-state'
import { showError } from '@nextcloud/dialogs'
import Email from './Email'
import HeaderBar from '../shared/HeaderBar'
import Email from './Email.vue'
import HeaderBar from '../shared/HeaderBar.vue'
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'
import logger from '../../../logger'
import { ACCOUNT_PROPERTY_READABLE_ENUM, DEFAULT_ADDITIONAL_EMAIL_SCOPE, NAME_READABLE_ENUM } from '../../../constants/AccountPropertyConstants.js'
import { savePrimaryEmail, savePrimaryEmailScope, removeAdditionalEmail } from '../../../service/PersonalInfo/EmailService.js'
import { validateEmail } from '../../../utils/validate.js'
import logger from '../../../logger.js'
const { emailMap: { additionalEmails, primaryEmail, notificationEmail } } = loadState('settings', 'personalInfoParameters', {})
const { displayNameChangeSupported } = loadState('settings', 'accountParameters', {})
@ -89,7 +89,7 @@ export default {
accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL,
additionalEmails: additionalEmails.map(properties => ({ ...properties, key: this.generateUniqueKey() })),
displayNameChangeSupported,
primaryEmail,
primaryEmail: { ...primaryEmail, readable: NAME_READABLE_ENUM[primaryEmail.name] },
savePrimaryEmailScope,
notificationEmail,
}
@ -103,6 +103,10 @@ export default {
return null
},
inputId() {
return `account-property-${this.primaryEmail.name}`
},
isValidSection() {
return validateEmail(this.primaryEmail.value)
&& this.additionalEmails.map(({ value }) => value).every(validateEmail)

View file

@ -1,9 +1,9 @@
<!--
- @copyright 2021, Christopher Ng <chrng8@gmail.com>
- @copyright 2022 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @license GNU AGPL version 3 or any later version
- @license AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
@ -12,7 +12,7 @@
-
- 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
- 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
@ -21,23 +21,16 @@
-->
<template>
<section>
<HeaderBar :account-property="accountProperty"
label-for="headline"
:scope.sync="headline.scope" />
<Headline :headline.sync="headline.value"
:scope.sync="headline.scope" />
</section>
<AccountPropertySection v-bind.sync="headline"
:placeholder="t('settings', 'Your headline')" />
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import Headline from './Headline'
import HeaderBar from '../shared/HeaderBar'
import AccountPropertySection from './shared/AccountPropertySection.vue'
import { ACCOUNT_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants'
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
const { headline } = loadState('settings', 'personalInfoParameters', {})
@ -45,25 +38,13 @@ export default {
name: 'HeadlineSection',
components: {
Headline,
HeaderBar,
AccountPropertySection,
},
data() {
return {
accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.HEADLINE,
headline,
headline: { ...headline, readable: NAME_READABLE_ENUM[headline.name] },
}
},
}
</script>
<style lang="scss" scoped>
section {
padding: 10px 10px;
&::v-deep button:disabled {
cursor: default;
}
}
</style>

View file

@ -1,175 +0,0 @@
<!--
- @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="headline">
<input id="headline"
type="text"
:placeholder="t('settings', 'Your headline')"
:value="headline"
autocapitalize="none"
autocomplete="on"
autocorrect="off"
@input="onHeadlineChange">
<div class="headline__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 { emit } from '@nextcloud/event-bus'
import debounce from 'debounce'
import { ACCOUNT_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService'
import logger from '../../../logger'
export default {
name: 'Headline',
props: {
headline: {
type: String,
required: true,
},
scope: {
type: String,
required: true,
},
},
data() {
return {
initialHeadline: this.headline,
localScope: this.scope,
showCheckmarkIcon: false,
showErrorIcon: false,
}
},
methods: {
onHeadlineChange(e) {
this.$emit('update:headline', e.target.value)
this.debounceHeadlineChange(e.target.value.trim())
},
debounceHeadlineChange: debounce(async function(headline) {
await this.updatePrimaryHeadline(headline)
}, 500),
async updatePrimaryHeadline(headline) {
try {
const responseData = await savePrimaryAccountProperty(ACCOUNT_PROPERTY_ENUM.HEADLINE, headline)
this.handleResponse({
headline,
status: responseData.ocs?.meta?.status,
})
} catch (e) {
this.handleResponse({
errorMessage: t('settings', 'Unable to update headline'),
error: e,
})
}
},
handleResponse({ headline, status, errorMessage, error }) {
if (status === 'ok') {
// Ensure that local state reflects server state
this.initialHeadline = headline
emit('settings:headline:updated', headline)
this.showCheckmarkIcon = true
setTimeout(() => { this.showCheckmarkIcon = false }, 2000)
} else {
showError(errorMessage)
logger.error(errorMessage, error)
this.showErrorIcon = true
setTimeout(() => { this.showErrorIcon = false }, 2000)
}
},
onScopeChange(scope) {
this.$emit('update:scope', scope)
},
},
}
</script>
<style lang="scss" scoped>
.headline {
display: grid;
align-items: center;
input {
grid-area: 1 / 1;
width: 100%;
height: 34px;
margin: 3px 3px 3px 0;
padding: 7px 6px;
color: var(--color-main-text);
border: 1px solid var(--color-border-dark);
border-radius: var(--border-radius);
background-color: var(--color-main-background);
font-family: var(--font-face);
cursor: text;
}
.headline__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>

View file

@ -22,7 +22,7 @@
<template>
<div class="language">
<select id="language"
<select :id="inputId"
:placeholder="t('settings', 'Language')"
@change="onLanguageChange">
<option v-for="commonLanguage in commonLanguages"
@ -53,15 +53,19 @@
<script>
import { showError } from '@nextcloud/dialogs'
import { ACCOUNT_SETTING_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService'
import { validateLanguage } from '../../../utils/validate'
import logger from '../../../logger'
import { ACCOUNT_SETTING_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants.js'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService.js'
import { validateLanguage } from '../../../utils/validate.js'
import logger from '../../../logger.js'
export default {
name: 'Language',
props: {
inputId: {
type: String,
default: null,
},
commonLanguages: {
type: Array,
required: true,

View file

@ -22,11 +22,12 @@
<template>
<section>
<HeaderBar :account-property="accountProperty"
label-for="language" />
<HeaderBar :input-id="inputId"
:readable="propertyReadable" />
<template v-if="isEditable">
<Language :common-languages="commonLanguages"
<Language :input-id="inputId"
:common-languages="commonLanguages"
:other-languages="otherLanguages"
:language.sync="language" />
</template>
@ -40,10 +41,10 @@
<script>
import { loadState } from '@nextcloud/initial-state'
import Language from './Language'
import HeaderBar from '../shared/HeaderBar'
import Language from './Language.vue'
import HeaderBar from '../shared/HeaderBar.vue'
import { ACCOUNT_SETTING_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants'
import { ACCOUNT_SETTING_PROPERTY_ENUM, ACCOUNT_SETTING_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants.js'
const { languageMap: { activeLanguage, commonLanguages, otherLanguages } } = loadState('settings', 'personalInfoParameters', {})
@ -57,7 +58,7 @@ export default {
data() {
return {
accountProperty: ACCOUNT_SETTING_PROPERTY_READABLE_ENUM.LANGUAGE,
propertyReadable: ACCOUNT_SETTING_PROPERTY_READABLE_ENUM.LANGUAGE,
commonLanguages,
otherLanguages,
language: activeLanguage,
@ -65,6 +66,10 @@ export default {
},
computed: {
inputId() {
return `account-setting-${ACCOUNT_SETTING_PROPERTY_ENUM.LANGUAGE}`
},
isEditable() {
return Boolean(this.language)
},

View file

@ -1,9 +1,9 @@
<!--
- @copyright 2021, Christopher Ng <chrng8@gmail.com>
- @copyright 2022 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @license GNU AGPL version 3 or any later version
- @license AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
@ -12,7 +12,7 @@
-
- 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
- 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
@ -21,23 +21,16 @@
-->
<template>
<section>
<HeaderBar :account-property="accountProperty"
label-for="organisation"
:scope.sync="organisation.scope" />
<Organisation :organisation.sync="organisation.value"
:scope.sync="organisation.scope" />
</section>
<AccountPropertySection v-bind.sync="organisation"
:placeholder="t('settings', 'Your organisation')" />
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import Organisation from './Organisation'
import HeaderBar from '../shared/HeaderBar'
import AccountPropertySection from './shared/AccountPropertySection.vue'
import { ACCOUNT_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants'
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
const { organisation } = loadState('settings', 'personalInfoParameters', {})
@ -45,25 +38,13 @@ export default {
name: 'OrganisationSection',
components: {
Organisation,
HeaderBar,
AccountPropertySection,
},
data() {
return {
accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.ORGANISATION,
organisation,
organisation: { ...organisation, readable: NAME_READABLE_ENUM[organisation.name] },
}
},
}
</script>
<style lang="scss" scoped>
section {
padding: 10px 10px;
&::v-deep button:disabled {
cursor: default;
}
}
</style>

View file

@ -1,175 +0,0 @@
<!--
- @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="organisation">
<input id="organisation"
type="text"
:placeholder="t('settings', 'Your organisation')"
:value="organisation"
autocapitalize="none"
autocomplete="on"
autocorrect="off"
@input="onOrganisationChange">
<div class="organisation__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 { emit } from '@nextcloud/event-bus'
import debounce from 'debounce'
import { ACCOUNT_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService'
import logger from '../../../logger'
export default {
name: 'Organisation',
props: {
organisation: {
type: String,
required: true,
},
scope: {
type: String,
required: true,
},
},
data() {
return {
initialOrganisation: this.organisation,
localScope: this.scope,
showCheckmarkIcon: false,
showErrorIcon: false,
}
},
methods: {
onOrganisationChange(e) {
this.$emit('update:organisation', e.target.value)
this.debounceOrganisationChange(e.target.value.trim())
},
debounceOrganisationChange: debounce(async function(organisation) {
await this.updatePrimaryOrganisation(organisation)
}, 500),
async updatePrimaryOrganisation(organisation) {
try {
const responseData = await savePrimaryAccountProperty(ACCOUNT_PROPERTY_ENUM.ORGANISATION, organisation)
this.handleResponse({
organisation,
status: responseData.ocs?.meta?.status,
})
} catch (e) {
this.handleResponse({
errorMessage: t('settings', 'Unable to update organisation'),
error: e,
})
}
},
handleResponse({ organisation, status, errorMessage, error }) {
if (status === 'ok') {
// Ensure that local state reflects server state
this.initialOrganisation = organisation
emit('settings:organisation:updated', organisation)
this.showCheckmarkIcon = true
setTimeout(() => { this.showCheckmarkIcon = false }, 2000)
} else {
showError(errorMessage)
logger.error(errorMessage, error)
this.showErrorIcon = true
setTimeout(() => { this.showErrorIcon = false }, 2000)
}
},
onScopeChange(scope) {
this.$emit('update:scope', scope)
},
},
}
</script>
<style lang="scss" scoped>
.organisation {
display: grid;
align-items: center;
input {
grid-area: 1 / 1;
width: 100%;
height: 34px;
margin: 3px 3px 3px 0;
padding: 7px 6px;
color: var(--color-main-text);
border: 1px solid var(--color-border-dark);
border-radius: var(--border-radius);
background-color: var(--color-main-background);
font-family: var(--font-face);
cursor: text;
}
.organisation__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>

View file

@ -37,10 +37,10 @@
import { showError } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService'
import { validateBoolean } from '../../../utils/validate'
import { ACCOUNT_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants'
import logger from '../../../logger'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService.js'
import { validateBoolean } from '../../../utils/validate.js'
import { ACCOUNT_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants.js'
import logger from '../../../logger.js'
export default {
name: 'ProfileCheckbox',

View file

@ -22,7 +22,7 @@
<template>
<section>
<HeaderBar :account-property="accountProperty" />
<HeaderBar :readable="propertyReadable" />
<ProfileCheckbox :profile-enabled.sync="profileEnabled" />
@ -39,12 +39,12 @@
import { loadState } from '@nextcloud/initial-state'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import EditProfileAnchorLink from './EditProfileAnchorLink'
import HeaderBar from '../shared/HeaderBar'
import ProfileCheckbox from './ProfileCheckbox'
import ProfilePreviewCard from './ProfilePreviewCard'
import EditProfileAnchorLink from './EditProfileAnchorLink.vue'
import HeaderBar from '../shared/HeaderBar.vue'
import ProfileCheckbox from './ProfileCheckbox.vue'
import ProfilePreviewCard from './ProfilePreviewCard.vue'
import { ACCOUNT_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants'
import { ACCOUNT_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants.js'
const {
organisation: { value: organisation },
@ -65,7 +65,7 @@ export default {
data() {
return {
accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.PROFILE_ENABLED,
propertyReadable: ACCOUNT_PROPERTY_READABLE_ENUM.PROFILE_ENABLED,
organisation,
displayName,
profileEnabled,

View file

@ -24,7 +24,7 @@
<!-- TODO remove this inline margin placeholder once the settings layout is updated -->
<section id="profile-visibility"
:style="{ marginLeft }">
<HeaderBar :account-property="heading" />
<HeaderBar :readable="heading" />
<em :class="{ disabled }">
{{ t('settings', 'The more restrictive setting of either visibility or scope is respected on your Profile. For example, if visibility is set to "Show to everyone" and scope is set to "Private", "Private" is respected.') }}
@ -47,9 +47,9 @@
import { loadState } from '@nextcloud/initial-state'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import HeaderBar from '../shared/HeaderBar'
import VisibilityDropdown from './VisibilityDropdown'
import { PROFILE_READABLE_ENUM } from '../../../constants/AccountPropertyConstants'
import HeaderBar from '../shared/HeaderBar.vue'
import VisibilityDropdown from './VisibilityDropdown.vue'
import { PROFILE_READABLE_ENUM } from '../../../constants/AccountPropertyConstants.js'
const { profileConfig } = loadState('settings', 'profileParameters', {})
const { profileEnabled } = loadState('settings', 'personalInfoParameters', false)

View file

@ -43,10 +43,9 @@ import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import NcMultiselect from '@nextcloud/vue/dist/Components/NcMultiselect'
import { saveProfileParameterVisibility } from '../../../service/ProfileService'
import { validateStringInput } from '../../../utils/validate'
import { VISIBILITY_PROPERTY_ENUM } from '../../../constants/ProfileConstants'
import logger from '../../../logger'
import { saveProfileParameterVisibility } from '../../../service/ProfileService.js'
import { VISIBILITY_PROPERTY_ENUM } from '../../../constants/ProfileConstants.js'
import logger from '../../../logger.js'
const { profileEnabled } = loadState('settings', 'personalInfoParameters', false)
@ -112,7 +111,7 @@ export default {
const { name: visibility } = visibilityObject
this.$emit('update:visibility', visibility)
if (validateStringInput(visibility)) {
if (visibility !== '') {
await this.updateVisibility(visibility)
}
}

View file

@ -1,9 +1,9 @@
<!--
- @copyright 2021, Christopher Ng <chrng8@gmail.com>
- @copyright 2022 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @license GNU AGPL version 3 or any later version
- @license AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
@ -12,7 +12,7 @@
-
- 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
- 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
@ -21,23 +21,16 @@
-->
<template>
<section>
<HeaderBar :account-property="accountProperty"
label-for="role"
:scope.sync="role.scope" />
<Role :role.sync="role.value"
:scope.sync="role.scope" />
</section>
<AccountPropertySection v-bind.sync="role"
:placeholder="t('settings', 'Your role')" />
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import Role from './Role'
import HeaderBar from '../shared/HeaderBar'
import AccountPropertySection from './shared/AccountPropertySection.vue'
import { ACCOUNT_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants'
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
const { role } = loadState('settings', 'personalInfoParameters', {})
@ -45,25 +38,13 @@ export default {
name: 'RoleSection',
components: {
Role,
HeaderBar,
AccountPropertySection,
},
data() {
return {
accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.ROLE,
role,
role: { ...role, readable: NAME_READABLE_ENUM[role.name] },
}
},
}
</script>
<style lang="scss" scoped>
section {
padding: 10px 10px;
&::v-deep button:disabled {
cursor: default;
}
}
</style>

View file

@ -1,175 +0,0 @@
<!--
- @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="role">
<input id="role"
type="text"
:placeholder="t('settings', 'Your role')"
:value="role"
autocapitalize="none"
autocomplete="on"
autocorrect="off"
@input="onRoleChange">
<div class="role__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 { emit } from '@nextcloud/event-bus'
import debounce from 'debounce'
import { ACCOUNT_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService'
import logger from '../../../logger'
export default {
name: 'Role',
props: {
role: {
type: String,
required: true,
},
scope: {
type: String,
required: true,
},
},
data() {
return {
initialRole: this.role,
localScope: this.scope,
showCheckmarkIcon: false,
showErrorIcon: false,
}
},
methods: {
onRoleChange(e) {
this.$emit('update:role', e.target.value)
this.debounceRoleChange(e.target.value.trim())
},
debounceRoleChange: debounce(async function(role) {
await this.updatePrimaryRole(role)
}, 500),
async updatePrimaryRole(role) {
try {
const responseData = await savePrimaryAccountProperty(ACCOUNT_PROPERTY_ENUM.ROLE, role)
this.handleResponse({
role,
status: responseData.ocs?.meta?.status,
})
} catch (e) {
this.handleResponse({
errorMessage: t('settings', 'Unable to update role'),
error: e,
})
}
},
handleResponse({ role, status, errorMessage, error }) {
if (status === 'ok') {
// Ensure that local state reflects server state
this.initialRole = role
emit('settings:role:updated', role)
this.showCheckmarkIcon = true
setTimeout(() => { this.showCheckmarkIcon = false }, 2000)
} else {
showError(errorMessage)
logger.error(errorMessage, error)
this.showErrorIcon = true
setTimeout(() => { this.showErrorIcon = false }, 2000)
}
},
onScopeChange(scope) {
this.$emit('update:scope', scope)
},
},
}
</script>
<style lang="scss" scoped>
.role {
display: grid;
align-items: center;
input {
grid-area: 1 / 1;
width: 100%;
height: 34px;
margin: 3px 3px 3px 0;
padding: 7px 6px;
color: var(--color-main-text);
border: 1px solid var(--color-border-dark);
border-radius: var(--border-radius);
background-color: var(--color-main-background);
font-family: var(--font-face);
cursor: text;
}
.role__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>

View file

@ -0,0 +1,265 @@
<!--
- @copyright 2022 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @license AGPL-3.0-or-later
-
- 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 :scope.sync="scope"
:readable.sync="readable"
:input-id="inputId"
:is-editable="isEditable" />
<div v-if="isEditable" class="property">
<textarea v-if="multiLine"
:id="inputId"
:placeholder="placeholder"
:value="value"
rows="8"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
@input="onPropertyChange" />
<input v-else
:id="inputId"
:placeholder="placeholder"
:type="type"
:value="value"
autocapitalize="none"
autocomplete="on"
autocorrect="off"
@input="onPropertyChange">
<div class="property__actions-container">
<transition name="fade">
<Check v-if="showCheckmarkIcon" :size="20" />
<AlertOctagon v-else-if="showErrorIcon" :size="20" />
</transition>
</div>
</div>
<span v-else>
{{ value || t('settings', 'No {property} set', { property: readable.toLocaleLowerCase() }) }}
</span>
</section>
</template>
<script>
import debounce from 'debounce'
import { showError } from '@nextcloud/dialogs'
import Check from 'vue-material-design-icons/Check'
import AlertOctagon from 'vue-material-design-icons/AlertOctagon'
import HeaderBar from '../shared/HeaderBar.vue'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService.js'
import logger from '../../../logger.js'
export default {
name: 'AccountPropertySection',
components: {
AlertOctagon,
Check,
HeaderBar,
},
props: {
name: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
scope: {
type: String,
required: true,
},
readable: {
type: String,
required: true,
},
placeholder: {
type: String,
required: true,
},
type: {
type: String,
default: 'text',
},
isEditable: {
type: Boolean,
default: true,
},
multiLine: {
type: Boolean,
default: false,
},
onValidate: {
type: Function,
default: null,
},
onSave: {
type: Function,
default: null,
},
},
data() {
return {
initialValue: this.value,
showCheckmarkIcon: false,
showErrorIcon: false,
}
},
computed: {
inputId() {
return `account-property-${this.name}`
},
},
methods: {
onPropertyChange(e) {
this.$emit('update:value', e.target.value)
this.debouncePropertyChange(e.target.value.trim())
},
debouncePropertyChange: debounce(async function(value) {
if (this.onValidate && !this.onValidate(value)) {
return
}
await this.updateProperty(value)
}, 500),
async updateProperty(value) {
try {
const responseData = await savePrimaryAccountProperty(
this.name,
value,
)
this.handleResponse({
value,
status: responseData.ocs?.meta?.status,
})
} catch (e) {
this.handleResponse({
errorMessage: t('settings', 'Unable to update {property}', { property: this.readable.toLocaleLowerCase() }),
error: e,
})
}
},
handleResponse({ value, status, errorMessage, error }) {
if (status === 'ok') {
this.initialValue = value
if (this.onSave) {
this.onSave(value)
}
this.showCheckmarkIcon = true
setTimeout(() => { this.showCheckmarkIcon = false }, 2000)
} else {
this.$emit('update:value', this.initialValue)
showError(errorMessage)
logger.error(errorMessage, error)
this.showErrorIcon = true
setTimeout(() => { this.showErrorIcon = false }, 2000)
}
},
},
}
</script>
<style lang="scss" scoped>
section {
padding: 10px 10px;
&::v-deep button:disabled {
cursor: default;
}
.property {
display: grid;
align-items: center;
textarea {
resize: vertical;
grid-area: 1 / 1;
width: 100%;
margin: 3px 3px 3px 0;
padding: 7px 6px;
color: var(--color-main-text);
border: 1px solid var(--color-border-dark);
border-radius: var(--border-radius);
background-color: var(--color-main-background);
font-family: var(--font-face);
cursor: text;
&:hover,
&:focus,
&:active {
border-color: var(--color-primary-element) !important;
outline: none !important;
}
}
input {
grid-area: 1 / 1;
width: 100%;
height: 34px;
margin: 3px 3px 3px 0;
padding: 7px 6px;
color: var(--color-main-text);
border: 1px solid var(--color-border-dark);
border-radius: var(--border-radius);
background-color: var(--color-main-background);
font-family: var(--font-face);
cursor: text;
}
.property__actions-container {
grid-area: 1 / 1;
justify-self: flex-end;
align-self: flex-end;
height: 30px;
display: flex;
gap: 0 2px;
margin-right: 5px;
margin-bottom: 5px;
}
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.fade-enter-active {
transition: opacity 200ms ease-out;
}
.fade-leave-active {
transition: opacity 300ms ease-out;
}
}
</style>

View file

@ -43,17 +43,19 @@ import NcActions from '@nextcloud/vue/dist/Components/NcActions'
import { loadState } from '@nextcloud/initial-state'
import { showError } from '@nextcloud/dialogs'
import FederationControlAction from './FederationControlAction'
import FederationControlAction from './FederationControlAction.vue'
import {
ACCOUNT_PROPERTY_READABLE_ENUM,
ACCOUNT_SETTING_PROPERTY_READABLE_ENUM,
PROFILE_READABLE_ENUM,
PROPERTY_READABLE_KEYS_ENUM,
PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM,
SCOPE_ENUM, SCOPE_PROPERTY_ENUM,
UNPUBLISHED_READABLE_PROPERTIES,
} from '../../../constants/AccountPropertyConstants'
import { savePrimaryAccountPropertyScope } from '../../../service/PersonalInfo/PersonalInfoService'
import logger from '../../../logger'
} from '../../../constants/AccountPropertyConstants.js'
import { savePrimaryAccountPropertyScope } from '../../../service/PersonalInfo/PersonalInfoService.js'
import logger from '../../../logger.js'
const { lookupServerUploadEnabled } = loadState('settings', 'accountParameters', {})
@ -66,10 +68,10 @@ export default {
},
props: {
accountProperty: {
readable: {
type: String,
required: true,
validator: (value) => Object.values(ACCOUNT_PROPERTY_READABLE_ENUM).includes(value),
validator: (value) => Object.values(ACCOUNT_PROPERTY_READABLE_ENUM).includes(value) || Object.values(ACCOUNT_SETTING_PROPERTY_READABLE_ENUM).includes(value) || value === PROFILE_READABLE_ENUM.PROFILE_VISIBILITY,
},
additional: {
type: Boolean,
@ -95,14 +97,14 @@ export default {
data() {
return {
accountPropertyLowerCase: this.accountProperty.toLocaleLowerCase(),
readableLowerCase: this.readable.toLocaleLowerCase(),
initialScope: this.scope,
}
},
computed: {
ariaLabel() {
return t('settings', 'Change scope level of {accountProperty}, current scope is {scope}', { accountProperty: this.accountPropertyLowerCase, scope: this.scopeDisplayNameLowerCase })
return t('settings', 'Change scope level of {property}, current scope is {scope}', { property: this.readableLowerCase, scope: this.scopeDisplayNameLowerCase })
},
scopeDisplayNameLowerCase() {
@ -118,15 +120,15 @@ export default {
},
supportedScopes() {
if (lookupServerUploadEnabled && !UNPUBLISHED_READABLE_PROPERTIES.includes(this.accountProperty)) {
if (lookupServerUploadEnabled && !UNPUBLISHED_READABLE_PROPERTIES.includes(this.readable)) {
return [
...PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM[this.accountProperty],
...PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM[this.readable],
SCOPE_ENUM.FEDERATED,
SCOPE_ENUM.PUBLISHED,
]
}
return PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM[this.accountProperty]
return PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM[this.readable]
},
},
@ -143,14 +145,14 @@ export default {
async updatePrimaryScope(scope) {
try {
const responseData = await savePrimaryAccountPropertyScope(PROPERTY_READABLE_KEYS_ENUM[this.accountProperty], scope)
const responseData = await savePrimaryAccountPropertyScope(PROPERTY_READABLE_KEYS_ENUM[this.readable], scope)
this.handleResponse({
scope,
status: responseData.ocs?.meta?.status,
})
} catch (e) {
this.handleResponse({
errorMessage: t('settings', 'Unable to update federation scope of the primary {accountProperty}', { accountProperty: this.accountPropertyLowerCase }),
errorMessage: t('settings', 'Unable to update federation scope of the primary {property}', { property: this.readableLowerCase }),
error: e,
})
}
@ -165,7 +167,7 @@ export default {
})
} catch (e) {
this.handleResponse({
errorMessage: t('settings', 'Unable to update federation scope of additional {accountProperty}', { accountProperty: this.accountPropertyLowerCase }),
errorMessage: t('settings', 'Unable to update federation scope of additional {property}', { property: this.readableLowerCase }),
error: e,
})
}

View file

@ -22,14 +22,14 @@
<template>
<h3 :class="{ 'setting-property': isSettingProperty, 'profile-property': isProfileProperty }">
<label :for="labelFor">
<label :for="inputId">
<!-- Already translated as required by prop validator -->
{{ accountProperty }}
{{ readable }}
</label>
<template v-if="scope">
<FederationControl class="federation-control"
:account-property="accountProperty"
:readable="readable"
:scope.sync="localScope"
@update:scope="onScopeChange" />
</template>
@ -49,10 +49,16 @@
</template>
<script>
import FederationControl from './FederationControl'
import NcButton from '@nextcloud/vue/dist/Components/NcButton'
import Plus from 'vue-material-design-icons/Plus'
import { ACCOUNT_PROPERTY_READABLE_ENUM, ACCOUNT_SETTING_PROPERTY_READABLE_ENUM, PROFILE_READABLE_ENUM } from '../../../constants/AccountPropertyConstants'
import FederationControl from './FederationControl.vue'
import {
ACCOUNT_PROPERTY_READABLE_ENUM,
ACCOUNT_SETTING_PROPERTY_READABLE_ENUM,
PROFILE_READABLE_ENUM,
} from '../../../constants/AccountPropertyConstants.js'
export default {
name: 'HeaderBar',
@ -64,11 +70,19 @@ export default {
},
props: {
accountProperty: {
scope: {
type: String,
default: null,
},
readable: {
type: String,
required: true,
validator: (value) => Object.values(ACCOUNT_PROPERTY_READABLE_ENUM).includes(value) || Object.values(ACCOUNT_SETTING_PROPERTY_READABLE_ENUM).includes(value) || value === PROFILE_READABLE_ENUM.PROFILE_VISIBILITY,
},
inputId: {
type: String,
default: null,
},
isEditable: {
type: Boolean,
default: true,
@ -79,15 +93,7 @@ export default {
},
isValidSection: {
type: Boolean,
default: false,
},
labelFor: {
type: String,
default: '',
},
scope: {
type: String,
default: null,
default: true,
},
},
@ -99,11 +105,11 @@ export default {
computed: {
isProfileProperty() {
return this.accountProperty === ACCOUNT_PROPERTY_READABLE_ENUM.PROFILE_ENABLED
return this.readable === ACCOUNT_PROPERTY_READABLE_ENUM.PROFILE_ENABLED
},
isSettingProperty() {
return Object.values(ACCOUNT_SETTING_PROPERTY_READABLE_ENUM).includes(this.accountProperty)
return Object.values(ACCOUNT_SETTING_PROPERTY_READABLE_ENUM).includes(this.readable)
},
},

View file

@ -61,6 +61,22 @@ export const ACCOUNT_PROPERTY_READABLE_ENUM = Object.freeze({
WEBSITE: t('settings', 'Website'),
})
export const NAME_READABLE_ENUM = Object.freeze({
[ACCOUNT_PROPERTY_ENUM.ADDRESS]: ACCOUNT_PROPERTY_READABLE_ENUM.ADDRESS,
[ACCOUNT_PROPERTY_ENUM.AVATAR]: ACCOUNT_PROPERTY_READABLE_ENUM.AVATAR,
[ACCOUNT_PROPERTY_ENUM.BIOGRAPHY]: ACCOUNT_PROPERTY_READABLE_ENUM.BIOGRAPHY,
[ACCOUNT_PROPERTY_ENUM.DISPLAYNAME]: ACCOUNT_PROPERTY_READABLE_ENUM.DISPLAYNAME,
[ACCOUNT_PROPERTY_ENUM.EMAIL_COLLECTION]: ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL_COLLECTION,
[ACCOUNT_PROPERTY_ENUM.EMAIL]: ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL,
[ACCOUNT_PROPERTY_ENUM.HEADLINE]: ACCOUNT_PROPERTY_READABLE_ENUM.HEADLINE,
[ACCOUNT_PROPERTY_ENUM.ORGANISATION]: ACCOUNT_PROPERTY_READABLE_ENUM.ORGANISATION,
[ACCOUNT_PROPERTY_ENUM.PHONE]: ACCOUNT_PROPERTY_READABLE_ENUM.PHONE,
[ACCOUNT_PROPERTY_ENUM.PROFILE_ENABLED]: ACCOUNT_PROPERTY_READABLE_ENUM.PROFILE_ENABLED,
[ACCOUNT_PROPERTY_ENUM.ROLE]: ACCOUNT_PROPERTY_READABLE_ENUM.ROLE,
[ACCOUNT_PROPERTY_ENUM.TWITTER]: ACCOUNT_PROPERTY_READABLE_ENUM.TWITTER,
[ACCOUNT_PROPERTY_ENUM.WEBSITE]: ACCOUNT_PROPERTY_READABLE_ENUM.WEBSITE,
})
/** Enum of profile specific sections to human readable names */
export const PROFILE_READABLE_ENUM = Object.freeze({
PROFILE_VISIBILITY: t('settings', 'Profile visibility'),

View file

@ -26,15 +26,15 @@ import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
import '@nextcloud/dialogs/styles/toast.scss'
import DisplayNameSection from './components/PersonalInfo/DisplayNameSection/DisplayNameSection'
import EmailSection from './components/PersonalInfo/EmailSection/EmailSection'
import LanguageSection from './components/PersonalInfo/LanguageSection/LanguageSection'
import ProfileSection from './components/PersonalInfo/ProfileSection/ProfileSection'
import OrganisationSection from './components/PersonalInfo/OrganisationSection/OrganisationSection'
import RoleSection from './components/PersonalInfo/RoleSection/RoleSection'
import HeadlineSection from './components/PersonalInfo/HeadlineSection/HeadlineSection'
import BiographySection from './components/PersonalInfo/BiographySection/BiographySection'
import ProfileVisibilitySection from './components/PersonalInfo/ProfileVisibilitySection/ProfileVisibilitySection'
import DisplayNameSection from './components/PersonalInfo/DisplayNameSection.vue'
import EmailSection from './components/PersonalInfo/EmailSection/EmailSection.vue'
import LanguageSection from './components/PersonalInfo/LanguageSection/LanguageSection.vue'
import ProfileSection from './components/PersonalInfo/ProfileSection/ProfileSection.vue'
import OrganisationSection from './components/PersonalInfo/OrganisationSection.vue'
import RoleSection from './components/PersonalInfo/RoleSection.vue'
import HeadlineSection from './components/PersonalInfo/HeadlineSection.vue'
import BiographySection from './components/PersonalInfo/BiographySection.vue'
import ProfileVisibilitySection from './components/PersonalInfo/ProfileVisibilitySection/ProfileVisibilitySection.vue'
__webpack_nonce__ = btoa(getRequestToken())

View file

@ -28,18 +28,6 @@
import { VALIDATE_EMAIL_REGEX } from '../constants/AccountPropertyConstants'
/**
* Validate the string input
*
* Generic validator just to check that input is not an empty string*
*
* @param {string} input the input
* @return {boolean}
*/
export function validateStringInput(input) {
return input !== ''
}
/**
* Validate the email input
*

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