Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(SILVA-550): Enhance Org Unit and Category Selection with Filterable Multi-Select Dropdown #442

Merged
merged 15 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
@Getter
@ToString
public class OpeningSearchFiltersDto {
private final String orgUnit;
private final String category;
private final List<String> orgUnit;
private final List<String> category;
private final List<String> statusList;
private final Boolean myOpenings;
private final Boolean submittedToFrpa;
Expand All @@ -38,8 +38,8 @@ public class OpeningSearchFiltersDto {

/** Creates an instance of the search opening filter dto. */
public OpeningSearchFiltersDto(
String orgUnit,
String category,
List<String> orgUnit,
List<String> category,
List<String> statusList,
Boolean myOpenings,
Boolean submittedToFrpa,
Expand All @@ -55,8 +55,18 @@ public OpeningSearchFiltersDto(
String cutBlockId,
String timberMark,
String mainSearchTerm) {
this.orgUnit = Objects.isNull(orgUnit) ? null : orgUnit.toUpperCase().trim();
this.category = Objects.isNull(category) ? null : category.toUpperCase().trim();
this.orgUnit = new ArrayList<>();
if (!Objects.isNull(orgUnit)) {
this.orgUnit.addAll(orgUnit.stream()
.map(s -> String.format("'%s'", s.toUpperCase().trim()))
.toList());
}
this.category = new ArrayList<>();
if (!Objects.isNull(category)) {
this.category.addAll(category.stream()
.map(s -> String.format("'%s'", s.toUpperCase().trim()))
.toList());
}
this.statusList = new ArrayList<>();
if (!Objects.isNull(statusList)) {
this.statusList.addAll(statusList.stream().map(s -> String.format("'%s'", s)).toList());
Expand Down Expand Up @@ -90,8 +100,8 @@ public OpeningSearchFiltersDto(
*/
public boolean hasValue(String prop) {
return switch (prop) {
case SilvaOracleConstants.ORG_UNIT -> !Objects.isNull(this.orgUnit);
case SilvaOracleConstants.CATEGORY -> !Objects.isNull(this.category);
case SilvaOracleConstants.ORG_UNIT -> !this.orgUnit.isEmpty();
case SilvaOracleConstants.CATEGORY -> !this.category.isEmpty();
case SilvaOracleConstants.STATUS_LIST -> !this.statusList.isEmpty();
case SilvaOracleConstants.MY_OPENINGS -> !Objects.isNull(this.myOpenings);
case SilvaOracleConstants.SUBMITTED_TO_FRPA -> !Objects.isNull(this.submittedToFrpa);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ public PaginatedResult<OpeningSearchResponseDto> openingSearch(
@RequestParam(value = "mainSearchTerm", required = false)
String mainSearchTerm,
@RequestParam(value = "orgUnit", required = false)
String orgUnit,
List<String> orgUnit,
@RequestParam(value = "category", required = false)
String category,
List<String> category,
@RequestParam(value = "statusList", required = false)
List<String> statusList,
@RequestParam(value = "myOpenings", required = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,13 +257,13 @@ private Query setQueryParameters(OpeningSearchFiltersDto filtersDto, String nati

// 1. Org Unit code
if (filtersDto.hasValue(SilvaOracleConstants.ORG_UNIT)) {
log.info("Setting orgUnit filter value");
query.setParameter("orgUnit", filtersDto.getOrgUnit());
log.info("Setting orgUnit filter values");
// No need to set value since the query already dit it. Didn't work set through named param
}
// 2. Category code
if (filtersDto.hasValue(SilvaOracleConstants.CATEGORY)) {
log.info("Setting category filter value");
query.setParameter("category", filtersDto.getCategory());
log.info("Setting category filter values");
// No need to set value since the query already dit it. Didn't work set through named param
}
// 3. Status list codes
if (filtersDto.hasValue(SilvaOracleConstants.STATUS_LIST)) {
Expand Down Expand Up @@ -414,13 +414,15 @@ private String createNativeSqlQuery(OpeningSearchFiltersDto filtersDto) {

// 1. Org Unit code
if (filtersDto.hasValue(SilvaOracleConstants.ORG_UNIT)) {
log.info("Filter orgUnit detected! orgUnit={}", filtersDto.getOrgUnit());
builder.append("AND ou.ORG_UNIT_CODE = :orgUnit ");
String orgUnits = String.join(",", filtersDto.getOrgUnit());
log.info("Filter orgUnit detected! orgUnit={}", orgUnits);
builder.append(String.format("AND ou.ORG_UNIT_CODE IN (%s) ", orgUnits));
}
// 2. Category code
if (filtersDto.hasValue(SilvaOracleConstants.CATEGORY)) {
log.info("Filter category detected! category={}", filtersDto.getCategory());
builder.append("AND o.OPEN_CATEGORY_CODE = :category ");
String categories = String.join(",", filtersDto.getCategory());
log.info("Filter category detected! statusList={}", categories);
builder.append(String.format("AND o.OPEN_CATEGORY_CODE IN (%s) ", categories));
}
// 3. Status code
if (filtersDto.hasValue(SilvaOracleConstants.STATUS_LIST)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ class OpeningSearchRepositoryTest {
private OpeningSearchRepository openingSearchRepository;

private OpeningSearchFiltersDto mockFilter(
String orgUnit,
String category,
List<String> orgUnit,
List<String> category,
List<String> statusList,
Boolean myOpenings,
Boolean submittedToFrpa,
Expand Down Expand Up @@ -79,16 +79,16 @@ private OpeningSearchFiltersDto mockFilter(
mainSearchTerm);
}

private OpeningSearchFiltersDto mockOrgUnit(String orgUnit) {
private OpeningSearchFiltersDto mockOrgUnit(List<String> orgUnit) {
return mockFilter(
orgUnit, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null);
}

private OpeningSearchFiltersDto mockAllFilters() {
return mockFilter(
"DCR",
"FTML",
List.of("DCR"),
List.of("FTML"),
List.of("APP"),
true,
false,
Expand Down Expand Up @@ -481,7 +481,7 @@ void searchOpeningQuery_mainFilterString_shouldSucceed() {
@Test
@DisplayName("Search opening query org unit filter should succeed")
void searchOpeningQuery_orgUnitFilter_shouldSucceed() {
OpeningSearchFiltersDto filters = mockOrgUnit("DCR");
OpeningSearchFiltersDto filters = mockOrgUnit(List.of("DCR"));

PaginationParameters pagination = new PaginationParameters(0, 10);

Expand Down Expand Up @@ -575,15 +575,15 @@ void searchOpeningQuery_allFilters_shouldSucceed() {

Integer openingId = 123456789;
String openingNumber = "589";
OpeningCategoryEnum category = OpeningCategoryEnum.of(filters.getCategory());
OpeningCategoryEnum category = OpeningCategoryEnum.of("FTML");
OpeningStatusEnum status = OpeningStatusEnum.of(filters.getStatusList().get(0));
String cuttingPermitId = "123";
String timberMark = "EM2184";
String cutBlockId = "456";
BigDecimal openingGrossArea = new BigDecimal("11");
Timestamp disturbanceStartDate = Timestamp.valueOf(LocalDateTime.now());
String forestFileId = "TFL47";
String orgUnitCode = filters.getOrgUnit();
String orgUnitCode = "DCR";
String orgUnitName = "Org Name";
String clientNumber = "00012797";
String clientLocation = "00";
Expand Down Expand Up @@ -655,7 +655,7 @@ void searchOpeningQuery_allFilters_shouldSucceed() {
@Test
@DisplayName("Search opening query no records found should succeed")
void searchOpeningQuery_noRecordsFound_shouldSucceed() {
OpeningSearchFiltersDto filters = mockOrgUnit("AAA");
OpeningSearchFiltersDto filters = mockOrgUnit(List.of("AAA"));

PaginationParameters pagination = new PaginationParameters(0, 10);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -153,7 +155,7 @@ void openingSearch_orgUnit_shouldSucceed() {

PaginatedResult<OpeningSearchResponseDto> result =
openingService.openingSearch(new OpeningSearchFiltersDto(
"TWO",
List.of("TWO"),
null,
null,
null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import "@testing-library/jest-dom";
import AdvancedSearchDropdown from "../../../../components/SilvicultureSearch/Openings/AdvancedSearchDropdown";
import { useOpeningFiltersQuery } from "../../../../services/queries/search/openingQueries";
import { useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch";
import React from "react";

// Mocking the toggleShowFilters function
const toggleShowFilters = vi.fn();

// Mocking useOpeningFiltersQuery to return mock data for filters
vi.mock("../../../../services/queries/search/openingQueries", () => ({
useOpeningFiltersQuery: vi.fn(),
}));

// Mocking useOpeningsSearch to return the necessary functions and state
vi.mock("../../../../contexts/search/OpeningsSearch", () => ({
useOpeningsSearch: vi.fn(),
}));

describe("AdvancedSearchDropdown", () => {
beforeEach(() => {
// Mock data to return for the filters query
(useOpeningFiltersQuery as jest.Mock).mockReturnValue({
data: {
categories: ["FTML", "CONT"],
orgUnits: ["DCK", "DCR"],
dateTypes: ["Disturbance", "Free Growing"],
},
isLoading: false,
isError: false,
});

// Mock implementation of useOpeningsSearch context
(useOpeningsSearch as jest.Mock).mockReturnValue({
filters: {
openingFilters: [],
orgUnit: [],
category: [],
clientAcronym: "",
clientLocationCode: "",
cutBlock: "",
cuttingPermit: "",
timberMark: "",
dateType: "",
startDate: null,
endDate: null,
status: [],
},
setFilters: vi.fn(),
clearFilters: vi.fn(),
});
});

it("displays an error message if there is an error", () => {
(useOpeningFiltersQuery as jest.Mock).mockReturnValue({
isLoading: false,
isError: true,
data: null,
});

render(<AdvancedSearchDropdown toggleShowFilters={toggleShowFilters} />);
expect(
screen.getByText("There was an error while loading the advanced filters.")
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// src/__test__/components/SilvicultureSearch/Openings/OpeningsSearchBar.test.tsx

import React from "react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import OpeningsSearchBar from "../../../../components/SilvicultureSearch/Openings/OpeningsSearchBar";
import { vi } from "vitest";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { OpeningsSearchProvider, useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch";

// Mock the useOpeningsSearch context to avoid rendering errors
vi.mock("../../../../contexts/search/OpeningsSearch", () => ({
useOpeningsSearch: vi.fn().mockReturnValue({
filters: [],
clearFilters: vi.fn(),
searchTerm: "",
setSearchTerm: vi.fn(),
}),
}));

describe("OpeningsSearchBar", () => {
// Create a new QueryClient instance for each test
const queryClient = new QueryClient();

it("renders the search input with the correct placeholder", () => {
render(
<QueryClientProvider client={queryClient}>
<OpeningsSearchBar onSearchClick={() => {}} />
</QueryClientProvider>
);

// Check if the search input field is present with the correct placeholder text
const searchInput = screen.getByPlaceholderText(
"Search by opening ID, opening number, timber mark or file ID"
);
expect(searchInput).toBeInTheDocument();
});

it("should call the onSearchClick function when the search button is clicked", () => {
// Create a mock function to pass as a prop
const onSearchClick = vi.fn();

render(
<QueryClientProvider client={queryClient}>
<OpeningsSearchBar onSearchClick={onSearchClick} />
</QueryClientProvider>
);

// Click the search button
const searchButton = screen.getAllByRole("button", { name: "Search" })[1];
searchButton.click();

// Check if the onSearchClick function was called
expect(onSearchClick).toHaveBeenCalled();
});

it("should show AdvancedSearchDropdown if isOpen is true", () => {
// Create a mock function to pass as a prop
const onSearchClick = vi.fn();
const isOpen = false;
// Mock the useState calls
vi.spyOn(React, 'useState')
.mockImplementationOnce(() => [true, vi.fn()]) // Mocking isOpen state as false
.mockImplementationOnce(() => [false, vi.fn()]) // Mocking showFilters state as false
.mockImplementationOnce(() => ["", vi.fn()]) // Mocking searchInput state
.mockImplementationOnce(() => [0, vi.fn()]) // Mocking filtersCount state
.mockImplementationOnce(() => [null, vi.fn()]); // Mocking filtersList state
render(
<QueryClientProvider client={queryClient}>
<OpeningsSearchBar onSearchClick={onSearchClick} />
</QueryClientProvider>
);

// Check if an element with the class 'd-none' exists within the structure
const dNoneElement = screen.getAllByText("", {selector: ".d-block"})[0];
expect(dNoneElement).toBeInTheDocument();
});

it("should not show AdvancedSearchDropdown if isOpen is false", () => {
// Create a mock function to pass as a prop
const onSearchClick = vi.fn();
const isOpen = false;
// Mock the useState calls
vi.spyOn(React, 'useState')
.mockImplementationOnce(() => [false, vi.fn()]) // Mocking isOpen state as false
.mockImplementationOnce(() => [false, vi.fn()]) // Mocking showFilters state as false
.mockImplementationOnce(() => ["", vi.fn()]) // Mocking searchInput state
.mockImplementationOnce(() => [0, vi.fn()]) // Mocking filtersCount state
.mockImplementationOnce(() => [null, vi.fn()]); // Mocking filtersList state
render(
<QueryClientProvider client={queryClient}>
<OpeningsSearchBar onSearchClick={onSearchClick} />
</QueryClientProvider>
);

// Check if an element with the class 'd-none' exists within the structure
const dNoneElement = screen.getAllByText("", {selector: ".d-none"})[0];
expect(dNoneElement).toBeInTheDocument();
});

it("should show correct filter count, when count is greater than 0", () => {
// Create a mock function to pass as a prop
const onSearchClick = vi.fn();
// Mock the useState calls
vi.spyOn(React, 'useState')
.mockImplementationOnce(() => [false, vi.fn()]) // Mocking isOpen state as false
.mockImplementationOnce(() => [false, vi.fn()]) // Mocking showFilters state as false
.mockImplementationOnce(() => ["", vi.fn()]) // Mocking searchInput state
.mockImplementationOnce(() => [2, vi.fn()]) // Mocking filtersCount state
.mockImplementationOnce(() => [null, vi.fn()]); // Mocking filtersList state

render(
<QueryClientProvider client={queryClient}>
<OpeningsSearchBar onSearchClick={onSearchClick} />
</QueryClientProvider>
);

console.log(screen.debug());

// Check if an element with the class 'd-none' exists within the structure
const dNoneElement = screen.getByText('+2');
expect(dNoneElement).toBeInTheDocument();
});
});
Loading
Loading