diff options
Diffstat (limited to 'apps/settings/src/components/PersonalInfo/shared')
4 files changed, 367 insertions, 223 deletions
diff --git a/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue b/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue new file mode 100644 index 00000000000..d039641ec72 --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue @@ -0,0 +1,243 @@ +<!-- + - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <section> + <HeaderBar :scope="scope" + :readable="readable" + :input-id="inputId" + :is-editable="isEditable" + @update:scope="(scope) => $emit('update:scope', scope)" /> + + <div v-if="isEditable" class="property"> + <NcTextArea v-if="multiLine" + :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" + autocapitalize="none" + :autocomplete="autocomplete" + :error="hasError || !!helperText" + :helper-text="helperText" + label-outside + :placeholder="placeholder" + spellcheck="false" + :success="isSuccess" + :type="type" + :value.sync="inputValue" /> + </div> + <span v-else> + {{ value || t('settings', 'No {property} set', { property: readable.toLocaleLowerCase() }) }} + </span> + </section> +</template> + +<script> +import debounce from 'debounce' +import NcInputField from '@nextcloud/vue/components/NcInputField' +import NcTextArea from '@nextcloud/vue/components/NcTextArea' + +import HeaderBar from './HeaderBar.vue' + +import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService.js' +import { handleError } from '../../../utils/handlers.ts' + +export default { + name: 'AccountPropertySection', + + components: { + HeaderBar, + NcInputField, + NcTextArea, + }, + + props: { + name: { + type: String, + required: true, + }, + value: { + type: String, + required: true, + }, + scope: { + type: String, + required: true, + }, + readable: { + type: String, + required: true, + }, + placeholder: { + type: String, + required: true, + }, + type: { + type: String, + default: 'text', + }, + isEditable: { + type: Boolean, + default: true, + }, + multiLine: { + type: Boolean, + default: false, + }, + onValidate: { + type: Function, + default: null, + }, + onSave: { + type: Function, + default: null, + }, + autocomplete: { + type: String, + default: null, + }, + }, + + emits: ['update:scope', 'update:value'], + + data() { + return { + initialValue: this.value, + helperText: '', + isSuccess: false, + hasError: false, + } + }, + + computed: { + inputId() { + return `account-property-${this.name}` + }, + + inputValue: { + get() { + return this.value + }, + set(value) { + this.$emit('update:value', value) + this.debouncePropertyChange(value.trim()) + }, + }, + + debouncePropertyChange() { + return debounce(async function(value) { + this.helperText = this.$refs.input?.$refs.input?.validationMessage || '' + if (this.helperText !== '') { + return + } + this.hasError = this.onValidate && !this.onValidate(value) + if (this.hasError) { + this.helperText = t('settings', 'Invalid value') + return + } + await this.updateProperty(value) + }, 1000) + }, + }, + + methods: { + async updateProperty(value) { + try { + this.hasError = false + const responseData = await savePrimaryAccountProperty( + this.name, + value, + ) + this.handleResponse({ + value, + status: responseData.ocs?.meta?.status, + }) + } catch (e) { + this.handleResponse({ + errorMessage: t('settings', 'Unable to update {property}', { property: this.readable.toLocaleLowerCase() }), + error: e, + }) + } + }, + + handleResponse({ value, status, errorMessage, error }) { + if (status === 'ok') { + this.initialValue = value + if (this.onSave) { + this.onSave(value) + } + this.isSuccess = true + setTimeout(() => { this.isSuccess = false }, 2000) + } else { + handleError(error, errorMessage) + this.hasError = true + } + }, + }, +} +</script> + +<style lang="scss" scoped> +section { + padding: 10px 10px; + + .property { + display: flex; + flex-direction: row; + align-items: start; + gap: 4px; + + .property__actions-container { + margin-top: 6px; + justify-self: flex-end; + align-self: flex-end; + + display: flex; + gap: 0 2px; + margin-inline-end: 5px; + margin-bottom: 5px; + } + } + + .property__helper-text-message { + padding: 4px 0; + display: flex; + align-items: center; + + &__icon { + margin-inline-end: 8px; + align-self: start; + margin-top: 4px; + } + + &--error { + color: var(--color-error); + } + } + + .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/shared/FederationControl.vue b/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue index b14bc5165b5..e55a50056d3 100644 --- a/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue +++ b/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue @@ -1,74 +1,72 @@ <!-- - - @copyright 2021, Christopher Ng <chrng8@gmail.com> - - - - @author Christopher Ng <chrng8@gmail.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero General Public License as - - published by the Free Software Foundation, either version 3 of the - - License, or (at your option) any later version. - - - - This program is distributed in the hope that it will be useful, - - but WITHOUT ANY WARRANTY; without even the implied warranty of - - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - - GNU Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - + - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later --> <template> - <Actions :class="{ 'federation-actions': !additional, 'federation-actions--additional': additional }" + <NcActions ref="federationActions" + class="federation-actions" :aria-label="ariaLabel" - :default-icon="scopeIcon" :disabled="disabled"> - <FederationControlAction v-for="federationScope in federationScopes" + <template #icon> + <NcIconSvgWrapper :path="scopeIcon" /> + </template> + + <NcActionButton v-for="federationScope in federationScopes" :key="federationScope.name" - :active-scope="scope" - :display-name="federationScope.displayName" - :handle-scope-change="changeScope" - :icon-class="federationScope.iconClass" - :is-supported-scope="supportedScopes.includes(federationScope.name)" - :name="federationScope.name" - :tooltip-disabled="federationScope.tooltipDisabled" - :tooltip="federationScope.tooltip" /> - </Actions> + :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> + </NcActions> </template> <script> -import Actions from '@nextcloud/vue/dist/Components/Actions' +import NcActions from '@nextcloud/vue/components/NcActions' +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' import { loadState } from '@nextcloud/initial-state' -import { showError } from '@nextcloud/dialogs' - -import FederationControlAction from './FederationControlAction' 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, + SCOPE_PROPERTY_ENUM, + SCOPE_ENUM, UNPUBLISHED_READABLE_PROPERTIES, -} from '../../../constants/AccountPropertyConstants' -import { savePrimaryAccountPropertyScope } from '../../../service/PersonalInfo/PersonalInfoService' +} from '../../../constants/AccountPropertyConstants.js' +import { savePrimaryAccountPropertyScope } from '../../../service/PersonalInfo/PersonalInfoService.js' +import { handleError } from '../../../utils/handlers.ts' -const { lookupServerUploadEnabled } = loadState('settings', 'accountParameters', {}) +const { + federationEnabled, + lookupServerUploadEnabled, +} = loadState('settings', 'accountParameters', {}) export default { name: 'FederationControl', components: { - Actions, - FederationControlAction, + NcActions, + NcActionButton, + NcIconSvgWrapper, }, props: { - accountProperty: { + readable: { type: String, required: true, - validator: (value) => Object.values(ACCOUNT_PROPERTY_READABLE_ENUM).includes(value), + validator: (value) => Object.values(ACCOUNT_PROPERTY_READABLE_ENUM).includes(value) || Object.values(ACCOUNT_SETTING_PROPERTY_READABLE_ENUM).includes(value) || value === PROFILE_READABLE_ENUM.PROFILE_VISIBILITY, }, additional: { type: Boolean, @@ -92,20 +90,26 @@ export default { }, }, + emits: ['update:scope'], + data() { return { - accountPropertyLowerCase: this.accountProperty.toLocaleLowerCase(), + readableLowerCase: this.readable.toLocaleLowerCase(), initialScope: this.scope, } }, computed: { ariaLabel() { - return t('settings', 'Change scope level of {accountProperty}', { accountProperty: this.accountPropertyLowerCase }) + return t('settings', 'Change scope level of {property}, current scope is {scope}', { property: this.readableLowerCase, scope: this.scopeDisplayNameLowerCase }) + }, + + scopeDisplayNameLowerCase() { + return SCOPE_PROPERTY_ENUM[this.scope].displayName.toLocaleLowerCase() }, scopeIcon() { - return SCOPE_PROPERTY_ENUM[this.scope].iconClass + return SCOPE_PROPERTY_ENUM[this.scope].icon }, federationScopes() { @@ -113,15 +117,21 @@ export default { }, supportedScopes() { - if (lookupServerUploadEnabled && !UNPUBLISHED_READABLE_PROPERTIES.includes(this.accountProperty)) { - return [ - ...PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM[this.accountProperty], - SCOPE_ENUM.FEDERATED, - SCOPE_ENUM.PUBLISHED, - ] + 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) } - return PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM[this.accountProperty] + if (lookupServerUploadEnabled) { + scopes.push(SCOPE_ENUM.PUBLISHED) + } + + return scopes }, }, @@ -134,18 +144,21 @@ export default { } else { await this.updateAdditionalScope(scope) } + + // TODO: provide focus method from NcActions + this.$refs.federationActions.$refs?.triggerButton?.$el?.focus?.() }, async updatePrimaryScope(scope) { try { - const responseData = await savePrimaryAccountPropertyScope(PROPERTY_READABLE_KEYS_ENUM[this.accountProperty], scope) + const responseData = await savePrimaryAccountPropertyScope(PROPERTY_READABLE_KEYS_ENUM[this.readable], scope) this.handleResponse({ scope, status: responseData.ocs?.meta?.status, }) } catch (e) { this.handleResponse({ - errorMessage: t('settings', 'Unable to update federation scope of the primary {accountProperty}', { accountProperty: this.accountPropertyLowerCase }), + errorMessage: t('settings', 'Unable to update federation scope of the primary {property}', { property: this.readableLowerCase }), error: e, }) } @@ -160,7 +173,7 @@ export default { }) } catch (e) { this.handleResponse({ - errorMessage: t('settings', 'Unable to update federation scope of additional {accountProperty}', { accountProperty: this.accountPropertyLowerCase }), + errorMessage: t('settings', 'Unable to update federation scope of additional {property}', { property: this.readableLowerCase }), error: e, }) } @@ -171,8 +184,7 @@ export default { this.initialScope = scope } else { this.$emit('update:scope', this.initialScope) - showError(errorMessage) - this.logger.error(errorMessage, error) + handleError(error, errorMessage) } }, }, @@ -180,25 +192,15 @@ export default { </script> <style lang="scss" scoped> - .federation-actions, - .federation-actions--additional { - opacity: 0.4 !important; - - &:hover, - &:focus, - &:active { - opacity: 0.8 !important; - } - } - - .federation-actions--additional { - &::v-deep button { +.federation-actions { + &--additional { + &:deep(button) { // TODO remove this hack - padding-bottom: 7px; height: 30px !important; min-height: 30px !important; width: 30px !important; min-width: 30px !important; } } +} </style> diff --git a/apps/settings/src/components/PersonalInfo/shared/FederationControlAction.vue b/apps/settings/src/components/PersonalInfo/shared/FederationControlAction.vue deleted file mode 100644 index f98d9bc7535..00000000000 --- a/apps/settings/src/components/PersonalInfo/shared/FederationControlAction.vue +++ /dev/null @@ -1,104 +0,0 @@ -<!-- - - @copyright 2021 Christopher Ng <chrng8@gmail.com> - - - - @author Christopher Ng <chrng8@gmail.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero General Public License as - - published by the Free Software Foundation, either version 3 of the - - License, or (at your option) any later version. - - - - This program is distributed in the hope that it will be useful, - - but WITHOUT ANY WARRANTY; without even the implied warranty of - - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - - GNU Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - ---> - -<template> - <ActionButton :aria-label="isSupportedScope ? tooltip : tooltipDisabled" - class="federation-actions__btn" - :class="{ 'federation-actions__btn--active': activeScope === name }" - :close-after-click="true" - :disabled="!isSupportedScope" - :icon="iconClass" - :title="displayName" - @click.stop.prevent="updateScope"> - {{ isSupportedScope ? tooltip : tooltipDisabled }} - </ActionButton> -</template> - -<script> -import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' - -export default { - name: 'FederationControlAction', - - components: { - ActionButton, - }, - - props: { - activeScope: { - type: String, - required: true, - }, - displayName: { - type: String, - required: true, - }, - handleScopeChange: { - type: Function, - default: () => {}, - }, - iconClass: { - type: String, - required: true, - }, - isSupportedScope: { - type: Boolean, - required: true, - }, - name: { - type: String, - required: true, - }, - tooltipDisabled: { - type: String, - default: '', - }, - tooltip: { - type: String, - required: true, - }, - }, - - methods: { - updateScope() { - this.handleScopeChange(this.name) - }, - }, -} -</script> - -<style lang="scss" scoped> - .federation-actions__btn { - &::v-deep p { - width: 150px !important; - padding: 8px 0 !important; - color: var(--color-main-text) !important; - font-size: 12.8px !important; - line-height: 1.5em !important; - } - } - - .federation-actions__btn--active { - background-color: var(--color-primary-light) !important; - box-shadow: inset 2px 0 var(--color-primary) !important; - } -</style> diff --git a/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue index 65eb5a110a3..7c95c2b8f4c 100644 --- a/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue +++ b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue @@ -1,41 +1,28 @@ <!-- - - @copyright 2021, Christopher Ng <chrng8@gmail.com> - - - - @author Christopher Ng <chrng8@gmail.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero General Public License as - - published by the Free Software Foundation, either version 3 of the - - License, or (at your option) any later version. - - - - This program is distributed in the hope that it will be useful, - - but WITHOUT ANY WARRANTY; without even the implied warranty of - - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - - GNU Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - + - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later --> <template> - <h3 :class="{ 'setting-property': isSettingProperty, 'profile-property': isProfileProperty }"> - <label :for="labelFor"> + <div class="headerbar-label" :class="{ 'setting-property': isSettingProperty, 'profile-property': isProfileProperty }"> + <h3 v-if="isHeading" class="headerbar__heading"> <!-- Already translated as required by prop validator --> - {{ accountProperty }} + {{ readable }} + </h3> + <label v-else :for="inputId"> + <!-- Already translated as required by prop validator --> + {{ readable }} </label> <template v-if="scope"> <FederationControl class="federation-control" - :account-property="accountProperty" + :readable="readable" :scope.sync="localScope" @update:scope="onScopeChange" /> </template> <template v-if="isEditable && isMultiValueSupported"> - <Button type="tertiary" + <NcButton type="tertiary" :disabled="!isValidSection" :aria-label="t('settings', 'Add additional email')" @click.stop.prevent="onAddAdditional"> @@ -43,31 +30,43 @@ <Plus :size="20" /> </template> {{ t('settings', 'Add') }} - </Button> + </NcButton> </template> - </h3> + </div> </template> <script> -import FederationControl from './FederationControl' -import Button from '@nextcloud/vue/dist/Components/Button' -import Plus from 'vue-material-design-icons/Plus' -import { ACCOUNT_PROPERTY_READABLE_ENUM, ACCOUNT_SETTING_PROPERTY_READABLE_ENUM, PROFILE_READABLE_ENUM } from '../../../constants/AccountPropertyConstants' +import NcButton from '@nextcloud/vue/components/NcButton' +import Plus from 'vue-material-design-icons/Plus.vue' + +import FederationControl from './FederationControl.vue' + +import { + ACCOUNT_PROPERTY_READABLE_ENUM, + PROFILE_READABLE_ENUM, +} from '../../../constants/AccountPropertyConstants.js' export default { name: 'HeaderBar', components: { FederationControl, - Button, + NcButton, Plus, }, props: { - accountProperty: { + scope: { + type: String, + default: null, + }, + readable: { type: String, required: true, - validator: (value) => Object.values(ACCOUNT_PROPERTY_READABLE_ENUM).includes(value) || Object.values(ACCOUNT_SETTING_PROPERTY_READABLE_ENUM).includes(value) || value === PROFILE_READABLE_ENUM.PROFILE_VISIBILITY, + }, + inputId: { + type: String, + default: null, }, isEditable: { type: Boolean, @@ -79,15 +78,11 @@ export default { }, isValidSection: { type: Boolean, - default: false, - }, - labelFor: { - type: String, - default: '', + default: true, }, - scope: { - type: String, - default: null, + isHeading: { + type: Boolean, + default: false, }, }, @@ -99,11 +94,11 @@ export default { computed: { isProfileProperty() { - return this.accountProperty === ACCOUNT_PROPERTY_READABLE_ENUM.PROFILE_ENABLED + return this.readable === ACCOUNT_PROPERTY_READABLE_ENUM.PROFILE_ENABLED }, isSettingProperty() { - return Object.values(ACCOUNT_SETTING_PROPERTY_READABLE_ENUM).includes(this.accountProperty) + return !Object.values(ACCOUNT_PROPERTY_READABLE_ENUM).includes(this.readable) && !Object.values(PROFILE_READABLE_ENUM).includes(this.readable) }, }, @@ -120,10 +115,13 @@ export default { </script> <style lang="scss" scoped> - h3 { + .headerbar-label { + font-weight: normal; display: inline-flex; width: 100%; margin: 12px 0 0 0; + gap: 8px; + align-items: center; font-size: 16px; color: var(--color-text-light); @@ -132,7 +130,7 @@ export default { } &.setting-property { - height: 32px; + height: 34px; } label { @@ -140,11 +138,16 @@ export default { } } + .headerbar__heading { + margin: 0; + } + .federation-control { - margin: -12px 0 0 8px; + margin: 0; } .button-vue { - margin: -6px 0 0 auto !important; + margin: 0 !important; + margin-inline-start: auto !important; } </style> |