@effect-app/vue-components 4.0.0-beta.22 → 4.0.0-beta.220

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.
Files changed (196) hide show
  1. package/README.md +13 -9
  2. package/dist/reset.css +39 -38
  3. package/dist/types/components/CommandButton.vue.d.ts +6 -4
  4. package/dist/types/components/OmegaForm/OmegaArray.vue.d.ts +1 -1
  5. package/dist/types/components/OmegaForm/OmegaAutoGen.vue.d.ts +1 -1
  6. package/dist/types/components/OmegaForm/OmegaErrorsInternal.vue.d.ts +1 -1
  7. package/dist/types/components/OmegaForm/OmegaFormInput.vue.d.ts +1 -1
  8. package/dist/types/components/OmegaForm/OmegaInput.vue.d.ts +1 -1
  9. package/dist/types/components/OmegaForm/OmegaInternalInput.vue.d.ts +2 -1
  10. package/dist/types/components/OmegaForm/OmegaWrapper.vue.d.ts +1 -1
  11. package/dist/types/components/OmegaForm/createUseFormWithCustomInput.d.ts +2 -2
  12. package/dist/types/components/OmegaForm/errors.d.ts +33 -0
  13. package/dist/types/components/OmegaForm/getOmegaStore.d.ts +1 -1
  14. package/dist/types/components/OmegaForm/hocs.d.ts +3 -0
  15. package/dist/types/components/OmegaForm/index.d.ts +13 -3
  16. package/dist/types/components/OmegaForm/inputs.d.ts +4 -0
  17. package/dist/types/components/OmegaForm/meta/checks.d.ts +4 -0
  18. package/dist/types/components/OmegaForm/meta/createMeta.d.ts +32 -0
  19. package/dist/types/components/OmegaForm/meta/defaults.d.ts +2 -0
  20. package/dist/types/components/OmegaForm/meta/redacted.d.ts +2 -0
  21. package/dist/types/components/OmegaForm/meta/types.d.ts +56 -0
  22. package/dist/types/components/OmegaForm/meta/walker.d.ts +18 -0
  23. package/dist/types/components/OmegaForm/persistency.d.ts +58 -0
  24. package/dist/types/components/OmegaForm/submit.d.ts +60 -0
  25. package/dist/types/components/OmegaForm/types.d.ts +281 -0
  26. package/dist/types/components/OmegaForm/useOmegaForm.d.ts +7 -213
  27. package/dist/types/components/OmegaForm/validation/localized.d.ts +10 -0
  28. package/dist/types/index.d.ts +0 -1
  29. package/dist/types/utils/index.d.ts +6 -6
  30. package/dist/vue-components.es.js +29 -44
  31. package/dist/vue-components10.es.js +5 -0
  32. package/dist/vue-components11.es.js +20 -0
  33. package/dist/vue-components12.es.js +49 -0
  34. package/dist/vue-components13.es.js +128 -0
  35. package/dist/vue-components14.es.js +65 -0
  36. package/dist/vue-components15.es.js +60 -0
  37. package/dist/vue-components16.es.js +22 -0
  38. package/dist/vue-components17.es.js +5 -0
  39. package/dist/vue-components18.es.js +80 -0
  40. package/dist/vue-components19.es.js +92 -0
  41. package/dist/vue-components2.es.js +11 -0
  42. package/dist/vue-components20.es.js +73 -0
  43. package/dist/vue-components21.es.js +12 -0
  44. package/dist/vue-components22.es.js +56 -0
  45. package/dist/vue-components23.es.js +5 -0
  46. package/dist/vue-components24.es.js +44 -0
  47. package/dist/vue-components25.es.js +5 -0
  48. package/dist/vue-components26.es.js +84 -0
  49. package/dist/vue-components28.es.js +8 -0
  50. package/dist/vue-components29.es.js +9 -0
  51. package/dist/vue-components3.es.js +86 -0
  52. package/dist/vue-components30.es.js +269 -0
  53. package/dist/vue-components32.es.js +8 -0
  54. package/dist/vue-components33.es.js +73 -0
  55. package/dist/vue-components34.es.js +5 -0
  56. package/dist/vue-components35.es.js +52 -0
  57. package/dist/vue-components36.es.js +5 -0
  58. package/dist/vue-components37.es.js +24 -0
  59. package/dist/vue-components38.es.js +5 -0
  60. package/dist/vue-components39.es.js +59 -0
  61. package/dist/vue-components4.es.js +5 -0
  62. package/dist/vue-components40.es.js +5 -0
  63. package/dist/vue-components41.es.js +12 -0
  64. package/dist/vue-components42.es.js +22 -0
  65. package/dist/vue-components44.es.js +9 -0
  66. package/dist/vue-components45.es.js +4 -0
  67. package/dist/vue-components46.es.js +38 -0
  68. package/dist/vue-components47.es.js +27 -0
  69. package/dist/vue-components48.es.js +28 -0
  70. package/dist/vue-components49.es.js +7 -0
  71. package/dist/vue-components5.es.js +24 -0
  72. package/dist/vue-components50.es.js +18 -0
  73. package/dist/vue-components51.es.js +36 -0
  74. package/dist/vue-components52.es.js +18 -0
  75. package/dist/vue-components53.es.js +21 -0
  76. package/dist/vue-components54.es.js +30 -0
  77. package/dist/vue-components55.es.js +7 -0
  78. package/dist/vue-components56.es.js +9 -0
  79. package/dist/vue-components57.es.js +38 -0
  80. package/dist/vue-components58.es.js +25 -0
  81. package/dist/vue-components59.es.js +128 -0
  82. package/dist/vue-components6.es.js +13 -0
  83. package/dist/vue-components60.es.js +24 -0
  84. package/dist/vue-components61.es.js +21 -0
  85. package/dist/vue-components62.es.js +9 -0
  86. package/dist/vue-components63.es.js +19 -0
  87. package/dist/vue-components64.es.js +5 -0
  88. package/dist/vue-components65.es.js +29 -0
  89. package/dist/vue-components66.es.js +5 -0
  90. package/dist/vue-components67.es.js +29 -0
  91. package/dist/vue-components68.es.js +6 -0
  92. package/dist/vue-components69.es.js +18 -0
  93. package/dist/vue-components7.es.js +13 -0
  94. package/dist/vue-components70.es.js +40 -0
  95. package/dist/vue-components71.es.js +81 -0
  96. package/dist/vue-components72.es.js +33 -0
  97. package/dist/vue-components73.es.js +19 -0
  98. package/dist/vue-components74.es.js +48 -0
  99. package/dist/vue-components8.es.js +35 -0
  100. package/dist/vue-components9.es.js +47 -0
  101. package/package.json +29 -29
  102. package/src/components/CommandButton.vue +55 -7
  103. package/src/components/OmegaForm/OmegaArray.vue +2 -4
  104. package/src/components/OmegaForm/OmegaAutoGen.vue +2 -1
  105. package/src/components/OmegaForm/OmegaErrorsInternal.vue +1 -1
  106. package/src/components/OmegaForm/OmegaFormInput.vue +1 -1
  107. package/src/components/OmegaForm/OmegaInput.vue +7 -36
  108. package/src/components/OmegaForm/OmegaInputVuetify.vue +5 -2
  109. package/src/components/OmegaForm/OmegaInternalInput.vue +12 -6
  110. package/src/components/OmegaForm/OmegaTaggedUnion.vue +2 -1
  111. package/src/components/OmegaForm/OmegaTaggedUnionInternal.vue +1 -1
  112. package/src/components/OmegaForm/OmegaWrapper.vue +1 -1
  113. package/src/components/OmegaForm/blockDialog.ts +18 -6
  114. package/src/components/OmegaForm/createUseFormWithCustomInput.ts +2 -1
  115. package/src/components/OmegaForm/errors.ts +136 -0
  116. package/src/components/OmegaForm/getOmegaStore.ts +1 -1
  117. package/src/components/OmegaForm/hocs.ts +19 -0
  118. package/src/components/OmegaForm/index.ts +16 -4
  119. package/src/components/OmegaForm/inputs.ts +22 -0
  120. package/src/components/OmegaForm/meta/checks.ts +81 -0
  121. package/src/components/OmegaForm/meta/createMeta.ts +138 -0
  122. package/src/components/OmegaForm/meta/defaults.ts +132 -0
  123. package/src/components/OmegaForm/meta/redacted.ts +66 -0
  124. package/src/components/OmegaForm/meta/types.ts +78 -0
  125. package/src/components/OmegaForm/meta/walker.ts +248 -0
  126. package/src/components/OmegaForm/persistency.ts +247 -0
  127. package/src/components/OmegaForm/submit.ts +128 -0
  128. package/src/components/OmegaForm/types.ts +751 -0
  129. package/src/components/OmegaForm/useOmegaForm.ts +58 -893
  130. package/src/components/OmegaForm/validation/localized.ts +202 -0
  131. package/src/index.ts +0 -1
  132. package/src/reset.css +39 -38
  133. package/src/utils/index.ts +10 -7
  134. package/dist/types/components/OmegaForm/OmegaFormStuff.d.ts +0 -159
  135. package/dist/types/constants/index.d.ts +0 -1
  136. package/dist/vue-components.es10.js +0 -239
  137. package/dist/vue-components.es11.js +0 -32
  138. package/dist/vue-components.es12.js +0 -503
  139. package/dist/vue-components.es13.js +0 -49
  140. package/dist/vue-components.es14.js +0 -4
  141. package/dist/vue-components.es15.js +0 -4
  142. package/dist/vue-components.es16.js +0 -6
  143. package/dist/vue-components.es17.js +0 -13
  144. package/dist/vue-components.es18.js +0 -57
  145. package/dist/vue-components.es19.js +0 -56
  146. package/dist/vue-components.es2.js +0 -30
  147. package/dist/vue-components.es20.js +0 -8
  148. package/dist/vue-components.es21.js +0 -8
  149. package/dist/vue-components.es22.js +0 -5
  150. package/dist/vue-components.es23.js +0 -5
  151. package/dist/vue-components.es24.js +0 -4
  152. package/dist/vue-components.es25.js +0 -4
  153. package/dist/vue-components.es26.js +0 -4
  154. package/dist/vue-components.es27.js +0 -4
  155. package/dist/vue-components.es28.js +0 -19
  156. package/dist/vue-components.es29.js +0 -13
  157. package/dist/vue-components.es3.js +0 -17
  158. package/dist/vue-components.es30.js +0 -194
  159. package/dist/vue-components.es32.js +0 -31
  160. package/dist/vue-components.es33.js +0 -6
  161. package/dist/vue-components.es34.js +0 -4
  162. package/dist/vue-components.es35.js +0 -4
  163. package/dist/vue-components.es36.js +0 -113
  164. package/dist/vue-components.es38.js +0 -9
  165. package/dist/vue-components.es39.js +0 -34
  166. package/dist/vue-components.es4.js +0 -52
  167. package/dist/vue-components.es41.js +0 -6
  168. package/dist/vue-components.es42.js +0 -25
  169. package/dist/vue-components.es43.js +0 -7
  170. package/dist/vue-components.es44.js +0 -23
  171. package/dist/vue-components.es45.js +0 -32
  172. package/dist/vue-components.es46.js +0 -24
  173. package/dist/vue-components.es47.js +0 -14
  174. package/dist/vue-components.es48.js +0 -7
  175. package/dist/vue-components.es49.js +0 -21
  176. package/dist/vue-components.es5.js +0 -52
  177. package/dist/vue-components.es50.js +0 -11
  178. package/dist/vue-components.es51.js +0 -33
  179. package/dist/vue-components.es52.js +0 -50
  180. package/dist/vue-components.es53.js +0 -28
  181. package/dist/vue-components.es54.js +0 -13
  182. package/dist/vue-components.es55.js +0 -67
  183. package/dist/vue-components.es56.js +0 -58
  184. package/dist/vue-components.es57.js +0 -19
  185. package/dist/vue-components.es58.js +0 -35
  186. package/dist/vue-components.es59.js +0 -31
  187. package/dist/vue-components.es6.js +0 -69
  188. package/dist/vue-components.es60.js +0 -44
  189. package/dist/vue-components.es61.js +0 -4
  190. package/dist/vue-components.es62.js +0 -46
  191. package/dist/vue-components.es63.js +0 -4
  192. package/dist/vue-components.es7.js +0 -83
  193. package/dist/vue-components.es8.js +0 -63
  194. package/dist/vue-components.es9.js +0 -21
  195. package/src/components/OmegaForm/OmegaFormStuff.ts +0 -1276
  196. package/src/constants/index.ts +0 -1
