Skip to content

Commit 4f16694

Browse files
authored
2117 add mvp full text search listings; "and" feature (#2856)
1 parent df05457 commit 4f16694

4 files changed

Lines changed: 81 additions & 8 deletions

File tree

client/src/components/FoodSeeker/SearchResults/ResultsFilters/FilterPanel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,10 @@ const FilterPanel: FC<FilterPanelProps> = ({ mealPantry, filterCount }) => {
172172
<Box sx={{ padding: "0 1rem 1rem 1rem", overflowY: "auto" }}>
173173
<Box sx={{ paddingBottom: "1rem" }}>
174174
<Typography variant="h4" sx={yPadding}>
175-
Organization Name
175+
Search
176176
</Typography>
177177
<OutlinedInput
178-
placeholder="i.e. church of"
178+
placeholder="i.e. kosher, senior, First Baptist, 90015"
179179
value={orgNameFilter}
180180
onChange={(e) =>
181181
dispatch({
@@ -186,7 +186,7 @@ const FilterPanel: FC<FilterPanelProps> = ({ mealPantry, filterCount }) => {
186186
endAdornment={
187187
<InputAdornment position="end">
188188
<IconButton
189-
aria-label="Clear organization name filter"
189+
aria-label="Clear search filter"
190190
edge="end"
191191
onClick={() =>
192192
dispatch({

client/src/hooks/useOrganizationBests.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@ interface SelectAllParams {
3737
categoryIds: number[];
3838
}
3939

40+
// Public-facing text fields searched by the "orgNameFilter" free-text search
41+
// (label reads "Search" in the UI). Deliberately excludes internal/admin-only
42+
// fields such as `adminNotes` -- only fields a food seeker can already see on
43+
// the listing/detail page belong here.
44+
const SEARCHABLE_FIELDS = [
45+
"name",
46+
"address1",
47+
"address2",
48+
"city",
49+
"zip",
50+
"phone",
51+
"email",
52+
"requirements",
53+
"notes",
54+
"services",
55+
"items",
56+
] as const;
57+
4058
interface UseOrganizationBestsState {
4159
data: SearchStakeholder[] | null;
4260
loading: boolean;
@@ -135,11 +153,17 @@ export default function useOrganizationBests() {
135153
});
136154
}
137155
if (filters.orgNameFilter) {
156+
const searchWords = filters
157+
.orgNameFilter!.toLowerCase()
158+
.split(" ")
159+
.filter(Boolean);
138160
filteredStakeholders = filteredStakeholders.filter((stakeholder) => {
139-
return filters.orgNameFilter!
140-
.toLowerCase()
141-
.split(" ")
142-
.every((word) => stakeholder.name.toLowerCase().includes(word));
161+
const searchableText = SEARCHABLE_FIELDS.map(
162+
(field) => stakeholder[field] || ""
163+
)
164+
.join(" ")
165+
.toLowerCase();
166+
return searchWords.every((word) => searchableText.includes(word));
143167
});
144168
}
145169
if (filters.foodTypeFilter) {

client/tests/helpers/mocks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ function makeStakeholdersResponse() {
377377
createdDate: "2020-03-30T01:39:28",
378378
modifiedDate: null,
379379
approvedDate: null,
380-
requirements: "",
380+
requirements: "Kosher options available for seniors",
381381
inactive: false,
382382
parentOrganization: "",
383383
physicalAccess: "",

client/tests/organizations.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,53 @@ test.describe("Organizations", () => {
108108
await expect(page.getByText("Stakeholder 2")).toBeVisible();
109109
await expect(page.getByText("Stakeholder 3")).toBeHidden();
110110
});
111+
112+
test("searching by address should show only the matching stakeholder", async ({
113+
page,
114+
}) => {
115+
await mockRequests(page);
116+
await page.goto("/organizations");
117+
await page.getByRole("button", { name: "More Filters" }).click();
118+
await page
119+
.getByPlaceholder("i.e. kosher, senior, First Baptist, 90015")
120+
.fill("222 Address");
121+
await expect(page.getByText("Stakeholder 1")).toBeHidden();
122+
await expect(page.getByText("Stakeholder 2")).toBeVisible();
123+
await expect(page.getByText("Stakeholder 3")).toBeHidden();
124+
});
125+
126+
test("searching by a multi-word term found in a non-name field (requirements) should show only the matching stakeholder", async ({
127+
page,
128+
}) => {
129+
await mockRequests(page);
130+
await page.goto("/organizations");
131+
await page.getByRole("button", { name: "More Filters" }).click();
132+
// "kosher" and "seniors" only both appear in Stakeholder 2's requirements
133+
// text -- this exercises the "AND" multi-word, non-name-field search.
134+
await page
135+
.getByPlaceholder("i.e. kosher, senior, First Baptist, 90015")
136+
.fill("kosher seniors");
137+
await expect(page.getByText("Stakeholder 1")).toBeHidden();
138+
await expect(page.getByText("Stakeholder 2")).toBeVisible();
139+
await expect(page.getByText("Stakeholder 3")).toBeHidden();
140+
});
141+
142+
test("searching by words matched across two different fields (name + address) on the same stakeholder should AND across fields", async ({
143+
page,
144+
}) => {
145+
await mockRequests(page);
146+
await page.goto("/organizations");
147+
await page.getByRole("button", { name: "More Filters" }).click();
148+
// "stakeholder" matches the `name` field of all three mock stakeholders,
149+
// but "222" only appears in Stakeholder 2's `address1` field. Only a
150+
// stakeholder satisfying both words -- one via name, one via a different
151+
// field -- should remain, proving the fields are searched as one
152+
// combined AND-across-fields blob rather than independently.
153+
await page
154+
.getByPlaceholder("i.e. kosher, senior, First Baptist, 90015")
155+
.fill("stakeholder 222");
156+
await expect(page.getByText("Stakeholder 1")).toBeHidden();
157+
await expect(page.getByText("Stakeholder 2")).toBeVisible();
158+
await expect(page.getByText("Stakeholder 3")).toBeHidden();
159+
});
111160
});

0 commit comments

Comments
 (0)