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/lazy-chairs-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect-app/vue-components": patch
---

the fix for integers
82 changes: 82 additions & 0 deletions packages/vue-components/__tests__/IntegerValidation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { S } from "effect-app"
import { describe, expect, it } from "vitest"
import { generateInputStandardSchemaFromFieldMeta, generateMetaFromSchema } from "../src/components/OmegaForm/OmegaFormStuff"

// mock German translations
const germanTranslations: Record<string, string> = {
"validation.integer.expected": "Es wird eine ganze Zahl erwartet, tatsächlich: {actualValue}",
"validation.number.expected": "Es wird eine Zahl erwartet, tatsächlich: {actualValue}",
"validation.empty": "Das Feld darf nicht leer sein"
}

const mockTrans = (id: string, values?: Record<string, any>) => {
let text = germanTranslations[id] || id
if (values) {
Object.entries(values).forEach(([key, value]) => {
text = text.replace(`{${key}}`, String(value))
})
}
return text
}

describe("Integer validation with German translations", () => {
it("should generate int metadata for S.Int fields", () => {
const TestSchema = S.Struct({
value: S.Int
})

const { meta } = generateMetaFromSchema(TestSchema)
console.log("Meta:", JSON.stringify(meta, null, 2))

expect(meta.value?.type).toBe("int")
})

it("should show German error for decimal values", async () => {
const TestSchema = S.Struct({
value: S.Int
})

const { meta } = generateMetaFromSchema(TestSchema)
const schema = generateInputStandardSchemaFromFieldMeta(meta.value!, mockTrans)

// test with a decimal value
const result = await schema["~standard"].validate(59.5)
console.log("Validation result for 59.5:", JSON.stringify(result, null, 2))

expect(result.issues).toBeDefined()
expect(result.issues?.[0]?.message).toContain("ganze Zahl")
})

it("should show German error for undefined values", async () => {
const TestSchema = S.Struct({
value: S.Int
})

const { meta } = generateMetaFromSchema(TestSchema)
const schema = generateInputStandardSchemaFromFieldMeta(meta.value!, mockTrans)

// test with undefined value
const result = await schema["~standard"].validate(undefined)
console.log("Validation result for undefined:", JSON.stringify(result, null, 2))

expect(result.issues).toBeDefined()
// should be German empty message
expect(result.issues?.[0]?.message).toBe("Das Feld darf nicht leer sein")
})

it("should accept valid integer values", async () => {
const TestSchema = S.Struct({
value: S.Int
})

const { meta } = generateMetaFromSchema(TestSchema)
const schema = generateInputStandardSchemaFromFieldMeta(meta.value!, mockTrans)

// test with a valid integer
const result = await schema["~standard"].validate(59)
console.log("Validation result for 59:", JSON.stringify(result, null, 2))

expect(result.issues).toBeUndefined()
expect("value" in result && result.value).toBe(59)
})
})
107 changes: 88 additions & 19 deletions packages/vue-components/src/components/OmegaForm/OmegaFormStuff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export type StringFieldMeta = BaseFieldMeta & {
}

