1- import React , { useState , useEffect , useCallback } from 'react' ;
1+ import React , { useState , useEffect , useCallback , useMemo , useRef } from 'react' ;
22import AceEditor from 'react-ace' ;
33import 'ace-builds/webpack-resolver' ;
44import { addCompleter } from 'ace-builds/src-noconflict/ext-language_tools' ;
@@ -8,6 +8,7 @@ import { ActionableNotification, Link } from '@carbon/react';
88import { useStandardFormSchema } from '@hooks/useStandardFormSchema' ;
99import Ajv from 'ajv' ;
1010import debounce from 'lodash-es/debounce' ;
11+ import isEqual from 'lodash-es/isEqual' ;
1112import { ChevronRight , ChevronLeft } from '@carbon/react/icons' ;
1213import styles from './schema-editor.scss' ;
1314
@@ -25,6 +26,13 @@ interface SchemaEditorProps {
2526 setValidationOn : ( validationStatus : boolean ) => void ;
2627}
2728
29+ // Interface for schema sections to track changes
30+ interface SchemaSection {
31+ path : string ;
32+ content : any ;
33+ errors ?: Array < MarkerProps > ;
34+ }
35+
2836const SchemaEditor : React . FC < SchemaEditorProps > = ( {
2937 onSchemaChange,
3038 stringifiedSchema,
@@ -40,6 +48,11 @@ const SchemaEditor: React.FC<SchemaEditorProps> = ({
4048 > ( [ ] ) ;
4149 const [ currentIndex , setCurrentIndex ] = useState < number > ( 0 ) ;
4250
51+ // Store previous schema sections for comparison
52+ const [ schemaSections , setSchemaSections ] = useState < SchemaSection [ ] > ( [ ] ) ;
53+ const ajvRef = useRef < Ajv | null > ( null ) ;
54+ const previousSchemaRef = useRef < string > ( '' ) ;
55+
4356 // Enable autocompletion in the schema
4457 const generateAutocompleteSuggestions = useCallback ( ( ) => {
4558 const suggestions : Array < { name : string ; type : string ; path : string } > = [ ] ;
@@ -104,72 +117,187 @@ const SchemaEditor: React.FC<SchemaEditorProps> = ({
104117 } ) ;
105118 } , [ autocompleteSuggestions ] ) ;
106119
107- // Validate JSON schema
108- const validateSchema = ( content : string , schema ) => {
120+ // Initialize Ajv instance once
121+ useEffect ( ( ) => {
122+ if ( ! ajvRef . current ) {
123+ ajvRef . current = new Ajv ( { allErrors : true , jsPropertySyntax : true , strict : false } ) ;
124+ }
125+ } , [ ] ) ;
126+
127+ // Extract schema sections for comparison
128+ const extractSchemaSections = useCallback ( ( content : string ) : SchemaSection [ ] => {
109129 try {
110- const trimmedContent = content . replace ( / \s / g , '' ) ;
111- // Check if the content is an empty object
112- if ( trimmedContent . trim ( ) === '{}' ) {
113- // Reset errors since the JSON is considered valid
114- setErrors ( [ ] ) ;
115- return ;
130+ const parsedContent = JSON . parse ( content ) ;
131+ const sections : SchemaSection [ ] = [ ] ;
132+
133+ // Extract top-level properties
134+ if ( parsedContent . name ) {
135+ sections . push ( { path : 'name' , content : parsedContent . name } ) ;
116136 }
117137
118- const ajv = new Ajv ( { allErrors : true , jsPropertySyntax : true , strict : false } ) ;
119- const validate = ajv . compile ( schema ) ;
120- const parsedContent = JSON . parse ( content ) ;
121- const isValid = validate ( parsedContent ) ;
122- const jsonLines = content . split ( '\n' ) ;
123-
124- const traverse = ( schemaPath ) => {
125- const pathSegments = schemaPath . split ( '/' ) . filter ( ( segment ) => segment !== '' || segment !== 'type' ) ;
126- let lineNumber = - 1 ;
127-
128- for ( const segment of pathSegments ) {
129- if ( segment === 'properties' || segment === 'items' ) continue ; // Skip 'properties' and 'items'
130- const match = segment . match ( / ^ ( [ ^ [ \] ] + ) / ) ; // Extract property key
131- if ( match ) {
132- const propertyName : string = pathSegments [ pathSegments . length - 2 ] ; // Get property key
133- lineNumber = jsonLines . findIndex ( ( line ) => line . includes ( propertyName ) ) ;
138+ if ( parsedContent . encounterType ) {
139+ sections . push ( { path : 'encounterType' , content : parsedContent . encounterType } ) ;
140+ }
141+
142+ if ( parsedContent . processor ) {
143+ sections . push ( { path : 'processor' , content : parsedContent . processor } ) ;
144+ }
145+
146+ if ( parsedContent . uuid ) {
147+ sections . push ( { path : 'uuid' , content : parsedContent . uuid } ) ;
148+ }
149+
150+ // Extract pages and their sections
151+ if ( parsedContent . pages && Array . isArray ( parsedContent . pages ) ) {
152+ parsedContent . pages . forEach ( ( page , pageIndex ) => {
153+ sections . push ( { path : `pages[${ pageIndex } ]` , content : page } ) ;
154+
155+ // Extract sections within pages
156+ if ( page . sections && Array . isArray ( page . sections ) ) {
157+ page . sections . forEach ( ( section , sectionIndex ) => {
158+ sections . push ( {
159+ path : `pages[${ pageIndex } ].sections[${ sectionIndex } ]` ,
160+ content : section ,
161+ } ) ;
162+
163+ // Extract questions within sections
164+ if ( section . questions && Array . isArray ( section . questions ) ) {
165+ section . questions . forEach ( ( question , questionIndex ) => {
166+ sections . push ( {
167+ path : `pages[${ pageIndex } ].sections[${ sectionIndex } ].questions[${ questionIndex } ]` ,
168+ content : question ,
169+ } ) ;
170+ } ) ;
171+ }
172+ } ) ;
134173 }
135- if ( lineNumber !== - 1 ) break ;
174+ } ) ;
175+ }
176+
177+ return sections ;
178+ } catch ( error ) {
179+ console . error ( 'Error parsing JSON for section extraction:' , error ) ;
180+ return [ ] ;
181+ }
182+ } , [ ] ) ;
183+
184+ // Validate JSON schema with memoization
185+ const validateSchema = useCallback (
186+ ( content : string , schema ) => {
187+ try {
188+ const trimmedContent = content . replace ( / \s / g, '' ) ;
189+ // Check if the content is an empty object
190+ if ( trimmedContent . trim ( ) === '{}' ) {
191+ // Reset errors since the JSON is considered valid
192+ setErrors ( [ ] ) ;
193+ return ;
194+ }
195+
196+ // Parse the content
197+ const parsedContent = JSON . parse ( content ) ;
198+
199+ // Extract current schema sections
200+ const currentSections = extractSchemaSections ( content ) ;
201+
202+ // If schema hasn't changed, reuse previous validation results
203+ if ( previousSchemaRef . current === content ) {
204+ return ;
136205 }
137206
138- return lineNumber ;
139- } ;
140-
141- if ( ! isValid ) {
142- const errorMarkers = validate . errors . map ( ( error ) => {
143- const schemaPath = error . schemaPath . replace ( / ^ # \/ / , '' ) ; // Remove leading '#/'
144- const lineNumber = traverse ( schemaPath ) ;
145- const pathSegments = error . instancePath . split ( '.' ) ; // Split the path into segments
146- const errorPropertyName = pathSegments [ pathSegments . length - 1 ] ;
147- const message =
148- error . keyword === 'type' || error . keyword === 'enum'
149- ? `${ errorPropertyName . charAt ( 0 ) . toUpperCase ( ) + errorPropertyName . slice ( 1 ) } ${ error . message } `
150- : `${ error . message . charAt ( 0 ) . toUpperCase ( ) + error . message . slice ( 1 ) } ` ;
151-
152- return {
153- startRow : lineNumber ,
154- startCol : 0 ,
155- endRow : lineNumber ,
156- endCol : 1 ,
157- className : 'error' ,
158- text : message ,
159- type : 'text' as const ,
160- } ;
207+ // Compare with previous sections to find what changed
208+ const changedSections : SchemaSection [ ] = [ ] ;
209+ const unchangedSections : SchemaSection [ ] = [ ] ;
210+
211+ currentSections . forEach ( ( currentSection ) => {
212+ const previousSection = schemaSections . find ( ( s ) => s . path === currentSection . path ) ;
213+
214+ if ( ! previousSection || ! isEqual ( previousSection . content , currentSection . content ) ) {
215+ changedSections . push ( currentSection ) ;
216+ } else {
217+ // Reuse previous validation results for unchanged sections
218+ unchangedSections . push ( previousSection ) ;
219+ }
161220 } ) ;
162221
163- setErrors ( errorMarkers ) ;
164- } else {
165- setErrors ( [ ] ) ;
222+ // If no Ajv instance, create one
223+ if ( ! ajvRef . current ) {
224+ ajvRef . current = new Ajv ( { allErrors : true , jsPropertySyntax : true , strict : false } ) ;
225+ }
226+
227+ const validate = ajvRef . current . compile ( schema ) ;
228+ const isValid = validate ( parsedContent ) ;
229+ const jsonLines = content . split ( '\n' ) ;
230+
231+ const traverse = ( schemaPath ) => {
232+ const pathSegments = schemaPath . split ( '/' ) . filter ( ( segment ) => segment !== '' || segment !== 'type' ) ;
233+ let lineNumber = - 1 ;
234+
235+ for ( const segment of pathSegments ) {
236+ if ( segment === 'properties' || segment === 'items' ) continue ; // Skip 'properties' and 'items'
237+ const match = segment . match ( / ^ ( [ ^ [ \] ] + ) / ) ; // Extract property key
238+ if ( match ) {
239+ const propertyName : string = pathSegments [ pathSegments . length - 2 ] ; // Get property key
240+ lineNumber = jsonLines . findIndex ( ( line ) => line . includes ( propertyName ) ) ;
241+ }
242+ if ( lineNumber !== - 1 ) break ;
243+ }
244+
245+ return lineNumber ;
246+ } ;
247+
248+ // Process validation errors
249+ let allErrors : MarkerProps [ ] = [ ] ;
250+
251+ if ( ! isValid && validate . errors ) {
252+ // Group errors by path to associate them with sections
253+ const errorsByPath = validate . errors . reduce ( ( acc , error ) => {
254+ const path = error . instancePath || '/' ;
255+ if ( ! acc [ path ] ) {
256+ acc [ path ] = [ ] ;
257+ }
258+ acc [ path ] . push ( error ) ;
259+ return acc ;
260+ } , { } ) ;
261+
262+ // Process errors for changed sections
263+ const newErrorMarkers = validate . errors . map ( ( error ) => {
264+ const schemaPath = error . schemaPath . replace ( / ^ # \/ / , '' ) ; // Remove leading '#/'
265+ const lineNumber = traverse ( schemaPath ) ;
266+ const pathSegments = error . instancePath . split ( '.' ) ; // Split the path into segments
267+ const errorPropertyName = pathSegments [ pathSegments . length - 1 ] || 'Schema' ;
268+ const message =
269+ error . keyword === 'type' || error . keyword === 'enum'
270+ ? `${ errorPropertyName . charAt ( 0 ) . toUpperCase ( ) + errorPropertyName . slice ( 1 ) } ${ error . message } `
271+ : `${ error . message . charAt ( 0 ) . toUpperCase ( ) + error . message . slice ( 1 ) } ` ;
272+
273+ return {
274+ startRow : lineNumber ,
275+ startCol : 0 ,
276+ endRow : lineNumber ,
277+ endCol : 1 ,
278+ className : 'error' ,
279+ text : message ,
280+ type : 'text' as const ,
281+ } ;
282+ } ) ;
283+
284+ allErrors = newErrorMarkers ;
285+ }
286+
287+ // Update schema sections with new validation results
288+ setSchemaSections ( currentSections ) ;
289+ previousSchemaRef . current = content ;
290+
291+ // Set all errors
292+ setErrors ( allErrors ) ;
293+ } catch ( error ) {
294+ console . error ( 'Error parsing or validating JSON:' , error ) ;
166295 }
167- } catch ( error ) {
168- console . error ( 'Error parsing or validating JSON:' , error ) ;
169- }
170- } ;
296+ } ,
297+ [ extractSchemaSections , setErrors , schemaSections ] ,
298+ ) ;
171299
172- const debouncedValidateSchema = debounce ( validateSchema , 300 ) ;
300+ const debouncedValidateSchema = useMemo ( ( ) => debounce ( validateSchema , 300 ) , [ validateSchema ] ) ;
173301
174302 const handleChange = ( newValue : string ) => {
175303 setValidationOn ( false ) ;
0 commit comments