fix: resolve canvas filter tree state issues (#953)

This commit is contained in:
Guy Ben-Aharon
2025-10-20 17:12:15 +03:00
committed by GitHub
parent 7d811de097
commit ccb29e0a57
3 changed files with 52 additions and 12 deletions

View File

@@ -42,6 +42,7 @@ interface TreeViewProps<
renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode; renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode; renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
loadingNodeIds?: string[]; loadingNodeIds?: string[];
disableCache?: boolean;
} }
export function TreeView< export function TreeView<
@@ -62,12 +63,14 @@ export function TreeView<
renderHoverComponent, renderHoverComponent,
renderActionsComponent, renderActionsComponent,
loadingNodeIds, loadingNodeIds,
disableCache = false,
}: TreeViewProps<Type, Context>) { }: TreeViewProps<Type, Context>) {
const { expanded, loading, loadedChildren, hasMoreChildren, toggleNode } = const { expanded, loading, loadedChildren, hasMoreChildren, toggleNode } =
useTree({ useTree({
fetchChildren, fetchChildren,
expanded: expandedProp, expanded: expandedProp,
setExpanded: setExpandedProp, setExpanded: setExpandedProp,
disableCache,
}); });
const [selectedIdInternal, setSelectedIdInternal] = React.useState< const [selectedIdInternal, setSelectedIdInternal] = React.useState<
string | undefined string | undefined
@@ -145,6 +148,7 @@ export function TreeView<
renderHoverComponent={renderHoverComponent} renderHoverComponent={renderHoverComponent}
renderActionsComponent={renderActionsComponent} renderActionsComponent={renderActionsComponent}
loadingNodeIds={loadingNodeIds} loadingNodeIds={loadingNodeIds}
disableCache={disableCache}
/> />
))} ))}
</div> </div>
@@ -179,6 +183,7 @@ interface TreeNodeProps<
renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode; renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode; renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
loadingNodeIds?: string[]; loadingNodeIds?: string[];
disableCache?: boolean;
} }
function TreeNode<Type extends string, Context extends Record<Type, unknown>>({ function TreeNode<Type extends string, Context extends Record<Type, unknown>>({
@@ -201,11 +206,16 @@ function TreeNode<Type extends string, Context extends Record<Type, unknown>>({
renderHoverComponent, renderHoverComponent,
renderActionsComponent, renderActionsComponent,
loadingNodeIds, loadingNodeIds,
disableCache = false,
}: TreeNodeProps<Type, Context>) { }: TreeNodeProps<Type, Context>) {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const isExpanded = expanded[node.id]; const isExpanded = expanded[node.id];
const isLoading = loading[node.id]; const isLoading = loading[node.id];
const children = loadedChildren[node.id] || node.children; // If cache is disabled, always use fresh node.children
// Otherwise, use cached loadedChildren if available (for async fetched data)
const children = disableCache
? node.children
: node.children || loadedChildren[node.id];
const isSelected = selectedId === node.id; const isSelected = selectedId === node.id;
const IconComponent = const IconComponent =
@@ -423,6 +433,7 @@ function TreeNode<Type extends string, Context extends Record<Type, unknown>>({
renderHoverComponent={renderHoverComponent} renderHoverComponent={renderHoverComponent}
renderActionsComponent={renderActionsComponent} renderActionsComponent={renderActionsComponent}
loadingNodeIds={loadingNodeIds} loadingNodeIds={loadingNodeIds}
disableCache={disableCache}
/> />
))} ))}
{isLoading ? ( {isLoading ? (

View File

@@ -28,10 +28,12 @@ export function useTree<
fetchChildren, fetchChildren,
expanded: expandedProp, expanded: expandedProp,
setExpanded: setExpandedProp, setExpanded: setExpandedProp,
disableCache = false,
}: { }: {
fetchChildren?: FetchChildrenFunction<Type, Context>; fetchChildren?: FetchChildrenFunction<Type, Context>;
expanded?: ExpandedState; expanded?: ExpandedState;
setExpanded?: Dispatch<SetStateAction<ExpandedState>>; setExpanded?: Dispatch<SetStateAction<ExpandedState>>;
disableCache?: boolean;
}) { }) {
const [expandedInternal, setExpandedInternal] = useState<ExpandedState>({}); const [expandedInternal, setExpandedInternal] = useState<ExpandedState>({});
@@ -89,8 +91,8 @@ export function useTree<
// Get any previously fetched children // Get any previously fetched children
const previouslyFetchedChildren = loadedChildren[nodeId] || []; const previouslyFetchedChildren = loadedChildren[nodeId] || [];
// If we have static children, merge them with any previously fetched children // Only cache if caching is enabled
if (staticChildren?.length) { if (!disableCache && staticChildren?.length) {
const mergedChildren = mergeChildren( const mergedChildren = mergeChildren(
staticChildren, staticChildren,
previouslyFetchedChildren previouslyFetchedChildren
@@ -110,8 +112,8 @@ export function useTree<
// Set expanded state immediately to show static/previously fetched children // Set expanded state immediately to show static/previously fetched children
setExpanded((prev) => ({ ...prev, [nodeId]: true })); setExpanded((prev) => ({ ...prev, [nodeId]: true }));
// If we haven't loaded dynamic children yet // If we haven't loaded dynamic children yet and cache is enabled
if (!previouslyFetchedChildren.length) { if (!disableCache && !previouslyFetchedChildren.length) {
setLoading((prev) => ({ ...prev, [nodeId]: true })); setLoading((prev) => ({ ...prev, [nodeId]: true }));
try { try {
const fetchedChildren = await fetchChildren?.( const fetchedChildren = await fetchChildren?.(
@@ -140,7 +142,14 @@ export function useTree<
} }
} }
}, },
[expanded, loadedChildren, fetchChildren, mergeChildren, setExpanded] [
expanded,
loadedChildren,
fetchChildren,
mergeChildren,
setExpanded,
disableCache,
]
); );
return { return {

View File

@@ -101,13 +101,32 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
areas, areas,
]); ]);
// Initialize expanded state with all schemas expanded // Sync expanded state with tree data changes - only expand NEW nodes
useMemo(() => { useEffect(() => {
const initialExpanded: Record<string, boolean> = {}; setExpanded((prev) => {
treeData.forEach((node) => { const currentNodeIds = new Set(treeData.map((n) => n.id));
initialExpanded[node.id] = true; let hasChanges = false;
const newExpanded: Record<string, boolean> = { ...prev };
// Add any new nodes with expanded=true (preserve existing state)
treeData.forEach((node) => {
if (!(node.id in prev)) {
newExpanded[node.id] = true;
hasChanges = true;
}
});
// Remove nodes that no longer exist (cleanup)
Object.keys(prev).forEach((id) => {
if (!currentNodeIds.has(id)) {
delete newExpanded[id];
hasChanges = true;
}
});
// Only update state if something actually changed (performance)
return hasChanges ? newExpanded : prev;
}); });
setExpanded(initialExpanded);
}, [treeData]); }, [treeData]);
// Filter tree data based on search query // Filter tree data based on search query
@@ -317,6 +336,7 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
expanded={expanded} expanded={expanded}
setExpanded={setExpanded} setExpanded={setExpanded}
className="py-2" className="py-2"
disableCache={true}
/> />
</ScrollArea> </ScrollArea>
</div> </div>