diff --git a/api/src/dtos/listings/listings-filter-params.dto.ts b/api/src/dtos/listings/listings-filter-params.dto.ts index d2c8771efbf..3b075eba33d 100644 --- a/api/src/dtos/listings/listings-filter-params.dto.ts +++ b/api/src/dtos/listings/listings-filter-params.dto.ts @@ -38,17 +38,17 @@ export class ListingFilterParams extends BaseFilter { @Expose() @ApiPropertyOptional({ - example: '3', + example: ['3'], }) - @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - [ListingFilterKeys.bedrooms]?: number; + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.bedrooms]?: string[]; @Expose() @ApiPropertyOptional({ - example: '3', + example: ['3'], }) - @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - [ListingFilterKeys.bathrooms]?: number; + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.bathrooms]?: string[]; @Expose() @ApiPropertyOptional({ diff --git a/api/src/services/listing.service.ts b/api/src/services/listing.service.ts index 8b30aa0a484..64f6142241e 100644 --- a/api/src/services/listing.service.ts +++ b/api/src/services/listing.service.ts @@ -48,6 +48,7 @@ import { startCronJob } from '../utilities/cron-job-starter'; import { PermissionService } from './permission.service'; import { permissionActions } from '../enums/permissions/permission-actions-enum'; import Unit from '../dtos/units/unit.dto'; +import { buildInclusiveWhereQuery } from '../utilities/unit-filter-utilities'; export type getListingsArgs = { skip: number; @@ -259,16 +260,18 @@ export class ListingService implements OnModuleInit { } if (filter[ListingFilterKeys.bedrooms]) { whereClauseArray.push( - `(combined_units->>'numBedrooms') = '${Math.floor( + buildInclusiveWhereQuery( + ListingFilterKeys.bedrooms, filter[ListingFilterKeys.bedrooms], - )}'`, + ), ); } if (filter[ListingFilterKeys.bathrooms]) { whereClauseArray.push( - `(combined_units->>'numBathrooms') = '${Math.floor( + buildInclusiveWhereQuery( + ListingFilterKeys.bathrooms, filter[ListingFilterKeys.bathrooms], - )}'`, + ), ); } if (filter[ListingFilterKeys.monthlyRent]) { diff --git a/api/src/utilities/unit-filter-utilities.ts b/api/src/utilities/unit-filter-utilities.ts new file mode 100644 index 00000000000..80afddc2d1d --- /dev/null +++ b/api/src/utilities/unit-filter-utilities.ts @@ -0,0 +1,53 @@ +import { BadRequestException } from '@nestjs/common'; + +enum ColumnName { + bedrooms = 'numBedrooms', + bathrooms = 'numBathrooms', +} +/* Takes a ColumnName and selected values to build a query that includes the values in the ColumnName + Returns a query String +*/ + +export const buildInclusiveWhereQuery = ( + key: keyof typeof ColumnName, + values: string[], +): string => { + const columnName = ColumnName[key]; + + const inclusiveWhereArray = []; + values.forEach((value) => { + switch (value) { + case '0': + inclusiveWhereArray.push( + `((combined_units->>'${columnName}')::INTEGER =0)`, + ); + break; + case '1': + inclusiveWhereArray.push( + `((combined_units->>'${columnName}')::INTEGER =1)`, + ); + break; + case '2': + inclusiveWhereArray.push( + `((combined_units->>'${columnName}')::INTEGER =2)`, + ); + break; + case '3': + inclusiveWhereArray.push( + `((combined_units->>'${columnName}')::INTEGER =3)`, + ); + break; + case '4': + inclusiveWhereArray.push( + `((combined_units->>'${columnName}')::INTEGER >=4)`, + ); + break; + default: + throw new BadRequestException( + `Invalid input for ${key} filter: "${value}"`, + ); + } + }); + + return `(${inclusiveWhereArray.join(' OR ')})`; +}; diff --git a/shared-helpers/package.json b/shared-helpers/package.json index b37dedfe65f..8ff9630a05d 100644 --- a/shared-helpers/package.json +++ b/shared-helpers/package.json @@ -19,7 +19,7 @@ "dependencies": { "@bloom-housing/doorway-ui-components": "^1.0.0", "@bloom-housing/ui-components": "12.1.6", - "@bloom-housing/ui-seeds": "1.14.0", + "@bloom-housing/ui-seeds": "1.16.1", "@heroicons/react": "^2.1.1", "axios-cookiejar-support": "4.0.6", "tough-cookie": "4.1.3" diff --git a/shared-helpers/src/auth/Timeout.tsx b/shared-helpers/src/auth/Timeout.tsx index c0da89d4302..ca5ccd9ffbf 100644 --- a/shared-helpers/src/auth/Timeout.tsx +++ b/shared-helpers/src/auth/Timeout.tsx @@ -5,7 +5,7 @@ import { Button } from "@bloom-housing/ui-seeds" import { Modal, t } from "@bloom-housing/ui-components" import { NavigationContext } from "@bloom-housing/doorway-ui-components" import { MessageContext } from "../utilities/MessageContext" - + const PROMPT_TIMEOUT = 60000 const events = ["mousemove", "keypress", "scroll"] diff --git a/shared-helpers/src/types/backend-swagger.ts b/shared-helpers/src/types/backend-swagger.ts index 9605f6f4c66..43821c18592 100644 --- a/shared-helpers/src/types/backend-swagger.ts +++ b/shared-helpers/src/types/backend-swagger.ts @@ -2207,10 +2207,10 @@ export interface ListingFilterParams { neighborhood?: string /** */ - bedrooms?: number + bedrooms?: string[] /** */ - bathrooms?: number + bathrooms?: string[] /** */ zipcode?: string diff --git a/sites/partners/package.json b/sites/partners/package.json index 8668b211323..b5e43739dc6 100644 --- a/sites/partners/package.json +++ b/sites/partners/package.json @@ -32,7 +32,7 @@ "@bloom-housing/doorway-ui-components": "^1.0.0", "@bloom-housing/shared-helpers": "^7.7.1", "@bloom-housing/ui-components": "12.1.6", - "@bloom-housing/ui-seeds": "1.14.0", + "@bloom-housing/ui-seeds": "1.16.1", "@heroicons/react": "^2.1.1", "@mapbox/mapbox-sdk": "^0.13.0", "ag-grid-community": "^26.0.0", diff --git a/sites/public/package.json b/sites/public/package.json index 1562ea882c0..cecb9e9a9cb 100644 --- a/sites/public/package.json +++ b/sites/public/package.json @@ -33,7 +33,7 @@ "@bloom-housing/doorway-ui-components": "^1.0.0", "@bloom-housing/shared-helpers": "^7.7.1", "@bloom-housing/ui-components": "12.1.6", - "@bloom-housing/ui-seeds": "1.14.0", + "@bloom-housing/ui-seeds": "1.16.1", "@heroicons/react": "^2.1.1", "@react-google-maps/api": "^2.18.1", "@sentry/nextjs": "^7.61.0", diff --git a/sites/public/src/components/listings/search/LandingSearch.module.scss b/sites/public/src/components/listings/search/LandingSearch.module.scss index 2e156e5257d..9ffb8d7cb98 100644 --- a/sites/public/src/components/listings/search/LandingSearch.module.scss +++ b/sites/public/src/components/listings/search/LandingSearch.module.scss @@ -12,6 +12,7 @@ @apply font-semibold; @apply text-sm; min-width: auto; + padding-bottom: var(--seeds-s2); @media (min-width: $screen-md) { @apply text-xl; diff --git a/sites/public/src/components/listings/search/LandingSearch.tsx b/sites/public/src/components/listings/search/LandingSearch.tsx index 3ed8807bee5..6fc27400a61 100644 --- a/sites/public/src/components/listings/search/LandingSearch.tsx +++ b/sites/public/src/components/listings/search/LandingSearch.tsx @@ -2,12 +2,10 @@ import React, { useState, useEffect } from "react" import { ListingSearchParams, buildSearchString } from "../../../lib/listings/search" import { Modal, - ButtonGroup, FieldGroup, FieldSingle, Card, Button, - ButtonGroupSpacing, Field, AppearanceSizeType, } from "@bloom-housing/doorway-ui-components" @@ -15,7 +13,8 @@ import { useForm } from "react-hook-form" import { LinkButton, t } from "@bloom-housing/ui-components" import styles from "./LandingSearch.module.scss" import { FormOption } from "./ListingsSearchModal" -import { numericSearchFieldGenerator } from "./helpers" +import { getCheckboxValues, getFormValues, numericSearchFieldGenerator } from "./helpers" +import { CheckboxGroup } from "@bloom-housing/ui-seeds" type LandingSearchProps = { bedrooms: FormOption[] @@ -24,7 +23,7 @@ type LandingSearchProps = { // TODO: Refactor LandingSearch to utilize react-hook-form. It is currently using a custom form object and custom valueSetters // which is mostly functional but fails to leverage UI-C's formatting, accessibility and any other future improvements to the // package. To expedite development and avoid excessive workarounds (ie. line 121), a full form refactor should be completed. -export function LandingSearch(props: LandingSearchProps) { +export const LandingSearch = (props: LandingSearchProps) => { // We hold a map of county label to county FormOption const countyLabelMap = {} const countyLabels = [] @@ -34,8 +33,8 @@ export function LandingSearch(props: LandingSearchProps) { }) const nullState: ListingSearchParams = { - bedrooms: null, - bathrooms: null, + bedrooms: [], + bathrooms: [], minRent: "", monthlyRent: "", counties: countyLabels, @@ -58,18 +57,13 @@ export function LandingSearch(props: LandingSearchProps) { // console.log(`${name} has been set to ${value}`) // uncomment to debug } - const updateValueMulti = (name: string, labels: string[]) => { + const updateValueMulti = (name: string, values: string[]) => { const newValues = { ...formValues } as ListingSearchParams - newValues[name] = labels + newValues[name] = values setFormValues(newValues) - // console.log(`${name} has been set to ${value}`) // uncomment to debug } const translatedBedroomOptions: FormOption[] = [ - { - label: t("listings.unitTypes.any"), - value: null, - }, { label: t("listings.unitTypes.studio"), value: "0", @@ -77,7 +71,7 @@ export function LandingSearch(props: LandingSearchProps) { ] const bedroomOptions: FormOption[] = [ ...translatedBedroomOptions, - ...numericSearchFieldGenerator(1, 3), + ...numericSearchFieldGenerator(1, 4), ] const mkCountyFields = (counties: FormOption[]): FieldSingle[] => { @@ -132,13 +126,14 @@ export function LandingSearch(props: LandingSearchProps) {
{t("t.bedrooms")}
- updateValueMulti("bedrooms", getFormValues(values))} + values={getCheckboxValues(formValues.bedrooms)} + size="md" + variant="primary-outlined" + checkedVariant="primary" />
diff --git a/sites/public/src/components/listings/search/ListingsSearchCombined.tsx b/sites/public/src/components/listings/search/ListingsSearchCombined.tsx index 6b64720e642..8387e61489e 100644 --- a/sites/public/src/components/listings/search/ListingsSearchCombined.tsx +++ b/sites/public/src/components/listings/search/ListingsSearchCombined.tsx @@ -31,8 +31,8 @@ function ListingsSearchCombined(props: ListingsSearchCombinedProps) { // Store the current search params for pagination const searchParams = useRef({ - bedrooms: null, - bathrooms: null, + bedrooms: [], + bathrooms: [], monthlyRent: null, counties: [], } as ListingSearchParams) diff --git a/sites/public/src/components/listings/search/ListingsSearchModal.module.scss b/sites/public/src/components/listings/search/ListingsSearchModal.module.scss new file mode 100644 index 00000000000..3af5724cdc2 --- /dev/null +++ b/sites/public/src/components/listings/search/ListingsSearchModal.module.scss @@ -0,0 +1,5 @@ +.checkbox-group { + --inner-button-gap: var(--seeds-s3); + padding-top: var(--seeds-s4); + padding-bottom: var(--seeds-s4); +} \ No newline at end of file diff --git a/sites/public/src/components/listings/search/ListingsSearchModal.tsx b/sites/public/src/components/listings/search/ListingsSearchModal.tsx index 9b408dd207e..aff8bd835a6 100644 --- a/sites/public/src/components/listings/search/ListingsSearchModal.tsx +++ b/sites/public/src/components/listings/search/ListingsSearchModal.tsx @@ -1,17 +1,11 @@ import React, { useEffect, useState } from "react" import { ListingSearchParams, parseSearchString } from "../../../lib/listings/search" import { t } from "@bloom-housing/ui-components" -import { - Modal, - ButtonGroup, - ButtonGroupSpacing, - Button, - Field, - FieldGroup, - FieldSingle, -} from "@bloom-housing/doorway-ui-components" +import { Modal, Field, FieldGroup, FieldSingle } from "@bloom-housing/doorway-ui-components" +import { CheckboxGroup, Button } from "@bloom-housing/ui-seeds" import { useForm } from "react-hook-form" -import { numericSearchFieldGenerator } from "./helpers" +import { getCheckboxValues, getFormValues, numericSearchFieldGenerator } from "./helpers" +import styles from "./ListingsSearchModal.module.scss" const inputSectionStyle: React.CSSProperties = { margin: "0px 15px", @@ -81,8 +75,8 @@ export function ListingsSearchModal(props: ListingsSearchModalProps) { }) const nullState: ListingSearchParams = { - bedrooms: null, - bathrooms: null, + bedrooms: [], + bathrooms: [], minRent: "", monthlyRent: "", counties: countyLabels, @@ -137,39 +131,25 @@ export function ListingsSearchModal(props: ListingsSearchModalProps) { // console.log(`${name} has been set to ${value}`) // uncomment to debug } - const updateValueMulti = (name: string, labels: string[]) => { + const updateValueMulti = (name: string, values: string[]) => { const newValues = { ...formValues } as ListingSearchParams - newValues[name] = labels + newValues[name] = values setFormValues(newValues) - // console.log(`${name} has been set to ${value}`) // uncomment to debug } const translatedBedroomOptions: FormOption[] = [ - { - label: t("listings.unitTypes.any"), - value: null, - }, { label: t("listings.unitTypes.studio"), value: "0", }, ] - const translatedBathroomOptions: FormOption[] = [ - { - label: t("listings.unitTypes.any"), - value: null, - }, - ] - const bedroomOptions: FormOption[] = [ ...translatedBedroomOptions, ...numericSearchFieldGenerator(1, 4), ] - const bathroomOptions: FormOption[] = [ - ...translatedBathroomOptions, - ...numericSearchFieldGenerator(1, 4), - ] + + const bathroomOptions: FormOption[] = [...numericSearchFieldGenerator(1, 4)] const mkCountyFields = (counties: FormOption[]): FieldSingle[] => { const countyFields: FieldSingle[] = [] as FieldSingle[] @@ -245,23 +225,29 @@ export function ListingsSearchModal(props: ListingsSearchModalProps) { >
{t("t.bedrooms")}
- updateValueMulti("bedrooms", getFormValues(values))} + size="md" + variant="primary-outlined" + checkedVariant="primary" + className={styles["checkbox-group"]} />
{t("t.bathrooms")}
- updateValueMulti("bathrooms", getFormValues(values))} + size="md" + variant="primary-outlined" + checkedVariant="primary" + className={styles["checkbox-group"]} />
diff --git a/sites/public/src/components/listings/search/helpers.ts b/sites/public/src/components/listings/search/helpers.ts index a0623c872a4..b96286e0464 100644 --- a/sites/public/src/components/listings/search/helpers.ts +++ b/sites/public/src/components/listings/search/helpers.ts @@ -1,4 +1,5 @@ import { FormOption } from "@bloom-housing/doorway-ui-components" +import { CheckboxItem } from "@bloom-housing/ui-seeds/src/forms/CheckboxGroup" // ie. [{label : "1" value: "1"}, {label : "2+" value: "2"} if includeMore is true export const numericSearchFieldGenerator = ( @@ -16,3 +17,11 @@ export const numericSearchFieldGenerator = ( } return fieldValues } + +export const getCheckboxValues = (formValues: string[]) => { + return formValues.map((value) => ({ label: value, value: value })) +} + +export const getFormValues = (checkboxValues: CheckboxItem[]) => { + return checkboxValues.map((value) => value.value) +} diff --git a/sites/public/src/lib/listings/search.test.ts b/sites/public/src/lib/listings/search.test.ts index 254b6ec3da7..70d81a77059 100644 --- a/sites/public/src/lib/listings/search.test.ts +++ b/sites/public/src/lib/listings/search.test.ts @@ -35,9 +35,9 @@ describe("parse search string", () => { describe("build search string", () => { it("should build expected string", () => { const example: ListingSearchParams = { - bedrooms: "2", + bedrooms: ["2"], counties: ["county1", "county2"], - bathrooms: null, + bathrooms: [], minRent: null, monthlyRent: null, } diff --git a/sites/public/src/lib/listings/search.ts b/sites/public/src/lib/listings/search.ts index 69d281180c4..fe06286bffc 100644 --- a/sites/public/src/lib/listings/search.ts +++ b/sites/public/src/lib/listings/search.ts @@ -1,8 +1,8 @@ import { ListingQueryBuilder } from "./listing-query-builder" export type ListingSearchParams = { - bedrooms: string - bathrooms: string + bedrooms: string[] + bathrooms: string[] minRent: string monthlyRent: string counties: string[] @@ -101,13 +101,12 @@ export function generateSearchQuery(params: ListingSearchParams) { const qb = new ListingQueryBuilder() // Find listings that have units with greater than or equal number of bedrooms - if (params.bedrooms != null) { - qb.whereGreaterThanEqual("bedrooms", params.bedrooms) + if (Array.isArray(params.bedrooms) && params.bedrooms.length > 0) { + qb.whereIn("bedrooms", params.bedrooms) } - // Find listings that have units with greater than or equal number of bathrooms - if (params.bathrooms != null) { - qb.whereGreaterThanEqual("bathrooms", params.bathrooms) + if (Array.isArray(params.bathrooms) && params.bathrooms.length > 0) { + qb.whereIn("bathrooms", params.bathrooms) } if (params.minRent && params.minRent != "") { diff --git a/yarn.lock b/yarn.lock index 2fb09e750ad..9bfaedff15f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3165,10 +3165,10 @@ tailwindcss-rtl "^0.9.0" typesafe-actions "^5.1.0" -"@bloom-housing/ui-seeds@1.14.0": - version "1.14.0" - resolved "https://registry.yarnpkg.com/@bloom-housing/ui-seeds/-/ui-seeds-1.14.0.tgz#9a8543bfa3e1c037555fb75297ab1ebbe91d6708" - integrity sha512-h3xmKs+J0GUMtBqS3IvaXBoO3ApRYjoHSiTZIfhhXkrlzxqK1mg/BANGTf19RR16AuMO/qLxDAUSFoSAkLUIBg== +"@bloom-housing/ui-seeds@1.16.1": + version "1.16.1" + resolved "https://registry.yarnpkg.com/@bloom-housing/ui-seeds/-/ui-seeds-1.16.1.tgz#da293bc9c38c485df0d12f2a8c81120134bb57e4" + integrity sha512-BsNNw954cH3eeEF/qlcd6O/NjWWtgWVoKGAEO3JRMFGMmItVxBGOVg3Ye0Eyfs7bWXTxiw0ucDv/ZoMHRJbgmQ== dependencies: "@fortawesome/fontawesome-svg-core" "^6.3.0" "@fortawesome/react-fontawesome" "^0.2.0"