refactor: update form fields definition in views

This commit is contained in:
axolotle 2024-08-11 18:44:27 +02:00
parent c17da2c93e
commit b2a233f1f1
9 changed files with 130 additions and 151 deletions

View file

@ -259,7 +259,7 @@ export type FormField<
link?:
| { text: string; name: RouteLocationRaw }
| { text: string; href: string }
rules?: FormFieldRules<MV> | ComputedRef<FormFieldRules<MV>>
rules?: FormFieldRules<MV>
prepend?: string
readonly?: false
}
@ -270,7 +270,7 @@ export type FormFieldReadonly<
label: string
cols?: Cols
readonly: true
rules: undefined
rules?: undefined
}
export type FormFieldDisplay<
@ -281,7 +281,7 @@ export type FormFieldDisplay<
visible?: boolean | ComputedRef<boolean>
hr?: boolean
readonly?: true
rules: undefined
rules?: undefined
}
export type FormFieldProps<
@ -293,7 +293,7 @@ export type FormFieldProps<
export type FormFieldReadonlyProps<
C extends AnyWritableComponents,
MV extends any,
> = Omit<FormFieldReadonly<C>, 'hr' | 'visible' | 'readonly'> & {
> = Omit<FormFieldReadonly<C>, 'hr' | 'visible' | 'readonly' | 'rules'> & {
modelValue?: MV
}

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter, type LocationQueryValue } from 'vue-router'
@ -26,12 +26,12 @@ const form = ref({
username: '',
password: '',
})
const fields = reactive({
const fields = {
username: {
component: 'InputItem',
label: t('user_username'),
rules: { required, alphalownumdot_ },
props: {
cProps: {
id: 'username',
autocomplete: 'username',
},
@ -41,13 +41,13 @@ const fields = reactive({
component: 'InputItem',
label: t('password'),
rules: { required, passwordLenght: minLength(4) },
props: {
cProps: {
id: 'password',
type: 'password',
autocomplete: 'current-password',
},
} satisfies FieldProps<'InputItem', Form['password']>,
} satisfies FormFieldDict<Form>)
} satisfies FormFieldDict<Form>
const { v, onSubmit } = useForm(form, fields)

View file

@ -6,7 +6,6 @@ import api from '@/api'
import { APIBadRequestError } from '@/api/errors'
import { useForm } from '@/composables/form'
import { useAutoModal } from '@/composables/useAutoModal'
import { asUnreffed } from '@/helpers/commons'
import {
alphalownumdot_,
minLength,
@ -26,7 +25,7 @@ type Steps = 'start' | 'domain' | 'user' | 'rootfsspace-error' | 'login'
const step = ref<Steps>('start')
const serverError = ref('')
const domain = ref('')
const dyndns_recovery_password = ref('')
const dyndns_recovery_password = ref<string | undefined>()
type Form = typeof form.value
const form = ref({
@ -35,25 +34,25 @@ const form = ref({
password: '',
confirmation: '',
})
const fields = reactive({
const fields = {
// FIXME satisfies FormFieldDict but not for CardForm?
alert: {
component: 'ReadOnlyAlertItem',
props: { label: t('postinstall.user.first_user_help'), type: 'info' },
cProps: { label: t('postinstall.user.first_user_help'), type: 'info' },
} satisfies FieldProps<'ReadOnlyAlertItem'>,
username: {
component: 'InputItem',
label: t('user_username'),
rules: { required, alphalownumdot_ },
props: { id: 'username', placeholder: t('placeholder.username') },
cProps: { id: 'username', placeholder: t('placeholder.username') },
} satisfies FieldProps<'InputItem', Form['username']>,
fullname: {
component: 'InputItem',
label: t('user_fullname'),
rules: { required, name },
props: { id: 'fullname', placeholder: t('placeholder.fullname') },
cProps: { id: 'fullname', placeholder: t('placeholder.fullname') },
} satisfies FieldProps<'InputItem', Form['fullname']>,
password: {
@ -62,21 +61,19 @@ const fields = reactive({
description: t('good_practices_about_admin_password'),
descriptionVariant: 'warning',
rules: { required, passwordLenght: minLength(8) },
props: { id: 'password', placeholder: '••••••••', type: 'password' },
cProps: { id: 'password', placeholder: '••••••••', type: 'password' },
} satisfies FieldProps<'InputItem', Form['password']>,
confirmation: {
confirmation: reactive({
component: 'InputItem',
label: t('password_confirmation'),
rules: asUnreffed(
computed(() => ({
rules: computed(() => ({
required,
passwordMatch: sameAs(form.value.password),
})),
),
props: { id: 'confirmation', placeholder: '••••••••', type: 'password' },
} satisfies FieldProps<'InputItem', Form['confirmation']>,
} satisfies FormFieldDict<Form>)
cProps: { id: 'confirmation', placeholder: '••••••••', type: 'password' },
}) satisfies FieldProps<'InputItem', Form['confirmation']>,
} satisfies FormFieldDict<Form>
const { v, onSubmit, serverErrors } = useForm(form, fields)
@ -85,7 +82,10 @@ function goToStep(step_: Steps) {
step.value = step_
}
function setDomain(data: { domain: string; dyndns_recovery_password: string }) {
function setDomain(data: {
domain: string
dyndns_recovery_password?: string
}) {
domain.value = data.domain
dyndns_recovery_password.value = data.dyndns_recovery_password
goToStep('user')

View file

@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n'
import { useDomains } from '@/composables/data'
import { useForm } from '@/composables/form'
import { asUnreffed } from '@/helpers/commons'
import {
domain,
dynDomain,
@ -12,7 +11,7 @@ import {
required,
sameAs,
} from '@/helpers/validators'
import { formatForm } from '@/helpers/yunohostArguments'
import { formatFormValue } from '@/helpers/yunohostArguments'
import type { AdressModelValue, FieldProps, FormFieldDict } from '@/types/form'
defineOptions({
@ -31,7 +30,7 @@ const props = withDefaults(
},
)
const emit = defineEmits<{
submit: [data: Partial<{ domain: string; dyndns_recovery_password: string }>]
submit: [data: { domain: string; dyndns_recovery_password?: string }]
}>()
const { t } = useI18n()
@ -63,26 +62,24 @@ const form = ref<Form>({
dynDomainPasswordConfirmation: '',
localDomain: { localPart: '', separator: '.', domain: 'local' },
})
const fields = reactive({
domain: {
const fields = {
domain: reactive({
component: 'InputItem',
label: t('domain_name'),
rules: asUnreffed(
computed(() =>
selected.value === 'domain' ? { required, domain } : undefined,
),
),
props: {
rules: computed(() => {
return selected.value === 'domain' ? { required, domain } : undefined
}),
cProps: {
id: 'domain',
placeholder: t('placeholder.domain'),
},
} satisfies FieldProps<'InputItem', Form['domain']>,
}) satisfies FieldProps<'InputItem', Form['domain']>,
dynDomain: {
component: 'AdressItem',
label: t('domain_name'),
rules: { localPart: { required, dynDomain } },
props: {
cProps: {
id: 'dyn-domain',
placeholder: t('placeholder.domain').split('.')[0],
type: 'domain',
@ -90,51 +87,51 @@ const fields = reactive({
},
} satisfies FieldProps<'AdressItem', Form['dynDomain']>,
dynDomainPassword: {
dynDomainPassword: reactive({
component: 'InputItem',
label: t('domain.add.dyn_dns_password'),
description: t('domain.add.dyn_dns_password_desc'),
rules: { passwordLenght: minLength(8) },
props: {
rules: computed(() => {
return selected.value === 'dynDomain'
? { required, passwordLenght: minLength(8) }
: undefined
}),
cProps: {
id: 'dyn-dns-password',
placeholder: '••••••••',
type: 'password',
},
} satisfies FieldProps<'InputItem', Form['dynDomainPassword']>,
}) satisfies FieldProps<'InputItem', Form['dynDomainPassword']>,
dynDomainPasswordConfirmation: {
dynDomainPasswordConfirmation: reactive({
component: 'InputItem',
label: t('password_confirmation'),
rules: asUnreffed(
computed(() => ({
rules: computed(() => ({
passwordMatch: sameAs(form.value.dynDomainPassword),
})),
),
props: {
cProps: {
id: 'dyn-dns-password-confirmation',
placeholder: '••••••••',
type: 'password',
},
} satisfies FieldProps<'InputItem', Form['dynDomainPasswordConfirmation']>,
}) satisfies FieldProps<'InputItem', Form['dynDomainPasswordConfirmation']>,
localDomain: {
localDomain: reactive({
component: 'AdressItem',
label: t('domain_name'),
rules: asUnreffed(
computed(() =>
rules: computed(() =>
selected.value === 'localDomain'
? { localPart: { required, dynDomain } }
: undefined,
),
),
props: {
cProps: {
id: 'dyn-domain',
placeholder: t('placeholder.domain').split('.')[0],
type: 'domain',
choices: ['local', 'test'],
},
} satisfies FieldProps<'AdressItem', Form['localDomain']>,
} satisfies FormFieldDict<Form>)
}) satisfies FieldProps<'AdressItem', Form['localDomain']>,
} satisfies FormFieldDict<Form>
const { v, onSubmit, serverErrors } = useForm(form, fields)
@ -168,16 +165,13 @@ const localDomainIsVisible = computed(() => {
const onDomainAdd = onSubmit(async () => {
const domainType = selected.value
if (!domainType) return
const domain = await formatFormValue(form.value[domainType])
const data = await formatForm(
{
domain: form.value[domainType],
emit('submit', {
domain,
dyndns_recovery_password:
domainType === 'dynDomain' ? form.value.dynDomainPassword : '',
},
{ removeEmpty: true },
)
emit('submit', data)
domainType === 'dynDomain' ? form.value.dynDomainPassword : undefined,
})
})
</script>
@ -214,7 +208,7 @@ const onDomainAdd = onSubmit(async () => {
<FormField
v-bind="fields.domain"
v-model="form.domain"
:validation="v.domain"
:validation="v.form.domain"
/>
</BCollapse>
@ -242,7 +236,7 @@ const onDomainAdd = onSubmit(async () => {
:key="key"
v-bind="fields[key]"
v-model="form[key]"
:validation="v[key]"
:validation="v.form[key]"
/>
</BCollapse>
@ -272,7 +266,7 @@ const onDomainAdd = onSubmit(async () => {
<FormField
v-bind="fields.localDomain"
v-model="form.localDomain"
:validation="v.localDomain"
:validation="v.form.localDomain"
/>
</BCollapse>
</CardForm>

View file

@ -18,7 +18,7 @@ const fields = {
label: t('group_name'),
description: t('group_format_name_help'),
rules: { required, alphalownumdot_ },
props: {
cProps: {
id: 'groupname',
placeholder: t('placeholder.groupname'),
},

View file

@ -35,7 +35,7 @@ const fields = {
component: 'SelectItem',
label: t('action'),
rules: { required },
props: {
cProps: {
id: 'input-action',
choices: [
{ value: 'allow', text: t('open') },
@ -49,7 +49,7 @@ const fields = {
component: 'InputItem',
label: t('port'),
rules: { number: required, integer, between: between(0, 65535) },
props: { id: 'input-port', placeholder: '0', type: 'number' },
cProps: { id: 'input-port', placeholder: '0', type: 'number' },
} satisfies FieldProps<'InputItem', Form['action']>,
connection: {
@ -57,7 +57,7 @@ const fields = {
component: 'SelectItem',
label: t('connection'),
rules: { required },
props: {
cProps: {
id: 'input-connection',
choices: [
{ value: 'ipv4', text: t('ipv4') },
@ -71,7 +71,7 @@ const fields = {
component: 'SelectItem',
label: t('protocol'),
rules: { required },
props: {
cProps: {
id: 'input-protocol',
choices: [
{ value: 'TCP', text: t('tcp') },

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@ -7,7 +7,6 @@ import api from '@/api'
import { useDomains, useUsersAndGroups } from '@/composables/data'
import { useForm } from '@/composables/form'
import { useInitialQueries } from '@/composables/useInitialQueries'
import { asUnreffed } from '@/helpers/commons'
import {
alphalownumdot_,
minLength,
@ -35,39 +34,37 @@ type Form = typeof form.value
const form = ref({
username: '',
fullname: '',
domain: '',
domain: mainDomain.value,
password: '',
confirmation: '',
})
const fields = {
username: {
username: reactive({
component: 'InputItem',
label: t('user_username'),
rules: asUnreffed(
computed(() => ({
rules: computed(() => ({
required,
alphalownumdot_,
notInUsers: unique(usernames),
})),
),
props: {
cProps: {
id: 'username',
placeholder: t('placeholder.username'),
},
} satisfies FieldProps<'InputItem', Form['username']>,
}) satisfies FieldProps<'InputItem', Form['username']>,
fullname: {
component: 'InputItem',
hr: true,
label: t('user_fullname'),
rules: { required, name },
props: {
cProps: {
id: 'fullname',
placeholder: t('placeholder.fullname'),
},
} satisfies FieldProps<'InputItem', Form['fullname']>,
domain: {
domain: reactive({
component: 'SelectItem',
hr: true,
id: 'mail',
@ -75,8 +72,8 @@ const fields = {
description: t('tip_about_user_email'),
descriptionVariant: 'info',
rules: { required },
props: { choices: asUnreffed(domainsAsChoices) },
} satisfies FieldProps<'SelectItem', Form['domain']>,
cProps: { choices: domainsAsChoices },
}) satisfies FieldProps<'SelectItem', Form['domain']>,
password: {
component: 'InputItem',
@ -84,28 +81,26 @@ const fields = {
description: t('good_practices_about_user_password'),
descriptionVariant: 'warning',
rules: { required, passwordLenght: minLength(8) },
props: {
cProps: {
id: 'password',
placeholder: '••••••••',
type: 'password',
},
} satisfies FieldProps<'InputItem', Form['password']>,
confirmation: {
confirmation: reactive({
component: 'InputItem',
label: t('password_confirmation'),
rules: asUnreffed(
computed(() => ({
rules: computed(() => ({
required,
passwordMatch: sameAs(form.value.password),
})),
),
props: {
cProps: {
id: 'confirmation',
placeholder: '••••••••',
type: 'password',
},
} satisfies FieldProps<'InputItem', Form['confirmation']>,
}) satisfies FieldProps<'InputItem', Form['confirmation']>,
} satisfies FormFieldDict<Form>
const { v, onSubmit } = useForm(form, fields)

View file

@ -8,7 +8,7 @@ import type ViewBase from '@/components/globals/ViewBase.vue'
import { useDomains, useUsersAndGroups } from '@/composables/data'
import { useArrayRule, useForm } from '@/composables/form'
import { useInitialQueries } from '@/composables/useInitialQueries'
import { arrayDiff, asUnreffed } from '@/helpers/commons'
import { arrayDiff } from '@/helpers/commons'
import {
emailForward,
emailLocalPart,
@ -58,30 +58,25 @@ const fields = reactive({
username: {
component: 'InputItem',
label: t('user_username'),
props: {
id: 'username',
disabled: true,
},
cProps: { id: 'username', disabled: true },
} satisfies FieldProps<'InputItem', Form['username']>,
fullname: {
component: 'InputItem',
label: t('user_fullname'),
rules: { required, nameValidator },
props: {
cProps: {
id: 'fullname',
placeholder: t('placeholder.fullname'),
},
} satisfies FieldProps<'InputItem', Form['fullname']>,
mail: {
mail: reactive({
component: 'AdressItem',
label: t('user_email'),
rules: {
localPart: { required, email: emailLocalPart },
},
props: { id: 'mail', choices: asUnreffed(domainsAsChoices) },
} satisfies FieldProps<'AdressItem', Form['mail']>,
rules: { localPart: { required, email: emailLocalPart } },
cProps: { id: 'mail', choices: domainsAsChoices },
}) satisfies FieldProps<'AdressItem', Form['mail']>,
mailbox_quota: {
append: 'M',
@ -90,35 +85,34 @@ const fields = reactive({
description: t('mailbox_quota_description'),
// example: t('mailbox_quota_example'),
rules: { integer, minValue: minValue(0) },
props: {
cProps: {
id: 'mailbox-quota',
placeholder: t('mailbox_quota_placeholder'),
},
} satisfies FieldProps<'InputItem', Form['mailbox_quota']>,
mail_aliases: {
mail_aliases: reactive({
component: 'AdressItem',
rules: asUnreffed(
useArrayRule(() => form.value.mail_aliases, {
rules: useArrayRule(() => form.value.mail_aliases, {
localPart: { required, email: emailLocalPart },
}),
),
props: {
cProps: {
placeholder: t('placeholder.username'),
choices: asUnreffed(domainsAsChoices),
choices: domainsAsChoices,
},
} satisfies FieldProps<'AdressItem', Form['mail_aliases']>,
}) satisfies FieldProps<'AdressItem', Form['mail_aliases']>,
mail_forward: {
mail_forward: reactive({
component: 'InputItem',
rules: asUnreffed(
useArrayRule(() => form.value.mail_forward, { required, emailForward }),
),
props: {
rules: useArrayRule(() => form.value.mail_forward, {
required,
emailForward,
}),
cProps: {
placeholder: t('user_new_forward'),
type: 'email',
},
} satisfies FieldProps<'InputItem', Form['mail_forward']>,
}) satisfies FieldProps<'InputItem', Form['mail_forward']>,
change_password: {
component: 'InputItem',
@ -126,7 +120,7 @@ const fields = reactive({
description: t('good_practices_about_user_password'),
descriptionVariant: 'warning',
rules: { passwordLenght: minLength(8) },
props: {
cProps: {
id: 'change_password',
type: 'password',
placeholder: '••••••••',
@ -134,19 +128,19 @@ const fields = reactive({
},
} satisfies FieldProps<'InputItem', Form['change_password']>,
confirmation: {
confirmation: reactive({
component: 'InputItem',
label: t('password_confirmation'),
rules: asUnreffed(
computed(() => ({ passwordMatch: sameAs(form.value.change_password) })),
),
props: {
rules: computed(() => ({
passwordMatch: sameAs(form.value.change_password),
})),
cProps: {
id: 'confirmation',
type: 'password',
placeholder: '••••••••',
autocomplete: 'new-password',
},
} satisfies FieldProps<'InputItem', Form['confirmation']>,
}) satisfies FieldProps<'InputItem', Form['confirmation']>,
} satisfies FormFieldDict<Form>)
const { v, onSubmit } = useForm(form, fields)

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@ -21,13 +21,13 @@ const form = ref({
update: false,
delete: false,
})
const fields = reactive({
const fields = {
csvfile: {
component: 'FileItem',
label: t('users_import_csv_file'),
description: t('users_import_csv_file_desc'),
rules: { file: required },
props: {
cProps: {
id: 'csvfile',
accept: 'text/csv',
placeholder: t('placeholder.file'),
@ -38,20 +38,16 @@ const fields = reactive({
label: t('users_import_update'),
description: t('users_import_update_desc'),
component: 'CheckboxItem',
props: {
id: 'update',
},
cProps: { id: 'update' },
} satisfies FieldProps<'CheckboxItem', Form['update']>,
delete: {
component: 'CheckboxItem',
label: t('users_import_delete'),
description: t('users_import_delete_desc'),
props: {
id: 'delete',
},
cProps: { id: 'delete' },
} satisfies FieldProps<'CheckboxItem', Form['delete']>,
} satisfies FormFieldDict<Form>)
} satisfies FormFieldDict<Form>
const { v, onSubmit } = useForm(form, fields)