refactor: use useForm in some views

This commit is contained in:
axolotle 2024-07-08 16:16:02 +02:00
parent a22ee09344
commit 026ccb68ed
8 changed files with 465 additions and 569 deletions

View file

@ -1,12 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVuelidate } from '@vuelidate/core'
import { reactive, ref } from 'vue' import { reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter, type LocationQueryValue } from 'vue-router' import { useRouter, type LocationQueryValue } from 'vue-router'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { useForm } from '@/composables/form'
import { alphalownumdot_, minLength, required } from '@/helpers/validators' import { alphalownumdot_, minLength, required } from '@/helpers/validators'
import { useStoreGetters } from '@/store/utils' import { useStoreGetters } from '@/store/utils'
import type { FieldProps, FormFieldDict } from '@/types/form'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -22,41 +23,40 @@ const store = useStore()
const router = useRouter() const router = useRouter()
const { installed } = useStoreGetters() const { installed } = useStoreGetters()
const serverError = ref('')
const form = reactive({ type Form = typeof form.value
const form = ref({
username: '', username: '',
password: '', password: '',
}) })
const v$ = useVuelidate( const fields = reactive({
{
username: { required, alphalownumdot_ },
password: { required, passwordLenght: minLength(4) },
},
form,
)
console.log(v$.value)
const fields = {
username: { username: {
component: 'InputItem',
label: t('user_username'), label: t('user_username'),
rules: { required, alphalownumdot_ },
props: { props: {
id: 'username', id: 'username',
autocomplete: 'username', autocomplete: 'username',
}, },
}, } satisfies FieldProps<'InputItem', Form['username']>,
password: { password: {
component: 'InputItem',
label: t('password'), label: t('password'),
rules: { required, passwordLenght: minLength(4) },
props: { props: {
id: 'password', id: 'password',
type: 'password', type: 'password',
autocomplete: 'current-password', autocomplete: 'current-password',
}, },
}, } satisfies FieldProps<'InputItem', Form['password']>,
} } satisfies FormFieldDict<Form>)
function login() { const { v, onSubmit } = useForm(form, fields)
const credentials = [form.username, form.password].join(':')
const onLogin = onSubmit((onError) => {
const { username, password } = form.value
const credentials = [username, password].join(':')
store store
.dispatch('LOGIN', credentials) .dispatch('LOGIN', credentials)
.then(() => { .then(() => {
@ -70,41 +70,27 @@ function login() {
) )
} }
}) })
.catch((err) => { .catch((err) => onError(err, t('wrong_password_or_username')))
if (err.name !== 'APIUnauthorizedError') throw err })
serverError.value = t('wrong_password_or_username')
})
}
</script> </script>
<template> <template>
<CardForm <CardForm
:title="t('login')" id="login-form"
v-model="form"
:fields="fields"
icon="lock" icon="lock"
:validation="v$" :title="t('login')"
:server-error="serverError" :validations="v"
@submit.prevent="login" @submit="onLogin"
> >
<!-- ADMIN USERNAME -->
<FormField
v-bind="fields.username"
v-model="form.username"
:validation="v$.username"
/>
<!-- ADMIN PASSWORD -->
<FormField
v-bind="fields.password"
v-model="form.password"
:validation="v$.password"
/>
<template #buttons> <template #buttons>
<!-- FIXME should we remove the disabled state? -->
<BButton <BButton
type="submit" type="submit"
variant="success" variant="success"
:disabled="!installed" :disabled="!installed"
form="ynh-form" form="login-form"
> >
{{ t('login') }} {{ t('login') }}
</BButton> </BButton>

View file

