diff options
Diffstat (limited to 'apps/settings/src/components/PersonalInfo')
23 files changed, 374 insertions, 104 deletions
diff --git a/apps/settings/src/components/PersonalInfo/AvatarSection.vue b/apps/settings/src/components/PersonalInfo/AvatarSection.vue index 3145cc28fe2..a99f228668c 100644 --- a/apps/settings/src/components/PersonalInfo/AvatarSection.vue +++ b/apps/settings/src/components/PersonalInfo/AvatarSection.vue @@ -80,15 +80,15 @@ import { getCurrentUser } from '@nextcloud/auth' import { getFilePickerBuilder, showError } from '@nextcloud/dialogs' import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' -import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcAvatar from '@nextcloud/vue/components/NcAvatar' +import NcButton from '@nextcloud/vue/components/NcButton' import VueCropper from 'vue-cropperjs' // eslint-disable-next-line n/no-extraneous-import import 'cropperjs/dist/cropper.css' import Upload from 'vue-material-design-icons/Upload.vue' import Folder from 'vue-material-design-icons/Folder.vue' -import Delete from 'vue-material-design-icons/Delete.vue' +import Delete from 'vue-material-design-icons/DeleteOutline.vue' import HeaderBar from './shared/HeaderBar.vue' import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js' @@ -257,9 +257,10 @@ section { grid-row: 1/3; padding: 10px 10px; } + .avatar { &__container { - margin: 0 auto; + margin: calc(var(--default-grid-baseline) * 2) auto 0 auto; display: flex; flex-direction: column; justify-content: center; @@ -296,7 +297,7 @@ section { justify-content: space-between; } - &::v-deep .cropper-view-box { + :deep(.cropper-view-box) { border-radius: 50%; } } diff --git a/apps/settings/src/components/PersonalInfo/BiographySection.vue b/apps/settings/src/components/PersonalInfo/BiographySection.vue index 32af1a03e2d..bbfb25e25cc 100644 --- a/apps/settings/src/components/PersonalInfo/BiographySection.vue +++ b/apps/settings/src/components/PersonalInfo/BiographySection.vue @@ -5,7 +5,7 @@ <template> <AccountPropertySection v-bind.sync="biography" - :placeholder="t('settings', 'Your biography')" + :placeholder="t('settings', 'Your biography. Markdown is supported.')" :multi-line="true" /> </template> diff --git a/apps/settings/src/components/PersonalInfo/BirthdaySection.vue b/apps/settings/src/components/PersonalInfo/BirthdaySection.vue index 7580e958023..f55f09c95e5 100644 --- a/apps/settings/src/components/PersonalInfo/BirthdaySection.vue +++ b/apps/settings/src/components/PersonalInfo/BirthdaySection.vue @@ -8,13 +8,11 @@ :input-id="inputId" :readable="birthdate.readable" /> - <template> - <NcDateTimePickerNative :id="inputId" - type="date" - label="" - :value="value" - @input="onInput" /> - </template> + <NcDateTimePickerNative :id="inputId" + type="date" + label="" + :value="value" + @input="onInput" /> <p class="property__helper-text-message"> {{ t('settings', 'Enter your date of birth') }} @@ -23,15 +21,15 @@ </template> <script> -import HeaderBar from './shared/HeaderBar.vue' -import AccountPropertySection from './shared/AccountPropertySection.vue' +import { loadState } from '@nextcloud/initial-state' import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js' -import { NcDateTimePickerNative } from '@nextcloud/vue' -import debounce from 'debounce' import { savePrimaryAccountProperty } from '../../service/PersonalInfo/PersonalInfoService' import { handleError } from '../../utils/handlers' -import AlertCircle from 'vue-material-design-icons/AlertCircleOutline.vue' -import { loadState } from '@nextcloud/initial-state' + +import debounce from 'debounce' + +import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative' +import HeaderBar from './shared/HeaderBar.vue' const { birthdate } = loadState('settings', 'personalInfoParameters', {}) @@ -39,8 +37,6 @@ export default { name: 'BirthdaySection', components: { - AlertCircle, - AccountPropertySection, NcDateTimePickerNative, HeaderBar, }, @@ -68,13 +64,13 @@ export default { get() { return new Date(this.birthdate.value) }, - /** @param {Date} value */ + /** @param {Date} value The date to set */ set(value) { const day = value.getDate().toString().padStart(2, '0') const month = (value.getMonth() + 1).toString().padStart(2, '0') const year = value.getFullYear() this.birthdate.value = `${year}-${month}-${day}` - } + }, }, }, @@ -122,7 +118,7 @@ export default { section { padding: 10px 10px; - &::v-deep button:disabled { + :deep(button:disabled) { cursor: default; } diff --git a/apps/settings/src/components/PersonalInfo/BlueskySection.vue b/apps/settings/src/components/PersonalInfo/BlueskySection.vue new file mode 100644 index 00000000000..65223d1ab53 --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/BlueskySection.vue @@ -0,0 +1,64 @@ +<!-- + - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <AccountPropertySection v-bind.sync="value" + :readable="readable" + :on-validate="onValidate" + :placeholder="t('settings', 'Bluesky handle')" /> +</template> + +<script setup lang="ts"> +import type { AccountProperties } from '../../constants/AccountPropertyConstants.js' + +import { loadState } from '@nextcloud/initial-state' +import { t } from '@nextcloud/l10n' +import { ref } from 'vue' +import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.ts' +import AccountPropertySection from './shared/AccountPropertySection.vue' + +const { bluesky } = loadState<AccountProperties>('settings', 'personalInfoParameters') + +const value = ref({ ...bluesky }) +const readable = NAME_READABLE_ENUM[bluesky.name] + +/** + * Validate that the text might be a bluesky handle + * @param text The potential bluesky handle + */ +function onValidate(text: string): boolean { + if (text === '') return true + + const lowerText = text.toLowerCase() + + if (lowerText === 'bsky.social') { + // Standalone bsky.social is invalid + return false + } + + if (lowerText.endsWith('.bsky.social')) { + // Enforce format: exactly one label + '.bsky.social' + const parts = lowerText.split('.') + + // Must be in form: [username, 'bsky', 'social'] + if (parts.length !== 3 || parts[1] !== 'bsky' || parts[2] !== 'social') { + return false + } + + const username = parts[0] + const validateRegex = /^[a-z0-9][a-z0-9-]{2,17}$/ + return validateRegex.test(username) + } + + // Else, treat as a custom domain + try { + const url = new URL(`https://${text}`) + // Ensure the parsed host matches exactly (case-insensitive already) + return url.host === lowerText + } catch { + return false + } +} +</script> diff --git a/apps/settings/src/components/PersonalInfo/DetailsSection.vue b/apps/settings/src/components/PersonalInfo/DetailsSection.vue index 22a0a185db7..d4bb0ce16ec 100644 --- a/apps/settings/src/components/PersonalInfo/DetailsSection.vue +++ b/apps/settings/src/components/PersonalInfo/DetailsSection.vue @@ -20,6 +20,7 @@ <div class="details__quota"> <CircleSlice :size="20" /> <div class="details__quota-info"> + <!-- eslint-disable-next-line vue/no-v-html --> <p class="details__quota-text" v-html="quotaText" /> <NcProgressBar size="medium" :value="usageRelative" @@ -32,9 +33,10 @@ <script> import { loadState } from '@nextcloud/initial-state' -import NcProgressBar from '@nextcloud/vue/dist/Components/NcProgressBar.js' +import { t } from '@nextcloud/l10n' -import Account from 'vue-material-design-icons/Account.vue' +import NcProgressBar from '@nextcloud/vue/components/NcProgressBar' +import Account from 'vue-material-design-icons/AccountOutline.vue' import CircleSlice from 'vue-material-design-icons/CircleSlice3.vue' import HeaderBar from './shared/HeaderBar.vue' @@ -64,12 +66,14 @@ export default { computed: { quotaText() { if (quota === SPACE_UNLIMITED) { - return t('settings', 'You are using <strong>{usage}</strong>', { usage }) + return t('settings', 'You are using {s}{usage}{/s}', { usage, s: '<strong>', '/s': '</strong>' }, undefined, { escape: false }) } return t( 'settings', - 'You are using <strong>{usage}</strong> of <strong>{totalSpace}</strong> (<strong>{usageRelative}%</strong>)', - { usage, totalSpace, usageRelative }, + 'You are using {s}{usage}{/s} of {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})', + { usage, totalSpace, usageRelative, s: '<strong>', '/s': '</strong>' }, + undefined, + { escape: false }, ) }, }, @@ -80,7 +84,8 @@ export default { .details { display: flex; flex-direction: column; - margin: 10px 32px 10px 0; + margin-block: 10px; + margin-inline: 0 32px; gap: 16px 0; color: var(--color-text-maxcontrast); diff --git a/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue b/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue index ecc463c7363..6a6baef8817 100644 --- a/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue +++ b/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue @@ -48,7 +48,7 @@ :disabled="deleteDisabled" @click="deleteEmail"> <template #icon> - <NcIconSvgWrapper :path="mdiTrashCan" /> + <NcIconSvgWrapper :path="mdiTrashCanOutline" /> </template> {{ deleteEmailLabel }} </NcActionButton> @@ -64,14 +64,14 @@ </template> <script> -import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' -import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' -import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' -import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' +import NcActions from '@nextcloud/vue/components/NcActions' +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcTextField from '@nextcloud/vue/components/NcTextField' import debounce from 'debounce' -import { mdiArrowLeft, mdiLock, mdiStar, mdiStarOutline, mdiTrashCan } from '@mdi/js' +import { mdiArrowLeft, mdiLockOutline, mdiStar, mdiStarOutline, mdiTrashCanOutline } from '@mdi/js' import FederationControl from '../shared/FederationControl.vue' import { handleError } from '../../../utils/handlers.ts' @@ -133,10 +133,10 @@ export default { setup() { return { mdiArrowLeft, - mdiLock, + mdiLockOutline, mdiStar, mdiStarOutline, - mdiTrashCan, + mdiTrashCanOutline, saveAdditionalEmailScope, } }, @@ -356,6 +356,9 @@ export default { handleDeleteAdditionalEmail(status) { if (status === 'ok') { this.$emit('delete-additional-email') + if (this.isNotificationEmail) { + this.$emit('update:notification-email', '') + } } else { this.handleResponse({ errorMessage: t('settings', 'Unable to delete additional email address'), diff --git a/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue b/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue index 8fd17922724..f9674a3163b 100644 --- a/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue +++ b/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue @@ -13,7 +13,7 @@ :scope.sync="primaryEmail.scope" @add-additional="onAddAdditionalEmail" /> - <template v-if="displayNameChangeSupported"> + <template v-if="emailChangeSupported"> <Email :input-id="inputId" :primary="true" :scope.sync="primaryEmail.scope" @@ -56,7 +56,7 @@ import { validateEmail } from '../../../utils/validate.js' import { handleError } from '../../../utils/handlers.ts' const { emailMap: { additionalEmails, primaryEmail, notificationEmail } } = loadState('settings', 'personalInfoParameters', {}) -const { displayNameChangeSupported } = loadState('settings', 'accountParameters', {}) +const { emailChangeSupported } = loadState('settings', 'accountParameters', {}) export default { name: 'EmailSection', @@ -70,7 +70,7 @@ export default { return { accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL, additionalEmails: additionalEmails.map(properties => ({ ...properties, key: this.generateUniqueKey() })), - displayNameChangeSupported, + emailChangeSupported, primaryEmail: { ...primaryEmail, readable: NAME_READABLE_ENUM[primaryEmail.name] }, notificationEmail, } diff --git a/apps/settings/src/components/PersonalInfo/FediverseSection.vue b/apps/settings/src/components/PersonalInfo/FediverseSection.vue index 9ba9c37ab80..043fa6e64b9 100644 --- a/apps/settings/src/components/PersonalInfo/FediverseSection.vue +++ b/apps/settings/src/components/PersonalInfo/FediverseSection.vue @@ -4,30 +4,47 @@ --> <template> - <AccountPropertySection v-bind.sync="fediverse" + <AccountPropertySection v-bind.sync="value" + :readable="readable" + :on-validate="onValidate" :placeholder="t('settings', 'Your handle')" /> </template> -<script> +<script setup lang="ts"> +import type { AccountProperties } from '../../constants/AccountPropertyConstants.js' import { loadState } from '@nextcloud/initial-state' - -import AccountPropertySection from './shared/AccountPropertySection.vue' - +import { t } from '@nextcloud/l10n' +import { ref } from 'vue' import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js' -const { fediverse } = loadState('settings', 'personalInfoParameters', {}) - -export default { - name: 'FediverseSection', - - components: { - AccountPropertySection, - }, +import AccountPropertySection from './shared/AccountPropertySection.vue' - data() { - return { - fediverse: { ...fediverse, readable: NAME_READABLE_ENUM[fediverse.name] }, - } - }, +const { fediverse } = loadState<AccountProperties>('settings', 'personalInfoParameters') + +const value = ref({ ...fediverse }) +const readable = NAME_READABLE_ENUM[fediverse.name] + +/** + * Validate a fediverse handle + * @param text The potential fediverse handle + */ +function onValidate(text: string): boolean { + // allow to clear the value + if (text === '') { + return true + } + + // check its in valid format + const result = text.match(/^@?([^@/]+)@([^@/]+)$/) + if (result === null) { + return false + } + + // check its a valid URL + try { + return URL.parse(`https://${result[2]}/`) !== null + } catch { + return false + } } </script> diff --git a/apps/settings/src/components/PersonalInfo/FirstDayOfWeekSection.vue b/apps/settings/src/components/PersonalInfo/FirstDayOfWeekSection.vue new file mode 100644 index 00000000000..98501db7ccc --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/FirstDayOfWeekSection.vue @@ -0,0 +1,126 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later + --> + +<template> + <section class="fdow-section"> + <HeaderBar :input-id="inputId" + :readable="propertyReadable" /> + + <NcSelect :aria-label-listbox="t('settings', 'Day to use as the first day of week')" + class="fdow-section__day-select" + :clearable="false" + :input-id="inputId" + label="label" + label-outside + :options="dayOptions" + :value="valueOption" + @option:selected="updateFirstDayOfWeek" /> + </section> +</template> + +<script lang="ts"> +import HeaderBar from './shared/HeaderBar.vue' +import NcSelect from '@nextcloud/vue/components/NcSelect' +import { + ACCOUNT_SETTING_PROPERTY_ENUM, + ACCOUNT_SETTING_PROPERTY_READABLE_ENUM, +} from '../../constants/AccountPropertyConstants' +import { getDayNames, getFirstDay } from '@nextcloud/l10n' +import { savePrimaryAccountProperty } from '../../service/PersonalInfo/PersonalInfoService' +import { handleError } from '../../utils/handlers.ts' +import { loadState } from '@nextcloud/initial-state' + +interface DayOption { + value: number, + label: string, +} + +const { firstDayOfWeek } = loadState<{firstDayOfWeek?: string}>( + 'settings', + 'personalInfoParameters', + {}, +) + +export default { + name: 'FirstDayOfWeekSection', + components: { + HeaderBar, + NcSelect, + }, + data() { + let firstDay = -1 + if (firstDayOfWeek) { + firstDay = parseInt(firstDayOfWeek) + } + + return { + firstDay, + } + }, + computed: { + inputId(): string { + return 'account-property-fdow' + }, + propertyReadable(): string { + return ACCOUNT_SETTING_PROPERTY_READABLE_ENUM.FIRST_DAY_OF_WEEK + }, + dayOptions(): DayOption[] { + const options = [{ + value: -1, + label: t('settings', 'Derived from your locale ({weekDayName})', { + weekDayName: getDayNames()[getFirstDay()], + }), + }] + for (const [index, dayName] of getDayNames().entries()) { + options.push({ value: index, label: dayName }) + } + return options + }, + valueOption(): DayOption | undefined { + return this.dayOptions.find((option) => option.value === this.firstDay) + }, + }, + methods: { + async updateFirstDayOfWeek(option: DayOption): Promise<void> { + try { + const responseData = await savePrimaryAccountProperty( + ACCOUNT_SETTING_PROPERTY_ENUM.FIRST_DAY_OF_WEEK, + option.value.toString(), + ) + this.handleResponse({ + value: option.value, + status: responseData.ocs?.meta?.status, + }) + window.location.reload() + } catch (e) { + this.handleResponse({ + errorMessage: t('settings', 'Unable to update first day of week'), + error: e, + }) + } + }, + + handleResponse({ value, status, errorMessage, error }): void { + if (status === 'ok') { + this.firstDay = value + } else { + this.$emit('update:value', this.firstDay) + handleError(error, errorMessage) + } + }, + }, +} +</script> + +<style lang="scss" scoped> +.fdow-section { + padding: 10px; + + &__day-select { + width: 100%; + margin-top: 6px; // align with other inputs + } +} +</style> diff --git a/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue b/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue index 3e5b9b67bf5..8f42b2771c0 100644 --- a/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue +++ b/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue @@ -29,7 +29,7 @@ import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/Person import { validateLanguage } from '../../../utils/validate.js' import { handleError } from '../../../utils/handlers.ts' -import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js' +import NcSelect from '@nextcloud/vue/components/NcSelect' export default { name: 'Language', diff --git a/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue b/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue index 311aa697adb..73300756472 100644 --- a/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue +++ b/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue @@ -32,7 +32,7 @@ <script> import moment from '@nextcloud/moment' -import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js' +import NcSelect from '@nextcloud/vue/components/NcSelect' import MapClock from 'vue-material-design-icons/MapClock.vue' import { ACCOUNT_SETTING_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants.js' diff --git a/apps/settings/src/components/PersonalInfo/PhoneSection.vue b/apps/settings/src/components/PersonalInfo/PhoneSection.vue index 13ac7fdca0f..8ddeada960e 100644 --- a/apps/settings/src/components/PersonalInfo/PhoneSection.vue +++ b/apps/settings/src/components/PersonalInfo/PhoneSection.vue @@ -39,6 +39,10 @@ export default { methods: { onValidate(value) { + if (value === '') { + return true + } + if (defaultPhoneRegion) { return isValidPhoneNumber(value, defaultPhoneRegion) } diff --git a/apps/settings/src/components/PersonalInfo/ProfileSection/EditProfileAnchorLink.vue b/apps/settings/src/components/PersonalInfo/ProfileSection/EditProfileAnchorLink.vue index 4514f11f6d8..3deb5340751 100644 --- a/apps/settings/src/components/PersonalInfo/ProfileSection/EditProfileAnchorLink.vue +++ b/apps/settings/src/components/PersonalInfo/ProfileSection/EditProfileAnchorLink.vue @@ -66,7 +66,7 @@ a { display: inline-block; vertical-align: middle; margin-top: 6px; - margin-right: 8px; + margin-inline-end: 8px; } &:hover, diff --git a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue index b9d8b1276eb..6eb7cf8c34c 100644 --- a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue +++ b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue @@ -19,7 +19,7 @@ import { emit } from '@nextcloud/event-bus' import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService.js' import { ACCOUNT_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants.js' -import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' import { handleError } from '../../../utils/handlers.ts' export default { diff --git a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfilePreviewCard.vue b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfilePreviewCard.vue index 317544ecbc3..47894f64f34 100644 --- a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfilePreviewCard.vue +++ b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfilePreviewCard.vue @@ -27,7 +27,7 @@ import { getCurrentUser } from '@nextcloud/auth' import { generateUrl } from '@nextcloud/router' -import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' +import NcAvatar from '@nextcloud/vue/components/NcAvatar' export default { name: 'ProfilePreviewCard', @@ -104,7 +104,7 @@ export default { box-shadow: 0 0 3px var(--color-box-shadow); & *, - &::v-deep * { + &:deep(*) { cursor: default; } } @@ -113,7 +113,7 @@ export default { // Override Avatar component position to fix positioning on rerender position: absolute !important; top: 40px; - left: 18px; + inset-inline-start: 18px; z-index: 1; &:not(.avatardiv--unknown) { @@ -128,7 +128,7 @@ export default { span { position: absolute; - left: 78px; + inset-inline-start: 78px; overflow: hidden; text-overflow: ellipsis; overflow-wrap: anywhere; @@ -151,7 +151,8 @@ export default { color: var(--color-primary-element-text); font-size: 18px; font-weight: bold; - margin: 0 4px 8px 0; + margin-block: 0 8px; + margin-inline: 0 4px; } } @@ -163,7 +164,8 @@ export default { color: var(--color-text-maxcontrast); font-size: 14px; font-weight: normal; - margin: 4px 4px 0 0; + margin-block: 4px 0; + margin-inline: 0 4px; line-height: 1.3; } } diff --git a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileSection.vue b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileSection.vue index f5f71eed075..22c03f72697 100644 --- a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileSection.vue +++ b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileSection.vue @@ -82,7 +82,7 @@ export default { section { padding: 10px 10px; - &::v-deep button:disabled { + &:deep(button:disabled) { cursor: default; } } diff --git a/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/ProfileVisibilitySection.vue b/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/ProfileVisibilitySection.vue index a780883eade..8acec883842 100644 --- a/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/ProfileVisibilitySection.vue +++ b/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/ProfileVisibilitySection.vue @@ -118,7 +118,7 @@ section { pointer-events: none; & *, - &::v-deep * { + &:deep(*) { cursor: default; pointer-events: none; } diff --git a/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/VisibilityDropdown.vue b/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/VisibilityDropdown.vue index 51c0203eb7f..aaa13e63e92 100644 --- a/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/VisibilityDropdown.vue +++ b/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/VisibilityDropdown.vue @@ -23,7 +23,7 @@ import { loadState } from '@nextcloud/initial-state' import { subscribe, unsubscribe } from '@nextcloud/event-bus' -import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js' +import NcSelect from '@nextcloud/vue/components/NcSelect' import { saveProfileParameterVisibility } from '../../../service/ProfileService.js' import { VISIBILITY_PROPERTY_ENUM } from '../../../constants/ProfileConstants.js' @@ -142,7 +142,7 @@ export default { pointer-events: none; & *, - &::v-deep * { + &:deep(*) { cursor: default; pointer-events: none; } diff --git a/apps/settings/src/components/PersonalInfo/PronounsSection.vue b/apps/settings/src/components/PersonalInfo/PronounsSection.vue new file mode 100644 index 00000000000..e345cb8e225 --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/PronounsSection.vue @@ -0,0 +1,47 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <AccountPropertySection v-bind.sync="pronouns" + :placeholder="randomPronounsPlaceholder" /> +</template> + +<script lang="ts"> +import type { IAccountProperty } from '../../constants/AccountPropertyConstants.ts' + +import { loadState } from '@nextcloud/initial-state' +import { t } from '@nextcloud/l10n' +import { defineComponent } from 'vue' +import AccountPropertySection from './shared/AccountPropertySection.vue' +import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.ts' + +const { pronouns } = loadState<{ pronouns: IAccountProperty }>('settings', 'personalInfoParameters') + +export default defineComponent({ + name: 'PronounsSection', + + components: { + AccountPropertySection, + }, + + data() { + return { + pronouns: { ...pronouns, readable: NAME_READABLE_ENUM[pronouns.name] }, + } + }, + + computed: { + randomPronounsPlaceholder() { + const pronouns = [ + t('settings', 'she/her'), + t('settings', 'he/him'), + t('settings', 'they/them'), + ] + const pronounsExample = pronouns[Math.floor(Math.random() * pronouns.length)] + return t('settings', 'Your pronouns. E.g. {pronounsExample}', { pronounsExample }) + }, + }, +}) +</script> diff --git a/apps/settings/src/components/PersonalInfo/TwitterSection.vue b/apps/settings/src/components/PersonalInfo/TwitterSection.vue index bb809c8d2b7..43d08f81e3f 100644 --- a/apps/settings/src/components/PersonalInfo/TwitterSection.vue +++ b/apps/settings/src/components/PersonalInfo/TwitterSection.vue @@ -4,30 +4,31 @@ --> <template> - <AccountPropertySection v-bind.sync="twitter" + <AccountPropertySection v-bind.sync="value" + :readable="readable" + :on-validate="onValidate" :placeholder="t('settings', 'Your X (formerly Twitter) handle')" /> </template> -<script> -import { loadState } from '@nextcloud/initial-state' +<script setup lang="ts"> +import type { AccountProperties } from '../../constants/AccountPropertyConstants.js' +import { loadState } from '@nextcloud/initial-state' +import { t } from '@nextcloud/l10n' +import { ref } from 'vue' +import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.ts' import AccountPropertySection from './shared/AccountPropertySection.vue' -import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js' - -const { twitter } = loadState('settings', 'personalInfoParameters', {}) - -export default { - name: 'TwitterSection', +const { twitter } = loadState<AccountProperties>('settings', 'personalInfoParameters') - components: { - AccountPropertySection, - }, +const value = ref({ ...twitter }) +const readable = NAME_READABLE_ENUM[twitter.name] - data() { - return { - twitter: { ...twitter, readable: NAME_READABLE_ENUM[twitter.name] }, - } - }, +/** + * Validate that the text might be a twitter handle + * @param text The potential twitter handle + */ +function onValidate(text: string): boolean { + return text === '' || text.match(/^@?([a-zA-Z0-9_]{2,15})$/) !== null } </script> diff --git a/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue b/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue index fc7fe7f7984..d039641ec72 100644 --- a/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue +++ b/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue @@ -46,8 +46,8 @@ <script> import debounce from 'debounce' -import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js' -import NcTextArea from '@nextcloud/vue/dist/Components/NcTextArea.js' +import NcInputField from '@nextcloud/vue/components/NcInputField' +import NcTextArea from '@nextcloud/vue/components/NcTextArea' import HeaderBar from './HeaderBar.vue' @@ -155,6 +155,7 @@ export default { methods: { async updateProperty(value) { try { + this.hasError = false const responseData = await savePrimaryAccountProperty( this.name, value, @@ -180,10 +181,8 @@ export default { this.isSuccess = true setTimeout(() => { this.isSuccess = false }, 2000) } else { - this.$emit('update:value', this.initialValue) handleError(error, errorMessage) this.hasError = true - setTimeout(() => { this.hasError = false }, 2000) } }, }, @@ -207,7 +206,7 @@ section { display: flex; gap: 0 2px; - margin-right: 5px; + margin-inline-end: 5px; margin-bottom: 5px; } } @@ -218,7 +217,7 @@ section { align-items: center; &__icon { - margin-right: 8px; + margin-inline-end: 8px; align-self: start; margin-top: 4px; } diff --git a/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue b/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue index dd4eb2b5181..e55a50056d3 100644 --- a/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue +++ b/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue @@ -30,9 +30,9 @@ </template> <script> -import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' -import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' -import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' +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 { @@ -146,7 +146,7 @@ export default { } // TODO: provide focus method from NcActions - this.$refs.federationActions.$refs.menuButton.$el.focus() + this.$refs.federationActions.$refs?.triggerButton?.$el?.focus?.() }, async updatePrimaryScope(scope) { diff --git a/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue index ca41db2a454..7c95c2b8f4c 100644 --- a/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue +++ b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue @@ -5,7 +5,7 @@ <template> <div class="headerbar-label" :class="{ 'setting-property': isSettingProperty, 'profile-property': isProfileProperty }"> - <h3 v-if="isHeading"> + <h3 v-if="isHeading" class="headerbar__heading"> <!-- Already translated as required by prop validator --> {{ readable }} </h3> @@ -36,7 +36,7 @@ </template> <script> -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcButton from '@nextcloud/vue/components/NcButton' import Plus from 'vue-material-design-icons/Plus.vue' import FederationControl from './FederationControl.vue' @@ -130,7 +130,7 @@ export default { } &.setting-property { - height: 44px; + height: 34px; } label { @@ -138,11 +138,16 @@ export default { } } + .headerbar__heading { + margin: 0; + } + .federation-control { margin: 0; } .button-vue { - margin: 0 0 0 auto !important; + margin: 0 !important; + margin-inline-start: auto !important; } </style> |