@effect-app/vue-components 4.0.0-beta.158 → 4.0.0-beta.159
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/components/OmegaForm/OmegaArray.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaAutoGen.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaErrorsInternal.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaFormInput.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaInput.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaInternalInput.vue.d.ts +2 -1
- package/dist/types/components/OmegaForm/OmegaWrapper.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/createUseFormWithCustomInput.d.ts +2 -2
- package/dist/types/components/OmegaForm/errors.d.ts +33 -0
- package/dist/types/components/OmegaForm/getOmegaStore.d.ts +1 -1
- package/dist/types/components/OmegaForm/hocs.d.ts +3 -0
- package/dist/types/components/OmegaForm/index.d.ts +13 -3
- package/dist/types/components/OmegaForm/inputs.d.ts +4 -0
- package/dist/types/components/OmegaForm/meta/checks.d.ts +4 -0
- package/dist/types/components/OmegaForm/meta/createMeta.d.ts +32 -0
- package/dist/types/components/OmegaForm/meta/defaults.d.ts +2 -0
- package/dist/types/components/OmegaForm/meta/redacted.d.ts +2 -0
- package/dist/types/components/OmegaForm/meta/types.d.ts +56 -0
- package/dist/types/components/OmegaForm/meta/walker.d.ts +18 -0
- package/dist/types/components/OmegaForm/persistency.d.ts +58 -0
- package/dist/types/components/OmegaForm/submit.d.ts +60 -0
- package/dist/types/components/OmegaForm/types.d.ts +281 -0
- package/dist/types/components/OmegaForm/useOmegaForm.d.ts +6 -212
- package/dist/types/components/OmegaForm/validation/localized.d.ts +10 -0
- package/dist/vue-components.es.js +24 -16
- package/dist/vue-components10.es.js +4 -4
- package/dist/vue-components11.es.js +19 -12
- package/dist/vue-components12.es.js +22 -444
- package/dist/vue-components13.es.js +126 -3
- package/dist/vue-components14.es.js +61 -34
- package/dist/vue-components15.es.js +57 -24
- package/dist/vue-components16.es.js +20 -26
- package/dist/vue-components17.es.js +4 -6
- package/dist/vue-components18.es.js +78 -16
- package/dist/vue-components19.es.js +86 -30
- package/dist/vue-components20.es.js +72 -17
- package/dist/vue-components21.es.js +10 -19
- package/dist/vue-components22.es.js +54 -28
- package/dist/vue-components23.es.js +4 -6
- package/dist/vue-components24.es.js +43 -8
- package/dist/vue-components25.es.js +4 -37
- package/dist/vue-components26.es.js +83 -24
- package/dist/vue-components28.es.js +6 -22
- package/dist/vue-components29.es.js +8 -20
- package/dist/vue-components3.es.js +2 -2
- package/dist/vue-components30.es.js +267 -7
- package/dist/vue-components32.es.js +7 -4
- package/dist/vue-components33.es.js +71 -27
- package/dist/vue-components34.es.js +4 -4
- package/dist/vue-components35.es.js +50 -27
- package/dist/vue-components36.es.js +4 -5
- package/dist/vue-components37.es.js +23 -17
- package/dist/vue-components38.es.js +4 -55
- package/dist/vue-components39.es.js +57 -3
- package/dist/vue-components40.es.js +4 -43
- package/dist/vue-components41.es.js +11 -4
- package/dist/vue-components42.es.js +17 -79
- package/dist/vue-components44.es.js +8 -7
- package/dist/vue-components45.es.js +3 -8
- package/dist/vue-components46.es.js +36 -267
- package/dist/vue-components47.es.js +27 -0
- package/dist/vue-components48.es.js +27 -7
- package/dist/vue-components49.es.js +6 -79
- package/dist/vue-components50.es.js +17 -4
- package/dist/vue-components51.es.js +32 -69
- package/dist/vue-components52.es.js +17 -4
- package/dist/vue-components53.es.js +19 -22
- package/dist/vue-components54.es.js +29 -4
- package/dist/vue-components55.es.js +6 -58
- package/dist/vue-components56.es.js +8 -4
- package/dist/vue-components57.es.js +37 -11
- package/dist/vue-components58.es.js +24 -21
- package/dist/{vue-components27.es.js → vue-components59.es.js} +2 -2
- package/dist/vue-components6.es.js +11 -11
- package/dist/vue-components60.es.js +23 -8
- package/dist/vue-components61.es.js +18 -232
- package/dist/vue-components62.es.js +7 -31
- package/dist/vue-components63.es.js +19 -8
- package/dist/vue-components64.es.js +4 -35
- package/dist/vue-components65.es.js +29 -0
- package/dist/vue-components66.es.js +5 -0
- package/dist/vue-components67.es.js +29 -0
- package/dist/vue-components68.es.js +6 -0
- package/dist/vue-components69.es.js +18 -0
- package/dist/vue-components7.es.js +11 -26
- package/dist/vue-components70.es.js +40 -0
- package/dist/vue-components71.es.js +81 -0
- package/dist/vue-components72.es.js +33 -0
- package/dist/vue-components73.es.js +19 -0
- package/dist/vue-components74.es.js +48 -0
- package/dist/vue-components8.es.js +33 -45
- package/dist/vue-components9.es.js +46 -4
- package/package.json +7 -7
- package/src/components/CommandButton.vue +3 -1
- package/src/components/OmegaForm/OmegaArray.vue +1 -1
- package/src/components/OmegaForm/OmegaAutoGen.vue +2 -1
- package/src/components/OmegaForm/OmegaErrorsInternal.vue +1 -1
- package/src/components/OmegaForm/OmegaFormInput.vue +1 -1
- package/src/components/OmegaForm/OmegaInput.vue +6 -68
- package/src/components/OmegaForm/OmegaInputVuetify.vue +1 -1
- package/src/components/OmegaForm/OmegaInternalInput.vue +5 -11
- package/src/components/OmegaForm/OmegaTaggedUnion.vue +2 -1
- package/src/components/OmegaForm/OmegaWrapper.vue +1 -1
- package/src/components/OmegaForm/blockDialog.ts +10 -1
- package/src/components/OmegaForm/createUseFormWithCustomInput.ts +2 -1
- package/src/components/OmegaForm/errors.ts +136 -0
- package/src/components/OmegaForm/getOmegaStore.ts +1 -1
- package/src/components/OmegaForm/hocs.ts +19 -0
- package/src/components/OmegaForm/index.ts +16 -4
- package/src/components/OmegaForm/inputs.ts +22 -0
- package/src/components/OmegaForm/meta/checks.ts +81 -0
- package/src/components/OmegaForm/meta/createMeta.ts +138 -0
- package/src/components/OmegaForm/meta/defaults.ts +132 -0
- package/src/components/OmegaForm/meta/redacted.ts +66 -0
- package/src/components/OmegaForm/meta/types.ts +78 -0
- package/src/components/OmegaForm/meta/walker.ts +247 -0
- package/src/components/OmegaForm/persistency.ts +247 -0
- package/src/components/OmegaForm/submit.ts +128 -0
- package/src/components/OmegaForm/types.ts +751 -0
- package/src/components/OmegaForm/useOmegaForm.ts +49 -913
- package/src/components/OmegaForm/validation/localized.ts +202 -0
- package/dist/types/components/OmegaForm/OmegaFormStuff.d.ts +0 -173
- package/dist/vue-components31.es.js +0 -19
- package/src/components/OmegaForm/OmegaFormStuff.ts +0 -1422
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component
|
|
3
3
|
:is="form.Field"
|
|
4
|
-
:key="fieldKey"
|
|
5
4
|
:name="name"
|
|
6
|
-
:validators="
|
|
7
|
-
...validators,
|
|
8
|
-
onSubmit: schema
|
|
9
|
-
}"
|
|
5
|
+
:validators="validators"
|
|
10
6
|
>
|
|
11
7
|
<template #default="{ field, state }">
|
|
12
8
|
<OmegaInternalInput
|
|
@@ -44,12 +40,13 @@
|
|
|
44
40
|
Name extends DeepKeys<From>
|
|
45
41
|
"
|
|
46
42
|
>
|
|
43
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- TanStack Form Field generic interop and slot prop typing */
|
|
47
44
|
import { type DeepKeys } from "@tanstack/vue-form"
|
|
48
45
|
import { computed, inject, type Ref, useAttrs } from "vue"
|
|
49
|
-
import {
|
|
50
|
-
import { type FieldMeta
|
|
46
|
+
import { useErrorLabel } from "./errors"
|
|
47
|
+
import { type FieldMeta } from "./meta/types"
|
|
51
48
|
import OmegaInternalInput from "./OmegaInternalInput.vue"
|
|
52
|
-
import {
|
|
49
|
+
import { type OmegaInputPropsBase } from "./types"
|
|
53
50
|
|
|
54
51
|
const props = defineProps<OmegaInputPropsBase<From, To, Name>>()
|
|
55
52
|
|
|
@@ -61,13 +58,10 @@ defineSlots<{
|
|
|
61
58
|
default?: (props: any) => any
|
|
62
59
|
}>()
|
|
63
60
|
|
|
64
|
-
defineOptions({
|
|
65
|
-
inheritAttrs: false
|
|
66
|
-
})
|
|
61
|
+
defineOptions({ inheritAttrs: false })
|
|
67
62
|
|
|
68
63
|
const attrs = useAttrs()
|
|
69
64
|
|
|
70
|
-
// Compute the class to use based on inputClass prop
|
|
71
65
|
const computedClass = computed(() => {
|
|
72
66
|
if (props.inputClass === null) return undefined
|
|
73
67
|
if (props.inputClass !== undefined) return props.inputClass
|
|
@@ -86,61 +80,5 @@ const meta = computed(() => {
|
|
|
86
80
|
return props.form.meta[propsName.value]
|
|
87
81
|
})
|
|
88
82
|
|
|
89
|
-
// Key to force Field re-mount when meta type changes (for TaggedUnion support)
|
|
90
|
-
const fieldKey = computed(() => {
|
|
91
|
-
const m = meta.value
|
|
92
|
-
if (!m) return propsName.value
|
|
93
|
-
// Include type and key constraints in the key so Field re-mounts when validation rules change
|
|
94
|
-
// Cast to any since not all FieldMeta variants have these properties
|
|
95
|
-
const fm = m as any
|
|
96
|
-
return `${propsName.value}-${fm.type}-${fm.minLength ?? ""}-${fm.maxLength ?? ""}-${fm.minimum ?? ""}-${
|
|
97
|
-
fm.maximum ?? ""
|
|
98
|
-
}`
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
// Call useIntl during setup to avoid issues when computed re-evaluates
|
|
102
|
-
const { trans } = useIntl()
|
|
103
|
-
|
|
104
|
-
const hasIssues = (result: any): boolean => Array.isArray(result?.issues) && result.issues.length > 0
|
|
105
|
-
|
|
106
|
-
const composeStandardSchemas = (
|
|
107
|
-
omegaSchema: any,
|
|
108
|
-
originalSchema: any
|
|
109
|
-
) => ({
|
|
110
|
-
"~standard": {
|
|
111
|
-
...omegaSchema["~standard"],
|
|
112
|
-
validate: (value: unknown) => {
|
|
113
|
-
const omegaResult = omegaSchema["~standard"].validate(value)
|
|
114
|
-
if (omegaResult && typeof omegaResult.then === "function") {
|
|
115
|
-
return omegaResult.then((resolved: any) => {
|
|
116
|
-
if (hasIssues(resolved)) {
|
|
117
|
-
return resolved
|
|
118
|
-
}
|
|
119
|
-
return originalSchema["~standard"].validate(value)
|
|
120
|
-
})
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (hasIssues(omegaResult)) {
|
|
124
|
-
return omegaResult
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return originalSchema["~standard"].validate(value)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
const schema = computed(() => {
|
|
133
|
-
if (!meta.value) {
|
|
134
|
-
console.log(props.name, Object.keys(props.form.meta), props.form.meta)
|
|
135
|
-
throw new Error("Meta is undefined")
|
|
136
|
-
}
|
|
137
|
-
const omegaSchema = generateInputStandardSchemaFromFieldMeta(meta.value, trans)
|
|
138
|
-
const fieldSchema = meta.value.originalSchema
|
|
139
|
-
if (fieldSchema) {
|
|
140
|
-
return composeStandardSchemas(omegaSchema, fieldSchema)
|
|
141
|
-
}
|
|
142
|
-
return omegaSchema
|
|
143
|
-
})
|
|
144
|
-
|
|
145
83
|
const errori18n = useErrorLabel(props.form)
|
|
146
84
|
</script>
|
|
@@ -207,8 +207,8 @@
|
|
|
207
207
|
generic="From extends Record<PropertyKey, any>, Name extends DeepKeys<From>"
|
|
208
208
|
>
|
|
209
209
|
import { type DeepKeys } from "@tanstack/vue-form"
|
|
210
|
-
import { getInputType } from "../OmegaForm/OmegaFormStuff"
|
|
211
210
|
import type { VuetifyInputProps } from "./InputProps"
|
|
211
|
+
import { getInputType } from "./inputs"
|
|
212
212
|
|
|
213
213
|
defineProps<VuetifyInputProps<From, Name>>()
|
|
214
214
|
|
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
lang="ts"
|
|
25
25
|
generic="From extends Record<PropertyKey, any>, Name extends DeepKeys<From>"
|
|
26
26
|
>
|
|
27
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- TanStack Form / Vue attrs interop */
|
|
27
28
|
import { type DeepKeys, useStore } from "@tanstack/vue-form"
|
|
28
29
|
import { computed, type ComputedRef, getCurrentInstance, useAttrs, useId, useSlots } from "vue"
|
|
29
30
|
import type { InputProps, OmegaFieldInternalApi } from "./InputProps"
|
|
30
|
-
import type {
|
|
31
|
+
import type { MetaRecord, NestedKeyOf } from "./meta/types"
|
|
31
32
|
import OmegaInputVuetify from "./OmegaInputVuetify.vue"
|
|
33
|
+
import type { FieldValidators, TypeOverride } from "./types"
|
|
32
34
|
|
|
33
35
|
defineOptions({
|
|
34
36
|
inheritAttrs: false
|
|
@@ -84,7 +86,8 @@ const id = useId()
|
|
|
84
86
|
|
|
85
87
|
const fieldApi = props.field
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
// Subscribed for side-effect: keeps component reactive to fieldApi.store changes
|
|
90
|
+
const _fieldState = useStore(fieldApi.store, (state) => state)
|
|
88
91
|
|
|
89
92
|
// Get errors from form-level fieldMeta (persists across Field re-mounts)
|
|
90
93
|
const formFieldMeta = useStore(fieldApi.form.store, (state) => state.fieldMeta)
|
|
@@ -119,7 +122,6 @@ const isFalsyButNotZero = (value: unknown): boolean => {
|
|
|
119
122
|
// we remove value and errors when the field is empty and not required
|
|
120
123
|
// convert nullish value to null or undefined based on schema
|
|
121
124
|
const handleChange: OmegaFieldInternalApi<From, Name>["handleChange"] = (value) => {
|
|
122
|
-
let fieldDeleted = false
|
|
123
125
|
if (isFalsyButNotZero(value) && props.meta?.type !== "boolean") {
|
|
124
126
|
// Only convert to null/undefined if the field is actually nullable or optional
|
|
125
127
|
if (props.meta?.nullableOrUndefined) {
|
|
@@ -136,7 +138,6 @@ const handleChange: OmegaFieldInternalApi<From, Name>["handleChange"] = (value)
|
|
|
136
138
|
// from `required: false`, which may also just mean "empty string
|
|
137
139
|
// is valid" for unconstrained `S.String` fields.
|
|
138
140
|
props.field.form.deleteField(props.field.name)
|
|
139
|
-
fieldDeleted = true
|
|
140
141
|
} else {
|
|
141
142
|
// Keep the actual value (e.g., empty string for S.String fields)
|
|
142
143
|
props.field.handleChange(value)
|
|
@@ -144,13 +145,6 @@ const handleChange: OmegaFieldInternalApi<From, Name>["handleChange"] = (value)
|
|
|
144
145
|
} else {
|
|
145
146
|
props.field.handleChange(value)
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
-
// whenever we change the field, regardless if we set it to null, we should reset onSubmit.
|
|
149
|
-
// not sure why this is not the case in tanstack form.
|
|
150
|
-
// Skip when the field was deleted — its meta no longer exists in the form store.
|
|
151
|
-
if (!fieldDeleted) {
|
|
152
|
-
props.field.setMeta((m) => ({ ...m, errorMap: { ...m.errorMap, onSubmit: undefined } }))
|
|
153
|
-
}
|
|
154
148
|
}
|
|
155
149
|
|
|
156
150
|
// Note: Default value normalization (converting empty strings to null/undefined for nullable fields)
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
import { type DeepKeys } from "@tanstack/vue-form"
|
|
11
11
|
import { computed, provide, ref, watch } from "vue"
|
|
12
12
|
import { type TaggedUnionOption } from "./InputProps"
|
|
13
|
-
import { type FieldPath } from "./OmegaFormStuff"
|
|
14
13
|
import OmegaTaggedUnionInternal from "./OmegaTaggedUnionInternal.vue"
|
|
14
|
+
import { type FieldPath } from "./types"
|
|
15
15
|
import { type useOmegaForm } from "./useOmegaForm"
|
|
16
16
|
|
|
17
17
|
const props = defineProps<{
|
|
@@ -32,6 +32,7 @@ watch(
|
|
|
32
32
|
() => {
|
|
33
33
|
const path = tagPath.value
|
|
34
34
|
// Navigate to the nested value
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- traversing arbitrary nested values
|
|
35
36
|
return path.split(".").reduce((acc: any, key) => acc?.[key], formValues.value) as string | null
|
|
36
37
|
},
|
|
37
38
|
(newTag) => {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
import { useStore } from "@tanstack/vue-form"
|
|
33
33
|
import { usePreventClose } from "./blockDialog"
|
|
34
34
|
import { getOmegaStore } from "./getOmegaStore"
|
|
35
|
-
import { type DefaultTypeProps, type OmegaFormApi, type OmegaFormState } from "./
|
|
35
|
+
import { type DefaultTypeProps, type OmegaFormApi, type OmegaFormState } from "./types"
|
|
36
36
|
import { type OmegaFormReturn } from "./useOmegaForm"
|
|
37
37
|
|
|
38
38
|
type OmegaWrapperProps = {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import mitt from "mitt"
|
|
2
2
|
import { inject, type InjectionKey, provide, type Ref } from "vue"
|
|
3
|
+
import { useIntl } from "../../utils"
|
|
3
4
|
import { onMountedWithCleanup } from "./onMountedWithCleanup"
|
|
4
5
|
|
|
5
6
|
export type DialogClosing = { prevent?: boolean | Promise<boolean> }
|
|
@@ -19,11 +20,19 @@ export const usePreventClose = (mkIsDirty: () => Ref<boolean>) => {
|
|
|
19
20
|
if (!bus) {
|
|
20
21
|
return
|
|
21
22
|
}
|
|
23
|
+
const { formatMessage, trans } = useIntl()
|
|
22
24
|
const isDirty = mkIsDirty()
|
|
25
|
+
const defaultMessage = "There are unsaved changes. Are you sure you want to close?"
|
|
23
26
|
onMountedWithCleanup(() => {
|
|
24
27
|
const onDialogClosing = (evt: DialogClosing) => {
|
|
25
28
|
if (isDirty.value) {
|
|
26
|
-
|
|
29
|
+
// Mirror the guard pattern in errors.ts: a custom `useIntl` mock may
|
|
30
|
+
// only provide `trans`, so fall back through trans → defaultMessage.
|
|
31
|
+
const message = formatMessage
|
|
32
|
+
? formatMessage({ id: "form.unsaved_changes_confirm", defaultMessage })
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- key may not be registered in the locale catalog
|
|
34
|
+
: trans?.("form.unsaved_changes_confirm" as any) ?? defaultMessage
|
|
35
|
+
if (!confirm(message)) {
|
|
27
36
|
evt.prevent = true
|
|
28
37
|
}
|
|
29
38
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- TanStack Form / Vue render-fn slot interop */
|
|
1
2
|
import type { DeepKeys } from "@tanstack/vue-form"
|
|
2
3
|
import { type Component, h } from "vue"
|
|
3
4
|
import type { MergedInputProps } from "./InputProps"
|
|
4
|
-
import { type DefaultTypeProps } from "./OmegaFormStuff"
|
|
5
5
|
import OmegaInput from "./OmegaInput.vue"
|
|
6
|
+
import { type DefaultTypeProps } from "./types"
|
|
6
7
|
import { useOmegaForm } from "./useOmegaForm"
|
|
7
8
|
|
|
8
9
|
export const createUseFormWithCustomInput = <
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
import { type Component, computed, type ComputedRef, type ConcreteComponent, h, onUnmounted, type Ref, ref, watch } from "vue"
|
|
4
|
+
import { useIntl } from "../../utils"
|
|
5
|
+
import type { OmegaError } from "./types"
|
|
6
|
+
import type { OF } from "./useOmegaForm"
|
|
7
|
+
|
|
8
|
+
export const useErrorLabel = (form: OF<any, any>) => {
|
|
9
|
+
const { formatMessage } = useIntl()
|
|
10
|
+
const humanize = (str: string) => {
|
|
11
|
+
return str
|
|
12
|
+
.replace(/([A-Z])/g, " $1") // Add space before capital letters
|
|
13
|
+
.replace(/^./, (char) => char.toUpperCase()) // Capitalize the first letter
|
|
14
|
+
.trim() // Remove leading/trailing spaces
|
|
15
|
+
}
|
|
16
|
+
const fallback = (propsName: string) =>
|
|
17
|
+
formatMessage
|
|
18
|
+
? formatMessage({ id: `general.fields.${propsName}`, defaultMessage: humanize(propsName) })
|
|
19
|
+
: humanize(propsName)
|
|
20
|
+
const i18n = (propsName: string) =>
|
|
21
|
+
form.i18nNamespace
|
|
22
|
+
? formatMessage({ id: `${form.i18nNamespace}.fields.${propsName}`, defaultMessage: fallback(propsName) })
|
|
23
|
+
: fallback(propsName)
|
|
24
|
+
|
|
25
|
+
return i18n
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const eHoc = (errorProps: {
|
|
29
|
+
form: OF<any, any>
|
|
30
|
+
fieldMap: Ref<Map<string, { id: string; label: string }>>
|
|
31
|
+
}) => {
|
|
32
|
+
return function FormHoc<P>(
|
|
33
|
+
WrappedComponent: Component<P>
|
|
34
|
+
): ConcreteComponent<P> {
|
|
35
|
+
return {
|
|
36
|
+
setup() {
|
|
37
|
+
const { fieldMap, form } = errorProps
|
|
38
|
+
const generalErrors = form.useStore((state) => state.errors)
|
|
39
|
+
const fieldMeta = form.useStore((state) => state.fieldMeta)
|
|
40
|
+
const errorMap = form.useStore((state) => state.errorMap)
|
|
41
|
+
|
|
42
|
+
const errorLabel = useErrorLabel(form)
|
|
43
|
+
|
|
44
|
+
const errors = computed(() => {
|
|
45
|
+
// Collect errors from fieldMeta (field-level errors for registered fields)
|
|
46
|
+
const fieldErrors = Object.entries(fieldMeta.value).reduce<OmegaError[]>((acc, [key, m]) => {
|
|
47
|
+
const fieldErrors = (m as { errors?: Array<{ message?: string }> } | undefined)?.errors ?? []
|
|
48
|
+
if (!fieldErrors.length) {
|
|
49
|
+
return acc
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fieldInfo = fieldMap.value.get(key)
|
|
53
|
+
if (!fieldInfo) {
|
|
54
|
+
return acc
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
acc.push({
|
|
58
|
+
label: fieldInfo.label,
|
|
59
|
+
inputId: fieldInfo.id,
|
|
60
|
+
errors: [fieldErrors[0]?.message].filter(Boolean) as string[]
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
return acc
|
|
64
|
+
}, [])
|
|
65
|
+
|
|
66
|
+
// Collect errors from errorMap.onDynamic / errorMap.onSubmit ONLY for fields that are NOT registered
|
|
67
|
+
// (registered fields already have their errors in fieldMeta).
|
|
68
|
+
// Our localized standard schema writes to onDynamic via validationLogic: revalidateLogic();
|
|
69
|
+
// caller-provided validators.onSubmit (via tanstackFormOptions spread) writes to onSubmit.
|
|
70
|
+
const submitErrors: OmegaError[] = []
|
|
71
|
+
const submitIssueMaps = [errorMap.value.onDynamic, errorMap.value.onSubmit].filter(
|
|
72
|
+
Boolean
|
|
73
|
+
) as unknown as Array<
|
|
74
|
+
Record<string, unknown>
|
|
75
|
+
>
|
|
76
|
+
const seenPaths = new Set<string>()
|
|
77
|
+
for (const issuesByPath of submitIssueMaps) {
|
|
78
|
+
for (const [_, issues] of Object.entries(issuesByPath)) {
|
|
79
|
+
if (Array.isArray(issues) && issues.length) {
|
|
80
|
+
for (const issue of issues) {
|
|
81
|
+
const issAny: any = issue
|
|
82
|
+
if (issAny?.path && Array.isArray(issAny.path) && issAny.path.length) {
|
|
83
|
+
const fieldPath = issAny.path.join(".")
|
|
84
|
+
if (!fieldMap.value.has(fieldPath) && !seenPaths.has(fieldPath)) {
|
|
85
|
+
seenPaths.add(fieldPath)
|
|
86
|
+
submitErrors.push({
|
|
87
|
+
label: errorLabel(fieldPath),
|
|
88
|
+
inputId: fieldPath,
|
|
89
|
+
errors: [issAny.message].filter(Boolean)
|
|
90
|
+
})
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Combine both error sources (no need to check for duplicates since they're mutually exclusive)
|
|
100
|
+
return [...fieldErrors, ...submitErrors]
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
generalErrors,
|
|
105
|
+
errors
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
render({ errors, generalErrors }: any) {
|
|
109
|
+
return h(WrappedComponent, {
|
|
110
|
+
errors,
|
|
111
|
+
generalErrors,
|
|
112
|
+
...this.$attrs
|
|
113
|
+
} as any, this.$slots)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const makeFieldMap = () => {
|
|
120
|
+
const fieldMap = ref(new Map<string, { label: string; id: string }>())
|
|
121
|
+
const registerField = (field: ComputedRef<{ name: string; label: string; id: string }>) => {
|
|
122
|
+
watch(field, (f) => {
|
|
123
|
+
fieldMap.value.set(f.name, { label: f.label, id: f.id })
|
|
124
|
+
}, { immediate: true })
|
|
125
|
+
onUnmounted(() => {
|
|
126
|
+
// Only delete if we still own this entry (id matches)
|
|
127
|
+
// This prevents old components from deleting entries registered by new components
|
|
128
|
+
// during re-mount transitions (e.g., when :key changes)
|
|
129
|
+
const currentEntry = fieldMap.value.get(field.value.name)
|
|
130
|
+
if (currentEntry?.id === field.value.id) {
|
|
131
|
+
fieldMap.value.delete(field.value.name)
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
return { fieldMap, registerField }
|
|
136
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
import { type Component, type ConcreteComponent, h } from "vue"
|
|
4
|
+
import type { OF } from "./useOmegaForm"
|
|
5
|
+
|
|
6
|
+
export const fHoc = (form: OF<any, any>) => {
|
|
7
|
+
return function FormHoc<P>(
|
|
8
|
+
WrappedComponent: Component<P>
|
|
9
|
+
): ConcreteComponent<P> {
|
|
10
|
+
return {
|
|
11
|
+
render() {
|
|
12
|
+
return h(WrappedComponent, {
|
|
13
|
+
form,
|
|
14
|
+
...this.$attrs
|
|
15
|
+
} as any, this.$slots)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { S } from "effect-app"
|
|
2
|
+
|
|
3
|
+
export { getInputType, type SupportedInputs } from "./inputs"
|
|
4
|
+
export { createMeta, generateMetaFromSchema, isNullableOrUndefined, metadataFromAst } from "./meta/createMeta"
|
|
5
|
+
export type { CreateMeta, FilterItems } from "./meta/createMeta"
|
|
6
|
+
export { defaultsValueFromSchema } from "./meta/defaults"
|
|
7
|
+
export { toFormSchema } from "./meta/redacted"
|
|
8
|
+
export type { BaseFieldMeta, BooleanFieldMeta, DateFieldMeta, FieldMeta, MetaRecord, MultipleFieldMeta, NestedKeyOf, NumberFieldMeta, SelectFieldMeta, StringFieldMeta, UnknownFieldMeta } from "./meta/types"
|
|
9
|
+
export { deepMerge } from "./persistency"
|
|
10
|
+
export type { BaseProps, DefaultTypeProps, FieldPath, FieldPath_, FieldValidators, FormComponent, FormProps, FormType, OmegaArrayProps, OmegaAutoGenMeta, OmegaError, OmegaFormApi, OmegaFormParams, OmegaFormState, OmegaInputProps, OmegaInputPropsBase, PrefixFromDepth, TypeOverride, TypesWithOptions } from "./types"
|
|
11
|
+
export { makeStandardSchemaV1Hooks, toLocalizedStandardSchemaV1 } from "./validation/localized"
|
|
12
|
+
|
|
13
|
+
export { FormErrors, OmegaFormKey, useErrorLabel, useOmegaForm } from "./useOmegaForm"
|
|
14
|
+
export type { defaultValuesPriorityUnion, OF, OmegaConfig, OmegaFormReturn, Policies } from "./useOmegaForm"
|
|
3
15
|
|
|
4
16
|
export { type ExtractTagValue, type ExtractUnionBranch, type InputProps, type MergedInputProps, type TaggedUnionOption, type TaggedUnionOptionsArray, type TaggedUnionProps } from "./InputProps"
|
|
5
17
|
export { default as OmegaInput } from "./OmegaInput.vue"
|
|
@@ -9,6 +21,6 @@ export { default as OmegaTaggedUnionInternal } from "./OmegaTaggedUnionInternal.
|
|
|
9
21
|
|
|
10
22
|
export { useOnClose, usePreventClose } from "./blockDialog"
|
|
11
23
|
|
|
12
|
-
export { getInputType } from "./OmegaFormStuff"
|
|
13
|
-
|
|
14
24
|
export { createUseFormWithCustomInput } from "./createUseFormWithCustomInput"
|
|
25
|
+
|
|
26
|
+
export const duplicateSchema = <From, To>(schema: S.Codec<To, From, never>) => schema
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const supportedInputs = [
|
|
2
|
+
"button",
|
|
3
|
+
"checkbox",
|
|
4
|
+
"color",
|
|
5
|
+
"date",
|
|
6
|
+
"email",
|
|
7
|
+
"number",
|
|
8
|
+
"password",
|
|
9
|
+
"radio",
|
|
10
|
+
"range",
|
|
11
|
+
"search",
|
|
12
|
+
"submit",
|
|
13
|
+
"tel",
|
|
14
|
+
"text",
|
|
15
|
+
"time",
|
|
16
|
+
"url"
|
|
17
|
+
] as const
|
|
18
|
+
|
|
19
|
+
export type SupportedInputs = typeof supportedInputs[number]
|
|
20
|
+
|
|
21
|
+
export const getInputType = (input: string): SupportedInputs =>
|
|
22
|
+
(supportedInputs as readonly string[]).includes(input) ? input as SupportedInputs : "text"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { type Record, S } from "effect-app"
|
|
3
|
+
import type { FieldMeta } from "./types"
|
|
4
|
+
|
|
5
|
+
export const getCheckMetas = (property: S.AST.AST): Array<Record<string, any>> => {
|
|
6
|
+
const checks = property.checks ?? []
|
|
7
|
+
|
|
8
|
+
return checks.flatMap((check) => {
|
|
9
|
+
if (check._tag === "FilterGroup") {
|
|
10
|
+
return check.checks.flatMap((inner) => {
|
|
11
|
+
const meta = inner.annotations?.meta
|
|
12
|
+
return meta && typeof meta === "object" ? [meta as Record<string, any>] : []
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const meta = check.annotations?.meta
|
|
17
|
+
return meta && typeof meta === "object" ? [meta as Record<string, any>] : []
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const getFieldMetadataFromAst = (property: S.AST.AST) => {
|
|
22
|
+
const base: Partial<FieldMeta> & Record<string, unknown> = {
|
|
23
|
+
description: S.AST.resolveDescription(property)
|
|
24
|
+
}
|
|
25
|
+
const checks = getCheckMetas(property)
|
|
26
|
+
|
|
27
|
+
if (S.AST.isString(property)) {
|
|
28
|
+
base.type = "string"
|
|
29
|
+
for (const check of checks) {
|
|
30
|
+
switch (check._tag) {
|
|
31
|
+
case "isMinLength":
|
|
32
|
+
base.minLength = check.minLength
|
|
33
|
+
break
|
|
34
|
+
case "isMaxLength":
|
|
35
|
+
base.maxLength = check.maxLength
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const format = property.annotations?.["format"]
|
|
41
|
+
if (format === "email") {
|
|
42
|
+
base.format = "email"
|
|
43
|
+
}
|
|
44
|
+
} else if (S.AST.isNumber(property)) {
|
|
45
|
+
base.type = "number"
|
|
46
|
+
for (const check of checks) {
|
|
47
|
+
switch (check._tag) {
|
|
48
|
+
case "isInt":
|
|
49
|
+
base.refinement = "int"
|
|
50
|
+
break
|
|
51
|
+
case "isGreaterThanOrEqualTo":
|
|
52
|
+
base.minimum = check.minimum
|
|
53
|
+
break
|
|
54
|
+
case "isLessThanOrEqualTo":
|
|
55
|
+
base.maximum = check.maximum
|
|
56
|
+
break
|
|
57
|
+
case "isBetween":
|
|
58
|
+
base.minimum = check.minimum
|
|
59
|
+
base.maximum = check.maximum
|
|
60
|
+
break
|
|
61
|
+
case "isGreaterThan":
|
|
62
|
+
base.exclusiveMinimum = check.exclusiveMinimum
|
|
63
|
+
break
|
|
64
|
+
case "isLessThan":
|
|
65
|
+
base.exclusiveMaximum = check.exclusiveMaximum
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} else if (S.AST.isBoolean(property)) {
|
|
70
|
+
base.type = "boolean"
|
|
71
|
+
} else if (
|
|
72
|
+
S.AST.isDeclaration(property)
|
|
73
|
+
&& (property.annotations as any)?.typeConstructor?._tag === "Date"
|
|
74
|
+
) {
|
|
75
|
+
base.type = "date"
|
|
76
|
+
} else {
|
|
77
|
+
base.type = "unknown"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return base
|
|
81
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { type Effect, type Record, S } from "effect-app"
|
|
3
|
+
import { getTransformationFrom } from "../../../utils"
|
|
4
|
+
import type { FieldMeta, MetaRecord } from "./types"
|
|
5
|
+
import { classifyAndWalkUnion, leafMetaForAst, type ParentMeta, type WalkerContext, walkStruct } from "./walker"
|
|
6
|
+
|
|
7
|
+
export type FilterItems = {
|
|
8
|
+
items: readonly [string, ...string[]]
|
|
9
|
+
message:
|
|
10
|
+
| string
|
|
11
|
+
| Effect.Effect<string, never, never>
|
|
12
|
+
| { readonly message: string | Effect.Effect<string> }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type CreateMeta =
|
|
16
|
+
& {
|
|
17
|
+
parent?: string
|
|
18
|
+
meta?: Record<string, any>
|
|
19
|
+
nullableOrUndefined?: false | "undefined" | "null"
|
|
20
|
+
}
|
|
21
|
+
& (
|
|
22
|
+
| {
|
|
23
|
+
propertySignatures: readonly S.AST.PropertySignature[]
|
|
24
|
+
property?: never
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
propertySignatures?: never
|
|
28
|
+
property: S.AST.AST
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
export const unwrapDeclaration = (property: S.AST.AST): S.AST.AST => {
|
|
33
|
+
let current = getTransformationFrom(property)
|
|
34
|
+
|
|
35
|
+
while (S.AST.isDeclaration(current) && current.typeParameters.length > 0) {
|
|
36
|
+
current = getTransformationFrom(current.typeParameters[0]!)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return current
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const isNullableOrUndefined = (property: false | S.AST.AST | undefined) => {
|
|
43
|
+
if (!property || !S.AST.isUnion(property)) return false
|
|
44
|
+
if (property.types.find((_) => S.AST.isUndefined(_))) {
|
|
45
|
+
return "undefined"
|
|
46
|
+
}
|
|
47
|
+
if (property.types.find((_) => S.AST.isNull(_))) return "null"
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const createMeta = <T = any>(
|
|
52
|
+
{ meta = {}, parent = "", property, propertySignatures }: CreateMeta,
|
|
53
|
+
acc: Partial<MetaRecord<T>> = {}
|
|
54
|
+
): MetaRecord<T> | FieldMeta => {
|
|
55
|
+
const ctx: WalkerContext<T> = { acc, unionMeta: {} }
|
|
56
|
+
|
|
57
|
+
if (propertySignatures) {
|
|
58
|
+
const parentMeta: ParentMeta = {
|
|
59
|
+
required: meta.required !== false,
|
|
60
|
+
nullableOrUndefined: meta.nullableOrUndefined ?? false
|
|
61
|
+
}
|
|
62
|
+
walkStruct(propertySignatures, parent, parentMeta, ctx)
|
|
63
|
+
return acc
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (property) {
|
|
67
|
+
const nullableOrUndefined = isNullableOrUndefined(property)
|
|
68
|
+
const unwrapped = unwrapDeclaration(property)
|
|
69
|
+
const required = !Object.hasOwnProperty.call(meta, "required")
|
|
70
|
+
? !nullableOrUndefined
|
|
71
|
+
: (meta.required as boolean)
|
|
72
|
+
|
|
73
|
+
const parentMeta: ParentMeta = {
|
|
74
|
+
required,
|
|
75
|
+
nullableOrUndefined: (meta.nullableOrUndefined ?? nullableOrUndefined) as false | "null" | "undefined"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (S.AST.isObjects(unwrapped)) {
|
|
79
|
+
walkStruct(unwrapped.propertySignatures, parent, parentMeta, ctx)
|
|
80
|
+
return acc
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (S.AST.isUnion(unwrapped)) {
|
|
84
|
+
// For property-mode, return a FieldMeta by running through classifyAndWalkUnion
|
|
85
|
+
// and then pulling out the result at `parent` key
|
|
86
|
+
const leafCtx: WalkerContext<T> = { acc: {}, unionMeta: {} }
|
|
87
|
+
classifyAndWalkUnion(unwrapped, parent, parentMeta, leafCtx)
|
|
88
|
+
const result = (leafCtx.acc as any)[parent]
|
|
89
|
+
if (result) return result as FieldMeta
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return leafMetaForAst(unwrapped, parentMeta)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return acc
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const metadataFromAst = <From, To>(
|
|
99
|
+
schema: S.Codec<To, From, never>
|
|
100
|
+
): {
|
|
101
|
+
meta: MetaRecord<To>
|
|
102
|
+
defaultValues: Record<string, any>
|
|
103
|
+
unionMeta: Record<string, MetaRecord<To>>
|
|
104
|
+
} => {
|
|
105
|
+
const ast = unwrapDeclaration(schema.ast)
|
|
106
|
+
const newMeta: Partial<MetaRecord<To>> = {}
|
|
107
|
+
const defaultValues: Record<string, any> = {}
|
|
108
|
+
const unionMeta: Record<string, MetaRecord<To>> = {}
|
|
109
|
+
|
|
110
|
+
const ctx: WalkerContext<To> = { acc: newMeta, unionMeta }
|
|
111
|
+
|
|
112
|
+
if (S.AST.isUnion(ast)) {
|
|
113
|
+
// Root-level discriminated union
|
|
114
|
+
classifyAndWalkUnion(ast, "", { required: true, nullableOrUndefined: false }, ctx)
|
|
115
|
+
|
|
116
|
+
return { meta: newMeta as MetaRecord<To>, defaultValues, unionMeta }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (S.AST.isObjects(ast)) {
|
|
120
|
+
walkStruct(ast.propertySignatures, "", { required: true, nullableOrUndefined: false }, ctx)
|
|
121
|
+
|
|
122
|
+
return { meta: newMeta as MetaRecord<To>, defaultValues, unionMeta }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { meta: newMeta as MetaRecord<To>, defaultValues, unionMeta }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const generateMetaFromSchema = <From, To>(
|
|
129
|
+
schema: S.Codec<To, From, never>
|
|
130
|
+
): {
|
|
131
|
+
schema: S.Codec<To, From, never>
|
|
132
|
+
meta: MetaRecord<To>
|
|
133
|
+
unionMeta: Record<string, MetaRecord<To>>
|
|
134
|
+
} => {
|
|
135
|
+
const { meta, unionMeta } = metadataFromAst(schema)
|
|
136
|
+
|
|
137
|
+
return { schema, meta, unionMeta }
|
|
138
|
+
}
|