@ -1,10 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVuelidate } from '@vuelidate/core'
import { computed, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import api from '@/api' import api from '@/api'
import { APIBadRequestError } from '@/api/errors'
import { useForm } from '@/composables/form'
import { useAutoModal } from '@/composables/useAutoModal' import { useAutoModal } from '@/composables/useAutoModal'
import { asUnreffed } from '@/helpers/commons'
import { import {
alphalownumdot_, alphalownumdot_,
minLength, minLength,
@ -13,92 +15,99 @@ import {
sameAs, sameAs,
} from '@/helpers/validators' } from '@/helpers/validators'
import { formatFormData } from '@/helpers/yunohostArguments' import { formatFormData } from '@/helpers/yunohostArguments'
import type { FieldProps, FormFieldDict } from '@/types/form'
import LoginView from '@/views/LoginView.vue' import LoginView from '@/views/LoginView.vue'
import { DomainForm } from '@/views/_partials' import { DomainForm } from '@/views/_partials'
const { t } = useI18n() const { t } = useI18n()
const modalConfirm = useAutoModal() const modalConfirm = useAutoModal()
const step = ref('start') type Steps = 'start' | 'domain' | 'user' | 'rootfsspace-error' | 'login'
const step = ref<Steps>('start')
const serverError = ref('') const serverError = ref('')
const domain = ref(undefined) const domain = ref('')
const dyndns_recovery_password = ref('') const dyndns_recovery_password = ref('')
const form = reactive({ type Form = typeof form.value
const form = ref({
username: '', username: '',
fullname: '', fullname: '',
password: '', password: '',
confirmation: '', confirmation: '',
}) })
const rules = computed(() => ({ const fields = reactive({
username: { required, alphalownumdot_ }, // FIXME satisfies FormFieldDict but not for CardForm?
fullname: { required, name }, alert: {
password: { required, passwordLenght: minLength(8) }, component: 'ReadOnlyAlertItem',
confirmation: { required, passwordMatch: sameAs(form.password) }, props: { label: t('postinstall.user.first_user_help'), type: 'info' },
})) } satisfies FieldProps<'ReadOnlyAlertItem'>,
const v$ = useVuelidate(rules, form)
const fields = {
username: { username: {
component: 'InputItem',
label: t('user_username'), label: t('user_username'),
props: { rules: { required, alphalownumdot_ },
id: 'username', props: { id: 'username', placeholder: t('placeholder.username') },
placeholder: t('placeholder.username'), } satisfies FieldProps<'InputItem', Form['username']>,
},
},
fullname: { fullname: {
component: 'InputItem',
label: t('user_fullname'), label: t('user_fullname'),
props: { rules: { required, name },
id: 'fullname', props: { id: 'fullname', placeholder: t('placeholder.fullname') },
placeholder: t('placeholder.fullname'), } satisfies FieldProps<'InputItem', Form['fullname']>,
},
},
password: { password: {
component: 'InputItem',
label: t('password'), label: t('password'),
description: t('good_practices_about_admin_password'), description: t('good_practices_about_admin_password'),
descriptionVariant: 'warning', descriptionVariant: 'warning',
rules: { required, passwordLenght: minLength(8) },
props: { id: 'password', placeholder: '••••••••', type: 'password' }, props: { id: 'password', placeholder: '••••••••', type: 'password' },
}, } satisfies FieldProps<'InputItem', Form['password']>,
confirmation: { confirmation: {
component: 'InputItem',
label: t('password_confirmation'), label: t('password_confirmation'),
props: { rules: asUnreffed(
id: 'confirmation', computed(() => ({
placeholder: '••••••••', required,
type: 'password', passwordMatch: sameAs(form.value.password),
}, })),
}, ),
} props: { id: 'confirmation', placeholder: '••••••••', type: 'password' },
} satisfies FieldProps<'InputItem', Form['confirmation']>,
} satisfies FormFieldDict<Form>)
function goToStep(step_) { const { v, onSubmit, serverErrors } = useForm(form, fields)
function goToStep(step_: Steps) {
serverError.value = '' serverError.value = ''
step.value = step_ step.value = step_
} }
function setDomain(data) { function setDomain(data: { domain: string; dyndns_recovery_password: string }) {
domain.value = data.domain domain.value = data.domain
dyndns_recovery_password.value = data.dyndns_recovery_password dyndns_recovery_password.value = data.dyndns_recovery_password
goToStep('user') goToStep('user')
} }
async function setUser() { const setUser = onSubmit(async () => {
const confirmed = await modalConfirm( const confirmed = await modalConfirm(
t('confirm_postinstall', { domain: domain.value }), t('confirm_postinstall', { domain: domain.value }),
) )
if (!confirmed) return if (!confirmed) return
performPostInstall() performPostInstall()
} })
async function performPostInstall(force = false) { async function performPostInstall(force = false) {
// FIXME update formatFormData to unwrap ref auto // FIXME update formatFormData to unwrap ref auto
const { username, fullname, password } = form.value
const data = await formatFormData({ const data = await formatFormData({
domain: domain.value, domain: domain.value,
dyndns_recovery_password: dyndns_recovery_password.value, dyndns_recovery_password: dyndns_recovery_password.value,
username: form.username, username,
fullname: form.fullname, fullname,
password: form.password, password,
}) })
// FIXME does the api will throw an error for bad passwords ? // FIXME does the api will throw an error for bad passwords ?
@ -111,19 +120,21 @@ async function performPostInstall(force = false) {
goToStep('login') goToStep('login')
}) })
.catch((err) => { .catch((err) => {
const hasWordsInError = (words) => const hasWordsInError = (words: string[]) =>
words.some((word) => (err.key || err.message).includes(word)) words.some((word) => (err.key || err.message).includes(word))
if (err.name !== 'APIBadRequestError') throw err if (!(err instanceof APIBadRequestError)) throw err
if (err.key === 'postinstall_low_rootfsspace') { if (err.key === 'postinstall_low_rootfsspace') {
step.value = 'rootfsspace-error' step.value = 'rootfsspace-error'
serverError.value = err.message
} else if (hasWordsInError(['domain', 'dyndns'])) { } else if (hasWordsInError(['domain', 'dyndns'])) {
step.value = 'domain' step.value = 'domain'
serverError.value = err.message
} else if (hasWordsInError(['password', 'user'])) { } else if (hasWordsInError(['password', 'user'])) {
step.value = 'user' step.value = 'user'
serverErrors.global = [err.message]
} else { } else {
throw err throw err
} }
serverError.value = err.message
}) })
} }
</script> </script>
@ -156,11 +167,11 @@ async function performPostInstall(force = false) {
@submit="setDomain" @submit="setDomain"
> >
<template #disclaimer> <template #disclaimer>
<p class="alert alert-info" v-t="'postinstall_domain'" /> <p v-t="'postinstall_domain'" class="alert alert-info" />
</template> </template>
</DomainForm> </DomainForm>
<BButton variant="primary" @click="goToStep('start')" class="mt-3"> <BButton variant="primary" class="mt-3" @click="goToStep('start')">
<YIcon iname="chevron-left" /> {{ $t('previous') }} <YIcon iname="chevron-left" /> {{ $t('previous') }}
</BButton> </BButton>
</template> </template>
@ -168,28 +179,16 @@ async function performPostInstall(force = false) {
<!-- FIRST USER SETUP STEP --> <!-- FIRST USER SETUP STEP -->
<template v-else-if="step === 'user'"> <template v-else-if="step === 'user'">
<CardForm <CardForm
:title="$t('postinstall.user.title')" v-model="form"
:fields="fields"
icon="user-plus" icon="user-plus"
:validation="v$"
:server-error="serverError"
:submit-text="$t('next')" :submit-text="$t('next')"
:title="$t('postinstall.user.title')"
:validations="v"
@submit.prevent="setUser" @submit.prevent="setUser"
> />
<ReadOnlyAlertItem
:label="$t('postinstall.user.first_user_help')"
type="info"
/>
<FormField <BButton variant="primary" class="mt-3" @click="goToStep('domain')">
v-for="(field, key) in fields"
:key="key"
v-bind="field"
v-model="form[key]"
:validation="v$.form[key]"
/>
</CardForm>
<BButton variant="primary" @click="goToStep('domain')" class="mt-3">
<YIcon iname="chevron-left" /> {{ $t('previous') }} <YIcon iname="chevron-left" /> {{ $t('previous') }}
</BButton> </BButton>
</template> </template>

View file

@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVuelidate } from '@vuelidate/core' import { computed, reactive, ref, watch } from 'vue'
import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useForm } from '@/composables/form'
import { asUnreffed } from '@/helpers/commons'
import { import {
domain, domain,
dynDomain, dynDomain,
@ -12,23 +13,26 @@ import {
} from '@/helpers/validators' } from '@/helpers/validators'
import { formatFormData } from '@/helpers/yunohostArguments' import { formatFormData } from '@/helpers/yunohostArguments'
import { useStoreGetters } from '@/store/utils' import { useStoreGetters } from '@/store/utils'
import type { AdressModelValue, FieldProps, FormFieldDict } from '@/types/form'
defineOptions({ defineOptions({
inheritAttrs: false, inheritAttrs: false,
}) })
withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
title: string title: string
submitText?: string | null submitText?: string
serverError?: string serverError?: string
}>(), }>(),
{ {
submitText: null, submitText: undefined,
serverError: '', serverError: undefined,
}, },
) )
const emit = defineEmits(['submit']) const emit = defineEmits<{
submit: [data: { domain: string; dyndns_recovery_password: string }]
}>()
const { t } = useI18n() const { t } = useI18n()
@ -42,82 +46,112 @@ const dynDnsForbiden = computed(() => {
}) })
}) })
const selected = ref(dynDnsForbiden.value ? 'domain' : '') type Selected = 'domain' | 'dynDomain' | 'localDomain'
const form = reactive({ const selected = ref<'' | Selected>(dynDnsForbiden.value ? 'domain' : '')
type Form = {
domain: string
dynDomain: AdressModelValue
dynDomainPassword: string
dynDomainPasswordConfirmation: string
localDomain: AdressModelValue
}
const form = ref<Form>({
domain: '', domain: '',
dynDomain: { localPart: '', separator: '.', domain: 'nohost.me' }, dynDomain: { localPart: '', separator: '.', domain: 'nohost.me' },
dynDomainPassword: '', dynDomainPassword: '',
dynDomainPasswordConfirmation: '', dynDomainPasswordConfirmation: '',
localDomain: { localPart: '', separator: '.', domain: 'local' }, localDomain: { localPart: '', separator: '.', domain: 'local' },
}) })
const fields = reactive({
const rules = computed(() => ({
selected: { required },
form: ['domain', 'localDomain'].includes(selected.value)
? {
[selected.value]:
selected.value === 'domain'
? { required, domain }
: { localPart: { required, dynDomain } },
}
: {
dynDomain: { localPart: { required, dynDomain } },
dynDomainPassword: { passwordLenght: minLength(8) },
dynDomainPasswordConfirmation: {
passwordMatch: sameAs(form.dynDomainPassword),
},
},
}))
const v$ = useVuelidate(rules, { selected, form })
const fields = {
domain: { domain: {
component: 'InputItem',
label: t('domain_name'), label: t('domain_name'),
rules: asUnreffed(
computed(() =>
selected.value === 'domain' ? { required, domain } : undefined,
),
),
props: { props: {
id: 'domain', id: 'domain',
placeholder: t('placeholder.domain'), placeholder: t('placeholder.domain'),
}, },
}, } satisfies FieldProps<'InputItem', Form['domain']>,
dynDomain: { dynDomain: {
component: 'AdressItem',
label: t('domain_name'), label: t('domain_name'),
rules: { localPart: { required, dynDomain } },
props: { props: {
id: 'dyn-domain', id: 'dyn-domain',
placeholder: t('placeholder.domain').split('.')[0], placeholder: t('placeholder.domain').split('.')[0],
type: 'domain', type: 'domain',
choices: dynDomains, choices: dynDomains,
}, },
}, } satisfies FieldProps<'AdressItem', Form['dynDomain']>,
dynDomainPassword: { dynDomainPassword: {
component: 'InputItem',
label: t('domain.add.dyn_dns_password'), label: t('domain.add.dyn_dns_password'),
description: t('domain.add.dyn_dns_password_desc'), description: t('domain.add.dyn_dns_password_desc'),
rules: { passwordLenght: minLength(8) },
props: { props: {
id: 'dyn-dns-password', id: 'dyn-dns-password',
placeholder: '••••••••', placeholder: '••••••••',
type: 'password', type: 'password',
}, },
}, } satisfies FieldProps<'InputItem', Form['dynDomainPassword']>,
dynDomainPasswordConfirmation: { dynDomainPasswordConfirmation: {
component: 'InputItem',
label: t('password_confirmation'), label: t('password_confirmation'),
rules: asUnreffed(
computed(() => ({
passwordMatch: sameAs(form.value.dynDomainPassword),
})),
),
props: { props: {
id: 'dyn-dns-password-confirmation', id: 'dyn-dns-password-confirmation',
placeholder: '••••••••', placeholder: '••••••••',
type: 'password', type: 'password',
}, },
}, } satisfies FieldProps<'InputItem', Form['dynDomainPasswordConfirmation']>,
localDomain: { localDomain: {
component: 'AdressItem',
label: t('domain_name'), label: t('domain_name'),
rules: asUnreffed(
computed(() =>
selected.value === 'localDomain'
? { localPart: { required, dynDomain } }
: undefined,
),
),
props: { props: {
id: 'dyn-domain', id: 'dyn-domain',
placeholder: t('placeholder.domain').split('.')[0], placeholder: t('placeholder.domain').split('.')[0],
type: 'domain', type: 'domain',
choices: ['local', 'test'], choices: ['local', 'test'],
}, },
} satisfies FieldProps<'AdressItem', Form['localDomain']>,
} satisfies FormFieldDict<Form>)
const { v, onSubmit, serverErrors } = useForm(form, fields)
watch(
() => props.serverError,
() => {
if (props.serverError) {
serverErrors.global = [props.serverError]
}
}, },
} )
const dynKeys = [
'dynDomain',
'dynDomainPassword',
'dynDomainPasswordConfirmation',
] as (keyof Form)[]
const domainIsVisible = computed(() => { const domainIsVisible = computed(() => {
return selected.value === 'domain' return selected.value === 'domain'
@ -131,15 +165,17 @@ const localDomainIsVisible = computed(() => {
return selected.value === 'localDomain' return selected.value === 'localDomain'
}) })
async function onSubmit() { const onDomainAdd = onSubmit(async () => {
const domainType = selected.value const domainType = selected.value
if (!domainType) return
const data = await formatFormData({ const data = await formatFormData({
domain: form[domainType], domain: form.value[domainType],
dyndns_recovery_password: dyndns_recovery_password:
domainType === 'dynDomain' ? form.dynDomainPassword : '', domainType === 'dynDomain' ? form.value.dynDomainPassword : '',
}) })
emit('submit', data) emit('submit', data)
} })
</script> </script>
<template> <template>
@ -147,9 +183,8 @@ async function onSubmit() {
:title="title" :title="title"
icon="globe" icon="globe"
:submit-text="submitText" :submit-text="submitText"
:validation="v$" :validations="v"
:server-error="serverError" @submit.prevent="onDomainAdd"
@submit.prevent="onSubmit"
> >
<template #disclaimer> <template #disclaimer>
<slot name="disclaimer" /> <slot name="disclaimer" />
@ -167,8 +202,8 @@ async function onSubmit() {
{{ $t('domain.add.from_registrar') }} {{ $t('domain.add.from_registrar') }}
</BFormRadio> </BFormRadio>
<BCollapse id="collapse-domain" v-model:visible="domainIsVisible"> <BCollapse id="collapse-domain" v-model="domainIsVisible">
<p class="mt-2 alert alert-info"> <p class="mt-2 mb-3 alert alert-info">
<YIcon iname="info-circle" /> <YIcon iname="info-circle" />
<span class="ps-1" v-html="$t('domain.add.from_registrar_desc')" /> <span class="ps-1" v-html="$t('domain.add.from_registrar_desc')" />
</p> </p>
@ -176,8 +211,7 @@ async function onSubmit() {
<FormField <FormField
v-bind="fields.domain" v-bind="fields.domain"
v-model="form.domain" v-model="form.domain"
:validation="v$.domain" :validation="v.domain"
class="mt-3"
/> />
</BCollapse> </BCollapse>
@ -194,34 +228,21 @@ async function onSubmit() {
{{ $t('domain.add.from_yunohost') }} {{ $t('domain.add.from_yunohost') }}
</BFormRadio> </BFormRadio>
<BCollapse id="collapse-dynDomain" v-model:visible="dynDomainIsVisible"> <BCollapse id="collapse-dynDomain" v-model="dynDomainIsVisible">
<p class="mt-2 alert alert-info"> <p class="mt-2 mb-3 alert alert-info">
<YIcon iname="info-circle" /> <YIcon iname="info-circle" />
<span class="ps-1" v-html="$t('domain.add.from_yunohost_desc')" /> <span class="ps-1" v-html="$t('domain.add.from_yunohost_desc')" />
</p> </p>
<FormField <FormField
v-bind="fields.dynDomain" v-for="key in dynKeys"
:validation="v$.dynDomain" :key="key"
class="mt-3" v-bind="fields[key]"
> v-model="form[key]"
<template #default="{ self }"> :validation="v[key]"
<AdressItem v-bind="self" v-model="form.dynDomain" />
</template>
</FormField>
<FormField
v-bind="fields.dynDomainPassword"
:validation="v$.dynDomainPassword"
v-model="form.dynDomainPassword"
/>
<FormField
v-bind="fields.dynDomainPasswordConfirmation"
:validation="v$.dynDomainPasswordConfirmation"
v-model="form.dynDomainPasswordConfirmation"
/> />
</BCollapse> </BCollapse>
<div <div
v-if="dynDnsForbiden" v-if="dynDnsForbiden"
class="alert alert-warning mt-2" class="alert alert-warning mt-2"
@ -239,21 +260,17 @@ async function onSubmit() {
{{ $t('domain.add.from_local') }} {{ $t('domain.add.from_local') }}
</BFormRadio> </BFormRadio>
<BCollapse id="collapse-localDomain" v-model:visible="localDomainIsVisible"> <BCollapse id="collapse-localDomain" v-model="localDomainIsVisible">
<p class="mt-2 alert alert-info"> <p class="mt-2 mb-3 alert alert-info">
<YIcon iname="info-circle" /> <YIcon iname="info-circle" />
<span class="ps-1" v-html="$t('domain.add.from_local_desc')" /> <span class="ps-1" v-html="$t('domain.add.from_local_desc')" />
</p> </p>
<FormField <FormField
v-bind="fields.localDomain" v-bind="fields.localDomain"
:validation="v$.localDomain" v-model="form.localDomain"
class="mt-3" :validation="v.localDomain"
> />
<template #default="{ self }">
<AdressItem v-bind="self" v-model="form.localDomain" />
</template>
</FormField>
</BCollapse> </BCollapse>
</CardForm> </CardForm>
</template> </template>

View file

@ -1,14 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVuelidate } from '@vuelidate/core'
import { computed, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import CardDeckFeed from '@/components/CardDeckFeed.vue' import CardDeckFeed from '@/components/CardDeckFeed.vue'
import { useForm } from '@/composables/form'
import { useAutoModal } from '@/composables/useAutoModal' import { useAutoModal } from '@/composables/useAutoModal'
import { useInitialQueries } from '@/composables/useInitialQueries'
import { randint } from '@/helpers/commons' import { randint } from '@/helpers/commons'
import { appRepoUrl, required } from '@/helpers/validators' import { appRepoUrl, required } from '@/helpers/validators'
import { useInitialQueries } from '@/composables/useInitialQueries' import type { FieldProps, FormFieldDict } from '@/types/form'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -37,8 +38,20 @@ const { loading } = useInitialQueries(
const apps = ref() const apps = ref()
const selectedApp = ref() const selectedApp = ref()
const antifeatures = ref() const antifeatures = ref()
const url = ref()
const v$ = useVuelidate({ url: { required, appRepoUrl } }, { url }) const form = ref({ url: '' })
const fields = {
url: {
component: 'InputItem',
label: t('url'),
rules: { required, appRepoUrl },
props: {
id: 'custom-install',
placeholder: 'https://some.git.forge.tld/USER/REPOSITORY',
},
} satisfies FieldProps<'InputItem', string>,
} satisfies FormFieldDict<typeof form.value>
const { v, onSubmit } = useForm(form, fields)
const qualityOptions = [ const qualityOptions = [
{ value: 'high_quality', text: t('only_highquality_apps') }, { value: 'high_quality', text: t('only_highquality_apps') },
@ -169,16 +182,16 @@ async function onInstallClick(appId: string) {
} }
// INSTALL CUSTOM APP // INSTALL CUSTOM APP
async function onCustomInstallClick() { const onCustomInstallClick = onSubmit(async () => {
const confirmed = await modalConfirm(t('confirm_install_custom_app')) const confirmed = await modalConfirm(t('confirm_install_custom_app'))
if (!confirmed) return if (!confirmed) return
const url_ = url.value const url = form.value.url
router.push({ router.push({
name: 'app-install-custom', name: 'app-install-custom',
params: { id: url_.endsWith('/') ? url_ : url_ + '/' }, params: { id: url.endsWith('/') ? url : url + '/' },
}) })
} })
</script> </script>
<template> <template>
@ -354,12 +367,14 @@ async function onCustomInstallClick() {
<template #bot> <template #bot>
<!-- INSTALL CUSTOM APP --> <!-- INSTALL CUSTOM APP -->
<CardForm <CardForm
:title="$t('custom_app_install')" v-model="form"
icon="download" icon="download"
@submit.prevent="onCustomInstallClick" :fields="fields"
:submit-text="$t('install')" :submit-text="$t('install')"
:validation="v$" :title="$t('custom_app_install')"
:validations="v"
class="mt-5" class="mt-5"
@submit.prevent="onCustomInstallClick"
> >
<template #disclaimer> <template #disclaimer>
<div class="alert alert-warning"> <div class="alert alert-warning">
@ -367,13 +382,6 @@ async function onCustomInstallClick() {
{{ $t('confirm_install_custom_app') }} {{ $t('confirm_install_custom_app') }}
</div> </div>
</template> </template>
<!-- URL -->
<FormField
v-bind="customInstall.field"
v-model="customInstall.url"
:validation="v$.customInstall.url"
/>
</CardForm> </CardForm>
</template> </template>

View file

@ -1,56 +1,52 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVuelidate } from '@vuelidate/core' import { ref } from 'vue'
import { reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import api from '@/api' import api from '@/api'
import { APIBadRequestError, APIError } from '@/api/errors' import { useForm } from '@/composables/form'
import { alphalownumdot_, required } from '@/helpers/validators' import { alphalownumdot_, required } from '@/helpers/validators'
import type { FieldProps, FormFieldDict } from '@/types/form'
const { t } = useI18n() const { t } = useI18n()
const router = useRouter() const router = useRouter()
const form = reactive({ groupname: '' }) const form = ref({ groupname: '' })
const v$ = useVuelidate({ groupname: { required, alphalownumdot_ } }, form) const fields = {
const serverError = ref('') groupname: {
const groupnameField = { component: 'InputItem',
label: t('group_name'), label: t('group_name'),
description: t('group_format_name_help'), description: t('group_format_name_help'),
props: { rules: { required, alphalownumdot_ },
id: 'groupname', props: {
placeholder: t('placeholder.groupname'), id: 'groupname',
}, placeholder: t('placeholder.groupname'),
} },
function onSubmit() { } satisfies FieldProps<'InputItem', string>,
} satisfies FormFieldDict<typeof form.value>
const { v, onSubmit } = useForm(form, fields)
const onAddGroup = onSubmit((onError) => {
api api
.post({ uri: 'users/groups', storeKey: 'groups' }, form, { .post({ uri: 'users/groups', storeKey: 'groups' }, form, {
key: 'groups.create', key: 'groups.create',
name: form.groupname, name: form.value.groupname,
}) })
.then(() => { .then(() => {
router.push({ name: 'group-list' }) router.push({ name: 'group-list' })
}) })
.catch((err: APIError) => { .catch(onError)
if (!(err instanceof APIBadRequestError)) throw err })
serverError.value = err.message
})
}
</script> </script>
<template> <template>
<CardForm <CardForm
:title="$t('group_new')" v-model="form"
icon="users" icon="users"
:validation="v$" :fields="fields"
:server-error="serverError" :title="$t('group_new')"
@submit.prevent="onSubmit" :validations="v"
> @submit.prevent="onAddGroup"
<!-- GROUP NAME --> />
<FormField
v-bind="groupnameField"
v-model="form.groupname"
:validation="v$.form.groupname"
/>
</CardForm>
</template> </template>

View file

@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVuelidate } from '@vuelidate/core' import { computed, ref } from 'vue'
import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import api from '@/api' import api from '@/api'
import { APIBadRequestError, type APIError } from '@/api/errors' import { useForm } from '@/composables/form'
import { useInitialQueries } from '@/composables/useInitialQueries' import { useInitialQueries } from '@/composables/useInitialQueries'
import { asUnreffed } from '@/helpers/commons'
import { import {
alphalownumdot_, alphalownumdot_,
minLength, minLength,
@ -17,6 +17,7 @@ import {
} from '@/helpers/validators' } from '@/helpers/validators'
import { formatFormData } from '@/helpers/yunohostArguments' import { formatFormData } from '@/helpers/yunohostArguments'
import { useStoreGetters } from '@/store/utils' import { useStoreGetters } from '@/store/utils'
import type { FieldProps, FormFieldDict } from '@/types/form'
const { t } = useI18n() const { t } = useI18n()
const router = useRouter() const router = useRouter()
@ -30,149 +31,122 @@ const { loading } = useInitialQueries(
const { userNames, domainsAsChoices, mainDomain } = useStoreGetters() const { userNames, domainsAsChoices, mainDomain } = useStoreGetters()
const fields = { type Form = typeof form.value
username: { const form = ref({
label: t('user_username'),
props: {
id: 'username',
placeholder: t('placeholder.username'),
},
},
fullname: {
label: t('user_fullname'),
props: {
id: 'fullname',
placeholder: t('placeholder.fullname'),
},
},
domain: {
id: 'mail',
label: t('user_email'),
description: t('tip_about_user_email'),
descriptionVariant: 'info',
props: { choices: domainsAsChoices },
},
password: {
label: t('password'),
description: t('good_practices_about_user_password'),
descriptionVariant: 'warning',
props: {
id: 'password',
placeholder: '••••••••',
type: 'password',
},
},
confirmation: {
label: t('password_confirmation'),
props: {
id: 'confirmation',
placeholder: '••••••••',
type: 'password',
},
},
}
const form = reactive({
username: '', username: '',
fullname: '', fullname: '',
domain: '', domain: '',
password: '', password: '',
confirmation: '', confirmation: '',
}) })
const rules = computed(() => ({ const fields = {
username: { username: {
required, component: 'InputItem',
alphalownumdot_, label: t('user_username'),
notInUsers: unique(userNames.value), rules: asUnreffed(
}, computed(() => ({
fullname: { required, name }, required,
domain: { required }, alphalownumdot_,
password: { required, passwordLenght: minLength(8) }, notInUsers: unique(userNames),
confirmation: { required, passwordMatch: sameAs(form.password) }, })),
})) ),
const v$ = useVuelidate(rules, form) props: {
const serverError = ref('') id: 'username',
placeholder: t('placeholder.username'),
},
} satisfies FieldProps<'InputItem', Form['username']>,
fullname: {
component: 'InputItem',
hr: true,
label: t('user_fullname'),
rules: { required, name },
props: {
id: 'fullname',
placeholder: t('placeholder.fullname'),
},
} satisfies FieldProps<'InputItem', Form['fullname']>,
domain: {
component: 'SelectItem',
hr: true,
id: 'mail',
label: t('user_email'),
description: t('tip_about_user_email'),
descriptionVariant: 'info',
rules: { required },
props: { choices: asUnreffed(domainsAsChoices) },
} satisfies FieldProps<'SelectItem', Form['domain']>,
password: {
component: 'InputItem',
label: t('password'),
description: t('good_practices_about_user_password'),
descriptionVariant: 'warning',
rules: { required, passwordLenght: minLength(8) },
props: {
id: 'password',
placeholder: '••••••••',
type: 'password',
},
} satisfies FieldProps<'InputItem', Form['password']>,
confirmation: {
component: 'InputItem',
label: t('password_confirmation'),
rules: asUnreffed(
computed(() => ({
required,
passwordMatch: sameAs(form.value.password),
})),
),
props: {
id: 'confirmation',
placeholder: '••••••••',
type: 'password',
},
} satisfies FieldProps<'InputItem', Form['confirmation']>,
} satisfies FormFieldDict<Form>
const { v, onSubmit } = useForm(form, fields)
function onQueriesResponse() { function onQueriesResponse() {
form.domain = mainDomain.value form.value.domain = mainDomain.value
} }
async function onSubmit() { const onUserCreate = onSubmit(async (onError) => {
const data = await formatFormData(form, { flatten: true }) const data = await formatFormData(form.value, { flatten: true })
api api
.post({ uri: 'users' }, data, { .post({ uri: 'users' }, data, {
key: 'users.create', key: 'users.create',
name: form.username, name: form.value.username,
}) })
.then(() => { .then(() => {
router.push({ name: 'user-list' }) router.push({ name: 'user-list' })
}) })
.catch((err: APIError) => { .catch(onError)
if (!(err instanceof APIBadRequestError)) throw err })
serverError.value = err.message
})
}
</script> </script>
<template> <template>
<ViewBase :loading="loading" skeleton="CardFormSkeleton"> <ViewBase :loading="loading" skeleton="CardFormSkeleton">
<CardForm <CardForm
:title="$t('users_new')" v-model="form"
icon="user-plus" icon="user-plus"
:validation="v$" :fields="fields"
:server-error="serverError" :title="$t('users_new')"
@submit.prevent="onSubmit" :validations="v"
@submit.prevent="onUserCreate"
> >
<!-- USER NAME --> <template #component:domain="componentProps">
<FormField <BInputGroup>
v-bind="fields.username" <BInputGroupText id="local-part" tag="label" class="border-right-0">
v-model="form.username" {{ form.username }}@
:validation="v$.form.username" </BInputGroupText>
/>
<!-- USER FULLNAME --> <SelectItem v-bind="componentProps" v-model="form.domain" />
<FormField </BInputGroup>
v-bind="fields.fullname" </template>
:validation="v$.form.fullname"
v-model="form.fullname"
/>
<hr />
<!-- USER MAIL DOMAIN -->
<FormField v-bind="fields.domain" :validation="v$.form.domain">
<template #default="{ self }">
<BInputGroup>
<BInputGroupText id="local-part" tag="label" class="border-right-0">
{{ form.username }}@
</BInputGroupText>
<SelectItem
aria-labelledby="local-part"
aria-describedby="mail__BV_description_"
v-model="form.domain"
v-bind="self"
/>
</BInputGroup>
</template>
</FormField>
<hr />
<!-- USER PASSWORD -->
<FormField
v-bind="fields.password"
v-model="form.password"
:validation="v$.form.password"
/>
<!-- USER PASSWORD CONFIRMATION -->
<FormField
v-bind="fields.confirmation"
v-model="form.confirmation"
:validation="v$.form.confirmation"
/>
</CardForm> </CardForm>
</ViewBase> </ViewBase>
</template> </template>

View file

@ -1,17 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVuelidate } from '@vuelidate/core' import { computed, reactive, ref } from 'vue'
import { computed, nextTick, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import api from '@/api' import api from '@/api'
import type ViewBase from '@/components/globals/ViewBase.vue' import type ViewBase from '@/components/globals/ViewBase.vue'
import { useArrayRule, useForm } from '@/composables/form'
import { useInitialQueries } from '@/composables/useInitialQueries' import { useInitialQueries } from '@/composables/useInitialQueries'
import { arrayDiff } from '@/helpers/commons' import { arrayDiff, asUnreffed } from '@/helpers/commons'
import { import {
emailForward, emailForward,
emailLocalPart, emailLocalPart,
helpers,
integer, integer,
minLength, minLength,
minValue, minValue,
@ -25,6 +24,7 @@ import {
sizeToM, sizeToM,
} from '@/helpers/yunohostArguments' } from '@/helpers/yunohostArguments'
import { useStoreGetters } from '@/store/utils' import { useStoreGetters } from '@/store/utils'
import type { AdressModelValue, FieldProps, FormFieldDict } from '@/types/form'
const props = defineProps<{ const props = defineProps<{
name: string name: string
@ -44,124 +44,138 @@ const viewElem = ref<InstanceType<typeof ViewBase> | null>(null)
const { user, domainsAsChoices, mainDomain } = useStoreGetters() const { user, domainsAsChoices, mainDomain } = useStoreGetters()
const fields = { type Form = typeof form.value
const form = ref({
username: props.name,
fullname: '',
mail: { localPart: '', separator: '@', domain: '' } as AdressModelValue,
mailbox_quota: '' as string | number,
mail_aliases: [] as AdressModelValue[],
mail_forward: [] as string[],
change_password: '',
confirmation: '',
})
const fields = reactive({
username: { username: {
component: 'InputItem',
label: t('user_username'), label: t('user_username'),
modelValue: props.name, props: {
props: { id: 'username', disabled: true }, id: 'username',
}, disabled: true,
},
} satisfies FieldProps<'InputItem', Form['username']>,
fullname: { fullname: {
component: 'InputItem',
label: t('user_fullname'), label: t('user_fullname'),
rules: { required, nameValidator },
props: { props: {
id: 'fullname', id: 'fullname',
placeholder: t('placeholder.fullname'), placeholder: t('placeholder.fullname'),
}, },
}, } satisfies FieldProps<'InputItem', Form['fullname']>,
mail: { mail: {
component: 'AdressItem',
label: t('user_email'), label: t('user_email'),
props: { id: 'mail', choices: domainsAsChoices }, rules: {
}, localPart: { required, email: emailLocalPart },
},
props: { id: 'mail', choices: asUnreffed(domainsAsChoices) },
} satisfies FieldProps<'AdressItem', Form['mail']>,
mailbox_quota: { mailbox_quota: {
append: 'M',
component: 'InputItem',
label: t('user_mailbox_quota'), label: t('user_mailbox_quota'),
description: t('mailbox_quota_description'), description: t('mailbox_quota_description'),
example: t('mailbox_quota_example'), // example: t('mailbox_quota_example'),
rules: { integer, minValue: minValue(0) },
props: { props: {
id: 'mailbox-quota', id: 'mailbox-quota',
placeholder: t('mailbox_quota_placeholder'), placeholder: t('mailbox_quota_placeholder'),
}, },
}, } satisfies FieldProps<'InputItem', Form['mailbox_quota']>,
mail_aliases: { mail_aliases: {
component: 'AdressItem',
rules: asUnreffed(
useArrayRule(() => form.value.mail_aliases, {
localPart: { required, email: emailLocalPart },
}),
),
props: { props: {
placeholder: t('placeholder.username'), placeholder: t('placeholder.username'),
choices: domainsAsChoices, choices: asUnreffed(domainsAsChoices),
}, },
}, } satisfies FieldProps<'AdressItem', Form['mail_aliases']>,
mail_forward: { mail_forward: {
component: 'InputItem',
rules: asUnreffed(
useArrayRule(() => form.value.mail_forward, { required, emailForward }),
),
props: { props: {
placeholder: t('user_new_forward'), placeholder: t('user_new_forward'),
type: 'email', type: 'email',
}, },
}, } satisfies FieldProps<'InputItem', Form['mail_forward']>,
change_password: { change_password: {
component: 'InputItem',
label: t('password'), label: t('password'),
description: t('good_practices_about_user_password'), description: t('good_practices_about_user_password'),
descriptionVariant: 'warning', descriptionVariant: 'warning',
rules: { passwordLenght: minLength(8) },
props: { props: {
id: 'change_password', id: 'change_password',
type: 'password', type: 'password',
placeholder: '••••••••', placeholder: '••••••••',
autocomplete: 'new-password', autocomplete: 'new-password',
}, },
}, } satisfies FieldProps<'InputItem', Form['change_password']>,
confirmation: { confirmation: {
component: 'InputItem',
label: t('password_confirmation'), label: t('password_confirmation'),
rules: asUnreffed(
computed(() => ({ passwordMatch: sameAs(form.value.change_password) })),
),
props: { props: {
id: 'confirmation', id: 'confirmation',
type: 'password', type: 'password',
placeholder: '••••••••', placeholder: '••••••••',
autocomplete: 'new-password', autocomplete: 'new-password',
}, },
}, } satisfies FieldProps<'InputItem', Form['confirmation']>,
} } satisfies FormFieldDict<Form>)
const form = reactive({
fullname: '', const { v, onSubmit } = useForm(form, fields)
mail: { localPart: '', separator: '@', domain: '' },
mailbox_quota: '',
mail_aliases: [],
mail_forward: [],
change_password: '',
confirmation: '',
})
const rules = computed(() => ({
fullname: { required, nameValidator },
mail: {
localPart: { required, email: emailLocalPart },
},
mailbox_quota: { integer, minValue: minValue(0) },
mail_aliases: {
$each: helpers.forEach({
localPart: { required, email: emailLocalPart },
}),
},
mail_forward: {
$each: helpers.forEach({
mail: { required, emailForward },
}),
},
change_password: { passwordLenght: minLength(8) },
confirmation: { passwordMatch: sameAs(form.change_password) },
}))
const v$ = useVuelidate(rules, form)
const serverError = ref('')
function onQueriesResponse(user_: any) { function onQueriesResponse(user_: any) {
form.fullname = user_.fullname form.value.fullname = user_.fullname
form.mail = adressToFormValue(user_.mail) form.value.mail = adressToFormValue(user_.mail)
if (user_['mail-aliases']) { if (user_['mail-aliases']) {
form.mail_aliases = user_['mail-aliases'].map((mail) => form.value.mail_aliases = user_['mail-aliases'].map((mail) =>
adressToFormValue(mail), adressToFormValue(mail),
) )
} }
if (user_['mail-forward']) { if (user_['mail-forward']) {
form.mail_forward = user_['mail-forward'].map((mail) => ({ mail })) // Copy value form.value.mail_forward = user_['mail-forward'].map((mail) => ({ mail })) // Copy value
} }
// mailbox-quota could be 'No quota' or 'Pas de quota'... // mailbox-quota could be 'No quota' or 'Pas de quota'...
if (parseInt(user_['mailbox-quota'].limit) > 0) { if (parseInt(user_['mailbox-quota'].limit) > 0) {
form.mailbox_quota = sizeToM(user_['mailbox-quota'].limit) form.value.mailbox_quota = sizeToM(user_['mailbox-quota'].limit)
} else { } else {
form.mailbox_quota = '' form.value.mailbox_quota = ''
} }
} }
async function onSubmit() { const onUserEdit = onSubmit(async (onError, serverErrors) => {
const formData = await formatFormData(form, { flatten: true }) const { data: formData } = await formatFormData(form.value, {
flatten: true,
extract: ['username'],
})
// FIXME not sure computed can be executed? // FIXME not sure computed can be executed?
const user_ = user.value(props.name) const user_ = user.value(props.name)
const data = {} const data = {}
@ -169,7 +183,7 @@ async function onSubmit() {
formData.mailbox_quota = '' formData.mailbox_quota = ''
} }
formData.mail_forward = formData.mail_forward?.map((v) => v.mail) // formData.mail_forward = formData.mail_forward?.map((v) => v.mail)
for (const key of ['mail_aliases', 'mail_forward']) { for (const key of ['mail_aliases', 'mail_forward']) {
const dashedKey = key.replace('_', '-') const dashedKey = key.replace('_', '-')
@ -193,7 +207,7 @@ async function onSubmit() {
} }
if (Object.keys(data).length === 0) { if (Object.keys(data).length === 0) {
serverError.value = t('error_modify_something') serverErrors.global = [t('error_modify_something')]
return return
} }
@ -205,132 +219,45 @@ async function onSubmit() {
.then(() => { .then(() => {
router.push({ name: 'user-info', param: { name: props.name } }) router.push({ name: 'user-info', param: { name: props.name } })
}) })
.catch((err) => { .catch(onError)
if (err.name !== 'APIBadRequestError') throw err })
serverError.value = err.message
})
}
function addEmailField(type: 'aliases' | 'forward') {
form['mail_' + type].push(
type === 'aliases'
? { localPart: '', separator: '@', domain: mainDomain.value }
: { mail: '' },
)
// Focus last input after rendering update
nextTick(() => {
const inputs = viewElem.value!.$el.querySelectorAll(`#mail-${type} input`)
inputs[inputs.length - 1].focus()
})
}
function removeEmailField(type: 'aliases' | 'forward', index: number) {
form['mail_' + type].splice(index, 1)
}
</script> </script>
<template> <template>
<ViewBase ref="viewElem" :loading="loading" skeleton="CardFormSkeleton"> <ViewBase ref="viewElem" :loading="loading" skeleton="CardFormSkeleton">
<CardForm <CardForm
:title="$t('user_username_edit', { name })" v-model="form"
icon="user" icon="user"
:validation="v$" :fields="fields"
:server-error="serverError" :title="$t('user_username_edit', { name })"
@submit.prevent="onSubmit" :validations="v"
@submit.prevent="onUserEdit"
> >
<!-- USERNAME (disabled) --> <template #field:mail_aliases="fieldProps">
<FormField v-bind="fields.username" /> <FormFieldMultiple
v-bind="fieldProps"
v-model="form.mail_aliases"
:add-btn-text="t('user_emailaliases_add')"
:default-value="
() => ({
localPart: '',
separator: '@',
domain: mainDomain,
})
"
:validation="v.form.mail_aliases"
/>
</template>
<!-- USER FULLNAME --> <template #field:mail_forward="fieldProps">
<FormField <FormFieldMultiple
v-bind="fields.fullname" v-bind="fieldProps"
v-model="form.fullname" v-model="form.mail_forward"
:validation="v$.form.fullname" :add-btn-text="t('user_emailforward_add')"
/> :default-value="() => ''"
:validation="v.form.mail_forward"
<hr /> />
</template>
<!-- USER EMAIL -->
<FormField v-bind="fields.mail" :validation="v$.form.mail">
<template #default="{ self }">
<AdressItem v-bind="self" v-model="form.mail" />
</template>
</FormField>
<!-- MAILBOX QUOTA -->
<FormField
v-bind="fields.mailbox_quota"
:validation="v$.form.mailbox_quota"
>
<template #default="{ self }">
<BInputGroup append="M">
<InputItem v-bind="self" v-model="form.mailbox_quota" />
</BInputGroup>
</template>
</FormField>
<hr />
<!-- MAIL ALIASES -->
<FormField :label="$t('user_emailaliases')" id="mail-aliases">
<div v-for="(mail, i) in form.mail_aliases" :key="i" class="mail-list">
<FormField
v-bind="fields.mail_aliases"
:id="'mail_aliases' + i"
:validation="v$.form.mail_aliases"
:validation-index="i"
>
<template #default="{ self }">
<AdressItem v-bind="self" v-model="form.mail_aliases[i]" />
</template>
</FormField>
<BButton variant="danger" @click="removeEmailField('aliases', i)">
<YIcon :title="$t('delete')" iname="trash-o" />
<span class="visually-hidden">{{ $t('delete') }}</span>
</BButton>
</div>
<BButton variant="success" @click="addEmailField('aliases')">
<YIcon iname="plus" /> {{ $t('user_emailaliases_add') }}
</BButton>
</FormField>
<!-- MAIL FORWARD -->
<FormField :label="$t('user_emailforward')" id="mail-forward">
<div v-for="(mail, i) in form.mail_forward" :key="i" class="mail-list">
<FormField
v-bind="fields.mail_forward"
v-model="form.mail_forward[i].mail"
:id="'mail-forward' + i"
:validation="v$.form.mail_forward"
:validation-index="i"
/>
<BButton variant="danger" @click="removeEmailField('forward', i)">
<YIcon :title="$t('delete')" iname="trash-o" />
<span class="visually-hidden">{{ $t('delete') }}</span>
</BButton>
</div>
<BButton variant="success" @click="addEmailField('forward')">
<YIcon iname="plus" /> {{ $t('user_emailforward_add') }}
</BButton>
</FormField>
<hr />
<!-- USER PASSWORD -->
<FormField
v-bind="fields.change_password"
v-model="form.change_password"
:validation="v$.form.change_password"
/>
<!-- USER PASSWORD CONFIRMATION -->
<FormField
v-bind="fields.confirmation"
v-model="form.confirmation"
:validation="v$.form.confirmation"
/>
</CardForm> </CardForm>
</ViewBase> </ViewBase>
</template> </template>

View file

@ -1,30 +1,39 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive, ref } from 'vue' import { reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import api from '@/api' import api from '@/api'
import { useForm } from '@/composables/form'
import { useAutoModal } from '@/composables/useAutoModal' import { useAutoModal } from '@/composables/useAutoModal'
import { required } from '@/helpers/validators'
import { formatFormData } from '@/helpers/yunohostArguments' import { formatFormData } from '@/helpers/yunohostArguments'
import { useVuelidate } from '@vuelidate/core' import type { FieldProps, FileModelValue, FormFieldDict } from '@/types/form'
const { t } = useI18n() const { t } = useI18n()
const router = useRouter() const router = useRouter()
const store = useStore() const store = useStore()
const modalConfirm = useAutoModal() const modalConfirm = useAutoModal()
const fields = { type Form = typeof form.value
const form = ref({
csvfile: { file: null } as FileModelValue,
update: false,
delete: false,
})
const fields = reactive({
csvfile: { csvfile: {
component: 'FileItem',
label: t('users_import_csv_file'), label: t('users_import_csv_file'),
description: t('users_import_csv_file_desc'), description: t('users_import_csv_file_desc'),
component: 'FileItem', rules: { file: required },
props: { props: {
id: 'csvfile', id: 'csvfile',
accept: 'text/csv', accept: 'text/csv',
placeholder: t('placeholder.file'), placeholder: t('placeholder.file'),
}, },
}, } satisfies FieldProps<'FileItem', Form['csvfile']>,
update: { update: {
label: t('users_import_update'), label: t('users_import_update'),
@ -33,28 +42,22 @@ const fields = {
props: { props: {
id: 'update', id: 'update',
}, },
}, } satisfies FieldProps<'CheckboxItem', Form['update']>,
delete: { delete: {
component: 'CheckboxItem',
label: t('users_import_delete'), label: t('users_import_delete'),
description: t('users_import_delete_desc'), description: t('users_import_delete_desc'),
component: 'CheckboxItem',
props: { props: {
id: 'delete', id: 'delete',
}, },
}, } satisfies FieldProps<'CheckboxItem', Form['delete']>,
} } satisfies FormFieldDict<Form>)
const form = reactive({
csvfile: { file: null },
update: false,
delete: false,
})
const rules = computed(() => ({}))
const v$ = useVuelidate(rules, form)
const serverError = ref('')
async function onSubmit() { const { v, onSubmit } = useForm(form, fields)
if (form.delete) {
const onUserImport = onSubmit(async (onError) => {
if (form.value.delete) {
const confirmed = await modalConfirm( const confirmed = await modalConfirm(
t('users_import_confirm_destructive'), t('users_import_confirm_destructive'),
{ okTitle: t('users_import_delete_others') }, { okTitle: t('users_import_delete_others') },
@ -62,7 +65,7 @@ async function onSubmit() {
if (!confirmed) return if (!confirmed) return
} }
const requestArgs = { ...form } as Partial<typeof form> const requestArgs = { ...form.value } as Partial<Form>
if (!requestArgs.delete) delete requestArgs.delete if (!requestArgs.delete) delete requestArgs.delete
if (!requestArgs.update) delete requestArgs.update if (!requestArgs.update) delete requestArgs.update
const data = await formatFormData(requestArgs) const data = await formatFormData(requestArgs)
@ -78,31 +81,17 @@ async function onSubmit() {
]) ])
router.push({ name: 'user-list' }) router.push({ name: 'user-list' })
}) })
.catch((error) => { .catch(onError)
serverError.value = error.message })
})
}
</script> </script>
<template> <template>
<CardForm <CardForm
:title="$t('users_import')" v-model="form"
icon="user-plus" icon="user-plus"
:validation="v$" :fields="fields"
:server-error="serverError" :title="$t('users_import')"
@submit.prevent="onSubmit" :validations="v"
> @submit.prevent="onUserImport"
<!-- CSV FILE --> />
<FormField
v-bind="fields.csvfile"
v-model="form.csvfile"
:validation="v$.form.csvfile"
/>
<!-- UPDATE -->
<FormField v-bind="fields.update" v-model="form.update" />
<!-- DELETE -->
<FormField v-bind="fields.delete" v-model="form.delete" />
</CardForm>
</template> </template>