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
104 changes: 91 additions & 13 deletions Website/components/datamodelview/SidebarDatamodelView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EntityType, GroupType } from "@/lib/Types";
import { useSidebar } from '@/contexts/SidebarContext';
import { Box, InputAdornment, Paper } from '@mui/material';
import { SearchRounded } from '@mui/icons-material';
import { useState, useEffect, useMemo, useCallback } from "react";
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
import { TextField } from "@mui/material";
import { useDatamodelView, useDatamodelViewDispatch } from "@/contexts/DatamodelViewContext";
import { useDatamodelData } from "@/contexts/DatamodelDataContext";
Expand All @@ -25,7 +25,45 @@ export const SidebarDatamodelView = ({ }: ISidebarDatamodelViewProps) => {
const [searchTerm, setSearchTerm] = useState("");
const [displaySearchTerm, setDisplaySearchTerm] = useState("");
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());

const [manuallyCollapsed, setManuallyCollapsed] = useState<Set<string>>(new Set());
const prevGroupRef = useRef<string | null>(null);

// Auto-expand the current group when it changes or when section changes (e.g., from scrolling)
useEffect(() => {
if (currentGroup) {
const groupChanged = prevGroupRef.current !== currentGroup;
const oldGroup = prevGroupRef.current;

// If the group changed, close the old group and open the new one
if (groupChanged) {
setManuallyCollapsed(prev => {
const newCollapsed = new Set(prev);
newCollapsed.delete(currentGroup);
return newCollapsed;
});
setExpandedGroups(prev => {
const newExpanded = new Set(prev);
// Close the old group
if (oldGroup) {
newExpanded.delete(oldGroup);
}
// Open the new group
newExpanded.add(currentGroup);
return newExpanded;
});
prevGroupRef.current = currentGroup;
}
// If the group didn't change but section did, only expand if not manually collapsed
else if (!manuallyCollapsed.has(currentGroup)) {
setExpandedGroups(prev => {
const newExpanded = new Set(prev);
newExpanded.add(currentGroup);
return newExpanded;
});
}
}
}, [currentGroup, currentSection, manuallyCollapsed]);

// Memoize search results to prevent recalculation on every render
const filteredGroups = useMemo(() => {
if (!searchTerm.trim() && !search) return groups;
Expand Down Expand Up @@ -85,39 +123,67 @@ export const SidebarDatamodelView = ({ }: ISidebarDatamodelViewProps) => {
const handleGroupClick = useCallback((groupName: string) => {
setExpandedGroups(prev => {
const newExpanded = new Set(prev);
if (newExpanded.has(groupName)) {
const isCurrentlyExpanded = newExpanded.has(groupName);

if (isCurrentlyExpanded) {
// User is manually collapsing this group
newExpanded.delete(groupName);
setManuallyCollapsed(prevCollapsed => {
const newCollapsed = new Set(prevCollapsed);
newCollapsed.add(groupName);
return newCollapsed;
});
} else {
if (currentGroup?.toLowerCase() === groupName.toLowerCase()) return newExpanded;
// User is manually expanding this group
newExpanded.add(groupName);
setManuallyCollapsed(prevCollapsed => {
const newCollapsed = new Set(prevCollapsed);
newCollapsed.delete(groupName);
return newCollapsed;
});
}
return newExpanded;
});
}, [currentGroup]);
}, []);

const clearCurrentGroup = useCallback(() => {
dataModelDispatch({ type: "SET_CURRENT_GROUP", payload: null });
dataModelDispatch({ type: "SET_CURRENT_SECTION", payload: null });
}, [dataModelDispatch]);

const handleScrollToGroup = useCallback((group: GroupType) => {
// If clicking on the current group, clear the selection
if (currentGroup?.toLowerCase() === group.Name.toLowerCase()) {
clearCurrentGroup();
return;
}

// Set current group and scroll to group header
dataModelDispatch({ type: "SET_CURRENT_GROUP", payload: group.Name });
if (group.Entities.length > 0)
if (group.Entities.length > 0)
dataModelDispatch({ type: "SET_CURRENT_SECTION", payload: group.Entities[0].SchemaName });

// Clear manually collapsed state and ensure the group is expanded when selected
setManuallyCollapsed(prev => {
const newCollapsed = new Set(prev);
newCollapsed.delete(group.Name);
return newCollapsed;
});
setExpandedGroups(prev => {
const newExpanded = new Set(prev);
if (newExpanded.has(group.Name)) {
newExpanded.delete(group.Name);
}
newExpanded.add(group.Name);
return newExpanded;
});

if (scrollToGroup) {
scrollToGroup(group.Name);
}

// On phone - close sidebar
if (!!isMobile) {
closeSidebar();
}
}, [dataModelDispatch, scrollToGroup, isMobile, closeSidebar]);
}, [currentGroup, clearCurrentGroup, dataModelDispatch, scrollToGroup, isMobile, closeSidebar]);

const handleEntityClick = useCallback((entity: EntityType, groupName: string) => {
// Use requestAnimationFrame to defer heavy operations
Expand All @@ -127,11 +193,23 @@ export const SidebarDatamodelView = ({ }: ISidebarDatamodelViewProps) => {
dataModelDispatch({ type: "SET_CURRENT_GROUP", payload: groupName });
dataModelDispatch({ type: 'SET_CURRENT_SECTION', payload: entity.SchemaName });

// Clear manually collapsed state and ensure the group is expanded when an entity is clicked
setManuallyCollapsed(prev => {
const newCollapsed = new Set(prev);
newCollapsed.delete(groupName);
return newCollapsed;
});
setExpandedGroups(prev => {
const newExpanded = new Set(prev);
newExpanded.add(groupName);
return newExpanded;
});

// On phone - close
if (!!isMobile) {
closeSidebar();
}

// Defer scroll operation to next frame to prevent blocking
requestAnimationFrame(() => {
if (scrollToSection) {
Expand Down Expand Up @@ -171,7 +249,7 @@ export const SidebarDatamodelView = ({ }: ISidebarDatamodelViewProps) => {
<EntityGroupAccordion
key={group.Name}
group={group}
isExpanded={expandedGroups.has(group.Name) || currentGroup?.toLowerCase() === group.Name.toLowerCase()}
isExpanded={expandedGroups.has(group.Name)}
onToggle={handleGroupClick}
onEntityClick={handleEntityClick}
onGroupClick={handleScrollToGroup}
Expand Down
43 changes: 22 additions & 21 deletions Website/components/datamodelview/TimeSlicedSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ interface TimeSlicedSearchProps {
}

// Time-sliced input that maintains 60fps regardless of background work
export const TimeSlicedSearch = ({
onSearch,
export const TimeSlicedSearch = ({
onSearch,
onLoadingChange,
onNavigateNext,
onNavigatePrevious,
Expand All @@ -37,7 +37,7 @@ export const TimeSlicedSearch = ({
const { isOpen } = useSidebar();
const { isSettingsOpen } = useSettings();
const isMobile = useIsMobile();

const searchTimeoutRef = useRef<number>();
const typingTimeoutRef = useRef<number>();
const frameRef = useRef<number>();
Expand Down Expand Up @@ -106,7 +106,7 @@ export const TimeSlicedSearch = ({
channel.port2.onmessage = () => {
onSearch(value);
setLastValidSearch(value);

// Reset typing state in next frame
frameRef.current = requestAnimationFrame(() => {
setIsTyping(false);
Expand All @@ -118,10 +118,10 @@ export const TimeSlicedSearch = ({

const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;

// Immediate visual update (highest priority)
setLocalValue(value);

// Only manage typing state and loading for searches >= 3 characters
if (value.length >= 3) {
// Manage typing state
Expand All @@ -134,7 +134,7 @@ export const TimeSlicedSearch = ({
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}

// Auto-reset typing state if user stops typing
typingTimeoutRef.current = window.setTimeout(() => {
setIsTyping(false);
Expand All @@ -143,13 +143,13 @@ export const TimeSlicedSearch = ({
// Clear typing state for short searches
setIsTyping(false);
onLoadingChange(false);

// Clear any pending timeouts
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
}

// Schedule search (will handle short searches internally)
scheduleSearch(value);

Expand All @@ -158,11 +158,12 @@ export const TimeSlicedSearch = ({
// Handle clear button
const handleClear = useCallback(() => {
if (localValue.length === 0) return; // No-op if already empty
handleClose();
setLocalValue('');
onSearch(''); // Clear search immediately
setIsTyping(false);
onLoadingChange(false);

// Clear any pending timeouts
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
Expand Down Expand Up @@ -202,7 +203,7 @@ export const TimeSlicedSearch = ({
container.style.zIndex = '9999';
document.body.appendChild(container);
}

const searchContainer = document.createElement('div');
searchContainer.style.pointerEvents = 'auto';
container.appendChild(searchContainer);
Expand Down Expand Up @@ -236,8 +237,8 @@ export const TimeSlicedSearch = ({
</InputAdornment>

<Divider orientation="vertical" className='mr-1 h-6' />
<InputBase

<InputBase
className='ml-1 flex-1'
type="text"
placeholder={placeholder}
Expand All @@ -249,7 +250,7 @@ export const TimeSlicedSearch = ({
autoCapitalize="off"
sx={{ backgroundColor: 'transparent' }}
/>

<InputAdornment position="end">
{isTyping && localValue.length >= 3 ? (
<CircularProgress size={20} />
Expand All @@ -260,9 +261,9 @@ export const TimeSlicedSearch = ({
</Box>
) : null}
</InputAdornment>

<Divider orientation="vertical" className='mx-1 h-6' />

<IconButton onClick={handleClick} size="small">
<InfoRounded fontSize="small" color="action" />
</IconButton>
Expand All @@ -275,36 +276,36 @@ export const TimeSlicedSearch = ({
className='mt-2'
>
<MenuList dense className='w-64'>
<MenuItem onClick={onNavigateNext}>
<MenuItem disabled={localValue.length < 3} onClick={onNavigateNext}>
<ListItemIcon>
<NavigateNextRounded />
</ListItemIcon>
<ListItemText>Next</ListItemText>
<Typography variant='body2' color="text.secondary">Enter</Typography>
</MenuItem>
<MenuItem onClick={onNavigatePrevious}>
<MenuItem disabled={localValue.length < 3} onClick={onNavigatePrevious}>
<ListItemIcon>
<NavigateBeforeRounded />
</ListItemIcon>
<ListItemText>Previous</ListItemText>
<Typography variant='body2' color="text.secondary">Shift + Enter</Typography>
</MenuItem>
<MenuItem onClick={onNavigateNext}>
<MenuItem disabled={localValue.length < 3} onClick={onNavigateNext}>
<ListItemIcon>
<NavigateNextRounded />
</ListItemIcon>
<ListItemText>Next</ListItemText>
<Typography variant='body2' color="text.secondary">Ctrl + <KeyboardArrowDownRounded /></Typography>
</MenuItem>
<MenuItem onClick={onNavigatePrevious}>
<MenuItem disabled={localValue.length < 3} onClick={onNavigatePrevious}>
<ListItemIcon>
<NavigateBeforeRounded />
</ListItemIcon>
<ListItemText>Previous</ListItemText>
<Typography variant='body2' color="text.secondary">Ctrl + <KeyboardArrowUpRounded /></Typography>
</MenuItem>
<Divider />
<MenuItem onClick={handleClear}>
<MenuItem disabled={localValue.length === 0} onClick={handleClear}>
<ListItemIcon>
<ClearRounded />
</ListItemIcon>
Expand Down
22 changes: 14 additions & 8 deletions Website/components/datamodelview/entity/AttributeDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { AttributeType, CalculationMethods, RequiredLevel } from "@/lib/Types";
import { AddCircleOutlineRounded, CalculateRounded, ElectricBoltRounded, ErrorRounded, FunctionsRounded, LockRounded, VisibilityRounded } from "@mui/icons-material";
import { Link, Tooltip } from "@mui/material";
import { Box, Link, Tooltip, Typography } from "@mui/material";

export function AttributeDetails({ entityName, attribute }: { entityName: string, attribute: AttributeType }) {
const details = [];
Expand Down Expand Up @@ -35,14 +35,20 @@ export function AttributeDetails({ entityName, attribute }: { entityName: string
}

if (attribute.AttributeUsages.length > 0) {
const tooltip = <span className="">Processes ${attribute.AttributeUsages.map(au => au.Name).join(", ")}.<br />Click to see more details.</span>;
details.push({ icon: (
<Link href={`/processes?ent=${entityName}&attr=${attribute.SchemaName}`}>
<ElectricBoltRounded className="h-4 w-4" />
</Link>
), tooltip });
const tooltip = <span className="">
<b>Usages ({attribute.AttributeUsages.length}):</b>
<Box className="flex flex-col my-1" gap={1}>{attribute.AttributeUsages.map(au => au.Name).join(", ")}</Box>
<Typography variant="caption">Click <ElectricBoltRounded className="h-4 w-4" /> to see more details.</Typography>
</span>;
details.push({
icon: (
<Link href={`/processes?ent=${entityName}&attr=${attribute.SchemaName}`}>
<ElectricBoltRounded className="h-4 w-4" />
</Link>
), tooltip
});
}

return (
<div className="flex flex-row gap-1">
{details.map((detail, index) => (
Expand Down
Loading
Loading