Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/polite-crews-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect-app/vue-components": patch
---

feat: Enhance OmegaForm with union metadata handling and nested meta flattening
53 changes: 41 additions & 12 deletions packages/vue-components/src/components/OmegaForm/OmegaFormStuff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,12 +681,31 @@ export const createMeta = <T = any>(
return acc
}

// Helper to flatten nested meta structure into dot-notation keys
const flattenMeta = <T>(meta: MetaRecord<T> | FieldMeta, parentKey: string = ""): MetaRecord<T> => {
const result: MetaRecord<T> = {}

for (const key in meta) {
const value = (meta as any)[key]
const newKey = parentKey ? `${parentKey}.${key}` : key

if (value && typeof value === "object" && "type" in value) {
result[newKey as DeepKeys<T>] = value as FieldMeta
} else if (value && typeof value === "object") {
Object.assign(result, flattenMeta<T>(value, newKey))
}
}

return result
}

const metadataFromAst = <From, To>(
schema: S.Schema<To, From, never>
): { meta: MetaRecord<To>; defaultValues: Record<string, any> } => {
): { meta: MetaRecord<To>; defaultValues: Record<string, any>; unionMeta: Record<string, MetaRecord<To>> } => {
const ast = schema.ast
const newMeta: MetaRecord<To> = {}
const defaultValues: Record<string, any> = {}
const unionMeta: Record<string, MetaRecord<To>> = {}

if (ast._tag === "Transformation" || ast._tag === "Refinement") {
return metadataFromAst(S.make(ast.from))
Expand All @@ -709,24 +728,31 @@ const metadataFromAst = <From, To>(
// Extract discriminator values from each union member
const discriminatorValues: any[] = []

// Merge metadata from all union members
// Store metadata for each union member by its tag value
for (const memberType of nonNullTypes) {
if ("propertySignatures" in memberType) {
// Find the discriminator field (usually _tag)
const tagProp = memberType.propertySignatures.find(
(p: any) => p.name.toString() === "_tag"
)

let tagValue: string | null = null
if (tagProp && S.AST.isLiteral(tagProp.type)) {
discriminatorValues.push(tagProp.type.literal)
tagValue = tagProp.type.literal as string
discriminatorValues.push(tagValue)
}

// Create metadata for this member's properties
const memberMeta = createMeta<To>({
propertySignatures: memberType.propertySignatures
})

// Merge into result
// Store per-tag metadata for reactive lookup
if (tagValue) {
unionMeta[tagValue] = flattenMeta<To>(memberMeta)
}

// Merge into result (for backward compatibility)
Object.assign(newMeta, memberMeta)
}
}
Expand All @@ -740,7 +766,7 @@ const metadataFromAst = <From, To>(
} as FieldMeta
}

return { meta: newMeta, defaultValues }
return { meta: newMeta, defaultValues, unionMeta }
}
}

Expand All @@ -750,7 +776,7 @@ const metadataFromAst = <From, To>(
})

if (Object.values(meta).every((value) => value && "type" in value)) {
return { meta: meta as MetaRecord<To>, defaultValues }
return { meta: meta as MetaRecord<To>, defaultValues, unionMeta }
}

const flattenObject = (
Expand All @@ -770,7 +796,7 @@ const metadataFromAst = <From, To>(
flattenObject(meta)
}

return { meta: newMeta, defaultValues }
return { meta: newMeta, defaultValues, unionMeta }
}

