mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
feat: add useConfigPanels composable
This commit is contained in:
parent
ab30cb8c04
commit
7a1b4ba453
2 changed files with 137 additions and 14 deletions
|
@ -1,11 +1,20 @@
|
||||||
import evaluate from 'simple-evaluate'
|
import evaluate from 'simple-evaluate'
|
||||||
import { computed, ref, toValue, type MaybeRefOrGetter, type Ref } from 'vue'
|
import type {
|
||||||
|
ComputedRef,
|
||||||
|
MaybeRefOrGetter,
|
||||||
|
Ref,
|
||||||
|
WritableComputedRef,
|
||||||
|
} from 'vue'
|
||||||
|
import { computed, ref, toValue, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { APIBadRequestError, APIError } from '@/api/errors'
|
||||||
|
import { deepSetErrors, useForm, type FormValidation } from '@/composables/form'
|
||||||
import { asUnreffed, isObjectLiteral } from '@/helpers/commons'
|
import { asUnreffed, isObjectLiteral } from '@/helpers/commons'
|
||||||
import * as validators from '@/helpers/validators'
|
import * as validators from '@/helpers/validators'
|
||||||
import { formatI18nField } from '@/helpers/yunohostArguments'
|
import { formatForm, formatI18nField } from '@/helpers/yunohostArguments'
|
||||||
import type { MergeUnion, Obj } from '@/types/commons'
|
import type { CustomRoute, KeyOfStr, MergeUnion, Obj } from '@/types/commons'
|
||||||
import type {
|
import type {
|
||||||
AnyFormField,
|
AnyFormField,
|
||||||
ConfigPanel,
|
ConfigPanel,
|
||||||
|
@ -218,7 +227,7 @@ function formatOption(option: AnyOption, form: Ref<Obj>): AnyFormField {
|
||||||
* @param options - a core Option array written by a packager
|
* @param options - a core Option array written by a packager
|
||||||
* @return An object with form and fields
|
* @return An object with form and fields
|
||||||
*/
|
*/
|
||||||
function formatOptions<MV extends Obj>(
|
export function formatOptions<MV extends Obj>(
|
||||||
options: AnyOption[],
|
options: AnyOption[],
|
||||||
): {
|
): {
|
||||||
fields: FormFieldDict<MV>
|
fields: FormFieldDict<MV>
|
||||||
|
@ -248,14 +257,16 @@ function formatConfigPanel<NestedMV extends Obj, MV extends Obj<NestedMV>>(
|
||||||
form: Ref<NestedMV>
|
form: Ref<NestedMV>
|
||||||
panel: ConfigPanel<NestedMV, MV>
|
panel: ConfigPanel<NestedMV, MV>
|
||||||
} {
|
} {
|
||||||
const options = panel.sections.flatMap((section) => section.options)
|
const options = panel.sections?.flatMap((section) => section.options)
|
||||||
const { form, fields } = formatOptions<NestedMV>(options)
|
const { form, fields } = options
|
||||||
|
? formatOptions<NestedMV>(options)
|
||||||
|
: { form: ref({}) as Ref<NestedMV>, fields: {} as FormFieldDict<NestedMV> }
|
||||||
let hasApplyButton = false
|
let hasApplyButton = false
|
||||||
|
|
||||||
const sections = panel.sections.map((section) => {
|
const sections = panel.sections?.map((section) => {
|
||||||
const sectionFieldsIds = section.options.map(
|
const sectionFieldsIds = section.options.map<
|
||||||
(option) => option.id,
|
KeyOfStr<FormFieldDict<NestedMV>>
|
||||||
) as ConfigPanel<NestedMV, MV>['sections'][number]['fields']
|
>((option) => option.id)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!section.is_action_section &&
|
!section.is_action_section &&
|
||||||
|
@ -370,3 +381,103 @@ function useEvaluation(expression: string, form: MaybeRefOrGetter<Obj>) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OnPanelApply<MV extends Obj = Obj> = (
|
||||||
|
data: { panelId: keyof MV; data: Obj; action?: string },
|
||||||
|
onError: (err: APIError, errorMessage?: string) => void,
|
||||||
|
) => void
|
||||||
|
|
||||||
|
export type ConfigPanelsProps<
|
||||||
|
NestedMV extends Obj = Obj,
|
||||||
|
MV extends Obj<NestedMV> = Obj<NestedMV>,
|
||||||
|
> = {
|
||||||
|
form: WritableComputedRef<NestedMV>
|
||||||
|
panel: ComputedRef<ConfigPanel<NestedMV, MV, FormFieldDict<NestedMV>>>
|
||||||
|
routes: CustomRoute[]
|
||||||
|
v: Ref<FormValidation<NestedMV>>
|
||||||
|
onPanelApply: (actionId?: KeyOfStr<FormFieldDict<NestedMV>>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConfigPanels<NestedMV extends Obj, MV extends Obj<NestedMV>>(
|
||||||
|
config: ConfigPanels<NestedMV, MV>,
|
||||||
|
tabId: MaybeRefOrGetter<keyof MV | undefined>,
|
||||||
|
onPanelApply: OnPanelApply<MV>,
|
||||||
|
): ConfigPanelsProps<NestedMV, MV> {
|
||||||
|
const router = useRouter()
|
||||||
|
watch(
|
||||||
|
() => toValue(tabId),
|
||||||
|
(id) => {
|
||||||
|
if (!id) {
|
||||||
|
router.replace({ params: { tabId: config.panels[0].id } })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const panelId = computed(() => toValue(tabId) || config.panels[0].id)
|
||||||
|
const panel = computed(() => {
|
||||||
|
return config.panels.find((panel) => panel.id === panelId.value)!
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = computed({
|
||||||
|
get: () => config.forms[panelId.value].value,
|
||||||
|
set: (form) => (config.forms[panelId.value].value = form),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { v, serverErrors } = useForm<NestedMV>(form, () => panel.value.fields)
|
||||||
|
|
||||||
|
function onErrorFn(err: APIError) {
|
||||||
|
if (!(err instanceof APIBadRequestError)) throw err
|
||||||
|
if (err.data.name) {
|
||||||
|
deepSetErrors(
|
||||||
|
serverErrors,
|
||||||
|
[err.message],
|
||||||
|
'form',
|
||||||
|
// FIXME probably need to remove panel + section id
|
||||||
|
...err.data.name.split('.'),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
serverErrors.global = [err.message]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBeforePanelApply = async (
|
||||||
|
actionId?: KeyOfStr<FormFieldDict<NestedMV>>,
|
||||||
|
) => {
|
||||||
|
const panelId = panel.value.id
|
||||||
|
let form: NestedMV | Partial<NestedMV> = config.forms[panelId].value
|
||||||
|
let action: undefined | string = undefined
|
||||||
|
|
||||||
|
if (actionId) {
|
||||||
|
const section = panel.value.sections!.find((section) =>
|
||||||
|
section.fields.includes(actionId),
|
||||||
|
)!
|
||||||
|
action = `${panelId}.${section.id}.${actionId}`
|
||||||
|
const actionForm: Partial<NestedMV> = {}
|
||||||
|
for (const id of section.fields) {
|
||||||
|
if (id in form) {
|
||||||
|
// FIXME check visible? skip validate and value if not visible?
|
||||||
|
if (!(await v.value.form[id].$validate())) return
|
||||||
|
actionForm[id] = form[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form = actionForm
|
||||||
|
} else {
|
||||||
|
if (!(await v.value.form.$validate())) return
|
||||||
|
}
|
||||||
|
const data = await formatFormData(form, {
|
||||||
|
removeEmpty: false,
|
||||||
|
removeNull: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
onPanelApply({ panelId, data, action }, onErrorFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
panel,
|
||||||
|
routes: config.routes,
|
||||||
|
v,
|
||||||
|
onPanelApply: onBeforePanelApply,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
import type {
|
import type {
|
||||||
BaseValidation,
|
BaseValidation,
|
||||||
ServerErrors,
|
ServerErrors,
|
||||||
|
Validation,
|
||||||
ValidationArgs,
|
ValidationArgs,
|
||||||
ValidationRuleCollection,
|
ValidationRuleCollection,
|
||||||
} from '@vuelidate/core'
|
} from '@vuelidate/core'
|
||||||
import useVuelidate from '@vuelidate/core'
|
import useVuelidate from '@vuelidate/core'
|
||||||
import type { ComputedRef, InjectionKey, MaybeRefOrGetter, Ref } from 'vue'
|
|
||||||
import { computed, inject, provide, reactive, toValue } from 'vue'
|
|
||||||
import { computedWithControl } from '@vueuse/core'
|
import { computedWithControl } from '@vueuse/core'
|
||||||
|
import type {
|
||||||
|
ComputedRef,
|
||||||
|
InjectionKey,
|
||||||
|
MaybeRefOrGetter,
|
||||||
|
Ref,
|
||||||
|
WritableComputedRef,
|
||||||
|
} from 'vue'
|
||||||
|
import { computed, inject, provide, reactive, toValue } from 'vue'
|
||||||
|
|
||||||
import { APIBadRequestError, type APIError } from '@/api/errors'
|
import { APIBadRequestError, type APIError } from '@/api/errors'
|
||||||
import type { Obj } from '@/types/commons'
|
import type { Obj } from '@/types/commons'
|
||||||
|
@ -42,10 +49,15 @@ export function useTouch(
|
||||||
return touch
|
return touch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FormValidation<MV extends Obj> = Validation<
|
||||||
|
{ global: { true: () => true }; form: ValidationArgs<MV> },
|
||||||
|
{ form: Ref<MV> | WritableComputedRef<MV>; global: null }
|
||||||
|
>
|
||||||
|
|
||||||
export function useForm<
|
export function useForm<
|
||||||
MV extends Obj,
|
MV extends Obj,
|
||||||
FFD extends FormFieldDict<MV> = FormFieldDict<MV>,
|
FFD extends FormFieldDict<MV> = FormFieldDict<MV>,
|
||||||
>(form: Ref<MV>, fields: MaybeRefOrGetter<FFD>) {
|
>(form: Ref<MV> | WritableComputedRef<MV>, fields: MaybeRefOrGetter<FFD>) {
|
||||||
const serverErrors = reactive<ServerErrors>({})
|
const serverErrors = reactive<ServerErrors>({})
|
||||||
const validByDefault: ValidationRuleCollection = { true: () => true }
|
const validByDefault: ValidationRuleCollection = { true: () => true }
|
||||||
const rules = computedWithControl(
|
const rules = computedWithControl(
|
||||||
|
@ -65,7 +77,7 @@ export function useForm<
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const v = useVuelidate(
|
const v: Ref<FormValidation<MV>> = useVuelidate(
|
||||||
rules,
|
rules,
|
||||||
{ form, global: null },
|
{ form, global: null },
|
||||||
{ $externalResults: serverErrors },
|
{ $externalResults: serverErrors },
|
||||||
|
|
Loading…
Reference in a new issue