aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-02-10 19:37:13 +0100
committerFerdinand Thiessen <opensource@fthiessen.de>2024-02-15 00:46:55 +0100
commit3e09295fa118085b0bf779986278749882437adf (patch)
tree7ca2c0fceed0bb2422662c32f20c6b9b45e31145 /apps
parentfe58d8aae985d13a00abcf9448b00a1a7f19a4b2 (diff)
downloadnextcloud-server-3e09295fa118085b0bf779986278749882437adf.tar.gz
nextcloud-server-3e09295fa118085b0bf779986278749882437adf.zip
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>
Diffstat (limited to 'apps')
-rw-r--r--apps/settings/src/components/PersonalInfo/DetailsSection.vue4
-rw-r--r--apps/settings/src/components/PersonalInfo/EmailSection/Email.vue251
-rw-r--r--apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue4
-rw-r--r--apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue63
-rw-r--r--apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue25
-rw-r--r--apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue84
-rw-r--r--apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue15
-rw-r--r--apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue2
-rw-r--r--apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue70
-rw-r--r--apps/settings/src/components/PersonalInfo/shared/FederationControl.vue116
-rw-r--r--apps/settings/src/components/PersonalInfo/shared/FederationControlActions.vue181
-rw-r--r--apps/settings/src/constants/AccountPropertyConstants.js9
12 files changed, 419 insertions, 405 deletions
diff --git a/apps/settings/src/components/PersonalInfo/DetailsSection.vue b/apps/settings/src/components/PersonalInfo/DetailsSection.vue
index 075ed6f71e2..2774f2c1c8e 100644
--- a/apps/settings/src/components/PersonalInfo/DetailsSection.vue
+++ b/apps/settings/src/components/PersonalInfo/DetailsSection.vue
@@ -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;
}
diff --git a/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue b/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue
index 1fff440c50e..99917bb4f7e 100644
--- a/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue
+++ b/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue
@@ -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>
diff --git a/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue b/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue
index 0cc94b4998a..16a866c6ee9 100644
--- a/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue
+++ b/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue
@@ -199,10 +199,6 @@ export default {
section {
padding: 10px 10px;
- &::v-deep button:disabled {
- cursor: default;
- }
-
.additional-emails-label {
display: block;
margin-top: 16px;
diff --git a/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue b/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue
index cf921b5809f..83611574ee5 100644
--- a/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue
+++ b/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue
@@ -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;
}
diff --git a/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue b/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue
index fdc1d31d10c..23fc7850546 100644
--- a/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue
+++ b/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue
@@ -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>
diff --git a/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue b/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue
index b405d7fced4..185b06785d8 100644
--- a/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue
+++ b/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue
@@ -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;
}
diff --git a/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue b/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue
index 61c98f3a27a..614a3e4bcf1 100644
--- a/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue
+++ b/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue
@@ -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>
diff --git a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue
index b8e8d6301d3..821cb981bb8 100644
--- a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue
+++ b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue
@@ -26,7 +26,7 @@
:checked.sync="isProfileEnabled"
:loading="loading"
@update:checked="saveEnableProfile">
- {{ t('settings', 'Enable Profile') }}
+ {{ t('settings', 'Enable profile') }}
</NcCheckboxRadioSwitch>
</div>
</template>
diff --git a/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue b/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue
index 3c8319302eb..15ec0191921 100644
--- a/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue
+++ b/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue
@@ -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;
diff --git a/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue b/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue
index bf22c0ac081..64f603932b6 100644
--- a/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue
+++ b/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue
@@ -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>
diff --git a/apps/settings/src/components/PersonalInfo/shared/FederationControlActions.vue b/apps/settings/src/components/PersonalInfo/shared/FederationControlActions.vue
new file mode 100644
index 00000000000..d37d7fa2fba
--- /dev/null
+++ b/apps/settings/src/components/PersonalInfo/shared/FederationControlActions.vue
@@ -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>
diff --git a/apps/settings/src/constants/AccountPropertyConstants.js b/apps/settings/src/constants/AccountPropertyConstants.js
index eb35482fb32..2dcb6c98f9c 100644
--- a/apps/settings/src/constants/AccountPropertyConstants.js
+++ b/apps/settings/src/constants/AccountPropertyConstants.js
@@ -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,
},
})