diff options
Diffstat (limited to 'apps/settings/src/components/PersonalInfo/shared')
3 files changed, 602 insertions, 0 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 new file mode 100644 index 00000000000..e55a50056d3 --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue @@ -0,0 +1,206 @@ +<!-- + - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <NcActions ref="federationActions" + class="federation-actions" + :aria-label="ariaLabel" + :disabled="disabled"> + <template #icon> + <NcIconSvgWrapper :path="scopeIcon" /> + </template> + + <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> + </NcActions> +</template> + +<script> +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 { + ACCOUNT_PROPERTY_READABLE_ENUM, + ACCOUNT_SETTING_PROPERTY_READABLE_ENUM, + PROFILE_READABLE_ENUM, + PROPERTY_READABLE_KEYS_ENUM, + PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM, + SCOPE_PROPERTY_ENUM, + SCOPE_ENUM, + UNPUBLISHED_READABLE_PROPERTIES, +} from '../../../constants/AccountPropertyConstants.js' +import { savePrimaryAccountPropertyScope } from '../../../service/PersonalInfo/PersonalInfoService.js' +import { handleError } from '../../../utils/handlers.ts' + +const { + federationEnabled, + lookupServerUploadEnabled, +} = loadState('settings', 'accountParameters', {}) + +export default { + name: 'FederationControl', + + components: { + NcActions, + 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: '', + }, + disabled: { + type: Boolean, + default: false, + }, + handleAdditionalScopeChange: { + type: Function, + default: null, + }, + scope: { + type: String, + required: true, + }, + }, + + emits: ['update:scope'], + + data() { + return { + readableLowerCase: this.readable.toLocaleLowerCase(), + initialScope: this.scope, + } + }, + + computed: { + ariaLabel() { + 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].icon + }, + + 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) + } + + // 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.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> + +<style lang="scss" scoped> +.federation-actions { + &--additional { + &:deep(button) { + // TODO remove this hack + height: 30px !important; + min-height: 30px !important; + width: 30px !important; + min-width: 30px !important; + } + } +} +</style> diff --git a/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue new file mode 100644 index 00000000000..7c95c2b8f4c --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue @@ -0,0 +1,153 @@ +<!-- + - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <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 --> + {{ readable }} + </h3> + <label v-else :for="inputId"> + <!-- Already translated as required by prop validator --> + {{ readable }} + </label> + + <template v-if="scope"> + <FederationControl class="federation-control" + :readable="readable" + :scope.sync="localScope" + @update:scope="onScopeChange" /> + </template> + + <template v-if="isEditable && isMultiValueSupported"> + <NcButton type="tertiary" + :disabled="!isValidSection" + :aria-label="t('settings', 'Add additional email')" + @click.stop.prevent="onAddAdditional"> + <template #icon> + <Plus :size="20" /> + </template> + {{ t('settings', 'Add') }} + </NcButton> + </template> + </div> +</template> + +<script> +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, + NcButton, + Plus, + }, + + props: { + scope: { + type: String, + default: null, + }, + readable: { + type: String, + required: true, + }, + inputId: { + type: String, + default: null, + }, + isEditable: { + type: Boolean, + default: true, + }, + isMultiValueSupported: { + type: Boolean, + default: false, + }, + isValidSection: { + type: Boolean, + default: true, + }, + isHeading: { + type: Boolean, + default: false, + }, + }, + + data() { + return { + localScope: this.scope, + } + }, + + computed: { + isProfileProperty() { + return this.readable === ACCOUNT_PROPERTY_READABLE_ENUM.PROFILE_ENABLED + }, + + isSettingProperty() { + return !Object.values(ACCOUNT_PROPERTY_READABLE_ENUM).includes(this.readable) && !Object.values(PROFILE_READABLE_ENUM).includes(this.readable) + }, + }, + + methods: { + onAddAdditional() { + this.$emit('add-additional') + }, + + onScopeChange(scope) { + this.$emit('update:scope', scope) + }, + }, +} +</script> + +<style lang="scss" scoped> + .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); + + &.profile-property { + height: 38px; + } + + &.setting-property { + height: 34px; + } + + label { + cursor: pointer; + } + } + + .headerbar__heading { + margin: 0; + } + + .federation-control { + margin: 0; + } + + .button-vue { + margin: 0 !important; + margin-inline-start: auto !important; + } +</style> |