mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
feat: add config panels sections display in CardForm
This commit is contained in:
parent
7a1b4ba453
commit
d3f843b1b1
1 changed files with 111 additions and 56 deletions
|
@ -1,21 +1,12 @@
|
||||||
<script
|
<script setup lang="ts" generic="MV extends Obj, FFD extends FormFieldDict<MV>">
|
||||||
setup
|
import { createReusableTemplate } from '@vueuse/core'
|
||||||
lang="ts"
|
import { computed, reactive, toValue } from 'vue'
|
||||||
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 { FormValidation } from '@/composables/form'
|
||||||
import { toEntries } from '@/helpers/commons'
|
import { toEntries } from '@/helpers/commons'
|
||||||
import type { Obj, VueClass } from '@/types/commons'
|
import type { KeyOfStr, Obj, VueClass } from '@/types/commons'
|
||||||
|
import type { ConfigSection } from '@/types/configPanels'
|
||||||
import type {
|
import type {
|
||||||
AnyDisplayComponents,
|
AnyDisplayComponents,
|
||||||
AnyWritableComponents,
|
AnyWritableComponents,
|
||||||
|
@ -29,12 +20,13 @@ const props = withDefaults(
|
||||||
id?: string
|
id?: string
|
||||||
modelValue?: MV
|
modelValue?: MV
|
||||||
fields?: FFD
|
fields?: FFD
|
||||||
validations?: V
|
validations?: FormValidation<MV>
|
||||||
submitText?: string
|
submitText?: string
|
||||||
inline?: boolean
|
inline?: boolean
|
||||||
formClasses?: VueClass
|
formClasses?: VueClass
|
||||||
noFooter?: boolean
|
noFooter?: boolean
|
||||||
hr?: boolean
|
hr?: boolean
|
||||||
|
sections?: ConfigSection<MV, FFD>[]
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
id: 'ynh-form',
|
id: 'ynh-form',
|
||||||
|
@ -46,24 +38,29 @@ const props = withDefaults(
|
||||||
formClasses: undefined,
|
formClasses: undefined,
|
||||||
noFooter: false,
|
noFooter: false,
|
||||||
hr: false,
|
hr: false,
|
||||||
|
sections: undefined,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: [e: SubmitEvent]
|
submit: [e: SubmitEvent]
|
||||||
|
action: [actionId: KeyOfStr<FFD>] //, sectionId?: ConfigSection<MV, FFD>['id']]
|
||||||
'update:modelValue': [modelValue: MV]
|
'update:modelValue': [modelValue: MV]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const slots = defineSlots<
|
const slots = defineSlots<
|
||||||
{
|
{
|
||||||
|
top?: any
|
||||||
disclaimer?: any
|
disclaimer?: any
|
||||||
|
'before-form'?: any
|
||||||
default?: any
|
default?: any
|
||||||
'server-error'?: any
|
'server-error'?: any
|
||||||
|
'after-form'?: any
|
||||||
buttons: any
|
buttons: any
|
||||||
} & {
|
} & {
|
||||||
[K in Extract<keyof MV, string> as `field:${K}`]?: (_: FFD[K]) => any
|
[K in KeyOfStr<FFD> as `field:${K}`]?: (_: FFD[K]) => any
|
||||||
} & {
|
} & {
|
||||||
[K in Extract<keyof FFD, string> as `component:${K}`]?: (
|
[K in KeyOfStr<FFD> as `component:${K}`]?: (
|
||||||
_: FFD[K]['component'] extends AnyWritableComponents
|
_: FFD[K]['component'] extends AnyWritableComponents
|
||||||
? FFD[K]['props'] & BaseItemComputedProps<MV[K]>
|
? FFD[K]['props'] & BaseItemComputedProps<MV[K]>
|
||||||
: FFD[K]['component'] extends AnyDisplayComponents
|
: FFD[K]['component'] extends AnyDisplayComponents
|
||||||
|
@ -83,6 +80,16 @@ const globalErrorFeedback = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const fields = computed(() => (props.fields ? toEntries(props.fields) : []))
|
const fields = computed(() => (props.fields ? toEntries(props.fields) : []))
|
||||||
|
const sections = computed(() => {
|
||||||
|
const { sections, fields } = props
|
||||||
|
if (!sections || !fields) return
|
||||||
|
return sections.map((section) => ({
|
||||||
|
...section,
|
||||||
|
fields: reactive(section.fields.map((id) => [id, fields[id]])) as {
|
||||||
|
[k in Extract<keyof FFD, string>]: [k, FFD[k]]
|
||||||
|
}[Extract<keyof FFD, string>][],
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
function onModelUpdate(key: keyof MV, value: MV[keyof MV]) {
|
function onModelUpdate(key: keyof MV, value: MV[keyof MV]) {
|
||||||
emit('update:modelValue', {
|
emit('update:modelValue', {
|
||||||
|
@ -90,13 +97,71 @@ function onModelUpdate(key: keyof MV, value: MV[keyof MV]) {
|
||||||
[key]: value,
|
[key]: value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Fields = createReusableTemplate<{
|
||||||
|
fieldsProps: { [k in Extract<keyof FFD, string>]: [k, FFD[k]] }[Extract<
|
||||||
|
keyof FFD,
|
||||||
|
string
|
||||||
|
>][]
|
||||||
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<YCard class="card-form">
|
<Fields.define v-slot="{ fieldsProps }">
|
||||||
|
<template v-for="[k, field] in fieldsProps" :key="k">
|
||||||
|
<template v-if="field.visible ?? true">
|
||||||
|
<slot
|
||||||
|
v-if="isWritableComponent<MV[typeof k]>(field)"
|
||||||
|
:name="`field:${k}`"
|
||||||
|
v-bind="field"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-if="!field.readonly"
|
||||||
|
v-bind="field"
|
||||||
|
:model-value="props.modelValue![k]"
|
||||||
|
:validation="props.validations?.form[k]"
|
||||||
|
@update:model-value="onModelUpdate(k, $event)"
|
||||||
|
>
|
||||||
|
<template v-if="slots[`component:${k}`]" #default="childProps">
|
||||||
|
<slot :name="`component:${k}`" v-bind="childProps" />
|
||||||
|
</template>
|
||||||
|
</FormField>
|
||||||
|
<FormFieldReadonly
|
||||||
|
v-else
|
||||||
|
v-bind="field"
|
||||||
|
:model-value="props.modelValue![k]"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
<slot
|
||||||
|
v-else-if="isDisplayComponent(field)"
|
||||||
|
:name="`component:${k}`"
|
||||||
|
v-bind="field.props"
|
||||||
|
>
|
||||||
|
<Component
|
||||||
|
:is="field.component"
|
||||||
|
v-if="field.component !== 'ButtonItem'"
|
||||||
|
v-bind="field.props"
|
||||||
|
/>
|
||||||
|
<ButtonItem
|
||||||
|
v-else
|
||||||
|
v-bind="field.props"
|
||||||
|
@action="emit('action', $event as typeof field.props.id)"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<hr v-if="field.hr ?? hr" />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Fields.define>
|
||||||
|
|
||||||
|
<YCard class="card-form" v-bind="$attrs">
|
||||||
<template #default>
|
<template #default>
|
||||||
|
<slot name="top" />
|
||||||
|
|
||||||
<slot name="disclaimer" />
|
<slot name="disclaimer" />
|
||||||
|
|
||||||
|
<slot name="before-form" />
|
||||||
|
|
||||||
<BForm
|
<BForm
|
||||||
:id="id"
|
:id="id"
|
||||||
:inline="inline"
|
:inline="inline"
|
||||||
|
@ -105,47 +170,25 @@ function onModelUpdate(key: keyof MV, value: MV[keyof MV]) {
|
||||||
@submit.prevent.stop="emit('submit', $event as SubmitEvent)"
|
@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="sections">
|
||||||
<template v-if="fieldProps.visible ?? true">
|
<template v-for="section in sections" :key="section.id">
|
||||||
<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
|
<Component
|
||||||
:is="fieldProps.component"
|
:is="section.name ? 'section' : 'div'"
|
||||||
v-bind="fieldProps.props"
|
v-if="section.visible"
|
||||||
/>
|
class="form-section"
|
||||||
</slot>
|
>
|
||||||
|
<BCardTitle v-if="section.name" title-tag="h3">
|
||||||
|
{{ section.name }}
|
||||||
|
<small v-if="section.help">{{ section.help }}</small>
|
||||||
|
</BCardTitle>
|
||||||
|
|
||||||
<hr v-if="fieldProps.hr ?? hr" />
|
<Fields.reuse :fields-props="section.fields" />
|
||||||
|
</Component>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="fields">
|
||||||
|
<Fields.reuse :fields-props="fields" />
|
||||||
|
</template>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<slot name="server-error">
|
<slot name="server-error">
|
||||||
|
@ -159,6 +202,8 @@ function onModelUpdate(key: keyof MV, value: MV[keyof MV]) {
|
||||||
</BAlert>
|
</BAlert>
|
||||||
</slot>
|
</slot>
|
||||||
</BForm>
|
</BForm>
|
||||||
|
|
||||||
|
<slot name="after-form" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="!noFooter" #buttons>
|
<template v-if="!noFooter" #buttons>
|
||||||
|
@ -170,3 +215,13 @@ function onModelUpdate(key: keyof MV, value: MV[keyof MV]) {
|
||||||
</template>
|
</template>
|
||||||
</YCard>
|
</YCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card-title {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border-bottom: solid $border-width $gray-500;
|
||||||
|
}
|
||||||
|
.form-section:not(:last-child) {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue