From 1b9d44f577015dde20bd025276baf21c29d352cc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 19 Jun 2026 01:36:59 +0000 Subject: [PATCH] Auto-expand to the selected ID --- src/backend/InvenTree/part/api.py | 12 +++++ .../src/components/nav/NavigationTree.tsx | 44 +++++++++++++------ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index 4e28658003..7e4e7fde9f 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -331,6 +331,7 @@ class CategoryTree(ListAPI): """Filter the queryset, and include all ancestors of matched items when searching.""" queryset = super().filter_queryset(queryset) + # If a search term is provided, include all ancestors of matched items in the results if self.request.query_params.get('search', '').strip(): ancestors = PartCategory.objects.get_queryset_ancestors( queryset, include_self=True @@ -338,6 +339,17 @@ class CategoryTree(ListAPI): queryset = (queryset | ancestors).distinct() queryset = part_serializers.CategoryTree.annotate_queryset(queryset) + # If a specific ID is provided to "expand_to", include all ancestors of that item in the results + expand_to = self.request.query_params.get('expand_to') + if expand_to: + try: + target = PartCategory.objects.get(pk=int(expand_to)) + ancestors = target.get_ancestors(include_self=True) + queryset = (queryset | ancestors).distinct() + queryset = part_serializers.CategoryTree.annotate_queryset(queryset) + except (PartCategory.DoesNotExist, ValueError): + pass + return queryset diff --git a/src/frontend/src/components/nav/NavigationTree.tsx b/src/frontend/src/components/nav/NavigationTree.tsx index de8c262c26..24386f469d 100644 --- a/src/frontend/src/components/nav/NavigationTree.tsx +++ b/src/frontend/src/components/nav/NavigationTree.tsx @@ -84,36 +84,54 @@ export default function NavigationTree({ // Data query — browse mode loads root nodes only; search mode loads all matches + ancestors const query = useQuery({ enabled: opened, - queryKey: [modelType, 'tree', opened, debouncedSearch], + queryKey: [modelType, 'tree', opened, debouncedSearch, selectedId], queryFn: async () => api .get(apiUrl(endpoint), { params: { ordering: 'level', search: debouncedSearch || undefined, - max_level: debouncedSearch ? undefined : 0 + max_level: debouncedSearch ? undefined : 0, + expand_to: debouncedSearch ? undefined : (selectedId ?? undefined) } }) .then((response) => response.data ?? []) }); - // When the browse-mode query settles, reset accumulated node list + // When the browse-mode query settles, reset accumulated node list and expand to selected node useEffect(() => { if (!debouncedSearch && query.data && !query.isFetching) { setAllNodes(query.data); setFetchedNodes(new Set()); + + if (selectedId) { + const nodeMap: Record = {}; + for (const n of query.data) nodeMap[n.pk] = n; + + // Walk from the selected node up to the root, expanding each ancestor + let current = nodeMap[selectedId]; + while (current?.parent) { + treeState.expand(current.parent.toString()); + current = nodeMap[current.parent]; + } + } + } + }, [debouncedSearch, query.data, query.isFetching, selectedId]); + + // Collapse all nodes when the search term changes (switching modes). + // Intentionally omits query.data so it does NOT fire when browse results arrive — + // that would undo the ancestor expansion done in the sync effect above. + useEffect(() => { + treeState.collapseAllNodes(); + }, [debouncedSearch]); + + // Expand all nodes once search results have fully arrived + useEffect(() => { + if (debouncedSearch && !query.isFetching && query.data?.length) { + treeState.expandAllNodes(); } }, [debouncedSearch, query.data, query.isFetching]); - // Expand all nodes when a search is active so ancestors are visible - useEffect(() => { - if (debouncedSearch) { - treeState.expandAllNodes(); - } else { - treeState.collapseAllNodes(); - } - }, [debouncedSearch, query.data]); - // Fetch direct children of a node (browse mode only); no-op if already fetched const fetchChildren = useCallback( async (nodeValue: string) => { @@ -280,7 +298,7 @@ export default function NavigationTree({ return (