Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions Website/components/datamodelview/DatamodelView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function DatamodelViewContent() {
const datamodelDataDispatch = useDatamodelDataDispatch();
const workerRef = useRef<Worker | null>(null);
const [currentSearchIndex, setCurrentSearchIndex] = useState(0);
const accumulatedResultsRef = useRef<Array<{ type: string; entity: { SchemaName: string }; group: { Name: string } }>>([]); // Track all results during search

// Calculate total search results
const totalResults = filtered.length > 0 ? filtered.filter(item => item.type === 'entity').length : 0;
Expand Down Expand Up @@ -114,29 +115,34 @@ function DatamodelViewContent() {
// setSearchProgress(0);
setCurrentSearchIndex(0);
// Start with empty results to show loading state
accumulatedResultsRef.current = []; // Reset accumulated results
datamodelDataDispatch({ type: "SET_FILTERED", payload: [] });
}
else if (message.type === 'results') {
// setSearchProgress(message.progress || 0);

// For chunked results, append to existing
// Accumulate results in ref for immediate access
accumulatedResultsRef.current = [...accumulatedResultsRef.current, ...message.data];

// For chunked results, always append to existing
datamodelDataDispatch({ type: "APPEND_FILTERED", payload: message.data });

// Only handle completion logic when all chunks are received
if (message.complete) {
datamodelDataDispatch({ type: "SET_FILTERED", payload: message.data });
datamodelDispatch({ type: "SET_LOADING", payload: false });
// Set to first result if we have any and auto-navigate to it
const entityResults = message.data.filter((item: { type: string }) => item.type === 'entity');
if (entityResults.length > 0) {
// Use accumulated results from ref for immediate access
const allFilteredResults = accumulatedResultsRef.current.filter((item: { type: string }) => item.type === 'entity');
if (allFilteredResults.length > 0) {
setCurrentSearchIndex(1);
const firstEntity = entityResults[0];
const firstEntity = allFilteredResults[0];
datamodelDispatch({ type: "SET_CURRENT_SECTION", payload: firstEntity.entity.SchemaName });
datamodelDispatch({ type: "SET_CURRENT_GROUP", payload: firstEntity.group.Name });
// Small delay to ensure virtual list is ready
setTimeout(() => {
scrollToSection(firstEntity.entity.SchemaName);
}, 100);
}
} else {
datamodelDataDispatch({ type: "APPEND_FILTERED", payload: message.data });
}
}
else {
Expand Down
71 changes: 69 additions & 2 deletions Website/components/datamodelview/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export const List = ({ }: IListProps) => {
const scrollTimeoutRef = useRef<NodeJS.Timeout>();
const sectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});

// Track position before search for restoration
const positionBeforeSearch = useRef<{ section: string | null; scrollTop: number } | null>(null);
const isTabSwitching = useRef(false);
const isIntentionalScroll = useRef(false);

const getSectionRefCallback = (schemaName: string) => (el: HTMLDivElement | null) => {
sectionRefs.current[schemaName] = el;
};
Expand Down Expand Up @@ -78,6 +83,25 @@ export const List = ({ }: IListProps) => {
if (!item) return 100;
return item.type === 'group' ? 92 : 300;
},
// Override scroll behavior to prevent jumping during tab switches
scrollToFn: (offset, options) => {
// When switching tabs during search, don't change scroll position
if (isTabSwitching.current && !isIntentionalScroll.current) {
return;
}

// Reset the intentional scroll flag after use
if (isIntentionalScroll.current) {
isIntentionalScroll.current = false;
}

// Default scroll behavior for other cases
const scrollElement = parentRef.current;
if (scrollElement) {
console.log("setting scroll to: " + offset + " - " + options.adjustments)
scrollElement.scrollTop = offset;
}
},
});

const scrollToSection = useCallback((sectionId: string) => {
Expand Down Expand Up @@ -109,6 +133,7 @@ export const List = ({ }: IListProps) => {
}

try {
isIntentionalScroll.current = true; // Mark this as intentional scroll
rowVirtualizer.scrollToIndex(sectionIndex, {
align: 'start'
});
Expand Down Expand Up @@ -143,17 +168,50 @@ export const List = ({ }: IListProps) => {
const currentSearchLength = search.length;
const prevSearchLength = prevSearchLengthRef.current;

// If we just crossed from < 3 to >= 3 characters (starting a search)
// Store position before starting search (crossing from < 3 to >= 3 characters)
if (prevSearchLength < 3 && currentSearchLength >= 3) {
positionBeforeSearch.current = {
section: datamodelView.currentSection,
scrollTop: parentRef.current?.scrollTop || 0
};

setTimeout(() => {
if (parentRef.current) {
parentRef.current.scrollTop = 0;
}
}, 50); // Small delay to ensure virtualizer has processed the new items
}
// Restore position when stopping search (crossing from >= 3 to < 3 characters)
else if (prevSearchLength >= 3 && currentSearchLength < 3) {
if (positionBeforeSearch.current) {
const { section, scrollTop } = positionBeforeSearch.current;

// Restore to the section where the user was before searching
if (section) {
setTimeout(() => {
const sectionIndex = flatItems.findIndex(item =>
item.type === 'entity' && item.entity.SchemaName === section
);

if (sectionIndex !== -1) {
// Scroll to the section they were at before search
isIntentionalScroll.current = true; // Mark this as intentional scroll
rowVirtualizer.scrollToIndex(sectionIndex, { align: 'start' });
} else {
// Fallback to original scroll position
if (parentRef.current) {
parentRef.current.scrollTop = scrollTop;
}
}
}, 100); // Delay to ensure flatItems is updated
}

positionBeforeSearch.current = null;
}
}

prevSearchLengthRef.current = currentSearchLength;
}, [search]);
}, [search, datamodelView.currentSection, flatItems, rowVirtualizer]);

// Throttled scroll handler to reduce calculations
const handleScroll = useCallback(() => {
Expand Down Expand Up @@ -306,6 +364,15 @@ export const List = ({ }: IListProps) => {
entity={item.entity}
group={item.group}
onContentChange={() => remeasureSection(item.entity.SchemaName)}
onTabChange={(isChanging: boolean) => {
isTabSwitching.current = isChanging;
if (isChanging) {
// Reset after a short delay to allow for the content change
setTimeout(() => {
isTabSwitching.current = false;
}, 100);
}
}}
search={search}
/>
</div>
Expand Down
13 changes: 11 additions & 2 deletions Website/components/datamodelview/Section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,25 @@ interface ISectionProps {
entity: EntityType;
group: GroupType;
onContentChange?: () => void;
onTabChange?: (isChanging: boolean) => void;
search?: string;
}

export const Section = React.memo(
({ entity, group, onContentChange, search }: ISectionProps) => {
({ entity, group, onContentChange, onTabChange, search }: ISectionProps) => {
// Use useRef to track previous props for comparison
const prevSearch = React.useRef(search);

const [tab, setTab] = React.useState("attributes");

// Handle tab changes to notify parent component
const handleTabChange = React.useCallback((value: string) => {
if (onTabChange) {
onTabChange(true); // Signal that tab switching is starting
}
setTab(value);
}, [onTabChange]);

// Only compute these counts when needed
const visibleAttributeCount = React.useMemo(() => entity.Attributes.length, [entity.Attributes]);
const visibleRelationshipCount = React.useMemo(() => entity.Relationships.length, [entity.Relationships]);
Expand Down Expand Up @@ -51,7 +60,7 @@ export const Section = React.memo(
)}
</div>

<Tabs defaultValue="attributes" value={tab} onValueChange={setTab}>
<Tabs defaultValue="attributes" value={tab} onValueChange={handleTabChange}>
<div className="bg-white rounded-lg border border-gray-100 shadow-sm">
<TabsList className="bg-transparent p-0 flex overflow-x-auto no-scrollbar gap-1 sm:gap-2">
<TabsTrigger value="attributes" className="flex items-center min-w-[120px] sm:min-w-[140px] px-2 sm:px-4 py-2 text-xs sm:text-sm truncate data-[state=active]:bg-gray-50 data-[state=active]:shadow-sm transition-all duration-200">
Expand Down
Loading