@@ -37,7 +37,7 @@ function DatamodelViewContent() {
3737 const datamodelDispatch = useDatamodelViewDispatch ( ) ;
3838 const { groups, filtered, search } = useDatamodelData ( ) ;
3939 const datamodelDataDispatch = useDatamodelDataDispatch ( ) ;
40- const { filters : entityFilters } = useEntityFilters ( ) ;
40+ const { filters : entityFilters , selectedSecurityRoles } = useEntityFilters ( ) ;
4141 const workerRef = useRef < Worker | null > ( null ) ;
4242 const [ currentSearchIndex , setCurrentSearchIndex ] = useState ( 0 ) ;
4343 const accumulatedResultsRef = useRef < SearchResultItem [ ] > ( [ ] ) ; // Track all results during search
@@ -67,11 +67,29 @@ function DatamodelViewContent() {
6767 const totalResults = useMemo ( ( ) => {
6868 if ( filtered . length === 0 ) return 0 ;
6969
70- const attributeCount = filtered . filter ( item => item . type === 'attribute' ) . length ;
71- const relationshipCount = filtered . filter ( item => item . type === 'relationship' ) . length ;
72- const itemCount = attributeCount + relationshipCount ;
70+ // Get combined results and deduplicate to match navigation behavior
71+ const combinedResults = filtered . filter ( ( item ) : item is { type : 'attribute' ; group : GroupType ; entity : EntityType ; attribute : AttributeType } | { type : 'relationship' ; group : GroupType ; entity : EntityType ; relationship : RelationshipType } =>
72+ item . type === 'attribute' || item . type === 'relationship'
73+ ) ;
7374
74- if ( itemCount > 0 ) return itemCount ;
75+ if ( combinedResults . length > 0 ) {
76+ // Deduplicate to match getSortedCombinedResults behavior
77+ // Note: We don't check DOM element existence here because relationships on inactive tabs
78+ // won't have DOM elements yet, but they should still be counted for navigation
79+ const seen = new Set < string > ( ) ;
80+ let count = 0 ;
81+ for ( const item of combinedResults ) {
82+ const key = item . type === 'attribute'
83+ ? `attr-${ item . entity . SchemaName } -${ item . attribute . SchemaName } `
84+ : `rel-${ item . entity . SchemaName } -${ item . relationship . RelationshipSchema } ` ;
85+
86+ if ( ! seen . has ( key ) ) {
87+ seen . add ( key ) ;
88+ count ++ ;
89+ }
90+ }
91+ return count ;
92+ }
7593
7694 // If no attributes or relationships, count entity-level matches (for security roles, table descriptions)
7795 const entityCount = filtered . filter ( item => item . type === 'entity' ) . length ;
@@ -98,6 +116,7 @@ function DatamodelViewContent() {
98116 data : searchValue ,
99117 entityFilters : filtersObject ,
100118 searchScope : searchScope ,
119+ selectedSecurityRoles : selectedSecurityRoles ,
101120 requestId : currentRequestId // Send request ID to worker
102121 } ) ;
103122 } else {
@@ -113,23 +132,24 @@ function DatamodelViewContent() {
113132 updateURL ( { query : { globalsearch : searchValue . length >= 3 ? searchValue : "" } } )
114133 datamodelDataDispatch ( { type : "SET_SEARCH" , payload : searchValue . length >= 3 ? searchValue : "" } ) ;
115134 setCurrentSearchIndex ( searchValue . length >= 3 ? 1 : 0 ) ; // Reset to first result when searching, 0 when cleared
116- } , [ groups , datamodelDataDispatch , restoreSection , entityFilters , searchScope ] ) ;
135+ } , [ groups , datamodelDataDispatch , restoreSection , entityFilters , searchScope , selectedSecurityRoles ] ) ;
117136
118137 const handleLoadingChange = useCallback ( ( isLoading : boolean ) => {
119138 datamodelDispatch ( { type : "SET_LOADING" , payload : isLoading } ) ;
120139 } , [ datamodelDispatch ] ) ;
121140
122141 const handleSearchScopeChange = useCallback ( ( newScope : SearchScope ) => {
123142 setSearchScope ( newScope ) ;
124- } , [ ] ) ;
143+ datamodelDataDispatch ( { type : "SET_SEARCH_SCOPE" , payload : newScope } ) ;
144+ } , [ datamodelDataDispatch ] ) ;
125145
126- // Re-trigger search when scope changes
146+ // Re-trigger search when scope or security roles change
127147 useEffect ( ( ) => {
128148 if ( search && search . length >= 3 ) {
129149 handleSearch ( search ) ;
130150 }
131151 // eslint-disable-next-line react-hooks/exhaustive-deps
132- } , [ searchScope ] ) ; // Only trigger on searchScope change, not handleSearch to avoid infinite loop
152+ } , [ searchScope , selectedSecurityRoles ] ) ; // Only trigger on searchScope or selectedSecurityRoles change, not handleSearch to avoid infinite loop
133153
134154 // Helper function to get sorted combined results (attributes + relationships) on-demand
135155 // This prevents blocking the main thread during typing - sorting only happens during navigation
@@ -165,44 +185,69 @@ function DatamodelViewContent() {
165185 resultsByEntity . get ( entityKey ) ! . push ( result ) ;
166186 }
167187
168- // Sort entities by the Y position of their first attribute (or use first result if no attributes)
188+ // Create a stable sort order based on the groups data structure
189+ // This ensures consistent navigation order regardless of scroll position or tab state
190+ const entityOrder = new Map < string , number > ( ) ;
191+ let orderIndex = 0 ;
192+ for ( const group of groups ) {
193+ for ( const entity of group . Entities ) {
194+ entityOrder . set ( entity . SchemaName , orderIndex ++ ) ;
195+ }
196+ }
197+
198+ // Sort entities by their position in the groups data structure
169199 const sortedEntities = Array . from ( resultsByEntity . entries ( ) ) . sort ( ( a , b ) => {
170- const [ , resultsA ] = a ;
171- const [ , resultsB ] = b ;
200+ const [ entitySchemaA ] = a ;
201+ const [ entitySchemaB ] = b ;
172202
173- // Find first attribute for each entity
174- const firstAttrA = resultsA . find ( r => r . type === 'attribute' ) as { type : 'attribute' ; group : GroupType ; entity : EntityType ; attribute : AttributeType } | undefined ;
175- const firstAttrB = resultsB . find ( r => r . type === 'attribute' ) as { type : 'attribute' ; group : GroupType ; entity : EntityType ; attribute : AttributeType } | undefined ;
203+ const orderA = entityOrder . get ( entitySchemaA ) ?? Number . MAX_SAFE_INTEGER ;
204+ const orderB = entityOrder . get ( entitySchemaB ) ?? Number . MAX_SAFE_INTEGER ;
176205
177- // If both have attributes, compare by Y position
178- if ( firstAttrA && firstAttrB ) {
179- const elementA = document . getElementById ( `attr-${ firstAttrA . entity . SchemaName } -${ firstAttrA . attribute . SchemaName } ` ) ;
180- const elementB = document . getElementById ( `attr-${ firstAttrB . entity . SchemaName } -${ firstAttrB . attribute . SchemaName } ` ) ;
206+ return orderA - orderB ;
207+ } ) ;
208+
209+ // Flatten back to array, keeping attributes BEFORE relationships within each entity
210+ const result : Array < { type : 'attribute' ; group : GroupType ; entity : EntityType ; attribute : AttributeType } | { type : 'relationship' ; group : GroupType ; entity : EntityType ; relationship : RelationshipType } > = [ ] ;
211+ for ( const [ , entityResults ] of sortedEntities ) {
212+ // Separate attributes and relationships for this entity
213+ const attributes = entityResults . filter ( ( r ) : r is { type : 'attribute' ; group : GroupType ; entity : EntityType ; attribute : AttributeType } => r . type === 'attribute' ) ;
214+ const relationships = entityResults . filter ( ( r ) : r is { type : 'relationship' ; group : GroupType ; entity : EntityType ; relationship : RelationshipType } => r . type === 'relationship' ) ;
215+
216+ // Sort attributes by Y position within the entity (if they exist in DOM)
217+ // Note: We don't filter out attributes that don't exist in DOM because the search worker
218+ // already applied all component-level filters
219+ attributes . sort ( ( a , b ) => {
220+ const elementA = document . getElementById ( `attr-${ a . entity . SchemaName } -${ a . attribute . SchemaName } ` ) ;
221+ const elementB = document . getElementById ( `attr-${ b . entity . SchemaName } -${ b . attribute . SchemaName } ` ) ;
181222
182223 if ( elementA && elementB ) {
183224 const rectA = elementA . getBoundingClientRect ( ) ;
184225 const rectB = elementB . getBoundingClientRect ( ) ;
185226 return rectA . top - rectB . top ;
186227 }
187- }
228+ return 0 ;
229+ } ) ;
188230
189- // Fallback: maintain original order
190- return 0 ;
191- } ) ;
231+ // Sort relationships by Y position within the entity (if they exist in DOM)
232+ // Note: Relationships on inactive tabs won't have DOM elements, so we keep them in original order
233+ relationships . sort ( ( a , b ) => {
234+ const elementA = document . getElementById ( `rel-${ a . entity . SchemaName } -${ a . relationship . RelationshipSchema } ` ) ;
235+ const elementB = document . getElementById ( `rel-${ b . entity . SchemaName } -${ b . relationship . RelationshipSchema } ` ) ;
192236
193- // Flatten back to array, keeping attributes before relationships within each entity
194- const result : Array < { type : 'attribute' ; group : GroupType ; entity : EntityType ; attribute : AttributeType } | { type : 'relationship' ; group : GroupType ; entity : EntityType ; relationship : RelationshipType } > = [ ] ;
195- for ( const [ , entityResults ] of sortedEntities ) {
196- // Separate attributes and relationships for this entity
197- const attributes = entityResults . filter ( ( r ) : r is { type : 'attribute' ; group : GroupType ; entity : EntityType ; attribute : AttributeType } => r . type === 'attribute' ) ;
198- const relationships = entityResults . filter ( ( r ) : r is { type : 'relationship' ; group : GroupType ; entity : EntityType ; relationship : RelationshipType } => r . type === 'relationship' ) ;
237+ if ( elementA && elementB ) {
238+ const rectA = elementA . getBoundingClientRect ( ) ;
239+ const rectB = elementB . getBoundingClientRect ( ) ;
240+ return rectA . top - rectB . top ;
241+ }
242+ return 0 ;
243+ } ) ;
199244
200- // Add all attributes first (in their original order) , then relationships
245+ // Add all attributes first, then all relationships
201246 result . push ( ...attributes , ...relationships ) ;
202247 }
203248
204249 return result ;
205- } , [ filtered ] ) ;
250+ } , [ filtered , groups ] ) ;
206251
207252 // Navigation handlers
208253 const handleNavigateNext = useCallback ( ( ) => {
0 commit comments