export const duplicateSchema = <From, To>(
Expand All @@ -784,16 +810,20 @@ export const generateMetaFromSchema = <From, To>(
): {
schema: S.Schema<To, From, never>
meta: MetaRecord<To>
unionMeta: Record<string, MetaRecord<To>>
} => {
const { meta } = metadataFromAst(schema)
const { meta, unionMeta } = metadataFromAst(schema)

return { schema, meta }
return { schema, meta, unionMeta }
}

export const generateInputStandardSchemaFromFieldMeta = (
meta: FieldMeta
meta: FieldMeta,
trans?: ReturnType<typeof useIntl>["trans"]
): StandardSchemaV1<any, any> => {
const { trans } = useIntl()
if (!trans) {
trans = useIntl().trans
}
let schema: S.Schema<any, any, never>
switch (meta.type) {
case "string":
Expand Down Expand Up @@ -850,7 +880,6 @@ export const generateInputStandardSchemaFromFieldMeta = (
})
}
if (typeof meta.minimum === "number") {
console.log("pippocazzo", meta)
schema = schema.pipe(S.greaterThanOrEqualTo(meta.minimum)).annotations({
message: () =>
trans(meta.minimum === 0 ? "validation.number.positive" : "validation.number.min", {
Expand Down
15 changes: 14 additions & 1 deletion packages/vue-components/src/components/OmegaForm/OmegaInput.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<component
:is="form.Field"
:key="fieldKey"
:name="name"
:validators="{
onChange: schema,
Expand Down Expand Up @@ -41,6 +42,7 @@
>
import { type DeepKeys } from "@tanstack/vue-form"
import { computed, inject, type Ref, useAttrs } from "vue"
import { useIntl } from "../../utils"
import { type FieldMeta, generateInputStandardSchemaFromFieldMeta, type OmegaInputPropsBase } from "./OmegaFormStuff"
import OmegaInternalInput from "./OmegaInternalInput.vue"
import { useErrorLabel } from "./useOmegaForm"
Expand Down Expand Up @@ -80,12 +82,23 @@ const meta = computed(() => {
return props.form.meta[propsName.value]
})

// Key to force Field re-mount when meta type changes (for TaggedUnion support)
const fieldKey = computed(() => {
const m = meta.value
if (!m) return propsName.value
// Include type and key constraints in the key so Field re-mounts when validation rules change
return `${propsName.value}-${m.type}-${m.minLength ?? ""}-${m.maxLength ?? ""}-${m.minimum ?? ""}-${m.maximum ?? ""}`
})

// Call useIntl during setup to avoid issues when computed re-evaluates
const { trans } = useIntl()

const schema = computed(() => {
if (!meta.value) {
console.log(props.name, Object.keys(props.form.meta), props.form.meta)
throw new Error("Meta is undefined")
}
return generateInputStandardSchemaFromFieldMeta(meta.value)
return generateInputStandardSchemaFromFieldMeta(meta.value, trans)
})

const errori18n = useErrorLabel(props.form)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,57 @@
Name extends DeepKeys<From> | undefined = DeepKeys<From>"
>
import { type DeepKeys } from "@tanstack/vue-form"
import { computed, provide, ref, watch } from "vue"
import { type TaggedUnionOption } from "./InputProps"
import { type FieldPath } from "./OmegaFormStuff"
import OmegaTaggedUnionInternal from "./OmegaTaggedUnionInternal.vue"
import { type useOmegaForm } from "./useOmegaForm"

defineProps<{
const props = defineProps<{
name?: Name
form: ReturnType<typeof useOmegaForm<From, To>>
type?: "select" | "radio"
options: TaggedUnionOption<From, Name>[]
label?: string
}>()

// Track the current tag value reactively
const currentTag = ref<string | null>(null)

// Watch the form's _tag field value
const tagPath = computed(() => props.name ? `${props.name}._tag` : "_tag")
const formValues = props.form.useStore((state) => state.values)
watch(
() => {
const path = tagPath.value
// Navigate to the nested value
return path.split(".").reduce((acc: any, key) => acc?.[key], formValues.value) as string | null
},
(newTag) => {
currentTag.value = newTag ?? null
},
{ immediate: true }
)

// Provide tag-specific metadata to all child Input components
const getMetaFromArray = computed(() => {
const tag = currentTag.value

const getMeta = (path: string) => {
if (!tag) return null

// Get the tag-specific metadata
const tagMeta = props.form.unionMeta[tag]
if (!tagMeta) return null

// Look up the meta for this path
return tagMeta[path as keyof typeof tagMeta] ?? null
}

return getMeta
})

provide("getMetaFromArray", getMetaFromArray)
</script>

<template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export type OmegaConfig<T> = {

export interface OF<From, To> extends OmegaFormApi<From, To> {
meta: MetaRecord<From>
unionMeta: Record<string, MetaRecord<From>>
clear: () => void
i18nNamespace?: string
ignorePreventCloseEvents?: boolean
Expand Down Expand Up @@ -682,7 +683,7 @@ export const useOmegaForm = <
const standardSchema = S.standardSchemaV1(schema)
const decode = S.decode(schema)

const { meta } = generateMetaFromSchema(schema)
const { meta, unionMeta } = generateMetaFromSchema(schema)

const persistencyKey = computed(() => {
if (omegaConfig?.persistency?.id) {
Expand Down Expand Up @@ -952,6 +953,7 @@ export const useOmegaForm = <
i18nNamespace: omegaConfig?.i18nNamespace,
ignorePreventCloseEvents: omegaConfig?.ignorePreventCloseEvents,
meta,
unionMeta,
clear,
handleSubmit: (meta?: Record<string, any>) => {
const span = api.trace.getSpan(api.context.active())
Expand Down
Loading