Skip to content

Commit

Permalink
Fixes to SmartNodeSelector/VectorSelector (#318)
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenthoms authored Sep 25, 2023
1 parent 14dd17b commit 1442657
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@ export class Suggestions extends React.Component<SuggestionsProps> {

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;
Expand Down Expand Up @@ -144,7 +149,7 @@ export class Suggestions extends React.Component<SuggestionsProps> {
}

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));
Expand All @@ -153,7 +158,7 @@ export class Suggestions extends React.Component<SuggestionsProps> {
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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type TagProps = {
removeTag: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>, index: number) => void;
updateSelectedTagsAndNodes: () => void;
shake: boolean;
maxNumSelectedNodes: number;
};

/**
Expand Down Expand Up @@ -58,7 +59,7 @@ export class Tag extends React.Component<TagProps> {
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)) {
Expand All @@ -78,7 +79,7 @@ export class Tag extends React.Component<TagProps> {

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,
Expand Down Expand Up @@ -119,13 +120,31 @@ export class Tag extends React.Component<TagProps> {
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 (
<span
key={"TagMatchesCounter_" + index}
className="items-center bg-blue-700 text-white rounded-full h-5 justify-center mr-2 pl-1.5 pr-1.5 min-w-[5] flex outline-none relative text-center text-xs leading-none"
title={"This expression matches " + matches + " options."}
className={resolveClassNames(
"items-center text-white rounded-full h-5 justify-center mr-2 pl-1.5 pr-1.5 min-w-[5] flex outline-none relative text-center text-xs leading-none",
{
"bg-blue-700":
matches <= this.props.maxNumSelectedNodes || this.props.maxNumSelectedNodes === -1,
"bg-amber-600":
matches > this.props.maxNumSelectedNodes && this.props.maxNumSelectedNodes !== -1,
}
)}
title={title}
>
{matches}
{fromMax}
</span>
);
}
Expand Down Expand Up @@ -220,8 +239,13 @@ export class Tag extends React.Component<TagProps> {
}

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";
Expand All @@ -230,7 +254,15 @@ export class Tag extends React.Component<TagProps> {
} 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")}`;
}
}

Expand Down Expand Up @@ -375,7 +407,7 @@ export class Tag extends React.Component<TagProps> {
<button
type="button"
key={"TagRemoveButton_" + index}
className="absolute -right-2 -top-2 bg-cyan-600 border border-white rounded-full cursor-pointer w-4 h-4 p-0 flex items-center justify-center hover:bg-cyan-500 z-20"
className="absolute -right-2 -top-2 bg-cyan-600 border border-white rounded-full cursor-pointer w-4 h-4 p-0 flex items-center justify-center hover:bg-cyan-500 z-8"
title="Remove"
onClick={(e): void => removeTag(e, index)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,21 @@ export class TreeData {
private _stringifiedData: string;
private _nodeData: TreeDataNodeMetaData[];
private _allowOrOperator: boolean;
private _allowWildcards: boolean;

constructor({
treeData,
delimiter,
allowOrOperator,
allowWildcards,
}: {
treeData: TreeDataNode[];
delimiter: string;
allowOrOperator: boolean;
allowWildcards: boolean;
}) {
this._treeData = treeData;
this._delimiter = delimiter;
this._nodeData = [];
this._stringifiedData = "";
this._allowOrOperator = allowOrOperator;
this._allowWildcards = allowWildcards;

this.populateNodes();
}
Expand Down Expand Up @@ -129,13 +125,6 @@ export class TreeData {
}

private adjustNodeName(nodeName: string): string {
if (!this._allowWildcards) {
return this.replaceAll(
this.replaceAll(this.replaceAll(this.escapeRegExp(nodeName), ":", ""), "*", "\\*"),
"?",
"\\."
);
}
return this.activateOrStatements(
this.replaceAll(
this.replaceAll(this.replaceAll(this.escapeRegExp(nodeName), ":", ""), "*", '[^:"]*'),
Expand Down
67 changes: 35 additions & 32 deletions frontend/src/lib/components/SmartNodeSelector/smartNodeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ export class SmartNodeSelectorComponent extends React.Component<SmartNodeSelecto
treeData: props.data,
delimiter: props.delimiter,
allowOrOperator: props.useBetaFeatures || false,
allowWildcards: props.maxNumSelectedNodes !== 1,
});
} catch (e) {
this.treeData = null;
Expand Down Expand Up @@ -237,10 +236,6 @@ export class SmartNodeSelectorComponent extends React.Component<SmartNodeSelecto
}

componentDidUpdate(prevProps: SmartNodeSelectorProps): void {
if (this.updateFromWithin) {
this.updateFromWithin = false;
return;
}
if (
(this.props.data && JSON.stringify(this.props.data) !== JSON.stringify(prevProps.data)) ||
(this.props.delimiter && this.props.delimiter !== prevProps.delimiter)
Expand All @@ -251,7 +246,6 @@ export class SmartNodeSelectorComponent extends React.Component<SmartNodeSelecto
treeData: this.props.data,
delimiter: this.props.delimiter,
allowOrOperator: this.props.useBetaFeatures || false,
allowWildcards: this.props.maxNumSelectedNodes !== 1,
});
} catch (e) {
this.treeData = null;
Expand Down Expand Up @@ -614,12 +608,15 @@ export class SmartNodeSelectorComponent extends React.Component<SmartNodeSelecto
const domNode = (this.tagFieldRef as React.RefObject<HTMLUListElement>).current as HTMLUListElement;
const suggestions = (this.suggestionsRef as React.RefObject<HTMLDivElement>).current as HTMLDivElement;
const eventTarget = event.target as Element;
if (domNode && domNode.getBoundingClientRect().width === 0) {
return;
}
if ((!domNode || !domNode.contains(eventTarget)) && (!suggestions || !suggestions.contains(eventTarget))) {
this.noUserInputSelect = false;
this.hideSuggestions({
callback: () => {
if (!this.selectionHasStarted) {
this.unselectAllTags({});

this.updateState({ currentTagIndex: -1 });
}
this.selectionHasStarted = false;
Expand Down Expand Up @@ -785,7 +782,7 @@ export class SmartNodeSelectorComponent extends React.Component<SmartNodeSelecto
}): void {
this.state.nodeSelections.forEach((selection) => selection.setSelected(false));
this.updateState({
currentTagIndex: newCurrentTagIndex === undefined ? this.countTags() - 1 : newCurrentTagIndex,
currentTagIndex: newCurrentTagIndex === undefined ? -1 : newCurrentTagIndex,
callback: () => {
if (showSuggestions) this.maybeShowSuggestions();
if (focusInput) this.focusCurrentTag();
Expand Down Expand Up @@ -1623,6 +1620,7 @@ export class SmartNodeSelectorComponent extends React.Component<SmartNodeSelecto
}

const frameless = maxNumSelectedNodes === 1;
let numSelectedNodes = maxNumSelectedNodes;

return (
<div id={id} ref={this.ref}>
Expand All @@ -1647,30 +1645,35 @@ export class SmartNodeSelectorComponent extends React.Component<SmartNodeSelecto
})}
ref={this.tagFieldRef}
>
{nodeSelections.map((selection, index) => (
<Tag
key={`${index}`}
index={index}
frameless={frameless}
active={index === this.currentTagIndex()}
placeholder={placeholder ? placeholder : "Add new tag"}
treeNodeSelection={selection}
countTags={this.countTags()}
currentTag={index === this.currentTagIndex()}
checkIfDuplicate={this.checkIfSelectionIsDuplicate}
inputKeyDown={this.handleInputKeyDown}
inputKeyUp={this.handleInputKeyUp}
inputChange={this.handleInputChange}
inputSelect={this.handleInputSelect}
inputBlur={this.handleInputBlur}
hideSuggestions={(cb?: () => void) => this.hideSuggestions({ callback: cb })}
removeTag={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, index: number) =>
this.removeTag(index, true, e)
}
updateSelectedTagsAndNodes={this.updateSelectedTagsAndNodes}
shake={this.state.currentTagShaking && index === this.currentTagIndex()}
/>
))}
{nodeSelections.map((selection, index) => {
const tag = (
<Tag
key={`${index}`}
index={index}
frameless={frameless}
active={index === this.currentTagIndex()}
placeholder={placeholder ? placeholder : "Add new tag"}
treeNodeSelection={selection}
countTags={this.countTags()}
currentTag={index === this.currentTagIndex()}
checkIfDuplicate={this.checkIfSelectionIsDuplicate}
inputKeyDown={this.handleInputKeyDown}
inputKeyUp={this.handleInputKeyUp}
inputChange={this.handleInputChange}
inputSelect={this.handleInputSelect}
inputBlur={this.handleInputBlur}
hideSuggestions={(cb?: () => void) => this.hideSuggestions({ callback: cb })}
removeTag={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, index: number) =>
this.removeTag(index, true, e)
}
updateSelectedTagsAndNodes={this.updateSelectedTagsAndNodes}
shake={this.state.currentTagShaking && index === this.currentTagIndex()}
maxNumSelectedNodes={numSelectedNodes === -1 ? -1 : numSelectedNodes}
/>
);
numSelectedNodes -= selection.numberOfExactlyMatchedNodes();
return tag;
})}
</ul>
<div className="absolute right-2 top-1/2 -mt-3">
<button
Expand Down
7 changes: 0 additions & 7 deletions frontend/src/lib/components/VectorSelector/vectorSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ export class VectorSelectorComponent extends SmartNodeSelectorComponent {
treeData: this.modifyTreeData(props.data, props.numMetaNodes, this.vectorDefinitions),
delimiter: props.delimiter,
allowOrOperator: props.useBetaFeatures || false,
allowWildcards: this.props.maxNumSelectedNodes !== 1,
});
} catch (e) {
this.treeData = null;
Expand Down Expand Up @@ -89,11 +88,6 @@ export class VectorSelectorComponent extends SmartNodeSelectorComponent {
}

componentDidUpdate(prevProps: VectorSelectorProps): void {
if (this.updateFromWithin) {
this.updateFromWithin = false;
return;
}

if (
this.props.customVectorDefinitions &&
JSON.stringify(this.props.customVectorDefinitions) !== JSON.stringify(prevProps.customVectorDefinitions)
Expand All @@ -117,7 +111,6 @@ export class VectorSelectorComponent extends SmartNodeSelectorComponent {
treeData: this.modifyTreeData(this.props.data, this.props.numMetaNodes, this.vectorDefinitions),
delimiter: this.props.delimiter,
allowOrOperator: this.props.useBetaFeatures || false,
allowWildcards: this.props.maxNumSelectedNodes !== 1,
});
} catch (e) {
this.treeData = null;
Expand Down

0 comments on commit 1442657

Please sign in to comment.