diff --git a/frontend/src/lib/components/SmartNodeSelector/private-components/suggestions.tsx b/frontend/src/lib/components/SmartNodeSelector/private-components/suggestions.tsx index d70127515..2e9483dd5 100644 --- a/frontend/src/lib/components/SmartNodeSelector/private-components/suggestions.tsx +++ b/frontend/src/lib/components/SmartNodeSelector/private-components/suggestions.tsx @@ -98,7 +98,12 @@ export class Suggestions extends React.Component { componentDidUpdate(previousProps: SuggestionsProps): void { const { visible, treeNodeSelection, suggestionsRef } = this.props; - if (previousProps.visible != visible || previousProps.treeNodeSelection != treeNodeSelection) { + if (previousProps.visible !== visible || previousProps.treeNodeSelection !== treeNodeSelection) { + if (this.props.treeNodeSelection) { + this._allOptions = this.props.treeNodeSelection.getSuggestions(); + this._currentNodeLevel = this.props.treeNodeSelection.getFocussedLevel(); + } + this._upperSpacerHeight = 0; if (suggestionsRef.current) { (suggestionsRef.current as HTMLDivElement).scrollTop = 0; @@ -144,7 +149,7 @@ export class Suggestions extends React.Component { } private handleGlobalKeyDown(e: globalThis.KeyboardEvent): void { - const { visible } = this.props; + const { visible, treeNodeSelection } = this.props; if (visible) { if (e.key === "ArrowUp") { this.markSuggestionAsHoveredAndMakeVisible(Math.max(0, this._currentlySelectedSuggestionIndex - 1)); @@ -153,7 +158,7 @@ export class Suggestions extends React.Component { Math.min(this._allOptions.length - 1, this._currentlySelectedSuggestionIndex + 1) ); } - if (e.key == "Enter" && this.currentlySelectedSuggestion() !== undefined) { + if (e.key == "Enter" && this.currentlySelectedSuggestion() !== undefined && this._allOptions.length > 0 && !treeNodeSelection?.focussedNodeNameContainsWildcard()) { this.useSuggestion(e, this.currentlySelectedSuggestion().getAttribute("data-use") as string); } } diff --git a/frontend/src/lib/components/SmartNodeSelector/private-components/tag.tsx b/frontend/src/lib/components/SmartNodeSelector/private-components/tag.tsx index f58253968..6c2bbe420 100644 --- a/frontend/src/lib/components/SmartNodeSelector/private-components/tag.tsx +++ b/frontend/src/lib/components/SmartNodeSelector/private-components/tag.tsx @@ -26,6 +26,7 @@ type TagProps = { removeTag: (e: React.MouseEvent, index: number) => void; updateSelectedTagsAndNodes: () => void; shake: boolean; + maxNumSelectedNodes: number; }; /** @@ -58,7 +59,7 @@ export class Tag extends React.Component { private innerTagClasses(invalid = false, duplicate = false): string { const { treeNodeSelection } = this.props; let ret = { - "text-sm flex flex-wrap rounded justify-left items-center min-w-0 m-0.5 text-slate-600 border-2 border-transparent whitespace-pre-wrap z-10 bg-no-repeat": + "text-sm flex flex-wrap rounded justify-left items-center min-w-0 m-0.5 text-slate-600 border-2 border-transparent whitespace-pre-wrap z-5 bg-no-repeat": true, }; if (this.addAdditionalClasses(invalid)) { @@ -78,7 +79,7 @@ export class Tag extends React.Component { private outerTagClasses(invalid: boolean, duplicate: boolean, frameless: boolean): string { return resolveClassNames( - "flex flex-wrap rounded justify-left items-center min-w-0 relative mr-2 mt-1 mb-1 text-slate-600 border-2 whitespace-pre-wrap z-10", + "flex flex-wrap rounded justify-left items-center min-w-0 relative mr-2 mt-1 mb-1 text-slate-600 border-2 whitespace-pre-wrap z-5", { "border-slate-400 bg-slate-50 SmartNodeSelector__Tag": this.displayAsTag() || frameless, "border-transparent bg-transparent": !this.displayAsTag() && !frameless, @@ -119,13 +120,31 @@ export class Tag extends React.Component { private createMatchesCounter(nodeSelection: TreeNodeSelection, index: number): JSX.Element | null { if (nodeSelection.containsWildcard() && nodeSelection.countExactlyMatchedNodePaths() > 0) { const matches = nodeSelection.countExactlyMatchedNodePaths(); + let fromMax = ""; + let title = "This expression matches " + matches + " options."; + if (this.props.maxNumSelectedNodes !== -1 && matches > this.props.maxNumSelectedNodes) { + fromMax = " / " + this.props.maxNumSelectedNodes; + title = `This expression matches ${matches} node${matches !== 1 && "s"}, but only ${ + this.props.maxNumSelectedNodes + } can be selected.`; + } + return ( this.props.maxNumSelectedNodes && this.props.maxNumSelectedNodes !== -1, + } + )} + title={title} > {matches} + {fromMax} ); } @@ -220,8 +239,13 @@ export class Tag extends React.Component { } private tagTitle(nodeSelection: TreeNodeSelection, index: number): string { - const { countTags, checkIfDuplicate } = this.props; - if (index === countTags - 1 && !nodeSelection.displayAsTag()) { + const { countTags, checkIfDuplicate, maxNumSelectedNodes } = this.props; + if ( + index === countTags - 1 && + !nodeSelection.displayAsTag() && + nodeSelection.displayText() === "" && + maxNumSelectedNodes !== 1 + ) { return "Enter a new name"; } else if (!nodeSelection.isValid()) { return "Invalid"; @@ -230,7 +254,15 @@ export class Tag extends React.Component { } else if (!nodeSelection.isComplete()) { return "Incomplete"; } else { - return nodeSelection.exactlyMatchedNodePaths().join("\n"); + const exactlyMatchedNodePaths = nodeSelection.exactlyMatchedNodePaths(); + if (exactlyMatchedNodePaths.length === -1 || exactlyMatchedNodePaths.length <= maxNumSelectedNodes) { + return exactlyMatchedNodePaths.join("\n"); + } + return `Matched ${exactlyMatchedNodePaths.length} node${ + exactlyMatchedNodePaths.length !== 1 && "s" + } but only ${maxNumSelectedNodes} ${ + maxNumSelectedNodes === 1 ? "is" : "are" + } allowed.\n\nSelected nodes:\n${exactlyMatchedNodePaths.slice(0, maxNumSelectedNodes).join("\n")}`; } } @@ -375,7 +407,7 @@ export class Tag extends React.Component {