diff --git a/console/pnpm-workspace.yaml b/console/pnpm-workspace.yaml index 25c0d18a..ef75c386 100644 --- a/console/pnpm-workspace.yaml +++ b/console/pnpm-workspace.yaml @@ -1,3 +1,6 @@ +allowBuilds: + '@clerk/shared': set this to true or false + esbuild: set this to true or false onlyBuiltDependencies: - '@clerk/shared' - esbuild diff --git a/console/src/lib/ui-utils.test.tsx b/console/src/lib/ui-utils.test.tsx new file mode 100644 index 00000000..3586d161 --- /dev/null +++ b/console/src/lib/ui-utils.test.tsx @@ -0,0 +1,26 @@ +import { render, screen } from "@testing-library/react" +import { highlightSearch } from "./ui-utils" + +describe("highlightSearch", () => { + it("highlights all exact matches with strong tags", () => { + render(
{highlightSearch("foo foo", "foo")}
) + + expect(screen.getAllByText("foo", { selector: "strong" })).toHaveLength(2) + }) + + it("does not render HTML from untrusted input", () => { + const payload = `` + const { container } = render(
{highlightSearch(payload, "img")}
) + + expect(container.querySelector("img")).toBeNull() + expect(screen.getByText("img", { selector: "strong" })).toBeInTheDocument() + expect(container).toHaveTextContent(payload) + }) + + it("returns plain text when search is empty", () => { + const { container } = render(
{highlightSearch("plain text", "")}
) + + expect(screen.getByText("plain text")).toBeInTheDocument() + expect(container.querySelector("strong")).toBeNull() + }) +}) diff --git a/console/src/lib/ui-utils.tsx b/console/src/lib/ui-utils.tsx index 071f7a1d..c4136e2f 100644 --- a/console/src/lib/ui-utils.tsx +++ b/console/src/lib/ui-utils.tsx @@ -1,4 +1,4 @@ -import type { Key } from "react" +import type { Key, ReactNode } from "react" import { useState } from "react" import type { Modifier } from "react-popper" import { usePopper } from "react-popper" @@ -61,10 +61,27 @@ export const defaultGetOptionDisplay = (option: unknown) => ? ((option as Record).label ?? (option as Record).name) : option) as string -export const highlightSearch = (text: string, search: string, matchClassName = "match") => - text && search - ? text.replaceAll(search, `$&`) - : (text ?? "") +export const highlightSearch = ( + text: string, + search: string, + matchClassName = "match", +): ReactNode => { + if (!text || !search) return text ?? "" + + const parts = text.split(search) + if (parts.length === 1) return text + + return parts.flatMap((part, index) => + index < parts.length - 1 + ? [ + part, + + {search} + , + ] + : [part], + ) +} /** * Navigate from inside a Radix-based overlay (Sheet, Dialog, DropdownMenu). diff --git a/console/src/views/users/rules/FilterRuleEdit.tsx b/console/src/views/users/rules/FilterRuleEdit.tsx index c69a7388..8f1f56a4 100644 --- a/console/src/views/users/rules/FilterRuleEdit.tsx +++ b/console/src/views/users/rules/FilterRuleEdit.tsx @@ -123,13 +123,7 @@ export default function FilterRuleEdit({ required inputClassName="rounded-none border-l-0" buttonClassName="rounded-none" - renderOption={(option, search) => ( - - )} + renderOption={(option, search) => highlightSearch(option.path, search)} />
diff --git a/console/src/views/users/rules/JourneyRuleEdit.tsx b/console/src/views/users/rules/JourneyRuleEdit.tsx index 8c3feb01..38fd7e27 100644 --- a/console/src/views/users/rules/JourneyRuleEdit.tsx +++ b/console/src/views/users/rules/JourneyRuleEdit.tsx @@ -103,13 +103,7 @@ export default function JourneyRuleEdit({ required inputClassName="rounded-none border-l-0" buttonClassName="rounded-none" - renderOption={(option, search) => ( - - )} + renderOption={(option, search) => highlightSearch(option.path, search)} />
diff --git a/console/src/views/users/rules/MemberConditionEdit.tsx b/console/src/views/users/rules/MemberConditionEdit.tsx index 462157a4..e61af9a2 100644 --- a/console/src/views/users/rules/MemberConditionEdit.tsx +++ b/console/src/views/users/rules/MemberConditionEdit.tsx @@ -113,13 +113,9 @@ export default function MemberConditionEdit({ required inputClassName="rounded-none border-l-0" buttonClassName="rounded-none" - renderOption={(option, search) => ( - - )} + renderOption={(option, search) => + highlightSearch(option.name || option.path, search) + } />