@@ -0,0 +1,248 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any -- AST walker interops with Effect Schema generics */
2
+ /* eslint-disable @typescript-eslint/no-use-before-define -- mutual recursion between walk and helpers (handleStruct/handleUnion/etc.) */
3
+ import { S } from "effect-app"
4
+ import { getFieldMetadataFromAst } from "./checks"
5
+ import { isNullableOrUndefined, unwrapDeclaration } from "./createMeta"
6
+ import type { FieldMeta, MetaRecord, NestedKeyOf, SelectFieldMeta } from "./types"
7
+
8
+ const isNullishType = (property: S.AST.AST) => S.AST.isUndefined(property) || S.AST.isNull(property)
9
+
10
+ // TODO: remove after manual _tag deprecation — S.Struct({ _tag: S.Literal("X") }) wraps as Union([Literal("X")])
11
+ const unwrapSingleLiteralUnion = (ast: S.AST.AST): S.AST.AST =>
12
+ S.AST.isUnion(ast) && ast.types.length === 1 && S.AST.isLiteral(ast.types[0])
13
+ ? ast.types[0]
14
+ : ast
15
+
16
+ const unwrapNestedUnions = (types: readonly S.AST.AST[]): readonly S.AST.AST[] =>
17
+ types.flatMap((type) => S.AST.isUnion(type) ? unwrapNestedUnions(type.types) : [type])
18
+
19
+ export type WalkerContext<T> = {
20
+ acc: Partial<MetaRecord<T>>
21
+ unionMeta: Record<string, MetaRecord<T>>
22
+ }
23
+
24
+ export type ParentMeta = {
25
+ required: boolean
26
+ nullableOrUndefined: false | "null" | "undefined"
27
+ /** Set when iterating the members of a nullable discriminated union */
28
+ isNullableDiscriminatedUnion?: boolean
29
+ /** Set when this property was declared with S.optionalKey */
30
+ isOptionalKey?: boolean
31
+ }
32
+
33
+ export const leafMetaForAst = (
34
+ ast: S.AST.AST,
35
+ parentMeta: ParentMeta
36
+ ): FieldMeta => {
37
+ const { nullableOrUndefined, required } = parentMeta
38
+
39
+ if (S.AST.isArrays(ast)) {
40
+ return {
41
+ required,
42
+ nullableOrUndefined,
43
+ type: "multiple",
44
+ members: ast.elements,
45
+ rest: ast.rest
46
+ } as FieldMeta
47
+ }
48
+
49
+ if (S.AST.isLiteral(ast)) {
50
+ return {
51
+ required,
52
+ nullableOrUndefined,
53
+ type: "select",
54
+ members: [ast.literal]
55
+ } as FieldMeta
56
+ }
57
+
58
+ return {
59
+ ...getFieldMetadataFromAst(ast),
60
+ required,
61
+ nullableOrUndefined
62
+ } as FieldMeta
63
+ }
64
+
65
+ export const walkStruct = <T>(
66
+ propertySignatures: readonly S.AST.PropertySignature[],
67
+ parent: string,
68
+ parentMeta: ParentMeta,
69
+ ctx: WalkerContext<T>
70
+ ): void => {
71
+ for (const p of propertySignatures) {
72
+ const key = parent ? `${parent}.${p.name.toString()}` : p.name.toString()
73
+ const nullableOrUndefined = isNullableOrUndefined(p.type)
74
+ const isOptionalKey = (p.type as any).context?.isOptional === true
75
+
76
+ let isRequired: boolean
77
+ if (parentMeta.isNullableDiscriminatedUnion && p.name.toString() === "_tag") {
78
+ isRequired = false
79
+ } else if (!parentMeta.required) {
80
+ isRequired = false
81
+ } else if (isOptionalKey) {
82
+ isRequired = false
83
+ } else {
84
+ isRequired = !nullableOrUndefined
85
+ }
86
+
87
+ walk(
88
+ p.type,
89
+ key,
90
+ { required: isRequired, nullableOrUndefined, isOptionalKey },
91
+ ctx
92
+ )
93
+ }
94
+ }
95
+
96
+ export const classifyAndWalkUnion = <T>(
97
+ unionAst: S.AST.Union,
98
+ key: string,
99
+ parentMeta: ParentMeta,
100
+ ctx: WalkerContext<T>
101
+ ): void => {
102
+ const { acc } = ctx
103
+ const unwrappedTypes = unwrapNestedUnions(unionAst.types).map(unwrapDeclaration)
104
+ const nonNullTypes = unwrappedTypes.filter((t) => !isNullishType(t))
105
+
106
+ // Boolean literal shortcut (single-value union wrapping a boolean literal)
107
+ if (nonNullTypes.length === 1 && S.AST.isLiteral(nonNullTypes[0]) && typeof nonNullTypes[0].literal === "boolean") {
108
+ acc[key as NestedKeyOf<T>] = leafMetaForAst(nonNullTypes[0], parentMeta)
109
+ return
110
+ }
111
+
112
+ if (nonNullTypes.some(S.AST.isObjects)) {
113
+ const isNullableDiscriminatedUnion = !!parentMeta.nullableOrUndefined && nonNullTypes.length > 1
114
+
115
+ // Mixed union: also create a parent leaf entry from the first non-struct member
116
+ if (!parentMeta.nullableOrUndefined && key) {
117
+ const firstNonStruct = nonNullTypes.find((t) => !S.AST.isObjects(t))
118
+ if (firstNonStruct) {
119
+ acc[key as NestedKeyOf<T>] = leafMetaForAst(firstNonStruct, parentMeta)
120
+ }
121
+ }
122
+
123
+ const discriminatorValues: any[] = []
124
+ const branchParentMeta: ParentMeta = isNullableDiscriminatedUnion
125
+ ? { required: true, nullableOrUndefined: false, isNullableDiscriminatedUnion: true }
126
+ : { required: true, nullableOrUndefined: false }
127
+
128
+ for (const memberType of nonNullTypes) {
129
+ if (!S.AST.isObjects(memberType)) continue
130
+
131
+ const tagProp = memberType.propertySignatures.find((p) => p.name.toString() === "_tag")
132
+ const resolvedTagType = tagProp ? unwrapSingleLiteralUnion(tagProp.type) : null
133
+ let tagValue: string | null = null
134
+
135
+ if (resolvedTagType && S.AST.isLiteral(resolvedTagType)) {
136
+ tagValue = resolvedTagType.literal as string
137
+ if (!discriminatorValues.includes(tagValue)) discriminatorValues.push(tagValue)
138
+ }
139
+
140
+ const branchCtx: WalkerContext<T> = { acc: {}, unionMeta: ctx.unionMeta }
141
+ walkStruct(memberType.propertySignatures, key, branchParentMeta, branchCtx)
142
+
143
+ if (tagValue) {
144
+ const existing = ctx.unionMeta[tagValue]
145
+ if (existing) Object.assign(existing, branchCtx.acc as MetaRecord<T>)
146
+ else ctx.unionMeta[tagValue] = branchCtx.acc as MetaRecord<T>
147
+ }
148
+
149
+ for (const [metaKey, metaValue] of Object.entries(branchCtx.acc)) {
150
+ const existing = acc[metaKey as NestedKeyOf<T>]
151
+ if (existing && existing.type === "select" && (metaValue as any)?.type === "select") {
152
+ existing.members = [
153
+ ...existing.members,
154
+ ...(metaValue as SelectFieldMeta).members.filter((m: any) => !existing.members.includes(m))
155
+ ]
156
+ } else {
157
+ acc[metaKey as NestedKeyOf<T>] = metaValue as FieldMeta
158
+ }
159
+ }
160
+ }
161
+
162
+ if (discriminatorValues.length > 0) {
163
+ const tagKey = key ? `${key}._tag` : "_tag"
164
+ const existing = acc[tagKey as NestedKeyOf<T>]
165
+ if (existing && existing.type === "select") {
166
+ for (const v of discriminatorValues) {
167
+ if (!existing.members.includes(v)) existing.members.push(v)
168
+ }
169
+ } else {
170
+ acc[tagKey as NestedKeyOf<T>] = {
171
+ type: "select",
172
+ members: discriminatorValues,
173
+ required: !isNullableDiscriminatedUnion
174
+ } as FieldMeta
175
+ }
176
+ }
177
+ return
178
+ }
179
+
180
+ const ast = nonNullTypes.find(S.AST.isArrays)
181
+ if (ast) {
182
+ walk(ast, key, parentMeta, ctx)
183
+ return
184
+ }
185
+
186
+ // Literal / primitive union (e.g. legacy _tag pattern)
187
+ const resolvedTypes = unwrappedTypes.map(unwrapSingleLiteralUnion)
188
+ if (resolvedTypes.every((_) => isNullishType(_) || S.AST.isLiteral(_))) {
189
+ const { isOptionalKey, nullableOrUndefined, required } = parentMeta
190
+ const leaf: FieldMeta = {
191
+ required,
192
+ nullableOrUndefined,
193
+ type: "select",
194
+ members: resolvedTypes.filter(S.AST.isLiteral).map((t) => t.literal)
195
+ } as FieldMeta
196
+ if (isOptionalKey) leaf.isOptionalKey = true
197
+ acc[key as NestedKeyOf<T>] = leaf
198
+ return
199
+ }
200
+
201
+ // Fallback: recurse into first non-null type
202
+ const nonNullType = nonNullTypes[0]
203
+ if (nonNullType) walk(nonNullType, key, parentMeta, ctx)
204
+ }
205
+
206
+ export const walk = <T>(
207
+ ast: S.AST.AST,
208
+ key: string,
209
+ parentMeta: ParentMeta,
210
+ ctx: WalkerContext<T>
211
+ ): void => {
212
+ ast = unwrapDeclaration(ast)
213
+ const { acc } = ctx
214
+
215
+ if (S.AST.isObjects(ast)) {
216
+ walkStruct(ast.propertySignatures, key, parentMeta, ctx)
217
+ return
218
+ }
219
+
220
+ if (S.AST.isUnion(ast)) {
221
+ classifyAndWalkUnion(ast, key, parentMeta, ctx)
222
+ return
223
+ }
224
+
225
+ if (S.AST.isArrays(ast)) {
226
+ const restElement = ast.rest.length > 0 ? unwrapDeclaration(ast.rest[0]) : null
227
+ if (restElement && S.AST.isObjects(restElement)) {
228
+ // Array-of-struct: skip creating a meta entry for the array itself,
229
+ // recurse into the element struct's properties instead
230
+ walkStruct(restElement.propertySignatures, key, { required: true, nullableOrUndefined: false }, ctx)
231
+ return
232
+ }
233
+
234
+ // Primitive or tuple array
235
+ acc[key as NestedKeyOf<T>] = leafMetaForAst(ast, parentMeta)
236
+ return
237
+ }
238
+
239
+ // Leaf primitive / literal / unknown
240
+ const { isOptionalKey, nullableOrUndefined, required } = parentMeta
241
+ const adjusted: ParentMeta = {
242
+ required: required && (!S.AST.isString(ast) || !!getFieldMetadataFromAst(ast).minLength),
243
+ nullableOrUndefined
244
+ }
245
+ const leaf = leafMetaForAst(ast, adjusted)
246
+ if (isOptionalKey) leaf.isOptionalKey = true
247
+ acc[key as NestedKeyOf<T>] = leaf
248
+ }
@@ -0,0 +1,247 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { isObject } from "@vueuse/core"
3
+ import { computed, type ComputedRef, onBeforeUnmount, onMounted, onUnmounted } from "vue"
4
+ import { type MetaRecord } from "./meta/types"
5
+
6
+ export type Policies = "local" | "session" | "querystring"
7
+ export type DefaultValuesPriorityUnion = "tanstack" | "persistency" | "schema"
8
+
9
+ // Backward-compatible alias for the legacy lowercased-prefix type name.
10
+ export type defaultValuesPriorityUnion = DefaultValuesPriorityUnion
11
+
12
+ export interface PersistencyConfig {
13
+ /** Order of importance:
14
+ * - "querystring": Highest priority when persisting
15
+ * - "local" and then "session": Lower priority storage options
16
+ */
17
+ policies?: ReadonlyArray<Policies>
18
+ overrideDefaultValues?: "deprecated: use defaultValuesPriority"
19
+ id?: string
20
+ keys?: ReadonlyArray<string> | "You should only use one of banKeys or keys, not both, moron"
21
+ banKeys?: ReadonlyArray<string> | "You should only use one of banKeys or keys, not both, moron"
22
+ }
23
+
24
+ export function deepMerge(target: any, source: any) {
25
+ const result = { ...target }
26
+ for (const key in source) {
27
+ if (Array.isArray(source[key])) {
28
+ // Arrays should be copied directly, not deep merged
29
+ result[key] = source[key]
30
+ } else if (source[key] && isObject(source[key])) {
31
+ result[key] = deepMerge(result[key], source[key])
32
+ } else {
33
+ result[key] = source[key]
34
+ }
35
+ }
36
+ return result
37
+ }
38
+
39
+ const includesPolicy = (arr: ReadonlyArray<Policies>, policy: Policies) => {
40
+ return arr.includes(policy)
41
+ }
42
+
43
+ export interface UsePersistencyOptions<From> {
44
+ meta: MetaRecord<From>
45
+ persistency?: PersistencyConfig
46
+ preventWindowExit?: "prevent" | "prevent-and-reset" | "nope"
47
+ defaultValuesPriority?: DefaultValuesPriorityUnion[] | readonly DefaultValuesPriorityUnion[]
48
+ /** Tanstack-provided default values (highest priority by default). */
49
+ tanstackDefaultValues?: any
50
+ /** Lazy schema-derived defaults factory. */
51
+ schemaDefaultValues: () => any
52
+ /**
53
+ * Lazy accessor for the form. Lazy because persistency is created BEFORE
54
+ * the form (its `defaultValues` are passed into `useForm`), but the
55
+ * persistence callbacks (`persistData`, `saveDataInUrl`, the
56
+ * `beforeunload` listener) only run later and need the live form.
57
+ */
58
+ getForm: () => {
59
+ store: { state: { values: any; isDirty: boolean } }
60
+ getFieldValue: (path: any) => any
61
+ }
62
+ }
63
+
64
+ export interface UsePersistencyReturn {
65
+ defaultValues: ComputedRef
66
+ persistencyKey: ComputedRef<string>
67
+ persistData: () => void
68
+ saveDataInUrl: () => void
69
+ clearUrlParams: () => void
70
+ }
71
+
72
+ /**
73
+ * Encapsulates form-data persistency: loading default values from
74
+ * localStorage / sessionStorage / querystring, persisting them on unmount
75
+ * or window blur, and the optional `preventWindowExit` warning listener.
76
+ *
77
+ * The `prevent-and-reset` reset-on-success behavior is intentionally NOT
78
+ * owned here — the consumer wires that to its own form submit lifecycle.
79
+ */
80
+ export const usePersistency = <From>(opts: UsePersistencyOptions<From>): UsePersistencyReturn => {
81
+ const { getForm, meta, persistency, preventWindowExit, schemaDefaultValues, tanstackDefaultValues } = opts
82
+
83
+ const persistencyKey = computed(() => {
84
+ if (persistency?.id) {
85
+ return persistency.id
86
+ }
87
+ const path = window.location.pathname
88
+ const keys = Object.keys(meta)
89
+ return `${path}-${keys.join("-")}`
90
+ })
91
+
92
+ const clearUrlParams = () => {
93
+ const params = new URLSearchParams(window.location.search)
94
+ params.delete(persistencyKey.value)
95
+ const url = new URL(window.location.href)
96
+ url.search = params.toString()
97
+ window.history.replaceState({}, "", url.toString())
98
+ }
99
+
100
+ const defaultValues = computed(() => {
101
+ // will contain what we get from querystring or local/session storage
102
+ let persistencyDefaultValues
103
+
104
+ if (
105
+ // query string has higher priority than local/session storage
106
+ persistency?.policies
107
+ && !persistencyDefaultValues
108
+ && (includesPolicy(persistency.policies, "local")
109
+ || includesPolicy(persistency.policies, "session"))
110
+ ) {
111
+ const storage = includesPolicy(persistency.policies, "local")
112
+ ? localStorage
113
+ : sessionStorage
114
+ if (storage) {
115
+ try {
116
+ const value = JSON.parse(
117
+ storage.getItem(persistencyKey.value) || "{}"
118
+ )
119
+ storage.removeItem(persistencyKey.value)
120
+ persistencyDefaultValues = value
121
+ } catch (error) {
122
+ console.error(error)
123
+ }
124
+ }
125
+ }
126
+ if (persistency?.policies && includesPolicy(persistency.policies, "querystring")) {
127
+ try {
128
+ const params = new URLSearchParams(window.location.search)
129
+ const value = params.get(persistencyKey.value)
130
+ clearUrlParams()
131
+ if (value) {
132
+ persistencyDefaultValues = deepMerge(persistencyDefaultValues || {}, JSON.parse(value))
133
+ }
134
+ } catch (error) {
135
+ console.error(error)
136
+ }
137
+ }
138
+
139
+ // to be sure we have a valid object at the end of the gathering process
140
+ persistencyDefaultValues ??= {}
141
+
142
+ const defaults: Record<DefaultValuesPriorityUnion, any> = {
143
+ tanstack: tanstackDefaultValues || {},
144
+ persistency: persistencyDefaultValues,
145
+ schema: schemaDefaultValues()
146
+ }
147
+
148
+ return [...(opts.defaultValuesPriority || ["tanstack", "persistency", "schema"] as const)].reverse().reduce(
149
+ (acc: any, m: DefaultValuesPriorityUnion) => {
150
+ if (!Object.keys(acc).length) {
151
+ return defaults[m]
152
+ }
153
+ return deepMerge(acc, defaults[m])
154
+ },
155
+ {}
156
+ )
157
+ })
158
+
159
+ const createNestedObjectFromPaths = (paths: string[]) =>
160
+ paths.reduce((result, path) => {
161
+ const parts = path.split(".")
162
+ parts.reduce((acc, part, i) => {
163
+ if (i === parts.length - 1) {
164
+ acc[part] = getForm().getFieldValue(path as any)
165
+ } else {
166
+ acc[part] = acc[part] ?? {}
167
+ }
168
+ return acc[part]
169
+ }, result)
170
+ return result
171
+ }, {} as Record<string, any>)
172
+
173
+ const persistFilter = (p: PersistencyConfig | undefined) => {
174
+ if (!p) return
175
+ const { banKeys, keys } = p
176
+ if (Array.isArray(keys)) {
177
+ return createNestedObjectFromPaths(keys as string[])
178
+ }
179
+ if (Array.isArray(banKeys)) {
180
+ const subs = Object.keys(meta).filter((metakey) => banKeys.includes(metakey))
181
+ return createNestedObjectFromPaths(subs)
182
+ }
183
+ return getForm().store.state.values
184
+ }
185
+
186
+ const persistData = () => {
187
+ if (!persistency?.policies || persistency.policies.length === 0) {
188
+ return
189
+ }
190
+ if (
191
+ includesPolicy(persistency.policies, "local")
192
+ || includesPolicy(persistency.policies, "session")
193
+ ) {
194
+ const storage = includesPolicy(persistency.policies, "local")
195
+ ? localStorage
196
+ : sessionStorage
197
+ if (!storage) return
198
+ const values = persistFilter(persistency)
199
+ return storage.setItem(persistencyKey.value, JSON.stringify(values))
200
+ }
201
+ }
202
+
203
+ const saveDataInUrl = () => {
204
+ if (!persistency?.policies || persistency.policies.length === 0) {
205
+ return
206
+ }
207
+ if (includesPolicy(persistency.policies, "querystring")) {
208
+ const values = persistFilter(persistency)
209
+ const searchParams = new URLSearchParams(window.location.search)
210
+ searchParams.set(persistencyKey.value, JSON.stringify(values))
211
+ const url = new URL(window.location.href)
212
+ url.search = searchParams.toString()
213
+ window.history.replaceState({}, "", url.toString())
214
+ }
215
+ }
216
+
217
+ const preventWindowExitListener = (e: BeforeUnloadEvent) => {
218
+ if (getForm().store.state.isDirty) {
219
+ e.preventDefault()
220
+ }
221
+ }
222
+
223
+ onUnmounted(persistData)
224
+
225
+ onMounted(() => {
226
+ window.addEventListener("beforeunload", persistData)
227
+ window.addEventListener("blur", saveDataInUrl)
228
+ if (preventWindowExit && preventWindowExit !== "nope") {
229
+ window.addEventListener("beforeunload", preventWindowExitListener)
230
+ }
231
+ })
232
+ onBeforeUnmount(() => {
233
+ window.removeEventListener("beforeunload", persistData)
234
+ window.removeEventListener("blur", saveDataInUrl)
235
+ if (preventWindowExit && preventWindowExit !== "nope") {
236
+ window.removeEventListener("beforeunload", preventWindowExitListener)
237
+ }
238
+ })
239
+
240
+ return {
241
+ defaultValues,
242
+ persistencyKey,
243
+ persistData,
244
+ saveDataInUrl,
245
+ clearUrlParams
246
+ }
247
+ }
@@ -0,0 +1,128 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import * as api from "@opentelemetry/api"
4
+ import type { DeepKeys, StandardSchemaV1Issue, ValidationError, ValidationErrorMap } from "@tanstack/vue-form"
5
+ import { Data, Effect, Fiber, Option } from "effect-app"
6
+ import { runtimeFiberAsPromise } from "effect-app/utils"
7
+ import type { Fiber as EffectFiber } from "effect/Fiber"
8
+ import type { OmegaFormApi, OmegaFormParams } from "./types"
9
+
10
+ export class FormErrors<From> extends Data.TaggedError("FormErrors")<{
11
+ form: {
12
+ // TODO: error shapes seem off, with `undefined` etc..
13
+ errors: (Record<string, StandardSchemaV1Issue[]> | undefined)[]
14
+ errorMap: ValidationErrorMap<
15
+ undefined,
16
+ undefined,
17
+ Record<string, StandardSchemaV1Issue[]>,
18
+ undefined,
19
+ undefined,
20
+ undefined,
21
+ undefined,
22
+ undefined,
23
+ undefined,
24
+ undefined
25
+ >
26
+ }
27
+ fields: Record<DeepKeys<From>, {
28
+ errors: ValidationError[]
29
+ errorMap: ValidationErrorMap
30
+ }>
31
+ }> {}
32
+
33
+ export const wrapWithSpan = (span: api.Span | undefined, toWrap: () => any) => {
34
+ return span ? api.context.with(api.trace.setSpan(api.context.active(), span), toWrap) : toWrap()
35
+ }
36
+
37
+ export type UserOnSubmit<From, To> = (props: {
38
+ formApi: OmegaFormParams<From, To>
39
+ meta: any
40
+ value: To
41
+ }) => Promise<any> | EffectFiber<any, any> | Effect.Effect<unknown, any>
42
+
43
+ export type RunPromise = <A, E>(eff: Effect.Effect<A, E>) => Promise<A>
44
+
45
+ /**
46
+ * Wraps the user's `onSubmit` to:
47
+ * - run inside the OpenTelemetry span passed via `meta.currentSpan`
48
+ * - decode the raw form `value` (validators only validate, they don't transform)
49
+ * - normalize Promise / Effect / Fiber return values to a Promise
50
+ *
51
+ * Returns `undefined` when `userOnSubmit` is `undefined` (so callers can pass it
52
+ * directly to `useForm({ onSubmit })` without changing semantics).
53
+ */
54
+ export const wrapOnSubmit = <From, To>(
55
+ userOnSubmit: UserOnSubmit<From, To> | undefined,
56
+ decode: (value: From) => Effect.Effect<To, any>,
57
+ runPromise: RunPromise
58
+ ) => {
59
+ if (!userOnSubmit) return undefined
60
+ return ({ formApi, meta, value }: { formApi: OmegaFormParams<From, To>; meta: any; value: From }) =>
61
+ wrapWithSpan(meta?.currentSpan, async () => {
62
+ // validators only validate, they don't actually transform, so we have to do that manually here.
63
+ const parsedValue = await runPromise(decode(value))
64
+ const r = userOnSubmit({
65
+ formApi: formApi as OmegaFormApi<From, To>,
66
+ meta,
67
+ value: parsedValue
68
+ })
69
+ if (Fiber.isFiber(r)) {
70
+ return await runtimeFiberAsPromise(r)
71
+ }
72
+ if (Effect.isEffect(r)) {
73
+ const effectResult = await runPromise(r)
74
+ return Fiber.isFiber(effectResult)
75
+ ? await runtimeFiberAsPromise(effectResult)
76
+ : effectResult
77
+ }
78
+ return r
79
+ })
80
+ }
81
+
82
+ /**
83
+ * Builds the public submit handlers from a `useForm`-returned `form`:
84
+ * - `handleSubmit` injects the current OpenTelemetry span as `meta.currentSpan`.
85
+ * - `handleSubmitEffect` runs `handleSubmit` inside an Effect that picks up the
86
+ * ambient `Effect.currentSpan`. With `checkErrors: true`, it fails with
87
+ * `FormErrors<From>` when validation produced errors.
88
+ */
89
+ export const makeSubmitHandlers = <From, To>(
90
+ form: OmegaFormApi<From, To>
91
+ ) => {
92
+ const hs = form.handleSubmit.bind(form)
93
+
94
+ const handleSubmitInner: typeof form.handleSubmit = async (meta?: Record<string, any>) => {
95
+ return await hs(meta)
96
+ }
97
+
98
+ const handleSubmit = (meta?: Record<string, any>) => {
99
+ const span = api.trace.getSpan(api.context.active())
100
+ return handleSubmitInner({ currentSpan: span, ...meta })
101
+ }
102
+
103
+ const handleSubmitEffect_ = (meta?: Record<string, any>) =>
104
+ Effect.currentSpan.pipe(
105
+ Effect.option,
106
+ Effect
107
+ .flatMap((span) =>
108
+ Effect.promise(() => handleSubmitInner(Option.isSome(span) ? { currentSpan: span.value, ...meta } : meta))
109
+ )
110
+ )
111
+
112
+ const handleSubmitEffect: {
113
+ (options: { checkErrors: true; meta?: Record<string, any> }): Effect.Effect<void, FormErrors<From>>
114
+ (options?: { meta?: Record<string, any> }): Effect.Effect<void>
115
+ } = (
116
+ options?: { meta?: Record<string, any>; checkErrors?: true }
117
+ ): any =>
118
+ options?.checkErrors
119
+ ? handleSubmitEffect_(options?.meta).pipe(Effect.flatMap(Effect.fnUntraced(function*() {
120
+ const errors = form.getAllErrors()
121
+ if (Object.keys(errors.fields).length || errors.form.errors.length) {
122
+ return yield* Effect.fail(new FormErrors({ form: errors.form, fields: errors.fields }))
123
+ }
124
+ })))
125
+ : handleSubmitEffect_(options?.meta)
126
+
127
+ return { handleSubmit, handleSubmitEffect }
128
+ }