refactor(frontend): migrate user settings to FormField component

Migrate ApiTokens, Caldav, DataExport, Deletion, EmailUpdate,
PasswordUpdate, TOTP, and DataExportDownload views to use the
new FormField component.
This commit is contained in:
kolaente 2026-01-08 16:05:47 +01:00
parent 0c23714a79
commit 908c241ec7
8 changed files with 134 additions and 259 deletions

View File

@ -3,32 +3,17 @@
<h1>{{ $t('user.export.downloadTitle') }}</h1>
<template v-if="isLocalUser">
<p>{{ $t('user.export.descriptionPasswordRequired') }}</p>
<div class="field">
<label
class="label"
for="currentPasswordDataExport"
>
{{ $t('user.settings.currentPassword') }}
</label>
<div class="control">
<input
id="currentPasswordDataExport"
ref="passwordInput"
v-model="password"
class="input"
:class="{'is-danger': errPasswordRequired}"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
@keyup="() => errPasswordRequired = password === ''"
>
</div>
<p
v-if="errPasswordRequired"
class="help is-danger"
>
{{ $t('user.deletion.passwordRequired') }}
</p>
</div>
<FormField
id="currentPasswordDataExport"
ref="passwordInput"
v-model="password"
:label="$t('user.settings.currentPassword')"
:class="{'is-danger': errPasswordRequired}"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
:error="errPasswordRequired ? $t('user.deletion.passwordRequired') : null"
@keyup="() => errPasswordRequired = password === ''"
/>
</template>
<XButton
@ -52,6 +37,7 @@
<script setup lang="ts">
import {ref, computed, reactive} from 'vue'
import DataExportService from '@/services/dataExport'
import FormField from '@/components/input/FormField.vue'
import {useAuthStore} from '@/stores/auth'
const dataExportService = reactive(new DataExportService())

View File

@ -12,6 +12,7 @@ import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import {useI18n} from 'vue-i18n'
import Message from '@/components/misc/Message.vue'
import FormField from '@/components/input/FormField.vue'
import type {IApiToken} from '@/modelTypes/IApiToken'
const service = new ApiTokenService()
@ -232,31 +233,18 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
@submit.prevent="createToken"
>
<!-- Title -->
<div class="field">
<label
class="label"
for="apiTokenTitle"
>{{ $t('user.settings.apiTokens.attributes.title') }}</label>
<div class="control">
<input
id="apiTokenTitle"
ref="apiTokenTitle"
v-model="newToken.title"
v-focus
class="input"
type="text"
:placeholder="$t('user.settings.apiTokens.attributes.titlePlaceholder')"
@keyup="() => newTokenTitleValid = newToken.title !== ''"
@focusout="() => newTokenTitleValid = newToken.title !== ''"
>
</div>
<p
v-if="!newTokenTitleValid"
class="help is-danger"
>
{{ $t('user.settings.apiTokens.titleRequired') }}
</p>
</div>
<FormField
id="apiTokenTitle"
ref="apiTokenTitle"
v-model="newToken.title"
v-focus
:label="$t('user.settings.apiTokens.attributes.title')"
type="text"
:placeholder="$t('user.settings.apiTokens.attributes.titlePlaceholder')"
:error="newTokenTitleValid ? null : $t('user.settings.apiTokens.titleRequired')"
@keyup="() => newTokenTitleValid = newToken.title !== ''"
@focusout="() => newTokenTitleValid = newToken.title !== ''"
/>
<!-- Expiry -->
<div class="field">

View File

@ -6,24 +6,20 @@
<p>
{{ $t('user.settings.caldav.howTo') }}
</p>
<div class="field has-addons no-input-mobile">
<div class="control is-expanded">
<input
v-model="caldavUrl"
type="text"
class="input"
readonly
>
</div>
<div class="control">
<FormField
v-model="caldavUrl"
type="text"
readonly
>
<template #addon>
<XButton
v-tooltip="$t('misc.copy')"
:shadow="false"
icon="paste"
@click="copy(caldavUrl)"
/>
</div>
</div>
</template>
</FormField>
<h5 class="mbs-5 mbe-4 has-text-weight-bold">
{{ $t('user.settings.caldav.tokens') }}
@ -108,6 +104,7 @@ import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {success} from '@/message'
import BaseButton from '@/components/base/BaseButton.vue'
import Message from '@/components/misc/Message.vue'
import FormField from '@/components/input/FormField.vue'
import CaldavTokenService from '@/services/caldavToken'
import { formatDateShort } from '@/helpers/time/formatDate'
import type {ICaldavToken} from '@/modelTypes/ICaldavToken'

View File

