Skip to content

Commit

Permalink
Add options for case sensitivity and regex in search (#4)
Browse files Browse the repository at this point in the history
This adds two new buttons to the search bar:
- Case sensitivity
- Option to use regular expressions.

This also includes various refactors and general improvements:
- The `+` button to manage tags now properly changes color if the node's color is dark
- Switch to using `title` instead of `aria-label` for buttons
- Add some missing tests
- Renaming
   - `titleColors` ➡ `nodeColors`
   - `NodeTitle` ➡ `NodeHeader`
   - `NodeTags` ➡ `NodeFooter`
  • Loading branch information
TranquilMarmot authored Jul 28, 2020
1 parent cb6d742 commit 3a3d292
Show file tree
Hide file tree
Showing 22 changed files with 581 additions and 101 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ When searching, nodes that do not contain the search term will be dimmed. Nodes

The "Title", "Body", and "Tags" buttons in the search bar control whether or not to search within node titles, bodies, and tags, respectfully. This is an "or" search, so if searching in all three locations and one node has "Sally" in the title, one has "Sally" in the body, and one has a tag of "Sally", then all three will show up in the search. Note that turning off all three options effectively disables the search.

There are also two buttons to enable/disable case sensitivity (![case sensitive icon](./loom-editor/src/icons/case-sensitive.svg)) and regular expressions (![regular expression icon](./loom-editor/src/icons/regex.svg)). Regular expressions are only run on enabled search contexts ("Title", "Body", and/or "Tags") and are done via JavaScript's built-in `RegExp` functionality, so anything regular expressions that work in JavaScript will work. [The MDN Web Docs have a nice regular expression cheat-sheet available](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet).

<details>
<summary>Expand for demo of searching for nodes</summary>
<img src="./images/searching.gif" alt="Demo of searching for nodes" />
Expand All @@ -141,6 +143,8 @@ The "Title", "Body", and "Tags" buttons in the search bar control whether or not

To quickly search for a tag, you can either click on it in the footer of a node or expand the tag list and select it from there. This will automatically fill in the search box with the tag and limit the search to tags.

Re-selecting the tag that is currently being searched for will de-select it and reset the search box.

<details>
<summary>Expand for demo of quick tag searching</summary>
<img src="./images/quick-tag-search.gif" alt="Demo of quickly searching for tags" />
Expand Down
Binary file modified images/searching.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions loom-editor/src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export interface State {
/** Whether or not we're currently searching by node tag */
searchingTags: boolean;

/** Whether or not to search via RegEx */
regexEnabled: boolean;

/** Whether or not the search should be case sensitive */
caseSensitivityEnabled: boolean;

/** The string we're actually searching for */
searchString: string;
};
Expand Down
4 changes: 2 additions & 2 deletions loom-editor/src/components/NodeGraphView/NodeColorChooser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { FunctionComponent } from "react";

import { setNodeColor } from "loom-common/EditorActions";

import { titleColors } from "./index";
import { nodeColors } from "./index";
import { buttonBase, nodeOverlayContainer } from "../../Styles";

const containerStyle = css`
Expand Down Expand Up @@ -54,7 +54,7 @@ const NodeGraphViewColorChooser: FunctionComponent<NodeGraphViewColorChooserProp
return (
<div css={containerStyle} data-testid="node-title-color-chooser">
<div css={buttonContainerStyle}>
{titleColors.map((color, index) => (
{nodeColors.map((color, index) => (
<button
onClick={() => onChooseColor(index)}
key={color}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,25 @@ import { renderWithProvider } from "../../utils/test-utils";

import { searchForTag } from "../../state/UiActions";

import NodeTags from "./NodeTags";
import NodeFooter from "./NodeFooter";
import { YarnNode } from "loom-common/YarnNode";

describe("<NodeTags />", () => {
describe("<NodeFooter />", () => {
const node: YarnNode = {
title: "Test node",
body: "",
tags: "",
};

it("renders", () => {
renderWithProvider(<NodeTags node={node} onOpenTagChooser={() => {}} />);
renderWithProvider(
<NodeFooter
node={node}
onOpenTagChooser={() => {}}
nodeColor="#fff"
nodeColorIsDark={false}
/>
);
});

it("renders all tags", () => {
Expand All @@ -24,7 +31,12 @@ describe("<NodeTags />", () => {
tags: "a lot of different tags",
};
renderWithProvider(
<NodeTags node={nodeWithTags} onOpenTagChooser={() => {}} />
<NodeFooter
node={nodeWithTags}
onOpenTagChooser={() => {}}
nodeColor="#fff"
nodeColorIsDark={false}
/>
);

expect(screen.getAllByTestId("node-tag-button")).toHaveLength(5);
Expand All @@ -39,7 +51,12 @@ describe("<NodeTags />", () => {
};

renderWithProvider(
<NodeTags node={nodeWithTags} onOpenTagChooser={() => {}} />,
<NodeFooter
node={nodeWithTags}
onOpenTagChooser={() => {}}
nodeColor="#fff"
nodeColorIsDark={false}
/>,
undefined,
dispatch
);
Expand All @@ -57,10 +74,15 @@ describe("<NodeTags />", () => {
const onOpenTagChooserSpy = jest.fn();

renderWithProvider(
<NodeTags node={node} onOpenTagChooser={onOpenTagChooserSpy} />
<NodeFooter
node={node}
onOpenTagChooser={onOpenTagChooserSpy}
nodeColor="#fff"
nodeColorIsDark={false}
/>
);

fireEvent.click(screen.getByLabelText("Add tags to node"));
fireEvent.click(screen.getByTitle("Manage node tags"));

expect(onOpenTagChooserSpy).toHaveBeenCalledTimes(1);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { jsx, css } from "@emotion/core";
import { FunctionComponent } from "react";

import { titleColors } from "../NodeGraphView";
import { useYarnState } from "../../state/YarnContext";
import { searchForTag } from "../../state/UiActions";
import UiActionType from "../../state/UiActionType";
Expand All @@ -29,6 +28,10 @@ const addTagButtonStyle = css`
background: none;
border: none;
display: flex;
align-items: center;
justify-content: center;
:hover {
cursor: pointer;
}
Expand All @@ -52,10 +55,11 @@ const tagStyle = css`
}
`;

interface NodeTagsProps {
interface NodeFooterProps {
node: YarnNode;

colorId?: number;
nodeColor: string;
nodeColorIsDark: boolean;
onOpenTagChooser: () => void;
}

Expand All @@ -71,33 +75,38 @@ const renderTags = (tags: string[], dispatch: (action: UiActionType) => void) =>
</button>
));

