mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
fix: form types & useForm fields rules reactivity
This commit is contained in:
parent
1b14c78195
commit
931e74ff29
7 changed files with 126 additions and 92 deletions
|
@ -11,6 +11,7 @@ import type {
|
|||
AnyDisplayComponents,
|
||||
AnyWritableComponents,
|
||||
BaseItemComputedProps,
|
||||
ButtonItemProps,
|
||||
FormFieldDict,
|
||||
} from '@/types/form'
|
||||
import { isDisplayComponent, isWritableComponent } from '@/types/form'
|
||||
|
@ -62,9 +63,9 @@ const slots = defineSlots<
|
|||
} & {
|
||||
[K in KeyOfStr<FFD> as `component:${K}`]?: (
|
||||
_: FFD[K]['component'] extends AnyWritableComponents
|
||||
? FFD[K]['props'] & BaseItemComputedProps<MV[K]>
|
||||
? FFD[K]['cProps'] & BaseItemComputedProps<MV[K]>
|
||||
: FFD[K]['component'] extends AnyDisplayComponents
|
||||
? FFD[K]['props']
|
||||
? FFD[K]['cProps']
|
||||
: never,
|
||||
) => any
|
||||
}
|
||||
|
@ -135,17 +136,17 @@ const Fields = createReusableTemplate<{
|
|||
<slot
|
||||
v-else-if="isDisplayComponent(field)"
|
||||
:name="`component:${k}`"
|
||||
v-bind="field.props"
|
||||
v-bind="field.cProps"
|
||||
>
|
||||
<Component
|
||||
:is="field.component"
|
||||
v-if="field.component !== 'ButtonItem'"
|
||||
v-bind="field.props"
|
||||
v-bind="field.cProps"
|
||||
/>
|
||||
<ButtonItem
|
||||
v-else
|
||||
v-bind="field.props"
|
||||
@action="emit('action', $event as typeof field.props.id)"
|
||||
v-bind="field.cProps as ButtonItemProps"
|
||||
@action="emit('action', $event as KeyOfStr<FFD>)"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
|
@ -181,12 +182,13 @@ const Fields = createReusableTemplate<{
|
|||
{{ section.name }}
|
||||
<small v-if="section.help">{{ section.help }}</small>
|
||||
</BCardTitle>
|
||||
|
||||
<!-- @vue-ignore-next-line -->
|
||||
<Fields.reuse :fields-props="section.fields" />
|
||||
</Component>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="fields">
|
||||
<!-- @vue-ignore-next-line -->
|
||||
<Fields.reuse :fields-props="fields" />
|
||||
</template>
|
||||
</slot>
|
||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
|||
BaseItemComputedProps,
|
||||
FormField,
|
||||
FormFieldProps,
|
||||
ItemComponentToItemProps,
|
||||
} from '@/types/form'
|
||||
|
||||
defineOptions({
|
||||
|
@ -24,6 +25,8 @@ defineOptions({
|
|||
const props = withDefaults(defineProps<FormFieldProps<C, MV>>(), {
|
||||
append: undefined,
|
||||
asInputGroup: false,
|
||||
component: undefined,
|
||||
cProps: undefined,
|
||||
description: undefined,
|
||||
descriptionVariant: undefined,
|
||||
id: undefined,
|
||||
|
@ -43,7 +46,7 @@ defineEmits<{
|
|||
|
||||
const slots = defineSlots<{
|
||||
default?: (
|
||||
componentProps: FormField<C, MV>['props'] & BaseItemComputedProps<MV>,
|
||||
componentProps: FormField<C, MV>['cProps'] & BaseItemComputedProps<MV>,
|
||||
) => any
|
||||
description?: any
|
||||
}>()
|
||||
|
@ -86,7 +89,7 @@ const computedAttrs = computed(() => {
|
|||
|
||||
const id = computed(() => {
|
||||
if (props.id) return props.id
|
||||
const childId = props.props?.id || props.labelFor
|
||||
const childId = props.cProps?.id || props.labelFor
|
||||
return childId ? `${childId}-field` : undefined
|
||||
})
|
||||
|
||||
|
@ -132,7 +135,7 @@ const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{
|
|||
<!-- Make field props and state available as scoped slot data -->
|
||||
<slot
|
||||
v-bind="{
|
||||
...props.props,
|
||||
...(props.cProps ?? ({} as ItemComponentToItemProps[C])),
|
||||
ariaDescribedby,
|
||||
modelValue: props.modelValue,
|
||||
state,
|
||||
|
@ -141,7 +144,7 @@ const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{
|
|||
>
|
||||
<!-- if no component was passed as slot, render a component from the props -->
|
||||
<Component
|
||||
v-bind="props.props"
|
||||
v-bind="props.cProps"
|
||||
:is="component"
|
||||
v-model="model"
|
||||
:aria-describedby="ariaDescribedby"
|
||||
|
@ -156,7 +159,7 @@ const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{
|
|||
v-bind="computedAttrs"
|
||||
:id="id"
|
||||
:label="label"
|
||||
:label-for="labelFor || props.props?.id"
|
||||
:label-for="labelFor || props.cProps?.id"
|
||||
:state="state"
|
||||
>
|
||||
<template #default="{ ariaDescribedby }">
|
||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
|||
BaseItemComputedProps,
|
||||
FormField,
|
||||
FormFieldProps,
|
||||
ItemComponentToItemProps,
|
||||
} from '@/types/form'
|
||||
|
||||
defineOptions({
|
||||
|
@ -23,13 +24,15 @@ defineOptions({
|
|||
const props = withDefaults(
|
||||
defineProps<
|
||||
FormFieldProps<C, MV> & {
|
||||
defaultValue: () => ArrInnerType<MV>
|
||||
addBtnText: string
|
||||
defaultValue?: () => ArrInnerType<MV>
|
||||
addBtnText?: string
|
||||
}
|
||||
>(),
|
||||
{
|
||||
append: undefined,
|
||||
asInputGroup: false,
|
||||
component: undefined,
|
||||
cProps: undefined,
|
||||
description: undefined,
|
||||
descriptionVariant: undefined,
|
||||
id: undefined,
|
||||
|
@ -38,6 +41,8 @@ const props = withDefaults(
|
|||
link: undefined,
|
||||
prepend: undefined,
|
||||
rules: undefined,
|
||||
defaultValue: undefined,
|
||||
addBtnText: undefined,
|
||||
|
||||
modelValue: undefined,
|
||||
validation: undefined,
|
||||
|
@ -50,7 +55,7 @@ const emit = defineEmits<{
|
|||
|
||||
const slots = defineSlots<{
|
||||
default?: (_: {
|
||||
componentProps: FormField<C, ArrInnerType<MV>>['props'] &
|
||||
componentProps: FormField<C, ArrInnerType<MV>>['cProps'] &
|
||||
BaseItemComputedProps<ArrInnerType<MV>>
|
||||
index: number
|
||||
}) => any
|
||||
|
@ -86,7 +91,7 @@ const computedAttrs = computed(() => {
|
|||
|
||||
const id = computed(() => {
|
||||
if (props.id) return props.id
|
||||
return props.props?.id ? props.props?.id + '_group' : undefined
|
||||
return props.cProps?.id ? props.cProps?.id + '_group' : undefined
|
||||
})
|
||||
|
||||
const error = computed(() => {
|
||||
|
@ -101,9 +106,9 @@ const subProps = computed<FormFieldProps<C, ArrInnerType<MV>>[]>(() => {
|
|||
return (
|
||||
props.modelValue?.map((modelValue: ArrInnerType<MV>, i) => {
|
||||
return {
|
||||
props: {
|
||||
...props.props,
|
||||
id: `${props.props.id}.${i}`,
|
||||
cProps: {
|
||||
...(props.cProps ?? ({} as ItemComponentToItemProps[C])),
|
||||
id: `${props.cProps?.id}.${i}`,
|
||||
},
|
||||
validation: props.validation?.[i],
|
||||
modelValue,
|
||||
|
@ -138,7 +143,7 @@ const errorMessage = computed(() => {
|
|||
})
|
||||
|
||||
function addElement() {
|
||||
const value = [...(props?.modelValue || []), props.defaultValue()] as MV
|
||||
const value = [...(props?.modelValue || []), props.defaultValue!()] as MV
|
||||
emit('update:modelValue', value)
|
||||
|
||||
// FIXME: Focus newly inserted form item
|
||||
|
@ -172,16 +177,25 @@ function updateElement(index: number, newValue: ArrInnerType<MV>) {
|
|||
</template>
|
||||
</FormField>
|
||||
|
||||
<BButton variant="danger" @click="removeElement(index)">
|
||||
<BButton
|
||||
v-if="defaultValue !== undefined"
|
||||
variant="danger"
|
||||
@click="removeElement(index)"
|
||||
>
|
||||
<YIcon :title="$t('delete')" iname="trash-o" />
|
||||
<span class="visually-hidden">{{ $t('delete') }}</span>
|
||||
</BButton>
|
||||
</div>
|
||||
|
||||
<BButton variant="success" @click="addElement()">
|
||||
<YIcon iname="plus" /> {{ $t('user_emailaliases_add') }}
|
||||
<BButton
|
||||
v-if="defaultValue !== undefined"
|
||||
variant="success"
|
||||
@click="addElement()"
|
||||
>
|
||||
<YIcon iname="plus" /> {{ addBtnText ?? $t('add') }}
|
||||
</BButton>
|
||||
|
||||
<!-- FIXME is it needed? or more generic error like "errors in this multiple fields" -->
|
||||
<template #invalid-feedback>
|
||||
<span v-html="errorMessage" />
|
||||
</template>
|
||||
|
|
|
@ -4,14 +4,13 @@ import { computed, toValue } from 'vue'
|
|||
import type { ButtonItemProps } from '@/types/form'
|
||||
|
||||
const props = withDefaults(defineProps<ButtonItemProps>(), {
|
||||
id: undefined,
|
||||
enabled: true,
|
||||
icon: undefined,
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: [value: ButtonItemProps['id']]
|
||||
action: [value: string]
|
||||
}>()
|
||||
|
||||
const icon = computed(() => {
|
||||
|
|
|
@ -6,7 +6,6 @@ import type {
|
|||
WritableComputedRef,
|
||||
} from 'vue'
|
||||
import { computed, ref, toValue, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { APIBadRequestError, APIError } from '@/api/errors'
|
||||
|
@ -14,6 +13,7 @@ import { deepSetErrors, useForm, type FormValidation } from '@/composables/form'
|
|||
import { isObjectLiteral } from '@/helpers/commons'
|
||||
import * as validators from '@/helpers/validators'
|
||||
import { formatForm, formatI18nField } from '@/helpers/yunohostArguments'
|
||||
import i18n from '@/i18n'
|
||||
import type { CustomRoute, KeyOfStr, MergeUnion, Obj } from '@/types/commons'
|
||||
import type {
|
||||
AnyFormField,
|
||||
|
@ -96,23 +96,24 @@ function formatOption(option: AnyOption, form: Ref<Obj>): AnyFormField {
|
|||
|
||||
if (isIn(ANY_DISPLAY_OPTION_TYPE, option)) {
|
||||
const component = OPTION_COMPONENT_RESOLVER[option.type]
|
||||
// TODO: could be improved, for simplicity props can be be any display item props
|
||||
// TODO: could be improved, for simplicity cProps can be be any display item props
|
||||
// but this is not type safe.
|
||||
const props = {
|
||||
const cProps = {
|
||||
label: formatI18nField(option.ask),
|
||||
id: option.id,
|
||||
} as MergeUnion<AnyDisplayItemProps>
|
||||
const field: FormFieldDisplay<typeof component> = {
|
||||
component,
|
||||
visible,
|
||||
props,
|
||||
cProps,
|
||||
rules: undefined,
|
||||
}
|
||||
|
||||
if (isIn(['button', 'alert'], option)) {
|
||||
props.type = option.style
|
||||
props.icon = option.icon
|
||||
cProps.type = option.style
|
||||
cProps.icon = option.icon
|
||||
if (option.type === 'button') {
|
||||
props.enabled = useExpression(option.enabled, form)
|
||||
cProps.enabled = useExpression(option.enabled, form)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,42 +125,42 @@ function formatOption(option: AnyOption, form: Ref<Obj>): AnyFormField {
|
|||
}
|
||||
|
||||
const component = OPTION_COMPONENT_RESOLVER[option.type]
|
||||
// TODO: could be improved, for simplicity props can be be any writable item props
|
||||
// TODO: could be improved, for simplicity cProps can be be any writable item props
|
||||
// but this is not type safe.
|
||||
const props = {
|
||||
const cProps = {
|
||||
id: option.id,
|
||||
placeholder: option.example,
|
||||
} as MergeUnion<AnyWritableItemProps>
|
||||
const rules: FormField['rules'] = {}
|
||||
const field:
|
||||
| FormField<typeof component>
|
||||
| FormFieldReadonly<typeof component> = {
|
||||
const field: FormField<typeof component> = {
|
||||
component,
|
||||
label: formatI18nField(option.ask),
|
||||
props,
|
||||
readonly: option.readonly,
|
||||
rules,
|
||||
rules: option.readonly ? undefined : rules,
|
||||
visible,
|
||||
description: formatI18nField(option.help),
|
||||
}
|
||||
|
||||
// We don't care about component props in case of readonly
|
||||
if (field.readonly) return field
|
||||
if (option.readonly) {
|
||||
return { ...field, readonly: true } as FormFieldReadonly<typeof component>
|
||||
} else {
|
||||
field.cProps = cProps
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const t = i18n.global.t
|
||||
|
||||
if (isIn(ANY_INPUT_OPTION_TYPE, option)) {
|
||||
props.type = isIn(['string', 'path'], option) ? 'text' : option.type
|
||||
cProps.type = isIn(['string', 'path'], option) ? 'text' : option.type
|
||||
// trim
|
||||
// autocomplete
|
||||
|
||||
if (option.type === 'password') {
|
||||
field.description ??= t('good_practices_about_admin_password')
|
||||
rules.passwordLenght = validators.minLength(8)
|
||||
props.placeholder = '••••••••••••'
|
||||
cProps.placeholder = '••••••••••••'
|
||||
} else if (isIn(['number', 'range'], option)) {
|
||||
rules.numValue = validators.integer
|
||||
props.step = option.step
|
||||
cProps.step = option.step
|
||||
|
||||
if (option.min !== undefined) {
|
||||
rules.minValue = validators.minValue(option.min)
|
||||
|
@ -169,7 +170,7 @@ function formatOption(option: AnyOption, form: Ref<Obj>): AnyFormField {
|
|||
}
|
||||
}
|
||||
} else if (isIn(['select', 'user', 'domain', 'app', 'group'], option)) {
|
||||
props.choices = isObjectLiteral(option.choices)
|
||||
cProps.choices = isObjectLiteral(option.choices)
|
||||
? Object.entries(option.choices).map(([k, v]) => ({
|
||||
text: v,
|
||||
value: k,
|
||||
|
@ -182,23 +183,23 @@ function formatOption(option: AnyOption, form: Ref<Obj>): AnyFormField {
|
|||
}
|
||||
}
|
||||
} else if (isIn(['tags', 'tags-select'], option)) {
|
||||
// props.limit = option.limit // FIXME limit is not defined in core?
|
||||
props.placeholder = option.placeholder
|
||||
props.tagIcon = option.icon
|
||||
// cProps.limit = option.limit // FIXME limit is not defined in core?
|
||||
cProps.placeholder = option.placeholder
|
||||
cProps.tagIcon = option.icon
|
||||
|
||||
if ('tags-select' === option.type) {
|
||||
props.options = option.choices
|
||||
props.auto = true
|
||||
props.itemsName = ''
|
||||
props.label = option.placeholder
|
||||
cProps.options = option.choices
|
||||
cProps.auto = true
|
||||
cProps.itemsName = ''
|
||||
cProps.label = option.placeholder
|
||||
}
|
||||
} else if ('boolean' === option.type) {
|
||||
// FIXME
|
||||
// props.choices = option.choices
|
||||
// cProps.choices = option.choices
|
||||
}
|
||||
|
||||
if ('file' === option.type) {
|
||||
props.accept = option.accept
|
||||
cProps.accept = option.accept
|
||||
}
|
||||
|
||||
if ('boolean' !== option.type && option.optional === false) {
|
||||
|
@ -233,6 +234,7 @@ export function formatOptions<MV extends Obj>(
|
|||
fields: FormFieldDict<MV>
|
||||
form: Ref<MV>
|
||||
} {
|
||||
// FIXME handle optional for app install ? or is already handled in core bookworm?
|
||||
const form = ref(
|
||||
Object.fromEntries(
|
||||
options
|
||||
|
@ -373,7 +375,6 @@ function useEvaluation(expression: string, form: MaybeRefOrGetter<Obj>) {
|
|||
|
||||
return computed(() => {
|
||||
const { exp, ctx } = buildContext(toValue(form))
|
||||
|
||||
try {
|
||||
return !!evaluate(ctx, exp)
|
||||
} catch {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
// eslint-disable-next-line vue/prefer-import-from-vue
|
||||
import { isFunction } from '@vue/shared'
|
||||
import type {
|
||||
BaseValidation,
|
||||
ServerErrors,
|
||||
Validation,
|
||||
ValidationArgs,
|
||||
ValidationRuleCollection,
|
||||
} from '@vuelidate/core'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { computedWithControl } from '@vueuse/core'
|
||||
import { watchImmediate } from '@vueuse/core'
|
||||
import type {
|
||||
ComputedRef,
|
||||
InjectionKey,
|
||||
|
@ -14,11 +15,11 @@ import type {
|
|||
Ref,
|
||||
WritableComputedRef,
|
||||
} from 'vue'
|
||||
import { computed, inject, provide, reactive, toValue } from 'vue'
|
||||
import { computed, inject, provide, reactive, ref, toValue } from 'vue'
|
||||
|
||||
import { APIBadRequestError, type APIError } from '@/api/errors'
|
||||
import type { Obj } from '@/types/commons'
|
||||
import type { FormField, FormFieldDict } from '@/types/form'
|
||||
import type { FormFieldDict } from '@/types/form'
|
||||
|
||||
export const clearServerErrorsSymbol = Symbol() as InjectionKey<
|
||||
(key?: string) => void
|
||||
|
@ -57,25 +58,32 @@ export type FormValidation<MV extends Obj> = Validation<
|
|||
export function useForm<
|
||||
MV extends Obj,
|
||||
FFD extends FormFieldDict<MV> = FormFieldDict<MV>,
|
||||
>(form: Ref<MV> | WritableComputedRef<MV>, fields: MaybeRefOrGetter<FFD>) {
|
||||
>(form: Ref<MV> | WritableComputedRef<MV>, fields: FFD | (() => FFD)) {
|
||||
const serverErrors = reactive<ServerErrors>({})
|
||||
const validByDefault: ValidationRuleCollection = { true: () => true }
|
||||
const rules = computedWithControl(
|
||||
() => toValue(fields),
|
||||
() => {
|
||||
const fs = toValue(fields)
|
||||
const validations = Object.keys(form.value).map((key: keyof MV) => [
|
||||
key,
|
||||
(fs[key] as FormField).rules ?? validByDefault,
|
||||
])
|
||||
const rules: ValidationArgs<MV> = Object.fromEntries(validations)
|
||||
return {
|
||||
// create a fake validation rule for global state to be able to add $externalResult errors to it
|
||||
global: { true: () => true },
|
||||
form: rules,
|
||||
}
|
||||
},
|
||||
)
|
||||
const validByDefault = { true: () => true as const }
|
||||
// create a fake validation rule for global state to be able to add $externalResult errors to it
|
||||
const rules = ref({ global: validByDefault, form: {} }) as Ref<{
|
||||
global: { true: () => true }
|
||||
form: ValidationArgs<MV>
|
||||
}>
|
||||
function updateRules(ffd: FFD) {
|
||||
const validations = Object.keys(form.value).map((key: keyof MV) => [
|
||||
key,
|
||||
ffd[key].rules ?? validByDefault,
|
||||
])
|
||||
const formRules: ValidationArgs<MV> = Object.fromEntries(validations)
|
||||
rules.value = { global: { true: () => true }, form: formRules }
|
||||
}
|
||||
if (isFunction(fields)) {
|
||||
watchImmediate(fields, () => {
|
||||
updateRules(toValue(fields))
|
||||
})
|
||||
} else {
|
||||
watchImmediate(
|
||||
Object.keys(form.value).map((key: keyof MV) => () => fields[key].rules),
|
||||
() => updateRules(fields),
|
||||
)
|
||||
}
|
||||
|
||||
const v: Ref<FormValidation<MV>> = useVuelidate(
|
||||
rules,
|
||||
|
|
|
@ -10,6 +10,7 @@ import { isObjectLiteral } from '@/helpers/commons'
|
|||
import type { ArrInnerType, Cols, Obj, StateVariant } from '@/types/commons'
|
||||
|
||||
type StateValidation = false | null
|
||||
type Choices = string[] | { text: string; value: string }[]
|
||||
|
||||
// DISPLAY
|
||||
|
||||
|
@ -20,6 +21,7 @@ type BaseDisplayItemProps = {
|
|||
|
||||
export type ButtonItemProps = BaseDisplayItemProps & {
|
||||
// FIXME compute enabled JSExpression
|
||||
id: string
|
||||
enabled?: boolean | ComputedRef<boolean>
|
||||
icon?: string
|
||||
type?: StateVariant
|
||||
|
@ -41,6 +43,7 @@ type BaseWritableItemProps = {
|
|||
name?: string
|
||||
placeholder?: string
|
||||
touchKey?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export type BaseItemComputedProps<MV extends any = any> = {
|
||||
|
@ -51,7 +54,7 @@ export type BaseItemComputedProps<MV extends any = any> = {
|
|||
}
|
||||
|
||||
export type AdressItemProps = BaseWritableItemProps & {
|
||||
choices: string[]
|
||||
choices: Choices
|
||||
type?: 'domain' | 'email'
|
||||
}
|
||||
export type AdressModelValue = {
|
||||
|
@ -64,7 +67,7 @@ export type CheckboxItemProps = BaseWritableItemProps & {
|
|||
label?: string
|
||||
labels?: { true: string; false: string }
|
||||
// FIXME unused?
|
||||
// choices: string[]
|
||||
// choices: Choices
|
||||
}
|
||||
|
||||
export type FileItemProps = BaseWritableItemProps & {
|
||||
|
@ -89,7 +92,7 @@ export type InputItemProps = BaseWritableItemProps & {
|
|||
| 'current-password'
|
||||
| 'url'
|
||||
// pattern?: object
|
||||
// choices?: string[] FIXME rm ?
|
||||
// choices?: Choices FIXME rm ?
|
||||
step?: number
|
||||
trim?: boolean
|
||||
type?:
|
||||
|
@ -107,7 +110,7 @@ export type InputItemProps = BaseWritableItemProps & {
|
|||
}
|
||||
|
||||
export type SelectItemProps = BaseWritableItemProps & {
|
||||
choices: string[] | { text: string; value: string }[]
|
||||
choices: Choices
|
||||
}
|
||||
|
||||
export type TagsItemProps = BaseWritableItemProps & {
|
||||
|
@ -205,7 +208,7 @@ export function isNonWritableComponent(
|
|||
return isDisplayComponent(field) || !!field.readonly
|
||||
}
|
||||
|
||||
type ItemComponentToItemProps = {
|
||||
export type ItemComponentToItemProps = {
|
||||
// DISPLAY
|
||||
ButtonItem: ButtonItemProps
|
||||
DisplayTextItem: DisplayTextItemProps
|
||||
|
@ -234,11 +237,11 @@ type BaseFormFieldComputedProps<MV extends any = any> = {
|
|||
}
|
||||
|
||||
type BaseFormField<C extends AnyItemComponents> = {
|
||||
component: C
|
||||
component?: C
|
||||
cProps?: ItemComponentToItemProps[C]
|
||||
hr?: boolean
|
||||
id?: string
|
||||
label?: string
|
||||
props?: ItemComponentToItemProps[C]
|
||||
readonly?: boolean
|
||||
visible?: boolean | ComputedRef<boolean>
|
||||
}
|
||||
|
@ -249,14 +252,14 @@ export type FormField<
|
|||
> = BaseFormField<C> & {
|
||||
append?: string
|
||||
asInputGroup?: boolean
|
||||
cProps?: ItemComponentToItemProps[C]
|
||||
description?: string
|
||||
descriptionVariant?: StateVariant
|
||||
labelFor?: string
|
||||
link?:
|
||||
| { text: string; name: RouteLocationRaw }
|
||||
| { text: string; href: string }
|
||||
props: ItemComponentToItemProps[C]
|
||||
rules?: FormFieldRules<MV>
|
||||
rules?: FormFieldRules<MV> | ComputedRef<FormFieldRules<MV>>
|
||||
prepend?: string
|
||||
readonly?: false
|
||||
}
|
||||
|
@ -267,16 +270,18 @@ export type FormFieldReadonly<
|
|||
label: string
|
||||
cols?: Cols
|
||||
readonly: true
|
||||
rules: undefined
|
||||
}
|
||||
|
||||
export type FormFieldDisplay<
|
||||
C extends AnyDisplayComponents = AnyDisplayComponents,
|
||||
> = {
|
||||
component: C
|
||||
props: ItemComponentToItemProps[C]
|
||||
component?: C
|
||||
cProps?: ItemComponentToItemProps[C]
|
||||
visible?: boolean | ComputedRef<boolean>
|
||||
hr?: boolean
|
||||
readonly?: true
|
||||
rules: undefined
|
||||
}
|
||||
|
||||
export type FormFieldProps<
|
||||
|
@ -305,12 +310,14 @@ export type FormFieldDict<T extends Obj = Obj> = {
|
|||
|
||||
// Type to check if object satisfies specified Field and Item
|
||||
export type FieldProps<
|
||||
C extends AnyItemComponents = 'InputItem',
|
||||
C extends AnyItemComponents = AnyItemComponents,
|
||||
MV extends any = never,
|
||||
> = C extends AnyWritableComponents
|
||||
? FormField<C, MV> | FormFieldReadonly<C>
|
||||
?
|
||||
| (FormField<C, MV> & { component: C })
|
||||
| (FormFieldReadonly<C> & { component: C })
|
||||
: C extends AnyDisplayComponents
|
||||
? FormFieldDisplay<C>
|
||||
? FormFieldDisplay<C> & { component: C }
|
||||
: never
|
||||
|
||||
export function isFileModelValue(value: any): value is FileModelValue {
|
||||
|
|
Loading…
Reference in a new issue