edit: separate info and password forms

This commit is contained in:
axolotle 2023-11-25 16:30:49 +01:00
parent f8b33f803e
commit 8e177e6907
4 changed files with 248 additions and 219 deletions

113
components/UserInfoForm.vue Normal file
View file

@ -0,0 +1,113 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/yup'
import * as yup from 'yup'
import { pick } from '@/utils/common'
import type { User } from '@/composables/states'
import type { Feedback } from '@/composables/form'
const { t } = useI18n()
const user = await useUser()
const loading: Ref<boolean | null> = ref(false)
const feedback: Ref<Feedback> = ref(null)
const { handleSubmit, setFieldError, resetForm, meta } = useForm({
validationSchema: toTypedSchema(
yup.object({
fullname: yup.string().required().min(2),
mailalias: yup.array().of(yup.string().email().required()).required(),
mailforward: yup.array().of(yup.string().email().required()).required(),
}),
),
initialValues: {
...pick(user.value, 'fullname', 'mailalias', 'mailforward'),
},
})
watch(
() => meta.value.dirty,
(value) => {
// remove loading and feedback on edition
if (value) {
loading.value = null
feedback.value = null
}
},
)
const onSubmit = handleSubmit(async (form) => {
loading.value = true
const { error, data } = await useApi<
Pick<User, 'fullname' | 'mailalias' | 'mailforward'>
>('/update', {
method: 'PUT',
body: form,
})
if (error.value) {
// Reset form dirty state but keep previous values
resetForm({ values: form })
const errData = error.value.data
let message
if (errData.path) {
setFieldError(errData.path, errData.error)
message = t('form_has_errors')
} else {
message = errData.error || errData
}
feedback.value = {
variant: 'error',
icon: 'alert',
message,
}
} else if (data.value) {
Object.assign(user.value, data)
resetForm({
values: data.value,
})
feedback.value = {
variant: 'success',
icon: 'thumb-up',
message: t('user_profile_updated'),
}
}
loading.value = false
})
</script>
<template>
<YForm :loading="loading" :feedback="feedback" @submit.prevent="onSubmit">
<FormField name="fullname" :label="$t('fullname')" class="mb-10">
<TextInput
name="fullname"
type="text"
:placeholder="$t('fullname')"
autocomplete="name"
class="w-full"
/>
</FormField>
<TextInputList
name="mailalias"
type="email"
:label="$t('mail_addresses')"
:input-label="$t('mail_address')"
:button-label="$t('add_mail')"
:placeholder="$t('new_mail')"
class="mb-10"
/>
<TextInputList
name="mailforward"
type="email"
:label="$t('mail_forwards')"
:input-label="$t('mail_forward')"
:button-label="$t('add_forward')"
:placeholder="$t('new_forward')"
/>
</YForm>
</template>

View file

@ -0,0 +1,119 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/yup'
import * as yup from 'yup'
import { exclude } from '@/utils/common'
import type { Feedback } from '@/composables/form'
const { t } = useI18n()
const loading: Ref<boolean | null> = ref(false)
const feedback: Ref<Feedback> = ref(null)
const { handleSubmit, setFieldError, resetForm, meta } = useForm({
validationSchema: toTypedSchema(
yup.object({
currentpassword: yup.string().required(),
newpassword: yup
.string()
.matches(/.{8,}/, {
excludeEmptyString: true,
message: { key: 'v.string_too_short', values: { min: 8 } },
})
.required(),
confirmpassword: yup
.string()
.oneOf([yup.ref('newpassword')], 'v.password_not_match')
.required(),
}),
),
initialValues: {
currentpassword: '',
newpassword: '',
confirmpassword: '',
},
})
watch(
() => meta.value.dirty,
(value) => {
// remove loading and feedback on edition
if (value) {
loading.value = null
feedback.value = null
}
},
)
const onSubmit = handleSubmit(async (form) => {
loading.value = true
const { error, data } = await useApi('/update', {
method: 'PUT',
body: exclude(form, 'confirmpassword'),
})
if (error.value) {
// Reset form dirty state and remove previous entries
resetForm({
values: { currentpassword: '', newpassword: '', confirmpassword: '' },
})
const errData = error.value.data
let message
if (errData.path) {
setFieldError(errData.path, errData.error)
message = t('form_has_errors')
} else {
message = errData.error || errData
}
feedback.value = {
variant: 'error',
icon: 'alert',
message,
}
} else if (data.value) {
// reset loggedin state and redirect to login
// FIXME toast ok message
useIsLoggedIn().value = false
return navigateTo('/login')
}
loading.value = false
})
</script>
<template>
<YForm :loading="loading" :feedback="feedback" @submit.prevent="onSubmit">
<FormField
name="currentpassword"
:label="$t('current_password')"
class="mb-3"
>
<TextInput
name="currentpassword"
type="password"
autocomplete="current-password"
class="w-full"
/>
</FormField>
<FormField
name="newpassword"
:label="$t('new_password')"
:description="$t('good_practices_about_user_password')"
class="mb-3"
>
<TextInput
name="newpassword"
type="password"
autocomplete="new-password"
class="w-full"
/>
</FormField>
<FormField name="confirmpassword" :label="$t('confirm_new_password')">
<TextInput name="confirmpassword" type="password" class="w-full" />
</FormField>
</YForm>
</template>

View file

@ -47,6 +47,7 @@
"footer": "Skip to footer" "footer": "Skip to footer"
}, },
"theme": "Theme", "theme": "Theme",
"user_profile_updated": "User personal information updated with success!",
"username": "Username", "username": "Username",
"visitor": "Visitor", "visitor": "Visitor",
"v": { "v": {

View file

@ -1,19 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/yup'
import * as yup from 'yup'
import { pick, exclude } from '@/utils/common'
import type { User } from '@/composables/states'
const { t, locale, locales } = useI18n() const { t, locale, locales } = useI18n()
useHead({ useHead({
title: t('footerlink_edit'), title: t('footerlink_edit'),
}) })
const user = await useUser()
// Browser
const localesAsOptions = computed(() => { const localesAsOptions = computed(() => {
return locales.value.map((locale) => ({ return locales.value.map((locale) => ({
text: locale.name, text: locale.name,
@ -60,227 +51,32 @@ const themesAsOptions = [
text: theme.charAt(0).toUpperCase() + theme.slice(1), text: theme.charAt(0).toUpperCase() + theme.slice(1),
value: theme, value: theme,
})) }))
// Server
const loading: Ref<boolean | null> = ref(false)
const feedback: Ref<{
variant: 'success' | 'warning' | 'error'
icon: string
message: string
} | null> = ref(null)
const { handleSubmit, setFieldError, resetForm, meta } = useForm({
validationSchema: toTypedSchema(
yup.object({
// username: yup.string().required(),
fullname: yup.string().required().min(2),
currentpassword: yup
.string()
.when('newpassword', ([newpassword], schema) => {
return newpassword ? schema.required() : schema
}),
newpassword: yup.string().matches(/.{8,}/, {
excludeEmptyString: true,
message: { key: 'v.string_too_short', values: { min: 8 } },
}),
confirmpassword: yup
.string()
.when('newpassword', ([newpassword], schema) => {
return newpassword
? schema.oneOf([yup.ref('newpassword')], 'v.password_not_match')
: schema
}),
mailalias: yup.array().of(yup.string().email().required()).required(),
mailforward: yup.array().of(yup.string().email().required()).required(),
}),
),
initialValues: {
currentpassword: '',
newpassword: '',
confirmpassword: '',
...pick(user.value, 'fullname', 'mailalias', 'mailforward'),
},
})
watch(
() => meta.value.dirty,
(value) => {
// remove loading and feedback on edition
if (value) {
loading.value = null
feedback.value = null
}
},
)
const onSubmit = handleSubmit(async (form) => {
loading.value = true
const { error, data } = await useApi<
Pick<User, 'fullname' | 'mailalias' | 'mailforward'>
>('/update', {
method: 'PUT',
body: exclude(form, 'confirmpassword'),
})
if (error.value) {
// Reset form dirty state but keep previous values
resetForm({ values: form })
const errData = error.value.data
let message
if (errData.path) {
setFieldError(errData.path, errData.error)
message = t('form_has_errors')
} else {
message = errData.error || errData
}
feedback.value = {
variant: 'error',
icon: 'alert',
message,
}
} else if (data.value) {
// redirect on password change
if (form.newpassword) {
useIsLoggedIn().value = false
return navigateTo('/login')
}
Object.assign(user.value, data)
resetForm({
values: {
...data.value,
currentpassword: '',
newpassword: '',
confirmpassword: '',
},
})
feedback.value = {
variant: 'success',
icon: 'thumb-up',
message: t('user_profile_updated'),
}
}
loading.value = false
})
</script> </script>
<template> <template>
<div> <div>
<PageTitle :text="$t('footerlink_edit')" /> <PageTitle :text="$t('footerlink_edit')" />
<section> <div class="lg:flex lg:justify-between">
<h2 class="text-3xl">{{ t('edit_personal_settings') }}</h2> <section
class="lg:w-1/2 lg:me-20 h-full card card-body border border-neutral my-10"
>
<h2 class="text-3xl mb-3">{{ t('edit_personal_settings') }}</h2>
<form novalidate class="my-10" @submit="onSubmit"> <UserInfoForm />
<div class="lg:flex lg:justify-between"> </section>
<div class="lg:w-1/2 lg:me-20">
<!-- <FormField name="username" :label="$t('username')" class="mb-3">
<TextInput
name="username"
type="text"
:placeholder="$t('username')"
disabled
class="w-full"
/>
</FormField> -->
<FormField name="fullname" :label="$t('fullname')" class="mb-10"> <section class="lg:w-1/2 card card-body border border-neutral my-10">
<TextInput <h2 class="text-3xl mb-3">{{ $t('change_password') }}</h2>
name="fullname"
type="text"
:placeholder="$t('fullname')"
autocomplete="name"
class="w-full"
/>
</FormField>
<TextInputList <UserPasswordForm />
name="mailalias" </section>
type="email" </div>
:label="$t('mail_addresses')"
:input-label="$t('mail_address')"
:button-label="$t('add_mail')"
:placeholder="$t('new_mail')"
class="mb-10"
/>
<TextInputList <section class="card card-body border border-neutral my-10">
name="mailforward" <h2 class="text-3xl mb-3">{{ t('edit_browser_settings') }}</h2>
type="email"
:label="$t('mail_forwards')"
:input-label="$t('mail_forward')"
:button-label="$t('add_forward')"
:placeholder="$t('new_forward')"
/>
</div>
<fieldset class="basis-1/2 mt-10 lg:mt-0"> <form novalidate @submit.prevent>
<legend class="text-xl mb-3">{{ $t('change_password') }}</legend>
<FormField
name="currentpassword"
:label="$t('current_password')"
class="mb-3"
>
<TextInput
name="currentpassword"
type="password"
autocomplete="current-password"
class="w-full"
/>
</FormField>
<FormField
name="newpassword"
:label="$t('new_password')"
:description="$t('good_practices_about_user_password')"
class="mb-3"
>
<TextInput
name="newpassword"
type="password"
autocomplete="new-password"
class="w-full"
/>
</FormField>
<FormField
name="confirmpassword"
:label="$t('confirm_new_password')"
>
<TextInput
name="confirmpassword"
type="password"
class="w-full"
/>
</FormField>
</fieldset>
</div>
<!-- Success + generic error announcement -->
<BaseAlert v-show="feedback" v-bind="feedback" class="mb-10" />
<!-- SR "loading" announcement -->
<BaseAlert
:message="loading ? $t('api.processing') : ''"
class="sr-only"
assertive
/>
<div class="flex mt-10">
<NuxtLink to="/" class="btn ms-auto me-2">
{{ $t('cancel') }}
</NuxtLink>
<SubmitButton :loading="loading" variant="success" />
</div>
</form>
</section>
<section class="my-10">
<h2 class="text-4xl font-bold">{{ t('edit_browser_settings') }}</h2>
<form novalidate class="my-10" @submit.prevent>
<div role="group" class="flex align mb-3"> <div role="group" class="flex align mb-3">
<!-- eslint-disable-next-line vuejs-accessibility/label-has-for --> <!-- eslint-disable-next-line vuejs-accessibility/label-has-for -->
<label for="language" class="label me-3">{{ t('language') }}</label> <label for="language" class="label me-3">{{ t('language') }}</label>