export type NumberFieldMeta = BaseFieldMeta & {
type: "number"
type: "number" | "int"
minimum?: number
maximum?: number
exclusiveMinimum?: number
Expand Down Expand Up @@ -346,12 +346,23 @@ export const createMeta = <T = any>(
): MetaRecord<T> | FieldMeta => {
// unwraps class (Class are transformations)
// this calls createMeta recursively, so wrapped transformations are also unwrapped
// BUT: check for Int title annotation first - S.Int and branded Int have title "Int" or "int"
// and we don't want to lose that information by unwrapping
if (property && property._tag === "Transformation") {
return createMeta<T>({
parent,
meta,
property: property.from
})
const titleOnTransform = S
.AST
.getAnnotation(property, S.AST.TitleAnnotationId)
.pipe(Option.getOrElse(() => ""))

// only unwrap if this is NOT an Int type
if (titleOnTransform !== "Int" && titleOnTransform !== "int") {
return createMeta<T>({
parent,
meta,
property: property.from
})
}
// if it's Int, fall through to process it with the Int type
}

if (property?._tag === "TypeLiteral" && "propertySignatures" in property) {
Expand Down Expand Up @@ -655,24 +666,32 @@ export const createMeta = <T = any>(

meta = { ...JSONAnnotation, ...meta }

if ("from" in property) {
// check the title annotation BEFORE following "from" to detect refinements like S.Int
const titleType = S
.AST
.getAnnotation(
property,
S.AST.TitleAnnotationId
)
.pipe(
Option.getOrElse(() => {
return "unknown"
})
)

// if this is S.Int (a refinement), set the type and skip following "from"
// otherwise we'd lose the "Int" information and get "number" instead
if (titleType === "Int" || titleType === "int") {
meta["type"] = "int"
// don't follow "from" for Int refinements
} else if ("from" in property) {
return createMeta<T>({
parent,
meta,
property: property.from
})
} else {
meta["type"] = S
.AST
.getAnnotation(
property,
S.AST.TitleAnnotationId
)
.pipe(
Option.getOrElse(() => {
return "unknown"
})
)
meta["type"] = titleType
}

return meta as FieldMeta
Expand Down Expand Up @@ -869,9 +888,59 @@ export const generateInputStandardSchemaFromFieldMeta = (
}
break

case "int": {
// create a custom integer schema with translations
// S.Number with empty message, then S.int with integer message
schema = S
.Number
.annotations({
message: () => trans("validation.empty")
})
.pipe(
S.int({ message: (issue) => trans("validation.integer.expected", { actualValue: String(issue.actual) }) })
)
if (typeof meta.minimum === "number") {
schema = schema.pipe(S.greaterThanOrEqualTo(meta.minimum)).annotations({
message: () =>
trans(meta.minimum === 0 ? "validation.number.positive" : "validation.number.min", {
minimum: meta.minimum,
isExclusive: true
})
})
}
if (typeof meta.maximum === "number") {
schema = schema.pipe(S.lessThanOrEqualTo(meta.maximum)).annotations({
message: () =>
trans("validation.number.max", {
maximum: meta.maximum,
isExclusive: true
})
})
}
if (typeof meta.exclusiveMinimum === "number") {
schema = schema.pipe(S.greaterThan(meta.exclusiveMinimum)).annotations({
message: () =>
trans(meta.exclusiveMinimum === 0 ? "validation.number.positive" : "validation.number.min", {
minimum: meta.exclusiveMinimum,
isExclusive: false
})
})
}
if (typeof meta.exclusiveMaximum === "number") {
schema = schema.pipe(S.lessThan(meta.exclusiveMaximum)).annotations({
message: () =>
trans("validation.number.max", {
maximum: meta.exclusiveMaximum,
isExclusive: false
})
})
}
break
}

case "number":
schema = S.Number.annotations({
message: () => trans("validation.empty")
message: () => trans("validation.number.expected", { actualValue: "NaN" })
})

if (meta.required) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@
</v-textarea>
<component
:is="inputProps.type === 'range' ? 'v-slider' : 'v-text-field'"
v-if="inputProps.type === 'number' || inputProps.type === 'range'"
v-if="inputProps.type === 'number' || inputProps.type === 'int' || inputProps.type === 'range'"
:id="inputProps.id"
:required="inputProps.required"
:min="inputProps.min"
:max="inputProps.max"
:type="inputProps.type"
:type="inputProps.type === 'int' ? 'number' : inputProps.type"
:name="field.name"
:label="inputProps.label"
:error-messages="inputProps.errorMessages"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ const PackageSchema = S.Struct({
})

const form = useOmegaForm(PackageSchema, {
onSubmit: (values) => {
console.log("Form submitted with values:", values)
alert(`Packen erfolgreich!\n${JSON.stringify(values, null, 2)}`)
onSubmit: ({ value }) => {
console.log("Form submitted with values:", value)
alert(`Packen erfolgreich!\n${JSON.stringify(value, null, 2)}`)
return undefined as any
}
})
Expand Down