aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/src/components/PersonalInfo/shared
diff options
context:
space:
mode:
Diffstat (limited to 'apps/settings/src/components/PersonalInfo/shared')
-rw-r--r--apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue243
-rw-r--r--apps/settings/src/components/PersonalInfo/shared/FederationControl.vue206
-rw-r--r--apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue153
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>