Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
101468e
feat: set up a new page for the Preferences AG table
jaredcwhite Dec 2, 2025
141c318
feat: use separate set of modals for new preferences page
jaredcwhite Dec 3, 2025
25a6472
fix: temp rename to pull main in
jaredcwhite Dec 12, 2025
43fb98c
Merge branch 'main' into 5597/new-preferences-table
jaredcwhite Dec 12, 2025
5eac551
feat: set up proper feature flag detection for preferences table
jaredcwhite Dec 15, 2025
0b81d89
feat: start relocating fields for new MSQ schema
jaredcwhite Dec 16, 2025
1787568
Merge branch 'main' into 5597/new-preferences-table
jaredcwhite Dec 16, 2025
7b2de0a
feat: viewing preferences now working correctly
jaredcwhite Dec 22, 2025
c58d1e2
feat: update seed data for newer preference schema
jaredcwhite Jan 7, 2026
375d8e1
feat: tighten up UI in various drawers/modals
jaredcwhite Jan 7, 2026
ef30979
feat: keep show/hide on listing as its own option, improve view drawer
jaredcwhite Jan 8, 2026
b5a29d3
test: update backend tests for the MSQv2 changes
jaredcwhite Jan 8, 2026
dece4d5
test: address linting issues
jaredcwhite Jan 8, 2026
79d8048
fix: use explicit call to save handler
jaredcwhite Jan 8, 2026
f10822f
feat: improve view UI
jaredcwhite Jan 12, 2026
ae1e700
Merge branch 'main' into 5597/new-preferences-table
jaredcwhite Jan 12, 2026
3489879
feat: use conditional CLI flag for MSQ V2 seed data
jaredcwhite Jan 12, 2026
b68ea77
feat: additional improvements to seed data, fixing bugs in adding prefs
jaredcwhite Jan 13, 2026
fae326c
test: add new unit tests for the V2 Preferences page
jaredcwhite Jan 21, 2026
772e994
test: cleanup, DRY
jaredcwhite Jan 21, 2026
0050c1e
fix: lint
jaredcwhite Jan 21, 2026
076cbd6
test: add test for viewing a preference
jaredcwhite Jan 22, 2026
47d57f6
fix: address additional feedback QA
jaredcwhite Jan 22, 2026
323d659
fix: cleanup multiple watch statements
jaredcwhite Jan 23, 2026
4f54324
chore: update all useMutate usage to import from shared-helpers
jaredcwhite Jan 23, 2026
951eeae
Merge branch 'main' into 5597/new-preferences-table
jaredcwhite Jan 23, 2026
8943b10
fix: MSG V2 seeding issues
jaredcwhite Jan 23, 2026
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 api/prisma/seed-helpers/multiselect-question-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const multiselectQuestionFactory = (
optionalParams?: {
optOut?: boolean;
multiselectQuestion?: Partial<Prisma.MultiselectQuestionsCreateInput>;
status?: MultiselectQuestionsStatusEnum;
},
version2 = false,
): Prisma.MultiselectQuestionsCreateInput => {
Expand Down Expand Up @@ -56,7 +57,7 @@ export const multiselectQuestionFactory = (
},
name: name,
subText: `sub text for ${name}`,
status: MultiselectQuestionsStatusEnum.draft,
status: optionalParams?.status || MultiselectQuestionsStatusEnum.draft,
// TODO: Can be removed after MSQ refactor
text: name,
};
Expand Down
81 changes: 74 additions & 7 deletions api/prisma/seed-staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Prisma,
PrismaClient,
UserRoleEnum,
MultiselectQuestionsStatusEnum,
} from '@prisma/client';
import { jurisdictionFactory } from './seed-helpers/jurisdiction-factory';
import { listingFactory } from './seed-helpers/listing-factory';
Expand Down Expand Up @@ -41,14 +42,17 @@ import dayjs from 'dayjs';
export const stagingSeed = async (
prismaClient: PrismaClient,
jurisdictionName: string,
msqV2: boolean,
) => {
// Seed feature flags
await createAllFeatureFlags(prismaClient);
const optionalMainFlags = msqV2 ? [FeatureFlagEnum.enableV2MSQ] : [];
// create main jurisdiction with as many feature flags turned on as possible
const mainJurisdiction = await prismaClient.jurisdictions.create({
data: jurisdictionFactory(jurisdictionName, {
listingApprovalPermissions: [UserRoleEnum.admin],
featureFlags: [
...optionalMainFlags,
FeatureFlagEnum.enableAccessibilityFeatures,
FeatureFlagEnum.enableCompanyWebsite,
FeatureFlagEnum.enableGeocodingPreferences,
Expand Down Expand Up @@ -396,25 +400,85 @@ export const stagingSeed = async (
simplifiedDCMap,
),
});
const cityEmployeeQuestion = await prismaClient.multiselectQuestions.create({
data: multiselectQuestionFactory(mainJurisdiction.id, {
let cityEmployeeMsqData: Prisma.MultiselectQuestionsCreateInput;
if (msqV2) {
cityEmployeeMsqData = multiselectQuestionFactory(
mainJurisdiction.id,
{
multiselectQuestion: {
status: MultiselectQuestionsStatusEnum.active,
name: 'City Employees',
description: 'Employees of the local city.',
applicationSection:
MultiselectQuestionsApplicationSectionEnum.preferences,
options: [
{
name: 'At least one member of my household is a city employee',
collectAddress: false,
ordinal: 0,
},
],
},
},
true,
);
} else {
cityEmployeeMsqData = multiselectQuestionFactory(mainJurisdiction.id, {
multiselectQuestion: {
text: 'City Employees',
description: 'Employees of the local city.',
applicationSection:
MultiselectQuestionsApplicationSectionEnum.preferences,
options: [
{
text: 'At least one member of my household is a city employee',
name: 'At least one member of my household is a city employee',
collectAddress: false,
ordinal: 0,
},
],
},
}),
});
}
const cityEmployeeQuestion = await prismaClient.multiselectQuestions.create({
data: cityEmployeeMsqData,
});
const workInCityQuestion = await prismaClient.multiselectQuestions.create({
data: multiselectQuestionFactory(mainJurisdiction.id, {
let workInCityMsqData: Prisma.MultiselectQuestionsCreateInput;
if (msqV2) {
workInCityMsqData = multiselectQuestionFactory(
mainJurisdiction.id,
{
optOut: true,
status: MultiselectQuestionsStatusEnum.active,
multiselectQuestion: {
name: 'Work in the city',
description: 'At least one member of my household works in the city',
applicationSection:
MultiselectQuestionsApplicationSectionEnum.preferences,
options: [
{
name: 'At least one member of my household works in the city',
ordinal: 0,
collectAddress: true,
collectName: true,
collectRelationship: true,
mapLayerId: mapLayer.id,
validationMethod: ValidationMethod.map,
},
{
name: 'All members of the household work in the city',
ordinal: 1,
collectAddress: true,
ValidationMethod: ValidationMethod.none,
collectName: false,
collectRelationship: false,
},
],
},
},
true,
);
} else {
workInCityMsqData = multiselectQuestionFactory(mainJurisdiction.id, {
optOut: true,
multiselectQuestion: {
text: 'Work in the city',
Expand All @@ -441,7 +505,10 @@ export const stagingSeed = async (
},
],
},
}),
});
}
const workInCityQuestion = await prismaClient.multiselectQuestions.create({
data: workInCityMsqData,
});
const veteranProgramQuestion = await prismaClient.multiselectQuestions.create(
{
Expand Down
5 changes: 3 additions & 2 deletions api/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import { reservedCommunityTypeFactoryAll } from './seed-helpers/reserved-communi
const options: { [name: string]: { type: 'string' | 'boolean' } } = {
environment: { type: 'string' },
jurisdictionName: { type: 'string' },
msqV2: { type: 'boolean' },
};

const prisma = new PrismaClient();
async function main() {
const {
values: { environment, jurisdictionName },
values: { environment, jurisdictionName, msqV2 },
} = parseArgs({ options });
switch (environment) {
case 'production':
Expand All @@ -30,7 +31,7 @@ async function main() {
case 'staging':
// Staging setup should have realistic looking data with a preset list of listings
// along with all of the required tables (ami, users, etc)
stagingSeed(prisma, jurisdictionName as string);
stagingSeed(prisma, jurisdictionName as string, msqV2 as boolean);
break;
case 'development':
default:
Expand Down
77 changes: 39 additions & 38 deletions api/src/services/multiselect-question.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,49 +276,50 @@ export class MultiselectQuestionService {
);
}

const rawMultiselectQuestion =
await this.prisma.multiselectQuestions.create({
data: {
...createData,
jurisdiction: {
connect: { id: rawJurisdiction.id },
},
links: links
? (links as unknown as Prisma.InputJsonArray)
: undefined,
const finalCreateData = {
data: {
...createData,
jurisdiction: {
connect: { id: rawJurisdiction.id },
},
links: links ? (links as unknown as Prisma.InputJsonArray) : undefined,

// TODO: Can be removed after MSQ refactor
options: options
// TODO: Can be removed after MSQ refactor
options:
!enableV2MSQ && options
? (options as unknown as Prisma.InputJsonArray)
: undefined,

// TODO: Use of the feature flag is temporary until after MSQ refactor
isExclusive: enableV2MSQ ? isExclusive : false,
name: enableV2MSQ ? name : createData.text,
status: enableV2MSQ ? status : MultiselectQuestionsStatusEnum.draft,
// TODO: Use of the feature flag is temporary until after MSQ refactor
isExclusive: enableV2MSQ ? isExclusive : false,
name: enableV2MSQ ? name : createData.text,
status: enableV2MSQ ? status : MultiselectQuestionsStatusEnum.draft,

multiselectOptions: enableV2MSQ
? {
createMany: {
data: multiselectOptions?.map((option) => {
// TODO: Can be removed after MSQ refactor
delete option['collectAddress'];
delete option['collectName'];
delete option['collectRelationship'];
delete option['exclusive'];
delete option['text'];
return {
...option,
links: option.links as unknown as Prisma.InputJsonArray,
name: option.name,
};
}),
},
}
: undefined,
},
include: includeViews.fundamentals,
};

multiselectOptions: enableV2MSQ
? {
createMany: {
data: multiselectOptions?.map((option) => {
// TODO: Can be removed after MSQ refactor
delete option['collectAddress'];
delete option['collectName'];
delete option['collectRelationship'];
delete option['exclusive'];
delete option['text'];
return {
...option,
links: option.links as unknown as Prisma.InputJsonArray,
name: option.name,
};
}),
},
}
: undefined,
},
include: includeViews.fundamentals,
});
const rawMultiselectQuestion =
await this.prisma.multiselectQuestions.create(finalCreateData);

// TODO: Can be removed after MSQ refactor
rawMultiselectQuestion['jurisdictions'] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,7 @@ describe('Testing multiselect question service', () => {
expect(prisma.multiselectQuestions.create).toHaveBeenCalledWith({
data: {
...params,
options: undefined,
jurisdiction: {
connect: { id: mockedMultiselectQuestion.jurisdiction.id },
},
Expand Down
1 change: 1 addition & 0 deletions shared-helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export * from "./src/utilities/regex"
export * from "./src/utilities/stringFormatting"
export * from "./src/utilities/token"
export * from "./src/utilities/unitTypes"
export * from "./src/utilities/useMutate"
export * from "./src/utilities/useIntersect"
export * from "./src/views/components/BloomCard"
export * from "./src/views/components/ClickableCard"
Expand Down
3 changes: 3 additions & 0 deletions shared-helpers/src/locales/general.json
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@
"errors.alert.badRequest": "Looks like something went wrong. Please try again. \n\nContact your housing department if you're still experiencing issues.",
"errors.alert.listingsApprovalEmailError": "This listing was updated, but the associated emails failed to send.",
"errors.alert.timeoutPleaseTryAgain": "Oops! Looks like something went wrong. Please try again.",
"errors.alphaNumericError": "Please use letters & numbers only",
"errors.backToHome": "Back to home",
"errors.cityError": "Please enter a city",
"errors.dateError": "Please enter a valid date",
Expand Down Expand Up @@ -1152,6 +1153,7 @@
"t.pleaseSelectOne": "Please select one",
"t.pleaseSelectYesNo": "Please select yes or no.",
"t.pm": "PM",
"t.preference": "Preference",
"t.preferences": "Preferences",
"t.preferNotToSay": "Prefer not to say",
"t.previous": "Previous",
Expand Down Expand Up @@ -1190,6 +1192,7 @@
"t.units": "units",
"t.unitType": "Unit type",
"t.uploadFile": "Upload file",
"t.view": "View",
"t.viewMap": "View map",
"t.viewOnMap": "View on map",
"t.website": "Website",
Expand Down
52 changes: 52 additions & 0 deletions shared-helpers/src/utilities/useMutate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useState } from "react"

export type UseMutateOptions = {
onSuccess?: () => void
onError?: (err: any) => void

Check warning on line 5 in shared-helpers/src/utilities/useMutate.ts

View workflow job for this annotation

GitHub Actions / Run linters

Unexpected any. Specify a different type
}

export const useMutate = <UseMutateResponse>() => {
const [data, setData] = useState<UseMutateResponse | undefined>(undefined)
const [isSuccess, setSuccess] = useState(false)
const [isLoading, setLoading] = useState(false)
const [isError, setError] = useState<unknown>(null)

const mutate = async (
mutateFn: (args?: unknown) => Promise<UseMutateResponse>,
options?: UseMutateOptions
) => {
let response: UseMutateResponse | undefined = undefined

try {
setLoading(true)
response = await mutateFn()
setData(response)
setSuccess(true)
options?.onSuccess?.()
setLoading(false)
} catch (err) {
setSuccess(false)
setLoading(false)
setError(err)
options?.onError?.(err)
}

return response
}

const reset = () => {
setData(undefined)
setSuccess(false)
setLoading(false)
setError(null)
}

return {
mutate,
reset,
data,
isSuccess,
isLoading,
isError,
}
}
1 change: 0 additions & 1 deletion sites/partners/__tests__/pages/application/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { application, listing, user } from "@bloom-housing/shared-helpers/__test
import ApplicationsList from "../../../src/pages/application/[id]"
import {
AlternateContactRelationship,
LanguagesEnum,
UnitTypeEnum,
YesNoEnum,
} from "@bloom-housing/shared-helpers/src/types/backend-swagger"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable import/no-named-as-default */
import React from "react"
import { setupServer } from "msw/lib/node"
import { fireEvent, mockNextRouter, queryByText, render, screen, within } from "../../../testUtils"
import { fireEvent, mockNextRouter, render, screen, within } from "../../../testUtils"
import { ListingContext } from "../../../../src/components/listings/ListingContext"
import { jurisdiction, listing, user } from "@bloom-housing/shared-helpers/__tests__/testHelpers"
import DetailListingData from "../../../../src/components/listings/PaperListingDetails/sections/DetailListingData"
Expand Down
2 changes: 2 additions & 0 deletions sites/partners/page_content/locale_overrides/general.json
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@
"settings.preferenceOptOutLabel": "Opt out label",
"settings.preferenceOptionDescription": "Preference option description",
"settings.preferenceOptionEdit": "Edit option",
"settings.preferenceOptionView": "View option",
"settings.preferenceShowOnListing": "Show preference on listing?",
"settings.preferenceValidatingAddress": "Do you need help validating the address?",
"settings.preferenceValidatingAddress.checkWithinRadius": "Yes, check if within geographic radius of property",
Expand All @@ -610,6 +611,7 @@
"settings.preferenceValidatingAddress.howManyMiles": "How many miles is the qualifying geographic radius?",
"settings.preferenceValidatingAddress.selectMapLayer": "Select a map layer",
"settings.preferenceValidatingAddress.selectMapLayerDescription": "Select your map layer based on your district. If you don't see your map contact us",
"settings.preferenceView": "View preference",
"settings.preferenceDeleteConfirmation": "Deleting a preference cannot be undone.",
"settings.preferenceChangesRequired": "Changes required before deleting",
"settings.preferenceChangesRequiredEdit": "Changes required before editing",
Expand Down
Loading
Loading