diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-02-10 19:37:13 +0100 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-02-15 00:46:55 +0100 |
commit | 3e09295fa118085b0bf779986278749882437adf (patch) | |
tree | 7ca2c0fceed0bb2422662c32f20c6b9b45e31145 /apps | |
parent | fe58d8aae985d13a00abcf9448b00a1a7f19a4b2 (diff) | |
download | nextcloud-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')
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, }, }) |