From 01fb74af254d29c5f58ee10e352c7e2bb13b23dc Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 19 Jun 2026 15:33:12 +1000 Subject: [PATCH] [UI] Tree improvements (#12204) * Hide expand icon for items without children * Add searching to CategoryTree API * Add "level" filter * Automatically include parent tree when searching * Include tree_id field * Add search input to NavigationTree * Add more API filters * Load child nodes iteratively * Fix dynamic loading of nodes * Highlight selected item * Include pathstring * Fix insertion order * Auto-expand to the selected ID * Add "no results" message * Refactor into generic components * Expand to multi level * Use async node loading functionality * Add hovercard * Implement same functionality for StockLocationTree API endpoint * Adjust spacing * Add connecting lines * Add playwright test * Bump API version * Add CHANGELOG entry * Update docs * Update screenshot --- CHANGELOG.md | 1 + .../images/concepts/ui_navigation_tree.png | Bin 25041 -> 41520 bytes docs/docs/concepts/user_interface.md | 12 + src/backend/InvenTree/InvenTree/api.py | 41 +++ .../InvenTree/InvenTree/api_version.py | 5 +- src/backend/InvenTree/part/api.py | 38 ++- src/backend/InvenTree/part/serializers.py | 15 +- src/backend/InvenTree/stock/api.py | 35 +- src/backend/InvenTree/stock/serializers.py | 13 +- .../src/components/nav/NavigationTree.tsx | 315 ++++++++++++++---- .../src/pages/part/CategoryDetail.tsx | 1 + src/frontend/src/pages/part/PartDetail.tsx | 1 + .../src/pages/stock/LocationDetail.tsx | 1 + src/frontend/src/pages/stock/StockDetail.tsx | 1 + src/frontend/tests/pages/pui_stock.spec.ts | 18 + 15 files changed, 411 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c441b199c..10a4265b85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- [#12204](https://github.com/inventree/InvenTree/pull/12204) adds new filtering options to PartCategoryTree and StockLocationTree API endpoints, allowing tree data to be fetched dynamically - [#12165](https://github.com/inventree/InvenTree/pull/12165) adds support for parameters against the PartCategory model - [#12103](https://github.com/inventree/InvenTree/pull/12103) adds column-based filtering to table views in the user interface. This extends the existing table filtering functionality by allowing users to apply filters directly to individual columns. - [#12093](https://github.com/inventree/InvenTree/pull/12093) adds "read_only" attribute to PluginSetting API endpoint, which indicates whether a particular plugin setting is read-only (i.e. cannot be modified via the API) diff --git a/docs/docs/assets/images/concepts/ui_navigation_tree.png b/docs/docs/assets/images/concepts/ui_navigation_tree.png index 5d274452ab8141e4bac90050a0e3631cc067519f..e124cd51bfa729103ee9ae47db7ca374c39ef765 100644 GIT binary patch literal 41520 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 diff --git a/docs/docs/concepts/user_interface.md b/docs/docs/concepts/user_interface.md index c9973c514c..80f9f231c2 100644 --- a/docs/docs/concepts/user_interface.md +++ b/docs/docs/concepts/user_interface.md @@ -91,6 +91,18 @@ Click on the navigation tree icon to expand the tree and view the available navi {{ image("concepts/ui_navigation_tree.png", "Navigation Tree") }} +#### Searching + +The navigation tree includes a search bar at the top of the panel. Typing into the search bar filters the tree to show only entries that match the search query. When a search is active, all matching results are expanded and displayed in a flat list. Clearing the search field returns the tree to its normal browsing mode. + +#### Highlight Selected Entry + +The currently selected entry in the navigation tree is highlighted with a distinct background color, making it easy to identify the active page or section within the hierarchy. + +#### Auto-Expand to Selected Entry + +When the navigation tree is opened, it automatically expands to reveal the currently selected entry. All ancestor nodes in the hierarchy are expanded so the active entry is immediately visible, without requiring manual navigation through the tree. + ## Dashboard The dashboard provides a customizable landing page for users when they log in to the system. The dashboard can be configured to display a variety of widgets and information panels, providing users with quick access to important data and actions. diff --git a/src/backend/InvenTree/InvenTree/api.py b/src/backend/InvenTree/InvenTree/api.py index 79b080bb1d..a85498596f 100644 --- a/src/backend/InvenTree/InvenTree/api.py +++ b/src/backend/InvenTree/InvenTree/api.py @@ -23,6 +23,7 @@ from rest_framework.serializers import ValidationError from rest_framework.views import APIView import InvenTree.config +import InvenTree.filters import InvenTree.permissions import InvenTree.version from common.settings import get_global_setting @@ -963,3 +964,43 @@ def meta_path(model, lookup_field: str = 'pk', lookup_field_ref: str = 'pk'): lookup_field_ref=lookup_field_ref, ), ) + + +class TreeMixin: + """A mixin class for supporting tree-structured data in the API.""" + + # Any API view which inherits from this mixin must define a 'model_class' attribute + model_class = None + + filter_backends = InvenTree.filters.SEARCH_ORDER_FILTER + search_fields = ['name', 'description'] + ordering_fields = ['level', 'name', 'subcategories'] + ordering_field_aliases = {'level': ['level', 'name'], 'name': ['name', 'level']} + ordering = ['level'] + + def filter_queryset(self, queryset): + """Filter the queryset, and provide extra support for tree-structured data.""" + 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 = self.model_class.objects.get_queryset_ancestors( + queryset, include_self=True + ) + queryset = queryset | ancestors + + # If a specific ID is provided to "expand_to", include all ancestors and siblings + if expand_to := self.request.query_params.get('expand_to'): + try: + target = self.model_class.objects.get(pk=int(expand_to)) + target_ancestors = target.get_ancestors(include_self=True) + queryset = queryset | target_ancestors + + # We also want to include the "sibling" nodes of the expanded item + siblings = target.get_siblings(include_self=True) + queryset = queryset | siblings + + except (self.model_class.DoesNotExist, ValueError): + pass + + return queryset.distinct() diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index fff7f52f77..14341d04fb 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 510 +INVENTREE_API_VERSION = 511 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v511 -> 2026-06-19 : https://github.com/inventree/InvenTree/pull/12204 + - Adds new filtering options to PartCategoryTree and StockLocationTree API endpoints + v510 -> 2026-06-18 : https://github.com/inventree/InvenTree/pull/12197 - Require "staff" access permissions for the machine restart API endpoint diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index 0df77ff826..1eb603dd7c 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -21,6 +21,7 @@ from InvenTree.api import ( BulkUpdateMixin, ListCreateDestroyAPIView, ParameterListMixin, + TreeMixin, meta_path, ) from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration @@ -284,25 +285,38 @@ class CategoryDetail(CategoryMixin, OutputOptionsMixin, CustomRetrieveUpdateDest ) -class CategoryTree(ListAPI): +class CategoryTreeFilter(FilterSet): + """Custom filterset class for the CategoryTree endpoint.""" + + class Meta: + """Metaclass options for this filterset.""" + + model = PartCategory + fields = ['parent', 'tree_id', 'level'] + + max_level = rest_filters.NumberFilter( + label=_('Max Level'), + method='filter_max_level', + help_text=_('Limit the depth of the category tree'), + ) + + def filter_max_level(self, queryset, name, value): + """Filter by the maximum depth of the category tree.""" + return queryset.filter(level__lte=value) + + +class CategoryTree(TreeMixin, ListAPI): """API endpoint for accessing a list of PartCategory objects ready for rendering a tree.""" + model_class = PartCategory queryset = PartCategory.objects.all() - serializer_class = part_serializers.CategoryTree - - filter_backends = ORDER_FILTER - - ordering_fields = ['level', 'name', 'subcategories'] - - ordering_field_aliases = {'level': ['level', 'name'], 'name': ['name', 'level']} - - # Order by tree level (top levels first) and then name - ordering = ['level', 'name'] + serializer_class = part_serializers.CategoryTreeSerializer + filterset_class = CategoryTreeFilter def get_queryset(self, *args, **kwargs): """Return an annotated queryset for the CategoryTree endpoint.""" queryset = super().get_queryset(*args, **kwargs) - queryset = part_serializers.CategoryTree.annotate_queryset(queryset) + queryset = part_serializers.CategoryTreeSerializer.annotate_queryset(queryset) return queryset diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index d0ce0d43fa..a9a87c7e80 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -181,14 +181,25 @@ class CategorySerializer( parameters = common.filters.enable_parameters_filter() -class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer): +class CategoryTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for PartCategory tree.""" class Meta: """Metaclass defining serializer fields.""" model = PartCategory - fields = ['pk', 'name', 'parent', 'icon', 'structural', 'subcategories'] + fields = [ + 'pk', + 'name', + 'description', + 'pathstring', + 'parent', + 'tree_id', + 'level', + 'icon', + 'structural', + 'subcategories', + ] subcategories = serializers.IntegerField(label=_('Subcategories'), read_only=True) diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 448477782d..31ef0f915a 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -33,11 +33,11 @@ from InvenTree.api import ( BulkCreateMixin, BulkUpdateMixin, ListCreateDestroyAPIView, + TreeMixin, meta_path, ) from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration from InvenTree.filters import ( - ORDER_FILTER, SEARCH_ORDER_FILTER, InvenTreeDateFilter, NumberOrNullFilter, @@ -455,20 +455,33 @@ class StockLocationDetail( ) -class StockLocationTree(ListAPI): +class LocationTreeFilter(FilterSet): + """Custom filterset class for the StockLocationTree endpoint.""" + + class Meta: + """Metaclass options for this filterset.""" + + model = StockLocation + fields = ['parent', 'tree_id', 'level'] + + max_level = rest_filters.NumberFilter( + label=_('Max Level'), + method='filter_max_level', + help_text=_('Limit the depth of the category tree'), + ) + + def filter_max_level(self, queryset, name, value): + """Filter by the maximum depth of the category tree.""" + return queryset.filter(level__lte=value) + + +class StockLocationTree(TreeMixin, ListAPI): """API endpoint for accessing a list of StockLocation objects, ready for rendering as a tree.""" + model_class = StockLocation queryset = StockLocation.objects.all() serializer_class = StockSerializers.LocationTreeSerializer - - filter_backends = ORDER_FILTER - - ordering_fields = ['level', 'name', 'sublocations'] - - # Order by tree level (top levels first) and then name - ordering = ['level', 'name'] - - ordering_field_aliases = {'level': ['level', 'name'], 'name': ['name', 'level']} + filterset_class = LocationTreeFilter def get_queryset(self, *args, **kwargs): """Return annotated queryset for the StockLocationTree endpoint.""" diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 4943dd6dfe..90b1770e62 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -1160,7 +1160,18 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Metaclass options.""" model = StockLocation - fields = ['pk', 'name', 'parent', 'icon', 'structural', 'sublocations'] + fields = [ + 'pk', + 'name', + 'description', + 'pathstring', + 'parent', + 'tree_id', + 'level', + 'icon', + 'structural', + 'sublocations', + ] sublocations = serializers.IntegerField(label=_('Sublocations'), read_only=True) diff --git a/src/frontend/src/components/nav/NavigationTree.tsx b/src/frontend/src/components/nav/NavigationTree.tsx index edca9924c5..ac214707d0 100644 --- a/src/frontend/src/components/nav/NavigationTree.tsx +++ b/src/frontend/src/components/nav/NavigationTree.tsx @@ -5,28 +5,36 @@ import { Divider, Drawer, Group, + HoverCard, + Loader, LoadingOverlay, type RenderTreeNodePayload, Space, Stack, + Text, + TextInput, Tree, type TreeNodeData, useTree } from '@mantine/core'; +import { useDebouncedValue } from '@mantine/hooks'; import { IconChevronDown, IconChevronRight, IconExclamationCircle, - IconSitemap + IconSearch, + IconSitemap, + IconX } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { StylishText } from '@lib/components/StylishText'; import type { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import type { ModelType } from '@lib/enums/ModelType'; import { apiUrl } from '@lib/functions/Api'; +import { resolveItem } from '@lib/functions/Conversion'; import { eventModified, getDetailUrl, @@ -45,6 +53,7 @@ export default function NavigationTree({ onClose, selectedId, modelType, + childIdentifier, endpoint }: Readonly<{ title: string; @@ -52,26 +61,136 @@ export default function NavigationTree({ onClose: () => void; selectedId?: number | null; modelType: ModelType; + childIdentifier?: string; endpoint: ApiEndpoints; }>) { const api = useApi(); const navigate = useNavigate(); const treeState = useTree(); - // Data query to fetch the tree data from server + const [searchValue, setSearchValue] = useState(''); + const [debouncedSearch] = useDebouncedValue(searchValue, 300); + + // Accumulated flat node list for browse (lazy-load) mode + const [allNodes, setAllNodes] = useState([]); + // PKs of nodes whose children are currently being fetched + const [loadingNodes, setLoadingNodes] = useState>(new Set()); + + // Reset everything when the drawer opens or closes + useEffect(() => { + setSearchValue(''); + setAllNodes([]); + setLoadingNodes(new Set()); + }, [opened]); + + // Data query — browse mode loads root nodes only; search mode loads all matches + ancestors const query = useQuery({ enabled: opened, - queryKey: [modelType, opened], + queryKey: [modelType, 'tree', opened, debouncedSearch, selectedId], queryFn: async () => api .get(apiUrl(endpoint), { - data: { - ordering: 'level' + params: { + ordering: 'level', + search: debouncedSearch || undefined, + max_level: debouncedSearch ? undefined : 0, + expand_to: debouncedSearch ? undefined : (selectedId ?? undefined) } }) .then((response) => response.data ?? []) }); + // When the browse-mode query settles, reset the node list and expand ancestors of the selection + useEffect(() => { + if (!debouncedSearch && query.data && !query.isFetching) { + setAllNodes(query.data); + setLoadingNodes(new Set()); + + if (selectedId) { + const nodeMap: Record = {}; + for (const n of query.data) nodeMap[n.pk] = n; + + // Collect every ancestor pk, then apply in one setExpandedState call to + // avoid closure/batching issues that arise from calling expand() in a loop. + const toExpand: Record = {}; + let current = nodeMap[selectedId]; + while (current?.parent) { + toExpand[current.parent.toString()] = true; + current = nodeMap[current.parent]; + } + if (Object.keys(toExpand).length) { + treeState.setExpandedState({ + ...treeState.expandedState, + ...toExpand + }); + } + } + } + }, [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 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]); + + // Fetch direct children of a node (browse mode only). + // Zeros out the childIdentifier count on success with no results so the node + // is treated as a leaf and won't be re-fetched on subsequent clicks. + const fetchChildren = useCallback( + async (nodeValue: string) => { + const pk = Number.parseInt(nodeValue); + if (loadingNodes.has(pk)) return; + + const nodeInfo = allNodes.find((n) => n.pk === pk); + if (!nodeInfo) return; + + setLoadingNodes((prev) => new Set([...prev, pk])); + + try { + const response = await api.get(apiUrl(endpoint), { + params: { + ordering: 'level', + parent: pk, + max_level: nodeInfo.level + 1 + } + }); + const children: any[] = response.data ?? []; + + setAllNodes((prev) => { + if (children.length === 0 && childIdentifier) { + // No children returned — zero out the count so this node is treated + // as a leaf and won't trigger another fetch on the next click. + return prev.map((n) => + n.pk === pk ? { ...n, [childIdentifier]: 0 } : n + ); + } + const existing = new Set(prev.map((n) => n.pk)); + return [...prev, ...children.filter((n) => !existing.has(n.pk))]; + }); + + if (children.length > 0) { + treeState.expand(nodeValue); + } + } finally { + setLoadingNodes((prev) => { + const next = new Set(prev); + next.delete(pk); + return next; + }); + } + }, + [loadingNodes, allNodes, api, endpoint, childIdentifier] + ); + const follow = useCallback( (node: TreeNodeData, event?: any) => { const url = getDetailUrl(modelType, node.value); @@ -85,107 +204,156 @@ export default function NavigationTree({ [modelType, navigate] ); - // Map returned query to a "tree" structure - const data: TreeNodeData[] = useMemo(() => { - /* - * Reconstruct the navigation tree from the provided data. - * It is required (and assumed) that the data is first sorted by level. - */ + // In search mode use the query results directly; in browse mode use the accumulated lazy-load list + const sourceNodes: any[] = useMemo( + () => (debouncedSearch ? (query.data ?? []) : allNodes), + [debouncedSearch, query.data, allNodes] + ); + // Map flat node list to a nested tree structure (parents must precede children) + const data: TreeNodeData[] = useMemo(() => { const nodes: Record = {}; const tree: TreeNodeData[] = []; - if (!query || !query?.data?.length) { - return []; - } + if (!sourceNodes.length) return []; - for (let ii = 0; ii < query.data.length; ii++) { + // Sort by level so parents are always inserted before their children, + // regardless of the order the API returns items (e.g. after ancestor union in search mode). + const sorted = [...sourceNodes].sort((a, b) => a.level - b.level); + + for (const raw of sorted) { const node = { - ...query.data[ii], + ...raw, children: [], label: ( - - {query.data[ii].name} + + {raw.name} ), - value: query.data[ii].pk.toString(), - selected: query.data[ii].pk === selectedId + value: raw.pk.toString(), + selected: raw.pk === selectedId }; const pk: number = node.pk; const parent: number | null = node.parent; if (!parent) { - // This is a top level node tree.push(node); } else { - // This is *not* a top level node, so the parent *must* already exist nodes[parent]?.children.push(node); } - // Finally, add this node nodes[pk] = node; - - if (pk === selectedId) { - // Expand all parents - let parent = nodes[node.parent]; - while (parent) { - parent.expanded = true; - parent = nodes[parent.parent]; - } - } } return tree; - }, [selectedId, query.data]); + }, [selectedId, sourceNodes]); const renderNode = useCallback( (payload: RenderTreeNodePayload) => { + const nodeInfo = payload.node as any; + const pk = Number.parseInt(payload.node.value); + const isLoading = loadingNodes.has(pk); + + // A node has children if they are already in the tree, or if the server-side + // count (childIdentifier) says so and they haven't been loaded yet. + const childrenLoaded = nodeInfo.children.length > 0; + const needsFetch = + !isLoading && + !debouncedSearch && + !childrenLoaded && + !!(childIdentifier && resolveItem(payload.node, childIdentifier)); + const hasChildren = childrenLoaded || needsFetch; + + const isSelected = nodeInfo.selected === true; + return ( { - if (payload.hasChildren) { + if (isLoading || !hasChildren) return; + if (needsFetch) { + fetchChildren(payload.node.value); + } else { treeState.toggleExpanded(payload.node.value); } }} > - - + {(isLoading || hasChildren || payload.expanded) && ( + + {isLoading ? ( + + ) : hasChildren ? ( + payload.expanded ? ( + + ) : ( + + ) + ) : null} + + )} + - {payload.hasChildren ? ( - payload.expanded ? ( - - ) : ( - - ) - ) : null} - - follow(payload.node, event)} - aria-label={`nav-tree-item-${payload.node.value}`} - c='var(--mantine-color-text)' - > - {payload.node.label} - + + follow(payload.node, event)} + aria-label={`nav-tree-item-${payload.node.value}`} + c='var(--mantine-color-text)' + > + {payload.node.label} + + + + + + {nodeInfo.icon && } + + {nodeInfo.name} + + + {nodeInfo.description && ( + + {nodeInfo.description} + + )} + + + ); }, - [treeState] + [ + treeState, + childIdentifier, + follow, + loadingNodes, + fetchChildren, + debouncedSearch + ] ); return ( + setSearchValue(event.currentTarget.value)} + leftSection={} + rightSection={ + searchValue ? ( + setSearchValue('')} + aria-label={t`Clear search`} + > + + + ) : null + } + /> {query.isError ? ( }> {t`Error loading navigation tree.`} + ) : !query.isFetching && !query.isLoading && data.length === 0 ? ( + }> + {t`No results found`} + ) : ( - + )} diff --git a/src/frontend/src/pages/part/CategoryDetail.tsx b/src/frontend/src/pages/part/CategoryDetail.tsx index 4c47958da5..9d0077e9c8 100644 --- a/src/frontend/src/pages/part/CategoryDetail.tsx +++ b/src/frontend/src/pages/part/CategoryDetail.tsx @@ -377,6 +377,7 @@ export default function CategoryDetail() { modelType={ModelType.partcategory} title={t`Part Categories`} endpoint={ApiEndpoints.category_tree} + childIdentifier='subcategories' opened={treeOpen} onClose={() => { setTreeOpen(false); diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 5182a1c249..af83e85522 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -1152,6 +1152,7 @@ export default function PartDetail() { {user.hasViewRole(UserRoles.part_category) && ( setTreeOpen(false)} selectedId={location?.pk} diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 451b787afa..5d73866074 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -1066,6 +1066,7 @@ export default function StockDetail() { title={t`Stock Locations`} modelType={ModelType.stocklocation} endpoint={ApiEndpoints.stock_location_tree} + childIdentifier='sublocations' opened={treeOpen} onClose={() => setTreeOpen(false)} selectedId={stockitem?.location} diff --git a/src/frontend/tests/pages/pui_stock.spec.ts b/src/frontend/tests/pages/pui_stock.spec.ts index 0f568349f1..bcf8d64d72 100644 --- a/src/frontend/tests/pages/pui_stock.spec.ts +++ b/src/frontend/tests/pages/pui_stock.spec.ts @@ -70,6 +70,24 @@ test('Stock - Location Tree', async ({ browser }) => { await page.getByLabel('breadcrumb-1-factory').click(); await page.getByRole('cell', { name: 'Factory' }).first().waitFor(); + + // Load the tree again - from a deeply nested location + // We expect it to auto-expand to the current location + await navigate(page, 'stock/location/17/stock-items'); + await page.getByLabel('nav-breadcrumb-action').click(); + + for (let ii = 0; ii <= 5; ii++) { + await page + .locator('div') + .filter({ hasText: `Location ${ii}` }) + .first() + .waitFor(); + } + + // Let's search for a particular location + await page.getByRole('textbox', { name: 'nav-tree-search' }).fill('room'); + await page.getByText('Storage Room A').first().waitFor(); + await page.getByText('Storage Room B').first().waitFor(); }); test('Stock - Location Delete', async ({ browser }) => {