diff --git a/app/src/composables/configPanels.ts b/app/src/composables/configPanels.ts index f303f82b..5da8ba78 100644 --- a/app/src/composables/configPanels.ts +++ b/app/src/composables/configPanels.ts @@ -1,11 +1,20 @@ 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 { 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 * as validators from '@/helpers/validators' -import { formatI18nField } from '@/helpers/yunohostArguments' -import type { MergeUnion, Obj } from '@/types/commons' +import { formatForm, formatI18nField } from '@/helpers/yunohostArguments' +import type { CustomRoute, KeyOfStr, MergeUnion, Obj } from '@/types/commons' import type { AnyFormField, ConfigPanel, @@ -218,7 +227,7 @@ function formatOption(option: AnyOption, form: Ref): AnyFormField { * @param options - a core Option array written by a packager * @return An object with form and fields */ -function formatOptions( +export function formatOptions( options: AnyOption[], ): { fields: FormFieldDict @@ -248,14 +257,16 @@ function formatConfigPanel>( form: Ref panel: ConfigPanel } { - const options = panel.sections.flatMap((section) => section.options) - const { form, fields } = formatOptions(options) + const options = panel.sections?.flatMap((section) => section.options) + const { form, fields } = options + ? formatOptions(options) + : { form: ref({}) as Ref, fields: {} as FormFieldDict } let hasApplyButton = false - const sections = panel.sections.map((section) => { - const sectionFieldsIds = section.options.map( - (option) => option.id, - ) as ConfigPanel['sections'][number]['fields'] + const sections = panel.sections?.map((section) => { + const sectionFieldsIds = section.options.map< + KeyOfStr> + >((option) => option.id) if ( !section.is_action_section && @@ -370,3 +381,103 @@ function useEvaluation(expression: string, form: MaybeRefOrGetter) { } }) } + +export type OnPanelApply = ( + 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 = Obj, +> = { + form: WritableComputedRef + panel: ComputedRef>> + routes: CustomRoute[] + v: Ref> + onPanelApply: (actionId?: KeyOfStr>) => void +} + +export function useConfigPanels>( + config: ConfigPanels, + tabId: MaybeRefOrGetter, + onPanelApply: OnPanelApply, +): ConfigPanelsProps { + 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(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>, + ) => { + const panelId = panel.value.id + let form: NestedMV | Partial = 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 = {} + 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, + } +} diff --git a/app/src/composables/form.ts b/app/src/composables/form.ts index 27d38613..8f9bca4b 100644 --- a/app/src/composables/form.ts +++ b/app/src/composables/form.ts @@ -1,13 +1,20 @@ import type { BaseValidation, ServerErrors, + Validation, ValidationArgs, ValidationRuleCollection, } 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 type { + ComputedRef, + InjectionKey, + MaybeRefOrGetter, + Ref, + WritableComputedRef, +} from 'vue' +import { computed, inject, provide, reactive, toValue } from 'vue' import { APIBadRequestError, type APIError } from '@/api/errors' import type { Obj } from '@/types/commons' @@ -42,10 +49,15 @@ export function useTouch( return touch } +export type FormValidation = Validation< + { global: { true: () => true }; form: ValidationArgs }, + { form: Ref | WritableComputedRef; global: null } +> + export function useForm< MV extends Obj, FFD extends FormFieldDict = FormFieldDict, ->(form: Ref, fields: MaybeRefOrGetter) { +>(form: Ref | WritableComputedRef, fields: MaybeRefOrGetter) { const serverErrors = reactive({}) const validByDefault: ValidationRuleCollection = { true: () => true } const rules = computedWithControl( @@ -65,7 +77,7 @@ export function useForm< }, ) - const v = useVuelidate( + const v: Ref> = useVuelidate( rules, { form, global: null }, { $externalResults: serverErrors },