@ -32,32 +32,17 @@
<p>
{{ $t('user.export.descriptionPasswordRequired') }}
</p>
<div class="field">
<label
class="label"
for="currentPasswordDataExport"
>
{{ $t('user.settings.currentPassword') }}
</label>
<div class="control">
<input
id="currentPasswordDataExport"
ref="passwordInput"
v-model="password"
class="input"
:class="{'is-danger': errPasswordRequired}"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
@keyup="() => errPasswordRequired = password === ''"
>
</div>
<p
v-if="errPasswordRequired"
class="help is-danger"
>
{{ $t('user.deletion.passwordRequired') }}
</p>
</div>
<FormField
id="currentPasswordDataExport"
ref="passwordInput"
v-model="password"
:label="$t('user.settings.currentPassword')"
:class="{'is-danger': errPasswordRequired}"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
:error="errPasswordRequired ? $t('user.deletion.passwordRequired') : null"
@keyup="() => errPasswordRequired = password === ''"
/>
</template>
<XButton
@ -81,6 +66,7 @@ import {useAuthStore} from '@/stores/auth'
import {formatISO, formatDateLong, formatDisplayDate} from '@/helpers/time/formatDate'
import Message from '@/components/misc/Message.vue'
import FormField from '@/components/input/FormField.vue'
defineOptions({name: 'UserSettingsDataExport'})

View File

@ -17,32 +17,17 @@
<p>
{{ $t('user.deletion.scheduledCancelText') }}
</p>
<div class="field">
<label
class="label"
for="currentPasswordAccountDelete"
>
{{ $t('user.settings.currentPassword') }}
</label>
<div class="control">
<input
id="currentPasswordAccountDelete"
ref="passwordInput"
v-model="password"
class="input"
:class="{'is-danger': errPasswordRequired}"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
@keyup="() => errPasswordRequired = password === ''"
>
</div>
<p
v-if="errPasswordRequired"
class="help is-danger"
>
{{ $t('user.deletion.passwordRequired') }}
</p>
</div>
<FormField
id="currentPasswordAccountDelete"
ref="passwordInput"
v-model="password"
:label="$t('user.settings.currentPassword')"
:class="{'is-danger': errPasswordRequired}"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
:error="errPasswordRequired ? $t('user.deletion.passwordRequired') : null"
@keyup="() => errPasswordRequired = password === ''"
/>
</template>
<p v-else>
{{ $t('user.deletion.scheduledCancelButton') }}
@ -68,32 +53,17 @@
<p>
{{ $t('user.deletion.text2') }}
</p>
<div class="field">
<label
class="label"
for="currentPasswordAccountDelete"
>
{{ $t('user.settings.currentPassword') }}
</label>
<div class="control">
<input
id="currentPasswordAccountDelete"
ref="passwordInput"
v-model="password"
class="input"
:class="{'is-danger': errPasswordRequired}"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
@keyup="() => errPasswordRequired = password === ''"
>
</div>
<p
v-if="errPasswordRequired"
class="help is-danger"
>
{{ $t('user.deletion.passwordRequired') }}
</p>
</div>
<FormField
id="currentPasswordAccountDelete"
ref="passwordInput"
v-model="password"
:label="$t('user.settings.currentPassword')"
:class="{'is-danger': errPasswordRequired}"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
:error="errPasswordRequired ? $t('user.deletion.passwordRequired') : null"
@keyup="() => errPasswordRequired = password === ''"
/>
</form>
<p v-else>
{{ $t('user.deletion.text3') }}
@ -121,6 +91,7 @@ import {useTitle} from '@/composables/useTitle'
import {success} from '@/message'
import {useAuthStore} from '@/stores/auth'
import {useConfigStore} from '@/stores/config'
import FormField from '@/components/input/FormField.vue'
defineOptions({name: 'UserSettingsDeletion'})

View File

@ -4,38 +4,22 @@
:title="$t('user.settings.updateEmailTitle')"
>
<form @submit.prevent="updateEmail">
<div class="field">
<label
class="label"
for="newEmail"
>{{ $t('user.settings.updateEmailNew') }}</label>
<div class="control">
<input
id="newEmail"
v-model="emailUpdate.newEmail"
class="input"
:placeholder="$t('user.auth.emailPlaceholder')"
type="email"
@keyup.enter="updateEmail"
>
</div>
</div>
<div class="field">
<label
class="label"
for="currentPasswordEmail"
>{{ $t('user.settings.currentPassword') }}</label>
<div class="control">
<input
id="currentPasswordEmail"
v-model="emailUpdate.password"
class="input"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
@keyup.enter="updateEmail"
>
</div>
</div>
<FormField
id="newEmail"
v-model="emailUpdate.newEmail"
:label="$t('user.settings.updateEmailNew')"
:placeholder="$t('user.auth.emailPlaceholder')"
type="email"
@keyup.enter="updateEmail"
/>
<FormField
id="currentPasswordEmail"
v-model="emailUpdate.password"
:label="$t('user.settings.currentPassword')"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
@keyup.enter="updateEmail"
/>
</form>
<XButton
@ -55,6 +39,7 @@ import {useI18n} from 'vue-i18n'
import EmailUpdateService from '@/services/emailUpdate'
import EmailUpdateModel from '@/models/emailUpdate'
import FormField from '@/components/input/FormField.vue'
import {success} from '@/message'
import {useTitle} from '@/composables/useTitle'
import {useAuthStore} from '@/stores/auth'

View File

@ -5,57 +5,33 @@
:loading="passwordUpdateService.loading"
>
<form @submit.prevent="updatePassword">
<div class="field">
<label
class="label"
for="newPassword"
>{{ $t('user.settings.newPassword') }}</label>
<div class="control">
<input
id="newPassword"
v-model="passwordUpdate.newPassword"
autocomplete="new-password"
class="input"
:placeholder="$t('user.auth.passwordPlaceholder')"
type="password"
@keyup.enter="updatePassword"
>
</div>
</div>
<div class="field">
<label
class="label"
for="newPasswordConfirm"
>{{ $t('user.settings.newPasswordConfirm') }}</label>
<div class="control">
<input
id="newPasswordConfirm"
v-model="passwordConfirm"
autocomplete="new-password"
class="input"
:placeholder="$t('user.auth.passwordPlaceholder')"
type="password"
@keyup.enter="updatePassword"
>
</div>
</div>
<div class="field">
<label
class="label"
for="currentPassword"
>{{ $t('user.settings.currentPassword') }}</label>
<div class="control">
<input
id="currentPassword"
v-model="passwordUpdate.oldPassword"
autocomplete="current-password"
class="input"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
@keyup.enter="updatePassword"
>
</div>
</div>
<FormField
id="newPassword"
v-model="passwordUpdate.newPassword"
:label="$t('user.settings.newPassword')"
autocomplete="new-password"
:placeholder="$t('user.auth.passwordPlaceholder')"
type="password"
@keyup.enter="updatePassword"
/>
<FormField
id="newPasswordConfirm"
v-model="passwordConfirm"
:label="$t('user.settings.newPasswordConfirm')"
autocomplete="new-password"
:placeholder="$t('user.auth.passwordPlaceholder')"
type="password"
@keyup.enter="updatePassword"
/>
<FormField
id="currentPassword"
v-model="passwordUpdate.oldPassword"
:label="$t('user.settings.currentPassword')"
autocomplete="current-password"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
@keyup.enter="updatePassword"
/>
</form>
<XButton
@ -75,6 +51,7 @@ import {useI18n} from 'vue-i18n'
import PasswordUpdateService from '@/services/passwordUpdateService'
import PasswordUpdateModel from '@/models/passwordUpdate'
import FormField from '@/components/input/FormField.vue'
import {useTitle} from '@/composables/useTitle'
import {success, error} from '@/message'

View File

@ -23,24 +23,16 @@
alt=""
>
</p>
<div class="field">
<label
class="label"
for="totpConfirmPasscode"
>{{ $t('user.settings.totp.passcode') }}</label>
<div class="control">
<input
id="totpConfirmPasscode"
v-model="totpConfirmPasscode"
autocomplete="one-time-code"
class="input"
:placeholder="$t('user.settings.totp.passcodePlaceholder')"
type="text"
inputmode="numeric"
@keyup.enter="totpConfirm"
>
</div>
</div>
<FormField
id="totpConfirmPasscode"
v-model="totpConfirmPasscode"
:label="$t('user.settings.totp.passcode')"
autocomplete="one-time-code"
:placeholder="$t('user.settings.totp.passcodePlaceholder')"
type="text"
inputmode="numeric"
@keyup.enter="totpConfirm"
/>
<XButton @click="totpConfirm">
{{ $t('misc.confirm') }}
</XButton>
@ -58,23 +50,15 @@
</XButton>
</p>
<div v-if="totpDisableForm">
<div class="field">
<label
class="label"
for="currentPassword"
>{{ $t('user.settings.totp.enterPassword') }}</label>
<div class="control">
<input
id="currentPassword"
v-model="totpDisablePassword"
v-focus
class="input"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
@keyup.enter="totpDisable"
>
</div>
</div>
<FormField
id="currentPassword"
v-model="totpDisablePassword"
v-focus
:label="$t('user.settings.totp.enterPassword')"
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
type="password"
@keyup.enter="totpDisable"
/>
<XButton
danger
@click="totpDisable"
@ -100,6 +84,7 @@ import {useI18n} from 'vue-i18n'
import TotpService from '@/services/totp'
import TotpModel from '@/models/totp'
import FormField from '@/components/input/FormField.vue'
import {success} from '@/message'