mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
refactor: CardForm ts + composition
This commit is contained in:
parent
f8b7f89488
commit
3c3f50ca60
3 changed files with 130 additions and 30 deletions
|
@ -1,79 +1,161 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
import type { BaseValidation } from '@vuelidate/core'
|
setup
|
||||||
import { computed } from 'vue'
|
lang="ts"
|
||||||
|
generic="
|
||||||
|
MV extends Obj,
|
||||||
|
FFD extends FormFieldDict<MV>,
|
||||||
|
V extends Validation<
|
||||||
|
ValidationArgs<unknown>,
|
||||||
|
{ form: Ref<MV>; global: null }
|
||||||
|
>
|
||||||
|
"
|
||||||
|
>
|
||||||
|
import type { Validation, ValidationArgs } from '@vuelidate/core'
|
||||||
|
import { computed, toValue } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import type { VueClass } from '@/types/commons'
|
import { toEntries } from '@/helpers/commons'
|
||||||
|
import type { Obj, VueClass } from '@/types/commons'
|
||||||
|
import type {
|
||||||
|
AnyDisplayComponents,
|
||||||
|
AnyWritableComponents,
|
||||||
|
BaseItemComputedProps,
|
||||||
|
FormFieldDict,
|
||||||
|
} from '@/types/form'
|
||||||
|
import { isDisplayComponent, isWritableComponent } from '@/types/form'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
id?: string
|
id?: string
|
||||||
|
modelValue?: MV
|
||||||
|
fields?: FFD
|
||||||
|
validations?: V
|
||||||
submitText?: string
|
submitText?: string
|
||||||
validation?: BaseValidation
|
|
||||||
serverError?: string
|
|
||||||
inline?: boolean
|
inline?: boolean
|
||||||
formClasses?: VueClass
|
formClasses?: VueClass
|
||||||
noFooter?: boolean
|
noFooter?: boolean
|
||||||
|
hr?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
id: 'ynh-form',
|
id: 'ynh-form',
|
||||||
|
modelValue: undefined,
|
||||||
|
fields: undefined,
|
||||||
|
validations: undefined,
|
||||||
submitText: undefined,
|
submitText: undefined,
|
||||||
validation: undefined,
|
|
||||||
serverError: '',
|
|
||||||
inline: false,
|
inline: false,
|
||||||
formClasses: undefined,
|
formClasses: undefined,
|
||||||
noFooter: false,
|
noFooter: false,
|
||||||
|
hr: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: [e: SubmitEvent]
|
submit: [e: SubmitEvent]
|
||||||
|
'update:modelValue': [modelValue: MV]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const slots = defineSlots<
|
||||||
|
{
|
||||||
|
disclaimer?: any
|
||||||
|
default?: any
|
||||||
|
'server-error'?: any
|
||||||
|
buttons: any
|
||||||
|
} & {
|
||||||
|
[K in Extract<keyof MV, string> as `field:${K}`]?: (_: FFD[K]) => any
|
||||||
|
} & {
|
||||||
|
[K in Extract<keyof FFD, string> as `component:${K}`]?: (
|
||||||
|
_: FFD[K]['component'] extends AnyWritableComponents
|
||||||
|
? FFD[K]['props'] & BaseItemComputedProps<MV[K]>
|
||||||
|
: FFD[K]['component'] extends AnyDisplayComponents
|
||||||
|
? FFD[K]['props']
|
||||||
|
: never,
|
||||||
|
) => any
|
||||||
|
}
|
||||||
|
>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const errorFeedback = computed(() => {
|
const globalErrorFeedback = computed(() => {
|
||||||
const v = props.validation
|
const v = props.validations
|
||||||
return (
|
if (!v) return ''
|
||||||
props.serverError ||
|
const externalResults = toValue(v.global.$externalResults[0]?.$message)
|
||||||
(v && v.$errors.length ? t('form_errors.invalid_form') : '')
|
return externalResults ?? (v.form.$error ? t('form_errors.invalid_form') : '')
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function onSubmit(e: Event) {
|
const fields = computed(() => (props.fields ? toEntries(props.fields) : []))
|
||||||
const v = props.validation
|
|
||||||
if (v) {
|
function onModelUpdate(key: keyof MV, value: MV[keyof MV]) {
|
||||||
v.$touch()
|
emit('update:modelValue', {
|
||||||
if (v.$pending || v.$errors.length) return
|
...props.modelValue!,
|
||||||
}
|
[key]: value,
|
||||||
emit('submit', e as SubmitEvent)
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- FIXME inheritAttrs false? probably remove vbind instead -->
|
<YCard class="card-form">
|
||||||
<YCard v-bind="$attrs" class="card-form">
|
|
||||||
<template #default>
|
<template #default>
|
||||||
<slot name="disclaimer" />
|
<slot name="disclaimer" />
|
||||||
|
|
||||||
{{ serverError }}
|
|
||||||
<BForm
|
<BForm
|
||||||
:id="id"
|
:id="id"
|
||||||
:inline="inline"
|
:inline="inline"
|
||||||
:class="formClasses"
|
:class="formClasses"
|
||||||
@submit.prevent.stop="onSubmit"
|
|
||||||
novalidate
|
novalidate
|
||||||
|
@submit.prevent.stop="emit('submit', $event as SubmitEvent)"
|
||||||
>
|
>
|
||||||
<slot name="default" />
|
<slot name="default">
|
||||||
|
<template v-for="[key, fieldProps] in fields" :key="key">
|
||||||
|
<template v-if="fieldProps.visible ?? true">
|
||||||
|
<slot
|
||||||
|
v-if="isWritableComponent<MV[typeof key]>(fieldProps)"
|
||||||
|
:name="`field:${key}`"
|
||||||
|
v-bind="fieldProps"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-if="!fieldProps.readonly"
|
||||||
|
v-bind="fieldProps"
|
||||||
|
:model-value="props.modelValue![key]"
|
||||||
|
:validation="props.validations?.form[key]"
|
||||||
|
@update:model-value="onModelUpdate(key, $event)"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-if="slots[`component:${key}`]"
|
||||||
|
#default="childProps"
|
||||||
|
>
|
||||||
|
<slot :name="`component:${key}`" v-bind="childProps" />
|
||||||
|
</template>
|
||||||
|
</FormField>
|
||||||
|
<FormFieldReadonly
|
||||||
|
v-else
|
||||||
|
v-bind="fieldProps"
|
||||||
|
:model-value="props.modelValue![key]"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
<slot
|
||||||
|
v-else-if="isDisplayComponent(fieldProps)"
|
||||||
|
:name="`component:${key}`"
|
||||||
|
v-bind="fieldProps.props"
|
||||||
|
>
|
||||||
|
<Component
|
||||||
|
:is="fieldProps.component"
|
||||||
|
v-bind="fieldProps.props"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<hr v-if="fieldProps.hr ?? hr" />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</slot>
|
||||||
|
|
||||||
<slot name="server-error">
|
<slot name="server-error">
|
||||||
<BAlert
|
<BAlert
|
||||||
variant="danger"
|
variant="danger"
|
||||||
class="my-3"
|
class="my-3"
|
||||||
icon="ban"
|
icon="ban"
|
||||||
:modelValue="errorFeedback !== ''"
|
:model-value="globalErrorFeedback !== ''"
|
||||||
>
|
>
|
||||||
<div v-html="errorFeedback" />
|
<div v-html="globalErrorFeedback" />
|
||||||
</BAlert>
|
</BAlert>
|
||||||
</slot>
|
</slot>
|
||||||
</BForm>
|
</BForm>
|
||||||
|
@ -82,7 +164,7 @@ function onSubmit(e: Event) {
|
||||||
<template v-if="!noFooter" #buttons>
|
<template v-if="!noFooter" #buttons>
|
||||||
<slot name="buttons">
|
<slot name="buttons">
|
||||||
<BButton type="submit" variant="success" :form="id">
|
<BButton type="submit" variant="success" :form="id">
|
||||||
{{ submitText ? submitText : $t('save') }}
|
{{ submitText ?? $t('save') }}
|
||||||
</BButton>
|
</BButton>
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -145,6 +145,13 @@ export function getFileContent(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toEntries<T extends Record<PropertyKey, unknown>>(
|
||||||
|
obj: T,
|
||||||
|
): { [K in keyof T]: [K, T[K]] }[keyof T][] {
|
||||||
|
return Object.entries(obj) as { [K in keyof T]: [K, T[K]] }[keyof T][]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function omit<T extends Obj, K extends (keyof T)[]>(
|
export function omit<T extends Obj, K extends (keyof T)[]>(
|
||||||
obj: T,
|
obj: T,
|
||||||
keys: K,
|
keys: K,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import type {
|
||||||
} from '@vuelidate/core'
|
} from '@vuelidate/core'
|
||||||
import type { RouteLocationRaw } from 'vue-router'
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
|
|
||||||
import type { ArrInnerType, Cols } from '@/types/commons'
|
import type { ArrInnerType, Cols, Obj } from '@/types/commons'
|
||||||
|
|
||||||
type StateValidation = false | null
|
type StateValidation = false | null
|
||||||
type StateVariant = 'success' | 'info' | 'warning' | 'danger'
|
type StateVariant = 'success' | 'info' | 'warning' | 'danger'
|
||||||
|
@ -264,3 +264,14 @@ export type FormFieldReadonlyProps<
|
||||||
> = Omit<FormFieldReadonly<C>, 'hr' | 'visible' | 'readonly'> & {
|
> = Omit<FormFieldReadonly<C>, 'hr' | 'visible' | 'readonly'> & {
|
||||||
modelValue?: MV
|
modelValue?: MV
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FormFieldDict<T extends Obj = Obj> = {
|
||||||
|
[k in keyof T | string]: k extends keyof T
|
||||||
|
?
|
||||||
|
| FormField<AnyWritableComponents, T[k]>
|
||||||
|
| FormFieldReadonly<AnyWritableComponents>
|
||||||
|
:
|
||||||
|
| FormField<AnyWritableComponents>
|
||||||
|
| FormFieldReadonly<AnyWritableComponents>
|
||||||
|
| FormFieldDisplay<AnyDisplayComponents>
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue