Browse Source

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>
tags/v29.0.0beta1
Ferdinand Thiessen 3 months ago
parent
commit
3e09295fa1
No account linked to committer's email address

+ 2
- 2
apps/settings/src/components/PersonalInfo/DetailsSection.vue View File

@@ -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;
}

+ 115
- 136
apps/settings/src/components/PersonalInfo/EmailSection/Email.vue View File

@@ -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>

+ 0
- 4
apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue View File

@@ -199,10 +199,6 @@ export default {
section {
padding: 10px 10px;

&::v-deep button:disabled {
cursor: default;
}

.additional-emails-label {
display: block;
margin-top: 16px;

+ 25
- 38
apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue View File

@@ -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;
}

+ 13
- 12
apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue View File

@@ -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>

+ 32
- 52
apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue View File

@@ -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;
}

+ 5
- 10
apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue View File

@@ -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>

+ 1
- 1
apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue View File

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

+ 23
- 47
apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue View File

@@ -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;

+ 17
- 99
apps/settings/src/components/PersonalInfo/shared/FederationControl.vue View File

@@ -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>

+ 181
- 0
apps/settings/src/components/PersonalInfo/shared/FederationControlActions.vue View File

@@ -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>

+ 5
- 4
apps/settings/src/constants/AccountPropertyConstants.js View File

@@ -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