11import type { Field } from './field/type'
2- import type { JsfObjectSchema , JsfSchema , NonBooleanJsfSchema , SchemaValue } from './types'
2+ import type { JsfObjectSchema , JsfSchema , NonBooleanJsfSchema , ObjectValue , SchemaValue } from './types'
33import type { ValidationOptions } from './validation/schema'
44import { validateSchema } from './validation/schema'
55import { isObjectValue } from './validation/util'
66
7+ /**
8+ * Resets the visibility of all fields to true
9+ * @param fields - The fields to reset
10+ */
11+ function resetVisibility ( fields : Field [ ] ) {
12+ for ( const field of fields ) {
13+ field . isVisible = true
14+ if ( field . fields ) {
15+ resetVisibility ( field . fields )
16+ }
17+ }
18+ }
719/**
820 * Updates field visibility based on JSON schema conditional rules
921 * @param fields - The fields to update
@@ -21,41 +33,59 @@ export function updateFieldVisibility(
2133 return
2234 }
2335
36+ // Reseting fields visibility to the default before re-calculating it
37+ resetVisibility ( fields )
38+
2439 // Apply rules to current level of fields
2540 applySchemaRules ( fields , values , schema , options )
2641
2742 // Process nested object fields that have conditional logic
2843 for ( const fieldName in schema . properties ) {
2944 const fieldSchema = schema . properties [ fieldName ]
45+ const field = fields . find ( field => field . name === fieldName )
3046
31- // Only process object schemas with conditional logic (allOf)
32- if ( typeof fieldSchema !== 'object' || fieldSchema === null
33- || Array . isArray ( fieldSchema ) || ! fieldSchema . allOf ) {
34- continue
35- }
36-
37- const objectField = fields . find ( field => field . name === fieldSchema . title || field . name === fieldName )
38- if ( ! objectField || ! objectField . fields || objectField . fields . length === 0 ) {
39- continue
47+ if ( field ?. fields ) {
48+ applySchemaRules ( field . fields , values [ fieldName ] , fieldSchema as JsfObjectSchema , options )
4049 }
50+ }
51+ }
4152
42- const fieldValues = isObjectValue ( values [ fieldName ] )
43- ? values [ fieldName ]
44- : isObjectValue ( values [ objectField . name ] ) ? values [ objectField . name ] : { }
45-
46- // Apply rules to nested fields
47- applySchemaRules ( objectField . fields , fieldValues , fieldSchema as JsfObjectSchema , options )
48-
49- // Only process nested fields if parent is visible
50- if ( objectField . isVisible ) {
51- updateFieldVisibility (
52- objectField . fields ,
53- fieldValues ,
54- fieldSchema as JsfObjectSchema ,
55- options ,
56- )
57- }
53+ /**
54+ * Evaluates the conditional rules for a field
55+ * @param values - The current field values
56+ * @param schema - The JSON schema definition
57+ * @param rule - Schema identifying the conditional rule
58+ * @param options - Validation options
59+ * @returns An object containing the rule and whether it matches
60+ */
61+ function evaluateConditional (
62+ values : ObjectValue ,
63+ schema : JsfObjectSchema ,
64+ rule : NonBooleanJsfSchema ,
65+ options : ValidationOptions = { } ,
66+ ) {
67+ const ifErrors = validateSchema ( values , rule . if ! , options )
68+ const matches = ifErrors . length === 0
69+
70+ // Prevent fields from being shown when required fields have type errors
71+ let hasTypeErrors = false
72+ if ( matches
73+ && typeof rule . if === 'object'
74+ && rule . if !== null
75+ && Array . isArray ( rule . if . required ) ) {
76+ const requiredFields = rule . if . required
77+ hasTypeErrors = requiredFields . some ( ( fieldName ) => {
78+ if ( ! schema . properties || ! schema . properties [ fieldName ] ) {
79+ return false
80+ }
81+ const fieldSchema = schema . properties [ fieldName ]
82+ const fieldValue = values [ fieldName ]
83+ const fieldErrors = validateSchema ( fieldValue , fieldSchema , options )
84+ return fieldErrors . some ( error => error . validation === 'type' )
85+ } )
5886 }
87+
88+ return { rule, matches : matches && ! hasTypeErrors }
5989}
6090
6191/**
@@ -65,99 +95,71 @@ export function updateFieldVisibility(
6595 * @param schema - The JSON schema containing the rules
6696 * @param options - Validation options
6797 *
68- * Fields start with visibility based on their required status.
69- * Conditional rules in the schema's allOf property can then:
70- * - Make fields visible by including them in a required array
71- * - Make fields hidden by setting them to false in properties
98+ * Fields start visible by default, and they're set to hidden if their schema is
99+ * set to false (a falsy schema means the schema fails whenever a value is sent for that field)
100+ *
72101 */
73102function applySchemaRules (
74103 fields : Field [ ] ,
75104 values : SchemaValue ,
76105 schema : JsfObjectSchema ,
77106 options : ValidationOptions = { } ,
78107) {
79- if ( ! schema . allOf || ! Array . isArray ( schema . allOf ) || ! isObjectValue ( values ) ) {
108+ if ( ! isObjectValue ( values ) ) {
80109 return
81110 }
82111
83- const conditionalRules = schema . allOf
84- . filter ( rule => typeof rule === 'object' && rule !== null && 'if' in rule )
85- . map ( ( rule ) => {
86- const ruleObj = rule as NonBooleanJsfSchema
87-
88- const ifErrors = validateSchema ( values , ruleObj . if ! , options )
89- const matches = ifErrors . length === 0
112+ const conditionalRules : { rule : NonBooleanJsfSchema , matches : boolean } [ ] = [ ]
90113
91- // Prevent fields from being shown when required fields have type errors
92- let hasTypeErrors = false
93- if ( matches
94- && typeof ruleObj . if === 'object'
95- && ruleObj . if !== null
96- && Array . isArray ( ruleObj . if . required ) ) {
97- const requiredFields = ruleObj . if . required
98- hasTypeErrors = requiredFields . some ( ( fieldName ) => {
99- if ( ! schema . properties || ! schema . properties [ fieldName ] ) {
100- return false
101- }
102- const fieldSchema = schema . properties [ fieldName ]
103- const fieldValue = values [ fieldName ]
104- const fieldErrors = validateSchema ( fieldValue , fieldSchema , options )
105- return fieldErrors . some ( error => error . validation === 'type' )
106- } )
107- }
114+ // If the schema has an if property, evaluate it and add it to the conditional rules array
115+ if ( schema . if ) {
116+ conditionalRules . push ( evaluateConditional ( values , schema , schema , options ) )
117+ }
108118
109- return { rule : ruleObj , matches : matches && ! hasTypeErrors }
119+ // If the schema has an allOf property, evaluate each rule and add it to the conditional rules array
120+ ( schema . allOf ?? [ ] )
121+ . filter ( rule => typeof rule === 'object' && rule !== null && 'if' in rule )
122+ . forEach ( ( rule ) => {
123+ const result = evaluateConditional ( values , schema , rule as NonBooleanJsfSchema , options )
124+ conditionalRules . push ( result )
110125 } )
111126
112- for ( const field of fields ) {
113- // Default visibility is based on required status
114- let isVisible = field . required
115-
116- for ( const { rule, matches } of conditionalRules ) {
117- if ( matches && rule . then ) {
118- isVisible = isFieldVisible ( field . name , rule . then , isVisible )
119- }
120- else if ( ! matches && rule . else ) {
121- isVisible = isFieldVisible ( field . name , rule . else , isVisible )
122- }
127+ // Process the conditional rules
128+ for ( const { rule, matches } of conditionalRules ) {
129+ // If the rule matches, process the then branch
130+ if ( matches && rule . then ) {
131+ processBranch ( fields , rule . then )
132+ }
133+ // If the rule doesn't match, process the else branch
134+ else if ( ! matches && rule . else ) {
135+ processBranch ( fields , rule . else )
123136 }
124-
125- field . isVisible = isVisible
126137 }
127138}
128139
129140/**
130- * Determines whether a field should be visible based on a schema branch
131- * @param fieldName - The name of the field
132- * @param branch - The schema clause (either 'then' or 'else')
133- * @param currentVisibility - The current visibility state
134- * @returns The updated visibility state
135- *
136- * Visibility logic:
137- * - If the field is in the required array → make visible
138- * - If the field is explicitly false in properties → make hidden
139- * - Otherwise, preserve current visibility
141+ * Processes a branch of a conditional rule, updating the visibility of fields based on the branch's schema
142+ * @param fields - The fields to process
143+ * @param branch - The branch (schema representing and then/else) to process
140144 */
141- function isFieldVisible (
142- fieldName : string ,
143- branch : JsfSchema ,
144- currentVisibility : boolean ,
145- ) : boolean {
146- let isVisible = currentVisibility
147-
148- if ( typeof branch === 'object' && branch !== null ) {
149- if ( Array . isArray ( branch . required ) ) {
150- if ( branch . required . includes ( fieldName ) ) {
151- isVisible = true
152- }
153- }
154-
155- if ( branch . properties !== undefined ) {
156- if ( fieldName in branch . properties && branch . properties [ fieldName ] === false ) {
157- isVisible = false
145+ function processBranch ( fields : Field [ ] , branch : JsfSchema ) {
146+ if ( branch . properties ) {
147+ // Cycle through each property in the schema and search for any (possibly nested)
148+ // fields that have a false boolean schema. If found, set the field's visibility to false
149+ for ( const fieldName in branch . properties ) {
150+ const fieldSchema = branch . properties [ fieldName ]
151+ const field = fields . find ( e => e . name === fieldName )
152+ if ( field ) {
153+ if ( field ?. fields ) {
154+ processBranch ( field . fields , fieldSchema )
155+ }
156+ else if ( fieldSchema === false ) {
157+ field . isVisible = false
158+ }
159+ else {
160+ field . isVisible = true
161+ }
158162 }
159163 }
160164 }
161-
162- return isVisible
163165}
0 commit comments