Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@
},
"resolutions": {
"@types/react": "^17.x",
"nwsapi": "^2.2.20"
"nwsapi": "^2.2.20",
"@zendeskgarden/container-utilities": "^1.0.0"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was to fix a Rollup build error. The @zendeskgarden/react-dropdowns.legacy package imports KEY_CODES from @zendeskgarden/container-utilities, but that export only exists in v1.x. Our package.json has @zendeskgarden/container-utilities at ^2.0.2 in dependencies, and v2.x removed the KEY_CODES export — so the legacy dropdowns package couldn't resolve it and the build failed.

},
"commitlint": {
"extends": [
Expand Down
37 changes: 37 additions & 0 deletions src/modules/ticket-fields/fields/HighlightMatch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
interface HighlightMatchProps {
text: string;
searchValue: string;
}

/**
* Highlights the matching portion of text by making it bold.
* Case-insensitive matching.
*/
export function HighlightMatch({
text,
searchValue,
}: HighlightMatchProps): JSX.Element {
if (!searchValue) {
return <span>{text}</span>;
}

const lowerText = text.toLowerCase();
const lowerSearch = searchValue.toLowerCase();
const index = lowerText.indexOf(lowerSearch);

if (index === -1) {
return <span>{text}</span>;
}

const before = text.slice(0, index);
const match = text.slice(index, index + searchValue.length);
const after = text.slice(index + searchValue.length);

return (
<span>
{before}
<strong>{match}</strong>
{after}
</span>
);
}
188 changes: 188 additions & 0 deletions src/modules/ticket-fields/fields/MultiSelect.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { render } from "../../test/render";
import { screen } from "@testing-library/react";
import { MultiSelect } from "./MultiSelect";
import type { TicketFieldObject } from "../data-types/TicketFieldObject";

describe("MultiSelect", () => {
const mockOnChange = jest.fn();

const baseField: TicketFieldObject = {
id: 1,
name: "test_multiselect",
label: "Test MultiSelect",
type: "multiselect",
value: [],
error: null,
required: false,
description: "",
options: [
{ name: "Option 1", value: "option_1" },
{ name: "Option 2", value: "option_2" },
{ name: "Option 3", value: "option_3" },
{ name: "Option 4", value: "option_4" },
],
};

beforeEach(() => {
jest.clearAllMocks();
});

it("renders field with label", () => {
render(<MultiSelect field={baseField} onChange={mockOnChange} />);

expect(screen.getByText("Test MultiSelect")).toBeInTheDocument();
});

it("shows required indicator when field is required", () => {
const requiredField = { ...baseField, required: true };
render(<MultiSelect field={requiredField} onChange={mockOnChange} />);

expect(screen.getByText("*")).toBeInTheDocument();
});

it("displays description when provided", () => {
const fieldWithDescription = {
...baseField,
description: "<p>Select multiple options</p>",
};
render(
<MultiSelect field={fieldWithDescription} onChange={mockOnChange} />
);

expect(screen.getByText("Select multiple options")).toBeInTheDocument();
});

it("displays error message when error is present", () => {
const fieldWithError = {
...baseField,
error: "At least one option is required",
};
render(<MultiSelect field={fieldWithError} onChange={mockOnChange} />);

expect(
screen.getByText("At least one option is required")
).toBeInTheDocument();
});

it("renders hidden inputs for each selected value for form submission", () => {
const fieldWithValues = {
...baseField,
value: ["option_1", "option_2"],
};
const { container } = render(
<MultiSelect field={fieldWithValues} onChange={mockOnChange} />
);

const hiddenInputs = container.querySelectorAll(
'input[type="hidden"][name="test_multiselect[]"]'
);
expect(hiddenInputs).toHaveLength(2);
expect(hiddenInputs[0]).toHaveValue("option_1");
expect(hiddenInputs[1]).toHaveValue("option_2");
});

it("renders with no hidden inputs when no values selected", () => {
const { container } = render(
<MultiSelect field={baseField} onChange={mockOnChange} />
);

const hiddenInputs = container.querySelectorAll(
'input[type="hidden"][name="test_multiselect[]"]'
);
expect(hiddenInputs).toHaveLength(0);
});

it("handles nested options correctly", () => {
const fieldWithNestedOptions: TicketFieldObject = {
...baseField,
options: [
{ name: "Category::SubCategory::Item", value: "cat_subcat_item" },
{ name: "Another Option", value: "another" },
],
};

const { container } = render(
<MultiSelect field={fieldWithNestedOptions} onChange={mockOnChange} />
);

// Component should render without errors
expect(container).toBeInTheDocument();
});

it("renders with deeply nested selected options", () => {
const fieldWithNestedOptions: TicketFieldObject = {
...baseField,
value: ["cameras_dslr_consumer", "cameras_dslr_pro"],
options: [
{
name: "Cameras::Digital SLR::Consumer",
value: "cameras_dslr_consumer",
},
{
name: "Cameras::Digital SLR::Professional",
value: "cameras_dslr_pro",
},
{ name: "Lenses", value: "lenses" },
],
};

const { container } = render(
<MultiSelect field={fieldWithNestedOptions} onChange={mockOnChange} />
);

const hiddenInputs = container.querySelectorAll(
'input[type="hidden"][name="test_multiselect[]"]'
);
expect(hiddenInputs).toHaveLength(2);
expect(hiddenInputs[0]).toHaveValue("cameras_dslr_consumer");
expect(hiddenInputs[1]).toHaveValue("cameras_dslr_pro");
});

it("initializes with correct values from props", () => {
const fieldWithValues = {
...baseField,
value: ["option_1", "option_4"],
};
const { container } = render(
<MultiSelect field={fieldWithValues} onChange={mockOnChange} />
);

const hiddenInputs = container.querySelectorAll(
'input[type="hidden"][name="test_multiselect[]"]'
);
expect(hiddenInputs).toHaveLength(2);
expect(hiddenInputs[0]).toHaveValue("option_1");
expect(hiddenInputs[1]).toHaveValue("option_4");
});

it("handles empty array value", () => {
const fieldWithEmptyArray = {
...baseField,
value: [],
};
const { container } = render(
<MultiSelect field={fieldWithEmptyArray} onChange={mockOnChange} />
);

const hiddenInputs = container.querySelectorAll(
'input[type="hidden"][name="test_multiselect[]"]'
);
expect(hiddenInputs).toHaveLength(0);
});

it("renders with single selected value", () => {
const fieldWithSingleValue = {
...baseField,
value: ["option_1"],
};
const { container } = render(
<MultiSelect field={fieldWithSingleValue} onChange={mockOnChange} />
);

const hiddenInputs = container.querySelectorAll(
'input[type="hidden"][name="test_multiselect[]"]'
);
expect(hiddenInputs).toHaveLength(1);
expect(hiddenInputs[0]).toHaveValue("option_1");
});
});
Loading
Loading