fix(settings): Use status states from `NcInputField` instead of custom handling

Co-authored-by: Ferdinand Thiessen <opensource@fthiessen.de>
Co-authored-by: Pytal <24800714+Pytal@users.noreply.github.com>
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/43488/head
Ferdinand Thiessen 4 months ago
parent fe58d8aae9
commit 3e09295fa1
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400

@ -99,7 +99,7 @@ export default {
flex-direction: column;
margin: 10px 32px 10px 0;
gap: 16px 0;
color: var(--color-text-lighter);
color: var(--color-text-maxcontrast);
&__groups,
&__quota {
@ -117,7 +117,7 @@ export default {
font-weight: bold;
}
&::v-deep .material-design-icon {
&:deep(.material-design-icon) {
align-self: flex-start;
margin-top: 2px;
}

@ -23,63 +23,69 @@
<template>
<div>
<div class="email">
<input :id="inputIdWithDefault"
<NcInputField :id="inputIdWithDefault"
ref="email"
type="email"
autocapitalize="none"
autocomplete="email"
:aria-label="inputPlaceholder"
:error="hasError || !!helperText"
:helper-text="helperText || undefined"
:label="inputPlaceholder"
:placeholder="inputPlaceholder"
:value="email"
:aria-describedby="helperText ? `${inputIdWithDefault}-helper-text` : undefined"
autocapitalize="none"
spellcheck="false"
@input="onEmailChange">
<div class="email__actions-container">
<transition name="fade">
<Check v-if="showCheckmarkIcon" :size="20" />
<AlertOctagon v-else-if="showErrorIcon" :size="20" />
</transition>
<template v-if="!primary">
<FederationControl :readable="propertyReadable"
:additional="true"
:additional-value="email"
:disabled="federationDisabled"
:handle-additional-scope-change="saveAdditionalEmailScope"
:scope.sync="localScope"
@update:scope="onScopeChange" />
</template>
<NcActions class="email__actions"
:aria-label="t('settings', 'Email options')"
:force-menu="true">
<NcActionButton :aria-label="deleteEmailLabel"
:close-after-click="true"
:disabled="deleteDisabled"
icon="icon-delete"
@click.stop.prevent="deleteEmail">
{{ deleteEmailLabel }}
</NcActionButton>
<NcActionButton v-if="!primary || !isNotificationEmail"
:aria-label="setNotificationMailLabel"
:close-after-click="true"
:disabled="setNotificationMailDisabled"
icon="icon-favorite"
@click.stop.prevent="setNotificationMail">
{{ setNotificationMailLabel }}
</NcActionButton>
:success="isSuccess"
type="email"
:value.sync="emailAddress" />
<div class="email__actions">
<NcActions :aria-label="actionsLabel" @close="showFederationSettings = false">
<template v-if="showFederationSettings">
<NcActionButton @click="showFederationSettings = false">
<template #icon>
<NcIconSvgWrapper :path="mdiArrowLeft" />
</template>
{{ t('settings', 'Back') }}
</NcActionButton>
<FederationControlActions :readable="propertyReadable"
:additional="true"
:additional-value="email"
:disabled="federationDisabled"
:handle-additional-scope-change="saveAdditionalEmailScope"
:scope.sync="localScope"
@update:scope="onScopeChange" />
</template>
<template v-else>
<NcActionButton v-if="!federationDisabled && !primary"
@click="showFederationSettings = true">
<template #icon>
<NcIconSvgWrapper :path="mdiLock" />
</template>
{{ t('settings', 'Change scope level of {property}', { property: propertyReadable.toLocaleLowerCase() }) }}
</NcActionButton>
<NcActionCaption v-if="!isConfirmedAddress"
:name="t('settings', 'This address is not confirmed')" />
<NcActionButton close-after-click
:disabled="deleteDisabled"
@click="deleteEmail">
<template #icon>
<NcIconSvgWrapper :path="mdiTrashCan" />
</template>
{{ deleteEmailLabel }}
</NcActionButton>
<NcActionButton v-if="!primary || !isNotificationEmail"
close-after-click
:disabled="!isConfirmedAddress"
@click="setNotificationMail">
<template #icon>
<NcIconSvgWrapper v-if="isNotificationEmail" :path="mdiStar" />
<NcIconSvgWrapper v-else :path="mdiStarOutline" />
</template>
{{ setNotificationMailLabel }}
</NcActionButton>
</template>
</NcActions>
</div>
</div>
<p v-if="helperText"
:id="`${inputIdWithDefault}-helper-text`"
class="email__helper-text-message email__helper-text-message--error">
<AlertCircle class="email__helper-text-message__icon" :size="18" />
{{ helperText }}
</p>
<em v-if="isNotificationEmail">
{{ t('settings', 'Primary email for password reset and notifications') }}
</em>
@ -89,12 +95,13 @@
<script>
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import AlertCircle from 'vue-material-design-icons/AlertCircleOutline.vue'
import AlertOctagon from 'vue-material-design-icons/AlertOctagon.vue'
import Check from 'vue-material-design-icons/Check.vue'
import NcActionCaption from '@nextcloud/vue/dist/Components/NcActionCaption.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
import debounce from 'debounce'
import FederationControl from '../shared/FederationControl.vue'
import { mdiArrowLeft, mdiLock, mdiStar, mdiStarOutline, mdiTrashCan } from '@mdi/js'
import FederationControlActions from '../shared/FederationControlActions.vue'
import { handleError } from '../../../utils/handlers.js'
import { ACCOUNT_PROPERTY_READABLE_ENUM, VERIFICATION_ENUM } from '../../../constants/AccountPropertyConstants.js'
@ -114,10 +121,10 @@ export default {
components: {
NcActions,
NcActionButton,
AlertCircle,
AlertOctagon,
Check,
FederationControl,
NcActionCaption,
NcIconSvgWrapper,
NcInputField,
FederationControlActions,
},
props: {
@ -152,19 +159,38 @@ export default {
},
},
setup() {
return {
mdiArrowLeft,
mdiLock,
mdiStar,
mdiStarOutline,
mdiTrashCan,
saveAdditionalEmailScope,
}
},
data() {
return {
propertyReadable: ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL,
hasError: false,
helperText: null,
initialEmail: this.email,
isSuccess: false,
localScope: this.scope,
saveAdditionalEmailScope,
helperText: null,
showCheckmarkIcon: false,
showErrorIcon: false,
propertyReadable: ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL,
showFederationSettings: false,
}
},
computed: {
actionsLabel() {
if (this.primary) {
return t('settings', 'Email options')
} else {
return t('settings', 'Options for additional email address {index}', { index: this.index + 1 })
}
},
deleteDisabled() {
if (this.primary) {
// Disable for empty primary email as there is nothing to delete
@ -183,15 +209,13 @@ export default {
return t('settings', 'Delete email')
},
setNotificationMailDisabled() {
return !this.primary && this.localVerificationState !== VERIFICATION_ENUM.VERIFIED
isConfirmedAddress() {
return this.primary || this.localVerificationState === VERIFICATION_ENUM.VERIFIED
},
setNotificationMailLabel() {
setNotificationMailLabel() {
if (this.isNotificationEmail) {
return t('settings', 'Unset as primary email')
} else if (!this.primary && this.localVerificationState !== VERIFICATION_ENUM.VERIFIED) {
return t('settings', 'This address is not confirmed')
}
return t('settings', 'Set as primary email')
},
@ -213,25 +237,30 @@ export default {
return (this.email && this.email === this.activeNotificationEmail)
|| (this.primary && this.activeNotificationEmail === '')
},
emailAddress: {
get() {
return this.email
},
set(value) {
this.$emit('update:email', value)
this.debounceEmailChange(value.trim())
},
},
},
mounted() {
if (!this.primary && this.initialEmail === '') {
// $nextTick is needed here, otherwise it may not always work https://stackoverflow.com/questions/51922767/autofocus-input-on-mount-vue-ios/63485725#63485725
// $nextTick is needed here, otherwise it may not always work
// https://stackoverflow.com/questions/51922767/autofocus-input-on-mount-vue-ios/63485725#63485725
this.$nextTick(() => this.$refs.email?.focus())
}
},
methods: {
onEmailChange(e) {
this.$emit('update:email', e.target.value)
this.debounceEmailChange(e.target.value.trim())
},
debounceEmailChange: debounce(async function(email) {
this.helperText = null
if (this.$refs.email?.validationMessage) {
this.helperText = this.$refs.email.validationMessage
this.helperText = this.$refs.email?.$refs.input?.validationMessage || null
if (this.helperText !== null) {
return
}
if (validateEmail(email) || email === '') {
@ -356,12 +385,12 @@ export default {
} else if (notificationEmail !== undefined) {
this.$emit('update:notification-email', notificationEmail)
}
this.showCheckmarkIcon = true
setTimeout(() => { this.showCheckmarkIcon = false }, 2000)
this.isSuccess = true
setTimeout(() => { this.isSuccess = false }, 2000)
} else {
handleError(error, errorMessage)
this.showErrorIcon = true
setTimeout(() => { this.showErrorIcon = false }, 2000)
this.hasError = true
setTimeout(() => { this.hasError = false }, 2000)
}
},
@ -374,66 +403,16 @@ export default {
<style lang="scss" scoped>
.email {
display: grid;
align-items: center;
input {
grid-area: 1 / 1;
width: 100%;
}
.email__actions-container {
grid-area: 1 / 1;
justify-self: flex-end;
height: 30px;
display: flex;
flex-direction: row;
align-items: start;
gap: 4px;
&__actions {
display: flex;
gap: 0 2px;
margin-right: 5px;
.email__actions {
&:hover,
&:focus,
&:active {
opacity: 0.8 !important;
}
&::v-deep button {
height: 30px !important;
min-height: 30px !important;
width: 30px !important;
min-width: 30px !important;
}
}
}
&__helper-text-message {
padding: 4px 0;
display: flex;
align-items: center;
&__icon {
margin-right: 8px;
align-self: start;
margin-top: 4px;
}
&--error {
color: var(--color-error);
}
margin-top: 6px;
}
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.fade-enter-active {
transition: opacity 200ms ease-out;
}
.fade-leave-active {
transition: opacity 300ms ease-out;
}
</style>

@ -199,10 +199,6 @@ export default {
section {
padding: 10px 10px;
&::v-deep button:disabled {
cursor: default;
}
.additional-emails-label {
display: block;
margin-top: 16px;

@ -22,23 +22,15 @@
<template>
<div class="language">
<select :id="inputId" @change="onLanguageChange">
<option v-for="commonLanguage in commonLanguages"
:key="commonLanguage.code"
:selected="language.code === commonLanguage.code"
:value="commonLanguage.code">
{{ commonLanguage.name }}
</option>
<option disabled>
</option>
<option v-for="otherLanguage in otherLanguages"
:key="otherLanguage.code"
:selected="language.code === otherLanguage.code"
:value="otherLanguage.code">
{{ otherLanguage.name }}
</option>
</select>
<NcSelect :aria-label-listbox="t('settings', 'Languages')"
class="language__select"
:clearable="false"
:input-id="inputId"
label="name"
label-outside
:options="allLanguages"
:value="language"
@option:selected="onLanguageChange" />
<a href="https://www.transifex.com/nextcloud/nextcloud/"
target="_blank"
@ -54,9 +46,15 @@ import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/Person
import { validateLanguage } from '../../../utils/validate.js'
import { handleError } from '../../../utils/handlers.js'
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
export default {
name: 'Language',
components: {
NcSelect,
},
props: {
inputId: {
type: String,
@ -83,17 +81,18 @@ export default {
},
computed: {
/**
* All available languages, sorted like: current, common, other
*/
allLanguages() {
return Object.freeze(
[...this.commonLanguages, ...this.otherLanguages]
.reduce((acc, { code, name }) => ({ ...acc, [code]: name }), {}),
)
const common = this.commonLanguages.filter(l => l.code !== this.language.code)
const other = this.otherLanguages.filter(l => l.code !== this.language.code)
return [this.language, ...common, ...other]
},
},
methods: {
async onLanguageChange(e) {
const language = this.constructLanguage(e.target.value)
async onLanguageChange(language) {
this.$emit('update:language', language)
if (validateLanguage(language)) {
@ -108,7 +107,7 @@ export default {
language,
status: responseData.ocs?.meta?.status,
})
this.reloadPage()
window.location.reload()
} catch (e) {
this.handleResponse({
errorMessage: t('settings', 'Unable to update language'),
@ -117,13 +116,6 @@ export default {
}
},
constructLanguage(languageCode) {
return {
code: languageCode,
name: this.allLanguages[languageCode],
}
},
handleResponse({ language, status, errorMessage, error }) {
if (status === 'ok') {
// Ensure that local state reflects server state
@ -132,10 +124,6 @@ export default {
handleError(error, errorMessage)
}
},
reloadPage() {
location.reload()
},
},
}
</script>
@ -144,12 +132,11 @@ export default {
.language {
display: grid;
select {
width: 100%;
#{&}__select {
margin-top: 6px; // align with other inputs
}
a {
color: var(--color-main-text);
text-decoration: none;
width: max-content;
}

@ -25,12 +25,11 @@
<HeaderBar :input-id="inputId"
:readable="propertyReadable" />
<template v-if="isEditable">
<Language :input-id="inputId"
:common-languages="commonLanguages"
:other-languages="otherLanguages"
:language.sync="language" />
</template>
<Language v-if="isEditable"
:input-id="inputId"
:common-languages="commonLanguages"
:other-languages="otherLanguages"
:language.sync="language" />
<span v-else>
{{ t('settings', 'No language set') }}
@ -56,11 +55,17 @@ export default {
HeaderBar,
},
data() {
setup() {
// Non reactive instance properties
return {
propertyReadable: ACCOUNT_SETTING_PROPERTY_READABLE_ENUM.LANGUAGE,
commonLanguages,
otherLanguages,
propertyReadable: ACCOUNT_SETTING_PROPERTY_READABLE_ENUM.LANGUAGE,
}
},
data() {
return {
language: activeLanguage,
}
},
@ -80,9 +85,5 @@ export default {
<style lang="scss" scoped>
section {
padding: 10px 10px;
&::v-deep button:disabled {
cursor: default;
}
}
</style>

@ -22,26 +22,18 @@
<template>
<div class="locale">
<select :id="inputId" @change="onLocaleChange">
<option v-for="currentLocale in localesForLanguage"
:key="currentLocale.code"
:selected="locale.code === currentLocale.code"
:value="currentLocale.code">
{{ currentLocale.name }}
</option>
<option disabled>
</option>
<option v-for="currentLocale in otherLocales"
:key="currentLocale.code"
:selected="locale.code === currentLocale.code"
:value="currentLocale.code">
{{ currentLocale.name }}
</option>
</select>
<NcSelect :aria-label-listbox="t('settings', 'Locales')"
class="locale__select"
:clearable="false"
:input-id="inputId"
label="name"
label-outside
:options="allLocales"
:value="locale"
@option:selected="updateLocale" />
<div class="example">
<Web :size="20" />
<MapClock :size="20" />
<div class="example__text">
<p>
<span>{{ example.date }}</span>
@ -57,18 +49,19 @@
<script>
import moment from '@nextcloud/moment'
import Web from 'vue-material-design-icons/Web.vue'
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
import MapClock from 'vue-material-design-icons/MapClock.vue'
import { ACCOUNT_SETTING_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants.js'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService.js'
import { validateLocale } from '../../../utils/validate.js'
import { handleError } from '../../../utils/handlers.js'
export default {
name: 'Locale',
components: {
Web,
MapClock,
NcSelect,
},
props: {
@ -93,6 +86,7 @@ export default {
data() {
return {
initialLocale: this.locale,
intervalId: 0,
example: {
date: moment().format('L'),
time: moment().format('LTS'),
@ -102,28 +96,25 @@ export default {
},
computed: {
/**
* All available locale, sorted like: current, common, other
*/
allLocales() {
return Object.freeze(
[...this.localesForLanguage, ...this.otherLocales]
.reduce((acc, { code, name }) => ({ ...acc, [code]: name }), {}),
)
const common = this.localesForLanguage.filter(l => l.code !== this.locale.code)
const other = this.otherLocales.filter(l => l.code !== this.locale.code)
return [this.locale, ...common, ...other]
},
},
created() {
setInterval(this.refreshExample, 1000)
mounted() {
this.intervalId = window.setInterval(this.refreshExample, 1000)
},
methods: {
async onLocaleChange(e) {
const locale = this.constructLocale(e.target.value)
this.$emit('update:locale', locale)
if (validateLocale(locale)) {
await this.updateLocale(locale)
}
},
beforeDestroy() {
window.clearInterval(this.intervalId)
},
methods: {
async updateLocale(locale) {
try {
const responseData = await savePrimaryAccountProperty(ACCOUNT_SETTING_PROPERTY_ENUM.LOCALE, locale.code)
@ -131,7 +122,7 @@ export default {
locale,
status: responseData.ocs?.meta?.status,
})
this.reloadPage()
window.location.reload()
} catch (e) {
this.handleResponse({
errorMessage: t('settings', 'Unable to update locale'),
@ -140,13 +131,6 @@ export default {
}
},
constructLocale(localeCode) {
return {
code: localeCode,
name: this.allLocales[localeCode],
}
},
handleResponse({ locale, status, errorMessage, error }) {
if (status === 'ok') {
this.initialLocale = locale
@ -163,10 +147,6 @@ export default {
firstDayOfWeek: window.dayNames[window.firstDay],
}
},
reloadPage() {
location.reload()
},
},
}
</script>
@ -175,8 +155,8 @@ export default {
.locale {
display: grid;
select {
width: 100%;
#{&}__select {
margin-top: 6px; // align with other inputs
}
}
@ -184,9 +164,9 @@ export default {
margin: 10px 0;
display: flex;
gap: 0 10px;
color: var(--color-text-lighter);
color: var(--color-text-maxcontrast);
&::v-deep .material-design-icon {
&:deep(.material-design-icon) {
align-self: flex-start;
margin-top: 2px;
}

@ -25,12 +25,11 @@
<HeaderBar :input-id="inputId"
:readable="propertyReadable" />
<template v-if="isEditable">
<Locale :input-id="inputId"
:locales-for-language="localesForLanguage"
:other-locales="otherLocales"
:locale.sync="locale" />
</template>
<Locale v-if="isEditable"
:input-id="inputId"
:locales-for-language="localesForLanguage"
:other-locales="otherLocales"
:locale.sync="locale" />
<span v-else>
{{ t('settings', 'No locale set') }}
@ -80,9 +79,5 @@ export default {
<style lang="scss" scoped>
section {
padding: 10px 10px;
&::v-deep button:disabled {
cursor: default;
}
}
</style>

@ -26,7 +26,7 @@
:checked.sync="isProfileEnabled"
:loading="loading"
@update:checked="saveEnableProfile">
{{ t('settings', 'Enable Profile') }}
{{ t('settings', 'Enable profile') }}
</NcCheckboxRadioSwitch>
</div>
</template>

@ -33,40 +33,31 @@
:id="inputId"
autocapitalize="none"
autocomplete="off"
:error="hasError || !!helperText"
:helper-text="helperText"
label-outside
:placeholder="placeholder"
rows="8"
spellcheck="false"
:success="isSuccess"
:value.sync="inputValue" />
<NcInputField v-else
:id="inputId"
ref="input"
:aria-describedby="helperText ? `${name}-helper-text` : undefined"
autocapitalize="none"
:autocomplete="autocomplete"
:error="hasError || !!helperText"
:helper-text="helperText"
label-outside
:placeholder="placeholder"
spellcheck="false"
:success="isSuccess"
:type="type"
:value.sync="inputValue" />
<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>
<p v-if="helperText"
:id="`${name}-helper-text`"
class="property__helper-text-message property__helper-text-message--error">
<AlertCircle class="property__helper-text-message__icon" :size="18" />
{{ helperText }}
</p>
</section>
</template>
@ -74,9 +65,6 @@
import debounce from 'debounce'
import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
import NcTextArea from '@nextcloud/vue/dist/Components/NcTextArea.js'
import AlertCircle from 'vue-material-design-icons/AlertCircleOutline.vue'
import AlertOctagon from 'vue-material-design-icons/AlertOctagon.vue'
import Check from 'vue-material-design-icons/Check.vue'
import HeaderBar from './HeaderBar.vue'
@ -87,9 +75,6 @@ export default {
name: 'AccountPropertySection',
components: {
AlertCircle,
AlertOctagon,
Check,
HeaderBar,
NcInputField,
NcTextArea,
@ -147,9 +132,9 @@ export default {
data() {
return {
initialValue: this.value,
helperText: null,
showCheckmarkIcon: false,
showErrorIcon: false,
helperText: '',
isSuccess: false,
hasError: false,
}
},
@ -170,12 +155,13 @@ export default {
debouncePropertyChange() {
return debounce(async function(value) {
this.helperText = null
if (this.$refs.input && this.$refs.input.validationMessage) {
this.helperText = this.$refs.input.validationMessage
this.helperText = this.$refs.input?.$refs.input?.validationMessage || ''
if (this.helperText !== '') {
return
}
if (this.onValidate && !this.onValidate(value)) {
this.hasError = this.onValidate && !this.onValidate(value)
if (this.hasError) {
this.helperText = t('settings', 'Invalid value')
return
}
await this.updateProperty(value)
@ -208,13 +194,13 @@ export default {
if (this.onSave) {
this.onSave(value)
}
this.showCheckmarkIcon = true
setTimeout(() => { this.showCheckmarkIcon = false }, 2000)
this.isSuccess = true
setTimeout(() => { this.isSuccess = false }, 2000)
} else {
this.$emit('update:value', this.initialValue)
handleError(error, errorMessage)
this.showErrorIcon = true
setTimeout(() => { this.showErrorIcon = false }, 2000)
this.hasError = true
setTimeout(() => { this.hasError = false }, 2000)
}
},
},
@ -226,25 +212,15 @@ section {
padding: 10px 10px;
.property {
display: grid;
align-items: center;
textarea {
resize: vertical;
grid-area: 1 / 1;
width: 100%;
}
input {
grid-area: 1 / 1;
width: 100%;
}
display: flex;
flex-direction: row;
align-items: start;
gap: 4px;
.property__actions-container {
grid-area: 1 / 1;
margin-top: 6px;
justify-self: flex-end;
align-self: flex-end;
height: 30px;
display: flex;
gap: 0 2px;

@ -2,6 +2,7 @@
- @copyright 2021, Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
- @author Ferdinand Thiessen <opensource@fthiessen.de>
-
- @license GNU AGPL version 3 or any later version
-
@ -25,51 +26,37 @@
class="federation-actions"
:class="{ 'federation-actions--additional': additional }"
:aria-label="ariaLabel"
:default-icon="scopeIcon"
:disabled="disabled">
<NcActionButton v-for="federationScope in federationScopes"
:key="federationScope.name"
:close-after-click="true"
:disabled="!supportedScopes.includes(federationScope.name)"
:icon="federationScope.iconClass"
:name="federationScope.displayName"
type="radio"
:value="federationScope.name"
:model-value="scope"
@update:modelValue="changeScope">
{{ supportedScopes.includes(federationScope.name) ? federationScope.tooltip : federationScope.tooltipDisabled }}
</NcActionButton>
<template #icon>
<NcIconSvgWrapper :path="scopeIcon" />
</template>
<FederationControlActions :additional="additional"
:additional-value="additionalValue"
:handle-additional-scope-change="handleAdditionalScopeChange"
:readable="readable"
:scope="scope"
@update:scope="onUpdateScope" />
</NcActions>
</template>
<script>
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import { loadState } from '@nextcloud/initial-state'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
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,
SCOPE_PROPERTY_ENUM,
} from '../../../constants/AccountPropertyConstants.js'
import { savePrimaryAccountPropertyScope } from '../../../service/PersonalInfo/PersonalInfoService.js'
import { handleError } from '../../../utils/handlers.js'
const {
federationEnabled,
lookupServerUploadEnabled,
} = loadState('settings', 'accountParameters', {})
import FederationControlActions from './FederationControlActions.vue'
export default {
name: 'FederationControl',
components: {
NcActions,
NcActionButton,
NcIconSvgWrapper,
FederationControlActions,
},
props: {
@ -103,7 +90,6 @@ export default {
data() {
return {
readableLowerCase: this.readable.toLocaleLowerCase(),
initialScope: this.scope,
}
},
@ -117,84 +103,16 @@ export default {
},
scopeIcon() {
return SCOPE_PROPERTY_ENUM[this.scope].iconClass
},
federationScopes() {
return Object.values(SCOPE_PROPERTY_ENUM)
},
supportedScopes() {
const scopes = PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM[this.readable]
if (UNPUBLISHED_READABLE_PROPERTIES.includes(this.readable)) {
return scopes
}
if (federationEnabled) {
scopes.push(SCOPE_ENUM.FEDERATED)
}
if (lookupServerUploadEnabled) {
scopes.push(SCOPE_ENUM.PUBLISHED)
}
return scopes
return SCOPE_PROPERTY_ENUM[this.scope].icon
},
},
methods: {
async changeScope(scope) {
onUpdateScope(scope) {
this.$emit('update:scope', scope)
if (!this.additional) {
await this.updatePrimaryScope(scope)
} else {
await this.updateAdditionalScope(scope)
}
// TODO: provide focus method from NcActions
this.$refs.federationActions.$refs.menuButton.$el.focus()
},
async updatePrimaryScope(scope) {
try {
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 {property}', { property: this.readableLowerCase }),
error: e,
})
}
},
async updateAdditionalScope(scope) {
try {
const responseData = await this.handleAdditionalScopeChange(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 {property}', { property: this.readableLowerCase }),
error: e,
})
}
},
handleResponse({ scope, status, errorMessage, error }) {
if (status === 'ok') {
this.initialScope = scope
} else {
this.$emit('update:scope', this.initialScope)
handleError(error, errorMessage)
}
},
},
}
</script>

@ -0,0 +1,181 @@
<!--
- @copyright 2021, Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
- @author Ferdinand Thiessen <opensource@fthiessen.de>
-
- @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>
<Fragment>
<NcActionButton v-for="federationScope in federationScopes"
:key="federationScope.name"
:close-after-click="true"
:disabled="!supportedScopes.includes(federationScope.name)"
:name="federationScope.displayName"
type="radio"
:value="federationScope.name"
:model-value="scope"
@update:modelValue="changeScope">
<template #icon>
<NcIconSvgWrapper :path="federationScope.icon" />
</template>
{{ supportedScopes.includes(federationScope.name) ? federationScope.tooltip : federationScope.tooltipDisabled }}
</NcActionButton>
</Fragment>
</template>
<script>
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import { loadState } from '@nextcloud/initial-state'
import { Fragment } from 'vue-frag'
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.js'
import { savePrimaryAccountPropertyScope } from '../../../service/PersonalInfo/PersonalInfoService.js'
import { handleError } from '../../../utils/handlers.js'
const {
federationEnabled,
lookupServerUploadEnabled,
} = loadState('settings', 'accountParameters', {})
export default {
name: 'FederationControlActions',
components: {
Fragment,
NcActionButton,
NcIconSvgWrapper,
},
props: {
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,
},
additional: {
type: Boolean,
default: false,
},
additionalValue: {
type: String,
default: '',
},
handleAdditionalScopeChange: {
type: Function,
default: null,
},
scope: {
type: String,
required: true,
},
},
data() {
return {
readableLowerCase: this.readable.toLocaleLowerCase(),
initialScope: this.scope,
}
},
computed: {
federationScopes() {
return Object.values(SCOPE_PROPERTY_ENUM)
},
supportedScopes() {
const scopes = PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM[this.readable]
if (UNPUBLISHED_READABLE_PROPERTIES.includes(this.readable)) {
return scopes
}
if (federationEnabled) {
scopes.push(SCOPE_ENUM.FEDERATED)
}
if (lookupServerUploadEnabled) {
scopes.push(SCOPE_ENUM.PUBLISHED)
}
return scopes
},
},
methods: {
async changeScope(scope) {
this.$emit('update:scope', scope)
if (!this.additional) {
await this.updatePrimaryScope(scope)
} else {
await this.updateAdditionalScope(scope)
}
},
async updatePrimaryScope(scope) {
try {
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 {property}', { property: this.readableLowerCase }),
error: e,
})
}
},
async updateAdditionalScope(scope) {
try {
const responseData = await this.handleAdditionalScopeChange(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 {property}', { property: this.readableLowerCase }),
error: e,
})
}
},
handleResponse({ scope, status, errorMessage, error }) {
if (status === 'ok') {
this.initialScope = scope
} else {
this.$emit('update:scope', this.initialScope)
handleError(error, errorMessage)
}
},
},
}
</script>

@ -24,6 +24,7 @@
* SYNC to be kept in sync with `lib/public/Accounts/IAccountManager.php`
*/
import { mdiAccountGroup, mdiCellphone, mdiLock, mdiWeb } from '@mdi/js'
import { translate as t } from '@nextcloud/l10n'
/** Enum of account properties */
@ -167,28 +168,28 @@ export const SCOPE_PROPERTY_ENUM = Object.freeze({
displayName: t('settings', 'Private'),
tooltip: t('settings', 'Only visible to people matched via phone number integration through Talk on mobile'),
tooltipDisabled: t('settings', 'Not available as this property is required for core functionality including file sharing and calendar invitations'),
iconClass: 'icon-phone',
icon: mdiCellphone,
},
[SCOPE_ENUM.LOCAL]: {
name: SCOPE_ENUM.LOCAL,
displayName: t('settings', 'Local'),
tooltip: t('settings', 'Only visible to people on this instance and guests'),
// tooltipDisabled is not required here as this scope is supported by all account properties
iconClass: 'icon-password',
icon: mdiLock,
},
[SCOPE_ENUM.FEDERATED]: {
name: SCOPE_ENUM.FEDERATED,
displayName: t('settings', 'Federated'),
tooltip: t('settings', 'Only synchronize to trusted servers'),
tooltipDisabled: t('settings', 'Not available as federation has been disabled for your account, contact your system administration if you have any questions'),
iconClass: 'icon-contacts-dark',
icon: mdiAccountGroup,
},
[SCOPE_ENUM.PUBLISHED]: {
name: SCOPE_ENUM.PUBLISHED,
displayName: t('settings', 'Published'),
tooltip: t('settings', 'Synchronize to trusted servers and the global and public address book'),
tooltipDisabled: t('settings', 'Not available as publishing account specific data to the lookup server is not allowed, contact your system administration if you have any questions'),
iconClass: 'icon-link',
icon: mdiWeb,
},
})

Loading…
Cancel
Save