Scopes: Add filterNode and toggleExpandNode to ScopesSelectorService (#111553)

Add filterNode and toggleExpandNode to ScopesSelectorService
This commit is contained in:
Tobias Skarhed
2025-09-26 11:52:28 +02:00
committed by GitHub
parent 47e6528c74
commit 60fed679c4
8 changed files with 101 additions and 31 deletions
@@ -31,7 +31,7 @@ export function useScopeServicesState() {
}, },
}; };
} }
const { updateNode, selectScope, resetSelection, searchAllNodes, deselectScope, apply } = const { updateNode, filterNode, selectScope, resetSelection, searchAllNodes, deselectScope, apply } =
services.scopesSelectorService; services.scopesSelectorService;
const selectorServiceState: ScopesSelectorServiceState | undefined = useObservable( const selectorServiceState: ScopesSelectorServiceState | undefined = useObservable(
services.scopesSelectorService.stateObservable ?? new Observable(), services.scopesSelectorService.stateObservable ?? new Observable(),
@@ -39,6 +39,7 @@ export function useScopeServicesState() {
); );
return { return {
filterNode,
updateNode, updateNode,
selectScope, selectScope,
resetSelection, resetSelection,
@@ -65,10 +65,11 @@ export const ScopesSelector = () => {
removeAllScopes, removeAllScopes,
closeAndApply, closeAndApply,
closeAndReset, closeAndReset,
updateNode, filterNode,
selectScope, selectScope,
deselectScope, deselectScope,
getRecentScopes, getRecentScopes,
toggleExpandedNode,
} = scopesSelectorService; } = scopesSelectorService;
const recentScopes = getRecentScopes(); const recentScopes = getRecentScopes();
@@ -128,12 +129,13 @@ export const ScopesSelector = () => {
<ScopesTree <ScopesTree
tree={tree} tree={tree}
loadingNodeName={loadingNodeName} loadingNodeName={loadingNodeName}
onNodeUpdate={updateNode} filterNode={filterNode}
recentScopes={recentScopes} recentScopes={recentScopes}
selectedScopes={selectedScopes} selectedScopes={selectedScopes}
scopeNodes={nodes} scopeNodes={nodes}
selectScope={selectScope} selectScope={selectScope}
deselectScope={deselectScope} deselectScope={deselectScope}
toggleExpandedNode={toggleExpandedNode}
onRecentScopesSelect={(scopeIds: string[], parentNodeId?: string) => { onRecentScopesSelect={(scopeIds: string[], parentNodeId?: string) => {
scopesSelectorService.changeScopes(scopeIds, parentNodeId); scopesSelectorService.changeScopes(scopeIds, parentNodeId);
scopesSelectorService.closeAndReset(); scopesSelectorService.closeAndReset();
@@ -99,6 +99,61 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
} }
}; };
// Resets query and toggles expanded state of a node
public toggleExpandedNode = async (scopeNodeId: string) => {
const path = getPathOfNode(scopeNodeId, this.state.nodes);
const nodeToToggle = treeNodeAtPath(this.state.tree!, path);
if (!nodeToToggle) {
throw new Error(`Node ${scopeNodeId} not found in tree`);
}
if (nodeToToggle.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToToggle.scopeNodeId])) {
throw new Error(`Trying to expand node at id ${scopeNodeId} that is not expandable`);
}
// Collapse if expanded
if (nodeToToggle.expanded) {
const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => {
treeNode.expanded = false;
// Resets query when collapsing
treeNode.query = '';
});
this.updateState({ tree: newTree });
return;
}
// Expand if collapsed
const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => {
treeNode.expanded = true;
treeNode.query = '';
});
this.updateState({ tree: newTree });
await this.loadNodeChildren(path, nodeToToggle);
};
public filterNode = async (scopeNodeId: string, query: string) => {
const path = getPathOfNode(scopeNodeId, this.state.nodes);
const nodeToFilter = treeNodeAtPath(this.state.tree!, path);
if (!nodeToFilter) {
throw new Error(`Trying to filter node at path or id ${scopeNodeId} not found`);
}
if (nodeToFilter.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToFilter.scopeNodeId])) {
throw new Error(`Trying to filter node at id ${scopeNodeId} that is not expandable`);
}
const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => {
treeNode.expanded = true;
treeNode.query = query;
});
this.updateState({ tree: newTree });
await this.loadNodeChildren(path, nodeToFilter, query);
};
private expandOrFilterNode = async (scopeNodeId: string, query?: string) => { private expandOrFilterNode = async (scopeNodeId: string, query?: string) => {
this.interactionProfiler?.startInteraction('scopeNodeDiscovery'); this.interactionProfiler?.startInteraction('scopeNodeDiscovery');
@@ -135,15 +190,16 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
const path = getPathOfNode(scopeNodeId, this.state.nodes); const path = getPathOfNode(scopeNodeId, this.state.nodes);
const nodeToCollapse = treeNodeAtPath(this.state.tree!, path); const nodeToCollapse = treeNodeAtPath(this.state.tree!, path);
if (nodeToCollapse) {
const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => { if (!nodeToCollapse) {
treeNode.expanded = false;
treeNode.query = '';
});
this.updateState({ tree: newTree });
} else {
throw new Error(`Trying to collapse node at path or id ${scopeNodeId} not found`); throw new Error(`Trying to collapse node at path or id ${scopeNodeId} not found`);
} }
const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => {
treeNode.expanded = false;
treeNode.query = '';
});
this.updateState({ tree: newTree });
}; };
private loadNodeChildren = async (path: string[], treeNode: TreeNode, query?: string) => { private loadNodeChildren = async (path: string[], treeNode: TreeNode, query?: string) => {
@@ -165,7 +221,7 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
expanded: false, expanded: false,
scopeNodeId: node.metadata.name, scopeNodeId: node.metadata.name,
// Only set query on tree nodes if parent already has children (filtering vs first expansion). This is used for saerch highlighting. // Only set query on tree nodes if parent already has children (filtering vs first expansion). This is used for saerch highlighting.
query: '', query: query || '',
children: undefined, children: undefined,
}; };
} }
@@ -249,7 +305,7 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
this.updateState({ selectedScopes: newSelectedScopes }); this.updateState({ selectedScopes: newSelectedScopes });
}; };
// TODO: We should split this into two functions: expandNode and filterNode. // TODO: Replace all usage of this function with expandNode and filterNode.
// @deprecated // @deprecated
public updateNode = async (scopeNodeId: string, expanded: boolean, query: string) => { public updateNode = async (scopeNodeId: string, expanded: boolean, query: string) => {
if (expanded) { if (expanded) {
@@ -18,7 +18,7 @@ export interface ScopesTreeProps {
selectedScopes: SelectedScope[]; selectedScopes: SelectedScope[];
scopeNodes: NodesMap; scopeNodes: NodesMap;
onNodeUpdate: (scopeNodeId: string, expanded: boolean, query: string) => void; filterNode: (scopeNodeId: string, query: string) => void;
selectScope: (scopeNodeId: string) => void; selectScope: (scopeNodeId: string) => void;
deselectScope: (scopeNodeId: string) => void; deselectScope: (scopeNodeId: string) => void;
@@ -26,6 +26,8 @@ export interface ScopesTreeProps {
// Recent scopes are only shown at the root node // Recent scopes are only shown at the root node
recentScopes?: Scope[][]; recentScopes?: Scope[][];
onRecentScopesSelect?: (scopeIds: string[], parentNodeId?: string) => void; onRecentScopesSelect?: (scopeIds: string[], parentNodeId?: string) => void;
toggleExpandedNode: (scopeNodeId: string) => void;
} }
export function ScopesTree({ export function ScopesTree({
@@ -34,10 +36,11 @@ export function ScopesTree({
selectedScopes, selectedScopes,
recentScopes, recentScopes,
onRecentScopesSelect, onRecentScopesSelect,
onNodeUpdate, filterNode,
scopeNodes, scopeNodes,
selectScope, selectScope,
deselectScope, deselectScope,
toggleExpandedNode,
}: ScopesTreeProps) { }: ScopesTreeProps) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@@ -75,7 +78,7 @@ export function ScopesTree({
treeQuery: tree.query, treeQuery: tree.query,
scopeNodes, scopeNodes,
selectedScopes, selectedScopes,
onNodeUpdate, toggleExpandedNode,
selectScope, selectScope,
deselectScope, deselectScope,
}); });
@@ -91,7 +94,7 @@ export function ScopesTree({
<ScopesTreeSearch <ScopesTreeSearch
anyChildExpanded={anyChildExpanded} anyChildExpanded={anyChildExpanded}
searchArea={searchArea} searchArea={searchArea}
onNodeUpdate={onNodeUpdate} filterNode={filterNode}
treeNode={tree} treeNode={tree}
aria-controls={`${selectedNodesToShowId} ${childrenArrayId}`} aria-controls={`${selectedNodesToShowId} ${childrenArrayId}`}
aria-activedescendant={ariaActiveDescendant} aria-activedescendant={ariaActiveDescendant}
@@ -114,11 +117,12 @@ export function ScopesTree({
anyChildExpanded={anyChildExpanded} anyChildExpanded={anyChildExpanded}
lastExpandedNode={lastExpandedNode} lastExpandedNode={lastExpandedNode}
loadingNodeName={loadingNodeName} loadingNodeName={loadingNodeName}
onNodeUpdate={onNodeUpdate} filterNode={filterNode}
selectedScopes={selectedScopes} selectedScopes={selectedScopes}
scopeNodes={scopeNodes} scopeNodes={scopeNodes}
selectScope={selectScope} selectScope={selectScope}
deselectScope={deselectScope} deselectScope={deselectScope}
toggleExpandedNode={toggleExpandedNode}
maxHeight={`${Math.min(5, selectedNodesToShow.length) * 30}px`} maxHeight={`${Math.min(5, selectedNodesToShow.length) * 30}px`}
highlightedId={highlightedId} highlightedId={highlightedId}
id={selectedNodesToShowId} id={selectedNodesToShowId}
@@ -136,10 +140,11 @@ export function ScopesTree({
anyChildExpanded={anyChildExpanded} anyChildExpanded={anyChildExpanded}
lastExpandedNode={lastExpandedNode} lastExpandedNode={lastExpandedNode}
loadingNodeName={loadingNodeName} loadingNodeName={loadingNodeName}
onNodeUpdate={onNodeUpdate} filterNode={filterNode}
selectedScopes={selectedScopes} selectedScopes={selectedScopes}
scopeNodes={scopeNodes} scopeNodes={scopeNodes}
selectScope={selectScope} selectScope={selectScope}
toggleExpandedNode={toggleExpandedNode}
deselectScope={deselectScope} deselectScope={deselectScope}
maxHeight={'100%'} maxHeight={'100%'}
highlightedId={highlightedId} highlightedId={highlightedId}
@@ -18,22 +18,24 @@ export interface ScopesTreeItemProps {
selectedScopes: SelectedScope[]; selectedScopes: SelectedScope[];
highlighted: boolean; highlighted: boolean;
onNodeUpdate: (scopeNodeId: string, expanded: boolean, query: string) => void; filterNode: (scopeNodeId: string, query: string) => void;
selectScope: (scopeNodeId: string) => void; selectScope: (scopeNodeId: string) => void;
deselectScope: (scopeNodeId: string) => void; deselectScope: (scopeNodeId: string) => void;
toggleExpandedNode: (scopeNodeId: string) => void;
} }
export function ScopesTreeItem({ export function ScopesTreeItem({
anyChildExpanded, anyChildExpanded,
loadingNodeName, loadingNodeName,
treeNode, treeNode,
onNodeUpdate, filterNode,
scopeNodes, scopeNodes,
selected, selected,
selectedScopes, selectedScopes,
selectScope, selectScope,
deselectScope, deselectScope,
highlighted, highlighted,
toggleExpandedNode,
}: ScopesTreeItemProps) { }: ScopesTreeItemProps) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@@ -126,7 +128,7 @@ export function ScopesTreeItem({
data-testid={`scopes-tree-${treeNode.scopeNodeId}-expand`} data-testid={`scopes-tree-${treeNode.scopeNodeId}-expand`}
aria-label={treeNode.expanded ? t('scopes.tree.collapse', 'Collapse') : t('scopes.tree.expand', 'Expand')} aria-label={treeNode.expanded ? t('scopes.tree.collapse', 'Collapse') : t('scopes.tree.expand', 'Expand')}
onClick={() => { onClick={() => {
onNodeUpdate(treeNode.scopeNodeId, !treeNode.expanded, treeNode.query); toggleExpandedNode(treeNode.scopeNodeId);
}} }}
> >
<Icon name={!treeNode.expanded ? 'angle-right' : 'angle-down'} /> <Icon name={!treeNode.expanded ? 'angle-right' : 'angle-down'} />
@@ -145,11 +147,12 @@ export function ScopesTreeItem({
<ScopesTree <ScopesTree
tree={treeNode} tree={treeNode}
loadingNodeName={loadingNodeName} loadingNodeName={loadingNodeName}
onNodeUpdate={onNodeUpdate} filterNode={filterNode}
scopeNodes={scopeNodes} scopeNodes={scopeNodes}
selectedScopes={selectedScopes} selectedScopes={selectedScopes}
selectScope={selectScope} selectScope={selectScope}
deselectScope={deselectScope} deselectScope={deselectScope}
toggleExpandedNode={toggleExpandedNode}
/> />
)} )}
</div> </div>
@@ -15,11 +15,12 @@ type Props = {
maxHeight: string; maxHeight: string;
selectedScopes: SelectedScope[]; selectedScopes: SelectedScope[];
scopeNodes: NodesMap; scopeNodes: NodesMap;
onNodeUpdate: (scopeNodeId: string, expanded: boolean, query: string) => void; filterNode: (scopeNodeId: string, query: string) => void;
selectScope: (scopeNodeId: string) => void; selectScope: (scopeNodeId: string) => void;
deselectScope: (scopeNodeId: string) => void; deselectScope: (scopeNodeId: string) => void;
highlightedId: string | undefined; highlightedId: string | undefined;
id: string; id: string;
toggleExpandedNode: (scopeNodeId: string) => void;
}; };
export function ScopesTreeItemList({ export function ScopesTreeItemList({
@@ -30,11 +31,12 @@ export function ScopesTreeItemList({
selectedScopes, selectedScopes,
scopeNodes, scopeNodes,
loadingNodeName, loadingNodeName,
onNodeUpdate, filterNode,
selectScope, selectScope,
deselectScope, deselectScope,
highlightedId, highlightedId,
id, id,
toggleExpandedNode,
}: Props) { }: Props) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@@ -65,10 +67,11 @@ export function ScopesTreeItemList({
scopeNodes={scopeNodes} scopeNodes={scopeNodes}
loadingNodeName={loadingNodeName} loadingNodeName={loadingNodeName}
anyChildExpanded={anyChildExpanded} anyChildExpanded={anyChildExpanded}
onNodeUpdate={onNodeUpdate} filterNode={filterNode}
selectScope={selectScope} selectScope={selectScope}
deselectScope={deselectScope} deselectScope={deselectScope}
highlighted={childNode.scopeNodeId === highlightedId} highlighted={childNode.scopeNodeId === highlightedId}
toggleExpandedNode={toggleExpandedNode}
/> />
); );
})} })}
@@ -12,7 +12,7 @@ export interface ScopesTreeSearchProps {
anyChildExpanded: boolean; anyChildExpanded: boolean;
searchArea: string; searchArea: string;
treeNode: TreeNode; treeNode: TreeNode;
onNodeUpdate: (scopeNodeId: string, expanded: boolean, query: string) => void; filterNode: (scopeNodeId: string, query: string) => void;
onFocus: () => void; onFocus: () => void;
onBlur: () => void; onBlur: () => void;
'aria-controls': string; 'aria-controls': string;
@@ -22,7 +22,7 @@ export interface ScopesTreeSearchProps {
export function ScopesTreeSearch({ export function ScopesTreeSearch({
anyChildExpanded, anyChildExpanded,
treeNode, treeNode,
onNodeUpdate, filterNode,
searchArea, searchArea,
onFocus, onFocus,
onBlur, onBlur,
@@ -45,7 +45,7 @@ export function ScopesTreeSearch({
useDebounce( useDebounce(
() => { () => {
if (inputState.dirty) { if (inputState.dirty) {
onNodeUpdate(treeNode.scopeNodeId, true, inputState.value); filterNode(treeNode.scopeNodeId, inputState.value);
} }
}, },
500, 500,
@@ -11,7 +11,7 @@ interface UseScopesHighlightingParams {
treeQuery: string; treeQuery: string;
scopeNodes: NodesMap; scopeNodes: NodesMap;
selectedScopes: SelectedScope[]; selectedScopes: SelectedScope[];
onNodeUpdate: (scopeNodeId: string, expanded: boolean, query: string) => void; toggleExpandedNode: (scopeNodeId: string) => void;
selectScope: (scopeNodeId: string) => void; selectScope: (scopeNodeId: string) => void;
deselectScope: (scopeNodeId: string) => void; deselectScope: (scopeNodeId: string) => void;
} }
@@ -22,7 +22,7 @@ export function useScopesHighlighting({
treeQuery, treeQuery,
scopeNodes, scopeNodes,
selectedScopes, selectedScopes,
onNodeUpdate, toggleExpandedNode,
selectScope, selectScope,
deselectScope, deselectScope,
}: UseScopesHighlightingParams) { }: UseScopesHighlightingParams) {
@@ -47,7 +47,7 @@ export function useScopesHighlighting({
isNodeExpandable(scopeNodes[nodeId]); isNodeExpandable(scopeNodes[nodeId]);
if (isExpanding || isSelectingAndExpandable) { if (isExpanding || isSelectingAndExpandable) {
onNodeUpdate(nodeId, true, treeQuery); toggleExpandedNode(nodeId);
setHighlightEnabled(false); setHighlightEnabled(false);
return; return;
} }