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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 101 additions and 31 deletions

View File

@ -31,7 +31,7 @@ export function useScopeServicesState() {
},
};
}
const { updateNode, selectScope, resetSelection, searchAllNodes, deselectScope, apply } =
const { updateNode, filterNode, selectScope, resetSelection, searchAllNodes, deselectScope, apply } =
services.scopesSelectorService;
const selectorServiceState: ScopesSelectorServiceState | undefined = useObservable(
services.scopesSelectorService.stateObservable ?? new Observable(),
@ -39,6 +39,7 @@ export function useScopeServicesState() {
);
return {
filterNode,
updateNode,
selectScope,
resetSelection,

View File

@ -65,10 +65,11 @@ export const ScopesSelector = () => {
removeAllScopes,
closeAndApply,
closeAndReset,
updateNode,
filterNode,
selectScope,
deselectScope,
getRecentScopes,
toggleExpandedNode,
} = scopesSelectorService;
const recentScopes = getRecentScopes();
@ -128,12 +129,13 @@ export const ScopesSelector = () => {
<ScopesTree
tree={tree}
loadingNodeName={loadingNodeName}
onNodeUpdate={updateNode}
filterNode={filterNode}
recentScopes={recentScopes}
selectedScopes={selectedScopes}
scopeNodes={nodes}
selectScope={selectScope}
deselectScope={deselectScope}
toggleExpandedNode={toggleExpandedNode}
onRecentScopesSelect={(scopeIds: string[], parentNodeId?: string) => {
scopesSelectorService.changeScopes(scopeIds, parentNodeId);
scopesSelectorService.closeAndReset();

View File

@ -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) => {
this.interactionProfiler?.startInteraction('scopeNodeDiscovery');
@ -135,15 +190,16 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
const path = getPathOfNode(scopeNodeId, this.state.nodes);
const nodeToCollapse = treeNodeAtPath(this.state.tree!, path);
if (nodeToCollapse) {
const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => {
treeNode.expanded = false;
treeNode.query = '';
});
this.updateState({ tree: newTree });
} else {
if (!nodeToCollapse) {
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) => {
@ -165,7 +221,7 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
expanded: false,
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.
query: '',
query: query || '',
children: undefined,
};
}
@ -249,7 +305,7 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
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
public updateNode = async (scopeNodeId: string, expanded: boolean, query: string) => {
if (expanded) {

View File

@ -18,7 +18,7 @@ export interface ScopesTreeProps {
selectedScopes: SelectedScope[];
scopeNodes: NodesMap;
onNodeUpdate: (scopeNodeId: string, expanded: boolean, query: string) => void;
filterNode: (scopeNodeId: string, query: string) => void;
selectScope: (scopeNodeId: string) => void;
deselectScope: (scopeNodeId: string) => void;
@ -26,6 +26,8 @@ export interface ScopesTreeProps {
// Recent scopes are only shown at the root node
recentScopes?: Scope[][];
onRecentScopesSelect?: (scopeIds: string[], parentNodeId?: string) => void;
toggleExpandedNode: (scopeNodeId: string) => void;
}
export function ScopesTree({
@ -34,10 +36,11 @@ export function ScopesTree({
selectedScopes,
recentScopes,
onRecentScopesSelect,
onNodeUpdate,
filterNode,
scopeNodes,
selectScope,
deselectScope,
toggleExpandedNode,
}: ScopesTreeProps) {
const styles = useStyles2(getStyles);
@ -75,7 +78,7 @@ export function ScopesTree({
treeQuery: tree.query,
scopeNodes,
selectedScopes,
onNodeUpdate,
toggleExpandedNode,
selectScope,
deselectScope,
});
@ -91,7 +94,7 @@ export function ScopesTree({
<ScopesTreeSearch
anyChildExpanded={anyChildExpanded}
searchArea={searchArea}
onNodeUpdate={onNodeUpdate}
filterNode={filterNode}
treeNode={tree}
aria-controls={`${selectedNodesToShowId} ${childrenArrayId}`}
aria-activedescendant={ariaActiveDescendant}
@ -114,11 +117,12 @@ export function ScopesTree({
anyChildExpanded={anyChildExpanded}
lastExpandedNode={lastExpandedNode}
loadingNodeName={loadingNodeName}
onNodeUpdate={onNodeUpdate}
filterNode={filterNode}
selectedScopes={selectedScopes}
scopeNodes={scopeNodes}
selectScope={selectScope}
deselectScope={deselectScope}
toggleExpandedNode={toggleExpandedNode}
maxHeight={`${Math.min(5, selectedNodesToShow.length) * 30}px`}
highlightedId={highlightedId}
id={selectedNodesToShowId}
@ -136,10 +140,11 @@ export function ScopesTree({
anyChildExpanded={anyChildExpanded}
lastExpandedNode={lastExpandedNode}
loadingNodeName={loadingNodeName}
onNodeUpdate={onNodeUpdate}
filterNode={filterNode}
selectedScopes={selectedScopes}
scopeNodes={scopeNodes}
selectScope={selectScope}
toggleExpandedNode={toggleExpandedNode}
deselectScope={deselectScope}
maxHeight={'100%'}
highlightedId={highlightedId}

View File

@ -18,22 +18,24 @@ export interface ScopesTreeItemProps {
selectedScopes: SelectedScope[];
highlighted: boolean;
onNodeUpdate: (scopeNodeId: string, expanded: boolean, query: string) => void;
filterNode: (scopeNodeId: string, query: string) => void;
selectScope: (scopeNodeId: string) => void;
deselectScope: (scopeNodeId: string) => void;
toggleExpandedNode: (scopeNodeId: string) => void;
}
export function ScopesTreeItem({
anyChildExpanded,
loadingNodeName,
treeNode,
onNodeUpdate,
filterNode,
scopeNodes,
selected,
selectedScopes,
selectScope,
deselectScope,
highlighted,
toggleExpandedNode,
}: ScopesTreeItemProps) {
const styles = useStyles2(getStyles);
@ -126,7 +128,7 @@ export function ScopesTreeItem({
data-testid={`scopes-tree-${treeNode.scopeNodeId}-expand`}
aria-label={treeNode.expanded ? t('scopes.tree.collapse', 'Collapse') : t('scopes.tree.expand', 'Expand')}
onClick={() => {
onNodeUpdate(treeNode.scopeNodeId, !treeNode.expanded, treeNode.query);
toggleExpandedNode(treeNode.scopeNodeId);
}}
>
<Icon name={!treeNode.expanded ? 'angle-right' : 'angle-down'} />
@ -145,11 +147,12 @@ export function ScopesTreeItem({
<ScopesTree
tree={treeNode}
loadingNodeName={loadingNodeName}
onNodeUpdate={onNodeUpdate}
filterNode={filterNode}
scopeNodes={scopeNodes}
selectedScopes={selectedScopes}
selectScope={selectScope}
deselectScope={deselectScope}
toggleExpandedNode={toggleExpandedNode}
/>
)}
</div>

View File

@ -15,11 +15,12 @@ type Props = {
maxHeight: string;
selectedScopes: SelectedScope[];
scopeNodes: NodesMap;
onNodeUpdate: (scopeNodeId: string, expanded: boolean, query: string) => void;
filterNode: (scopeNodeId: string, query: string) => void;
selectScope: (scopeNodeId: string) => void;
deselectScope: (scopeNodeId: string) => void;
highlightedId: string | undefined;
id: string;
toggleExpandedNode: (scopeNodeId: string) => void;
};
export function ScopesTreeItemList({
@ -30,11 +31,12 @@ export function ScopesTreeItemList({
selectedScopes,
scopeNodes,
loadingNodeName,
onNodeUpdate,
filterNode,
selectScope,
deselectScope,
highlightedId,
id,
toggleExpandedNode,
}: Props) {
const styles = useStyles2(getStyles);
@ -65,10 +67,11 @@ export function ScopesTreeItemList({
scopeNodes={scopeNodes}
loadingNodeName={loadingNodeName}
anyChildExpanded={anyChildExpanded}
onNodeUpdate={onNodeUpdate}
filterNode={filterNode}
selectScope={selectScope}
deselectScope={deselectScope}
highlighted={childNode.scopeNodeId === highlightedId}
toggleExpandedNode={toggleExpandedNode}
/>
);
})}

View File

@ -12,7 +12,7 @@ export interface ScopesTreeSearchProps {
anyChildExpanded: boolean;
searchArea: string;
treeNode: TreeNode;
onNodeUpdate: (scopeNodeId: string, expanded: boolean, query: string) => void;
filterNode: (scopeNodeId: string, query: string) => void;
onFocus: () => void;
onBlur: () => void;
'aria-controls': string;
@ -22,7 +22,7 @@ export interface ScopesTreeSearchProps {
export function ScopesTreeSearch({
anyChildExpanded,
treeNode,
onNodeUpdate,
filterNode,
searchArea,
onFocus,
onBlur,
@ -45,7 +45,7 @@ export function ScopesTreeSearch({
useDebounce(
() => {
if (inputState.dirty) {
onNodeUpdate(treeNode.scopeNodeId, true, inputState.value);
filterNode(treeNode.scopeNodeId, inputState.value);
}
},
500,

View File

@ -11,7 +11,7 @@ interface UseScopesHighlightingParams {
treeQuery: string;
scopeNodes: NodesMap;
selectedScopes: SelectedScope[];
onNodeUpdate: (scopeNodeId: string, expanded: boolean, query: string) => void;
toggleExpandedNode: (scopeNodeId: string) => void;
selectScope: (scopeNodeId: string) => void;
deselectScope: (scopeNodeId: string) => void;
}
@ -22,7 +22,7 @@ export function useScopesHighlighting({
treeQuery,
scopeNodes,
selectedScopes,
onNodeUpdate,
toggleExpandedNode,
selectScope,
deselectScope,
}: UseScopesHighlightingParams) {
@ -47,7 +47,7 @@ export function useScopesHighlighting({
isNodeExpandable(scopeNodes[nodeId]);
if (isExpanding || isSelectingAndExpandable) {
onNodeUpdate(nodeId, true, treeQuery);
toggleExpandedNode(nodeId);
setHighlightEnabled(false);
return;
}