Skip to content

Commit e08b03b

Browse files
committed
fix: minor adjustment and fixes to search scroll and resultorder
1 parent 12574ce commit e08b03b

File tree

6 files changed

+133
-48
lines changed

6 files changed

+133
-48
lines changed

Website/components/datamodelview/Attributes.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { highlightMatch } from "../datamodelview/List";
1818
import { Box, Button, FormControl, InputAdornment, InputLabel, MenuItem, Select, Table, TableBody, TableCell, TableHead, TableRow, TextField, Tooltip, Typography, useTheme } from "@mui/material"
1919
import { ClearRounded, SearchRounded, Visibility, VisibilityOff, ArrowUpwardRounded, ArrowDownwardRounded } from "@mui/icons-material"
2020
import { useEntityFiltersDispatch } from "@/contexts/EntityFiltersContext"
21+
import { useDatamodelData } from "@/contexts/DatamodelDataContext"
2122

2223
type SortDirection = 'asc' | 'desc' | null
2324
type SortColumn = 'displayName' | 'schemaName' | 'type' | 'description' | null
@@ -37,6 +38,7 @@ export const Attributes = ({ entity, search = "", onVisibleCountChange }: IAttri
3738

3839
const theme = useTheme();
3940
const entityFiltersDispatch = useEntityFiltersDispatch();
41+
const { searchScope } = useDatamodelData();
4042

4143
// Report filter state changes to context
4244
useEffect(() => {
@@ -144,7 +146,10 @@ export const Attributes = ({ entity, search = "", onVisibleCountChange }: IAttri
144146
}
145147

146148
const sortedAttributes = getSortedAttributes();
147-
const highlightTerm = searchQuery || search; // Use internal search or parent search for highlighting
149+
// Only highlight if search scope includes columns
150+
// Use internal search query first, or parent search if column scopes are enabled
151+
const highlightTerm = searchQuery ||
152+
(search && (searchScope.columnNames || searchScope.columnDescriptions || searchScope.columnDataTypes) ? search : "");
148153

149154
// Notify parent of visible count changes
150155
useEffect(() => {

Website/components/datamodelview/DatamodelView.tsx

Lines changed: 76 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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(() => {

Website/components/datamodelview/List.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -214,21 +214,25 @@ export const List = ({ setCurrentIndex, entityActiveTabs }: IListProps) => {
214214

215215
const scrollToRelationship = useCallback((sectionId: string, relSchema: string) => {
216216
const relId = `rel-${sectionId}-${relSchema}`;
217-
const relationshipLocation = document.getElementById(relId);
218217

219-
if (relationshipLocation) {
220-
// Relationship is already rendered, scroll directly to it
221-
relationshipLocation.scrollIntoView({ behavior: 'smooth', block: 'center' });
222-
} else {
223-
// Relationship not found, need to scroll to section first
224-
scrollToSection(sectionId);
225-
setTimeout(() => {
226-
const relationshipLocationAfterScroll = document.getElementById(relId);
227-
if (relationshipLocationAfterScroll) {
228-
relationshipLocationAfterScroll.scrollIntoView({ behavior: 'smooth', block: 'center' });
229-
}
230-
}, 100);
231-
}
218+
// Helper function to attempt scrolling to relationship with retries
219+
const attemptScroll = (attemptsLeft: number) => {
220+
const relationshipLocation = document.getElementById(relId);
221+
222+
if (relationshipLocation) {
223+
// Relationship found, scroll to it
224+
relationshipLocation.scrollIntoView({ behavior: 'smooth', block: 'center' });
225+
} else if (attemptsLeft > 0) {
226+
// Relationship not rendered yet, retry after delay
227+
setTimeout(() => attemptScroll(attemptsLeft - 1), 100);
228+
} else {
229+
// Give up after all retries, just scroll to section
230+
scrollToSection(sectionId);
231+
}
232+
};
233+
234+
// Start attempting to scroll with 5 retries (total 500ms wait time)
235+
attemptScroll(5);
232236
}, [scrollToSection]);
233237

234238
const scrollToGroup = useCallback((groupName: string) => {

Website/components/datamodelview/Relationships.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const Relationships = ({ entity, search = "", onVisibleCountChange }: IRe
3131

3232
const dispatch = useDatamodelViewDispatch();
3333
const { scrollToSection } = useDatamodelView();
34-
const { groups } = useDatamodelData();
34+
const { groups, searchScope } = useDatamodelData();
3535

3636
// Helper function to check if an entity is in the solution
3737
const isEntityInSolution = (entitySchemaName: string): boolean => {
@@ -147,7 +147,10 @@ export const Relationships = ({ entity, search = "", onVisibleCountChange }: IRe
147147
]
148148

149149
const sortedRelationships = getSortedRelationships();
150-
const highlightTerm = searchQuery || search; // Use internal search or parent search for highlighting
150+
// Only highlight if search scope includes relationships
151+
// Use internal search query first, or parent search if relationships scope is enabled
152+
const highlightTerm = searchQuery ||
153+
(search && searchScope.relationships ? search : "");
151154

152155
// Notify parent of visible count changes
153156
useEffect(() => {

Website/components/datamodelview/searchWorker.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface SearchMessage {
2525
data: string;
2626
entityFilters?: Record<string, EntityFilterState>;
2727
searchScope?: SearchScope;
28+
selectedSecurityRoles?: string[];
2829
requestId?: number;
2930
}
3031

@@ -70,6 +71,7 @@ self.onmessage = async function (e: MessageEvent<WorkerMessage>) {
7071
const search = (typeof e.data === 'string' ? e.data : e.data?.data || '').trim().toLowerCase();
7172
const entityFilters: Record<string, EntityFilterState> = (typeof e.data === 'object' && 'entityFilters' in e.data) ? e.data.entityFilters || {} : {};
7273
const requestId = (typeof e.data === 'object' && 'requestId' in e.data) ? e.data.requestId : undefined;
74+
const selectedSecurityRoles: string[] = (typeof e.data === 'object' && 'selectedSecurityRoles' in e.data) ? e.data.selectedSecurityRoles || [] : [];
7375
const searchScope: SearchScope = (typeof e.data === 'object' && 'searchScope' in e.data) ? e.data.searchScope || {
7476
columnNames: true,
7577
columnDescriptions: true,
@@ -103,12 +105,27 @@ self.onmessage = async function (e: MessageEvent<WorkerMessage>) {
103105
| { type: 'relationship'; group: GroupType; entity: EntityType; relationship: RelationshipType }
104106
> = [];
105107

108+
// Helper function to check if entity has access from selected security roles
109+
const hasSecurityRoleAccess = (entity: EntityType): boolean => {
110+
if (selectedSecurityRoles.length === 0) return true; // No filter means show all
111+
112+
return entity.SecurityRoles.some(role =>
113+
selectedSecurityRoles.includes(role.Name) &&
114+
(role.Read !== null && role.Read >= 0) // Has any read access
115+
);
116+
};
117+
106118
////////////////////////////////////////////////
107119
// Finding matches part
108120
////////////////////////////////////////////////
109121
for (const group of groups) {
110122
let groupUsed = false;
111123
for (const entity of group.Entities) {
124+
// Filter by security roles first - skip entity if no access
125+
if (!hasSecurityRoleAccess(entity)) {
126+
continue;
127+
}
128+
112129
// Get entity-specific filters (default to showing all if not set)
113130
const entityFilter = entityFilters[entity.SchemaName] || { hideStandardFields: true, typeFilter: 'all' };
114131

Website/contexts/DatamodelDataContext.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import React, { createContext, useContext, useReducer, ReactNode } from "react";
44
import { AttributeType, EntityType, GroupType, RelationshipType, SolutionWarningType } from "@/lib/Types";
55
import { useSearchParams } from "next/navigation";
6+
import { SearchScope } from "@/components/datamodelview/TimeSlicedSearch";
67

78
interface DataModelAction {
89
getEntityDataBySchemaName: (schemaName: string) => EntityType | undefined;
@@ -14,6 +15,7 @@ interface DatamodelDataState extends DataModelAction {
1415
warnings: SolutionWarningType[];
1516
solutionCount: number;
1617
search: string;
18+
searchScope: SearchScope;
1719
filtered: Array<
1820
| { type: 'group'; group: GroupType }
1921
| { type: 'entity'; group: GroupType; entity: EntityType }
@@ -27,6 +29,13 @@ const initialState: DatamodelDataState = {
2729
warnings: [],
2830
solutionCount: 0,
2931
search: "",
32+
searchScope: {
33+
columnNames: true,
34+
columnDescriptions: true,
35+
columnDataTypes: false,
36+
tableDescriptions: false,
37+
relationships: false,
38+
},
3039
filtered: [],
3140

3241
getEntityDataBySchemaName: () => { throw new Error("getEntityDataBySchemaName not implemented.") },
@@ -47,6 +56,8 @@ const datamodelDataReducer = (state: DatamodelDataState, action: any): Datamodel
4756
return { ...state, solutionCount: action.payload };
4857
case "SET_SEARCH":
4958
return { ...state, search: action.payload };
59+
case "SET_SEARCH_SCOPE":
60+
return { ...state, searchScope: action.payload };
5061
case "SET_FILTERED":
5162
return { ...state, filtered: action.payload };
5263
case "APPEND_FILTERED":

0 commit comments

Comments
 (0)