const NodeTags: FunctionComponent<NodeTagsProps> = ({
const NodeFooter: FunctionComponent<NodeFooterProps> = ({
node,
colorId,
nodeColor,
nodeColorIsDark,
onOpenTagChooser,
}) => {
const dispatch = useYarnState()[1];

const iconFillStyle = css`
fill: ${nodeColorIsDark ? "white" : "black"};
`;

return (
<div
css={css`
${containerStyle}
background-color: ${titleColors[colorId || 0]}`}
background-color: ${nodeColor}`}
>
<div css={tagListContainerStyle}>
<div css={tagListStyle}>
{node.tags && renderTags(node.tags.split(" "), dispatch)}
</div>
<button
aria-label="Add tags to node"
title="Manage node tags"
css={addTagButtonStyle}
onClick={onOpenTagChooser}
>
<AddIcon />
<AddIcon css={iconFillStyle} />
</button>
</div>
</div>
);
};

export default NodeTags;
export default NodeFooter;
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,35 @@ import { renderWithProvider } from "../../utils/test-utils";

import { deleteNode, renameNode } from "loom-common/EditorActions";

import NodeTitle from "./NodeTitle";
import NodeHeader from "./NodeHeader";

describe("<NodeTitle />", () => {
describe("<NodeHeader />", () => {
const nodeTitle = "Some Title";

it("renders", () => {
renderWithProvider(
<NodeTitle title={nodeTitle} onOpenColorChooser={() => {}} />
<NodeHeader
title={nodeTitle}
onOpenColorChooser={() => {}}
nodeColor="#fff"
nodeColorIsDark={false}
/>
);
});

it("posts message when trying to delete node", () => {
window.vsCodeApi = { postMessage: jest.fn() };

renderWithProvider(
<NodeTitle title={nodeTitle} onOpenColorChooser={() => {}} />
<NodeHeader
title={nodeTitle}
onOpenColorChooser={() => {}}
nodeColor="#fff"
nodeColorIsDark={false}
/>
);

fireEvent.click(screen.getByLabelText("Delete node"));
fireEvent.click(screen.getByTitle("Delete node"));

// this posts a message to the extension which shows the confirm/cancel message
expect(window.vsCodeApi.postMessage).toHaveBeenCalledTimes(1);
Expand All @@ -35,10 +45,15 @@ describe("<NodeTitle />", () => {
const onOpenColorChooserSpy = jest.fn();

renderWithProvider(
<NodeTitle title={nodeTitle} onOpenColorChooser={onOpenColorChooserSpy} />
<NodeHeader
title={nodeTitle}
onOpenColorChooser={onOpenColorChooserSpy}
nodeColor="#fff"
nodeColorIsDark={false}
/>
);

fireEvent.click(screen.getByLabelText("Change node color"));
fireEvent.click(screen.getByTitle("Change node color"));

expect(onOpenColorChooserSpy).toHaveBeenCalledTimes(1);
});
Expand All @@ -47,10 +62,15 @@ describe("<NodeTitle />", () => {
window.vsCodeApi = { postMessage: jest.fn() };

renderWithProvider(
<NodeTitle title={nodeTitle} onOpenColorChooser={() => {}} />
<NodeHeader
title={nodeTitle}
onOpenColorChooser={() => {}}
nodeColor="#fff"
nodeColorIsDark={false}
/>
);

fireEvent.click(screen.getByLabelText("Rename node"));
fireEvent.click(screen.getByTitle("Rename node"));

expect(window.vsCodeApi.postMessage).toHaveBeenCalledTimes(1);
expect(window.vsCodeApi.postMessage).toHaveBeenCalledWith(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import { deleteNode, renameNode } from "loom-common/EditorActions";
import { ReactComponent as RenameIcon } from "../../icons/rename.svg";
import { ReactComponent as TrashIcon } from "../../icons/trash.svg";
import { ReactComponent as ColorIcon } from "../../icons/symbol-color.svg";
import { isDark } from "../../Util";

import { titleColors } from "./";

const titleStyle = css`
padding: 10px;
Expand All @@ -31,25 +28,29 @@ const settingsButtonStyle = css`
padding-top: 0;
padding-bottom: 0;
display: flex;
align-items: center;
justify-content: center;
:hover {
cursor: pointer;
}
`;

interface NodeTitleProps {
interface NodeHeaderProps {
title: string;
colorID?: number;
nodeColor: string;
nodeColorIsDark: boolean;
onOpenColorChooser: () => void;
}

const NodeTitle: FunctionComponent<NodeTitleProps> = ({
const NodeHeader: FunctionComponent<NodeHeaderProps> = ({
title,
colorID,
nodeColor,
nodeColorIsDark,
onOpenColorChooser,
}) => {
// grab the color by its ID and determine if it is dark or not
const color = titleColors[colorID || 0];
const fontColor = isDark(color) ? "white" : "black";
const fontColor = nodeColorIsDark ? "white" : "black";

const fontStyle = css`
color: ${fontColor};
Expand All @@ -68,33 +69,33 @@ const NodeTitle: FunctionComponent<NodeTitleProps> = ({
css={css`
${titleStyle}
${fontStyle}
background-color: ${color}
background-color: ${nodeColor}
`}
>
<div css={css`${titleLabelStyle}${fontStyle}`}>{title}</div>
<button
css={settingsButtonStyle}
onClick={() => window.vsCodeApi.postMessage(renameNode(title))}
aria-label="Rename node"
title="Rename node"
>
<RenameIcon css={iconStrokeStyle} />
</button>
<button
css={settingsButtonStyle}
onClick={onOpenColorChooser}
aria-label="Change node color"
title="Change node color"
>
<ColorIcon css={iconFillStyle} />
</button>
<button
css={settingsButtonStyle}
onClick={() => window.vsCodeApi.postMessage(deleteNode(title))}
aria-label="Delete node"
title="Delete node"
>
<TrashIcon css={iconFillStyle} />
</button>
</div>
);
};

export default NodeTitle;
export default NodeHeader;
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const renderTag = (
nodeTags: string[],
onClick: (tag: string) => void
) => (
<button css={listItemBase} onClick={() => onClick(tag)}>
<button key={tag} css={listItemBase} onClick={() => onClick(tag)}>
<div css={tagButtonContentStyle}>
<div data-testid="tag-chooser-tag-button-text">{tag}</div>
{nodeTags.includes(tag) && <CheckIcon />}
Expand Down
Loading

0 comments on commit 3a3d292

Please sign in to comment.