diff --git a/api/prisma/seed-helpers/multiselect-question-factory.ts b/api/prisma/seed-helpers/multiselect-question-factory.ts index 40dd243af4..b351a43d51 100644 --- a/api/prisma/seed-helpers/multiselect-question-factory.ts +++ b/api/prisma/seed-helpers/multiselect-question-factory.ts @@ -15,6 +15,7 @@ export const multiselectQuestionFactory = ( optionalParams?: { optOut?: boolean; multiselectQuestion?: Partial; + status?: MultiselectQuestionsStatusEnum; }, version2 = false, ): Prisma.MultiselectQuestionsCreateInput => { @@ -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, }; diff --git a/api/prisma/seed-staging.ts b/api/prisma/seed-staging.ts index b970bbcdbf..daca91dead 100644 --- a/api/prisma/seed-staging.ts +++ b/api/prisma/seed-staging.ts @@ -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'; @@ -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, @@ -396,8 +400,30 @@ 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.', @@ -405,16 +431,54 @@ export const stagingSeed = async ( 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', @@ -441,7 +505,10 @@ export const stagingSeed = async ( }, ], }, - }), + }); + } + const workInCityQuestion = await prismaClient.multiselectQuestions.create({ + data: workInCityMsqData, }); const veteranProgramQuestion = await prismaClient.multiselectQuestions.create( { diff --git a/api/prisma/seed.ts b/api/prisma/seed.ts index a7baca9fec..6158f47277 100644 --- a/api/prisma/seed.ts +++ b/api/prisma/seed.ts @@ -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': @@ -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: diff --git a/api/src/services/multiselect-question.service.ts b/api/src/services/multiselect-question.service.ts index fbc102bce7..36f9e56f34 100644 --- a/api/src/services/multiselect-question.service.ts +++ b/api/src/services/multiselect-question.service.ts @@ -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'] = [ diff --git a/api/test/unit/services/multiselect-question.service.spec.ts b/api/test/unit/services/multiselect-question.service.spec.ts index 4e27fd8939..814c6b1e57 100644 --- a/api/test/unit/services/multiselect-question.service.spec.ts +++ b/api/test/unit/services/multiselect-question.service.spec.ts @@ -703,6 +703,7 @@ describe('Testing multiselect question service', () => { expect(prisma.multiselectQuestions.create).toHaveBeenCalledWith({ data: { ...params, + options: undefined, jurisdiction: { connect: { id: mockedMultiselectQuestion.jurisdiction.id }, }, diff --git a/shared-helpers/index.ts b/shared-helpers/index.ts index 9a507db35d..ada0ff5ceb 100644 --- a/shared-helpers/index.ts +++ b/shared-helpers/index.ts @@ -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" diff --git a/shared-helpers/src/locales/general.json b/shared-helpers/src/locales/general.json index bf8344de2c..7ad3fcfcb7 100644 --- a/shared-helpers/src/locales/general.json +++ b/shared-helpers/src/locales/general.json @@ -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", @@ -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", @@ -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", diff --git a/shared-helpers/src/utilities/useMutate.ts b/shared-helpers/src/utilities/useMutate.ts new file mode 100644 index 0000000000..b31cc5f145 --- /dev/null +++ b/shared-helpers/src/utilities/useMutate.ts @@ -0,0 +1,52 @@ +import { useState } from "react" + +export type UseMutateOptions = { + onSuccess?: () => void + onError?: (err: any) => void +} + +export const useMutate = () => { + const [data, setData] = useState(undefined) + const [isSuccess, setSuccess] = useState(false) + const [isLoading, setLoading] = useState(false) + const [isError, setError] = useState(null) + + const mutate = async ( + mutateFn: (args?: unknown) => Promise, + 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, + } +} diff --git a/sites/partners/__tests__/pages/application/index.test.tsx b/sites/partners/__tests__/pages/application/index.test.tsx index fae2866731..c316eada03 100644 --- a/sites/partners/__tests__/pages/application/index.test.tsx +++ b/sites/partners/__tests__/pages/application/index.test.tsx @@ -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" diff --git a/sites/partners/__tests__/pages/listings/[id]/index.test.tsx b/sites/partners/__tests__/pages/listings/[id]/index.test.tsx index 4510782d3b..08c6fd5c8f 100644 --- a/sites/partners/__tests__/pages/listings/[id]/index.test.tsx +++ b/sites/partners/__tests__/pages/listings/[id]/index.test.tsx @@ -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" diff --git a/sites/partners/page_content/locale_overrides/general.json b/sites/partners/page_content/locale_overrides/general.json index 6ff98169f0..1df61daa8e 100644 --- a/sites/partners/page_content/locale_overrides/general.json +++ b/sites/partners/page_content/locale_overrides/general.json @@ -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", @@ -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", diff --git a/sites/partners/src/components/settings/SettingsViewHelpers.tsx b/sites/partners/src/components/settings/SettingsViewHelpers.tsx index b3d1387077..886c792847 100644 --- a/sites/partners/src/components/settings/SettingsViewHelpers.tsx +++ b/sites/partners/src/components/settings/SettingsViewHelpers.tsx @@ -8,12 +8,21 @@ export enum SettingsIndexEnum { properties, } -export const getSettingsTabs = (selectedIndex: SettingsIndexEnum, router: NextRouter) => { +export const getSettingsTabs = ( + selectedIndex: SettingsIndexEnum, + router: NextRouter, + newPreferences: boolean +) => { const baseUrl = "/settings/" + return ( void router.push(`${baseUrl}/${SettingsIndexEnum[index]}`)} + onSelect={(index) => { + let subpath = SettingsIndexEnum[index] + if (newPreferences && subpath === "preferences") subpath = `${subpath}-new` + void router.push(`${baseUrl}/${subpath}`) + }} selectedIndex={selectedIndex} > diff --git a/sites/partners/src/components/settings/preferences-new/EditPreference.tsx b/sites/partners/src/components/settings/preferences-new/EditPreference.tsx new file mode 100644 index 0000000000..9048add240 --- /dev/null +++ b/sites/partners/src/components/settings/preferences-new/EditPreference.tsx @@ -0,0 +1,152 @@ +import React, { useContext, useState } from "react" +import { useSWRConfig } from "swr" +import { AuthContext, MessageContext, useMutate } from "@bloom-housing/shared-helpers" +import { + MultiselectQuestion, + MultiselectQuestionCreate, + MultiselectQuestionsStatusEnum, + MultiselectQuestionUpdate, +} from "@bloom-housing/shared-helpers/src/types/backend-swagger" +import { t } from "@bloom-housing/ui-components" +import PreferenceEditDrawer from "./PreferenceEditDrawer" +import { PreferenceDeleteModal } from "./PreferenceDeleteModal" +import PreferenceViewDrawer from "./PreferenceViewDrawer" + +export type DrawerType = "add" | "edit" | "view" + +interface EditPreferenceProps { + cacheKey: string + preferenceDrawerOpen: DrawerType + setPreferenceDrawerOpen: React.Dispatch> + questionData: MultiselectQuestion + setQuestionData: React.Dispatch> +} + +const EditPreference = ({ + cacheKey, + preferenceDrawerOpen, + setPreferenceDrawerOpen, + questionData, + setQuestionData, +}: EditPreferenceProps) => { + const { mutate } = useSWRConfig() + + const { multiselectQuestionsService } = useContext(AuthContext) + const { addToast } = useContext(MessageContext) + + const { mutate: updateQuestion, isLoading: isUpdateLoading } = useMutate() + const { mutate: createQuestion, isLoading: isCreateLoading } = useMutate() + + const [updatedIds, setUpdatedIds] = useState([]) + const [deleteConfirmModalOpen, setDeleteConfirmModalOpen] = useState( + null + ) + + const copyQuestion = (data: MultiselectQuestionCreate) => { + const newData = { + ...data, + name: `${data.name} (Copy)`, + text: "", + options: null, + status: MultiselectQuestionsStatusEnum.draft, + hideFromListing: true, + } + newData.multiselectOptions.forEach((option) => (option.text = "")) + saveQuestion(newData, "add") + } + + const saveQuestion = (formattedData: MultiselectQuestionCreate, requestType: DrawerType) => { + if (requestType === "edit") { + void updateQuestion(() => + multiselectQuestionsService + .update({ + body: { + ...(formattedData as unknown as MultiselectQuestionUpdate), + id: questionData.id, + }, + }) + .then((result) => { + setUpdatedIds( + updatedIds.find((existingId) => existingId === result.id) + ? updatedIds + : [...updatedIds, result.id] + ) + addToast(t(`settings.preferenceAlertUpdated`), { variant: "success" }) + }) + .catch((e) => { + addToast(t(`errors.alert.badRequest`), { variant: "alert" }) + console.log(e) + }) + .finally(() => { + setPreferenceDrawerOpen(null) + void mutate(cacheKey) + }) + ) + } else { + void createQuestion(() => + multiselectQuestionsService + .create({ + body: formattedData as unknown as MultiselectQuestionCreate, + }) + .then((result) => { + setUpdatedIds( + updatedIds.find((existingId) => existingId === result.id) + ? updatedIds + : [...updatedIds, result.id] + ) + addToast(t(`settings.preferenceAlertCreated`), { variant: "success" }) + }) + .catch((e) => { + addToast(t(`errors.alert.badRequest`), { variant: "alert" }) + console.log(e) + }) + .finally(() => { + setPreferenceDrawerOpen(null) + void mutate(cacheKey) + }) + ) + } + } + + return ( + <> + { + setPreferenceDrawerOpen(null) + void mutate(cacheKey) + }} + saveQuestion={saveQuestion} + copyQuestion={copyQuestion} + setDeleteConfirmModalOpen={setDeleteConfirmModalOpen} + isLoading={isCreateLoading || isUpdateLoading} + /> + + { + setPreferenceDrawerOpen(null) + }} + /> + + {deleteConfirmModalOpen && ( + { + setDeleteConfirmModalOpen(null) + void mutate(cacheKey) + }} + /> + )} + + ) +} + +export default EditPreference diff --git a/sites/partners/src/components/settings/preferences-new/PreferenceDeleteModal.tsx b/sites/partners/src/components/settings/preferences-new/PreferenceDeleteModal.tsx new file mode 100644 index 0000000000..24d667353f --- /dev/null +++ b/sites/partners/src/components/settings/preferences-new/PreferenceDeleteModal.tsx @@ -0,0 +1,61 @@ +import React, { useContext } from "react" +import { t } from "@bloom-housing/ui-components" +import { Button, Dialog } from "@bloom-housing/ui-seeds" +import { AuthContext, MessageContext } from "@bloom-housing/shared-helpers" +import { MultiselectQuestion } from "@bloom-housing/shared-helpers/src/types/backend-swagger" + +type PreferenceDeleteModalProps = { + onClose: () => void + multiselectQuestion: MultiselectQuestion +} + +export const PreferenceDeleteModal = ({ + multiselectQuestion, + onClose, +}: PreferenceDeleteModalProps) => { + const { multiselectQuestionsService } = useContext(AuthContext) + const { addToast } = useContext(MessageContext) + + const deletePreference = () => { + multiselectQuestionsService + .delete({ + body: { id: multiselectQuestion.id }, + }) + .then(() => { + addToast(t("settings.preferenceAlertDeleted"), { variant: "success" }) + onClose() + }) + .catch((e) => { + addToast(t("errors.alert.timeoutPleaseTryAgain"), { variant: "alert" }) + console.log(e) + }) + } + + return ( + + {t("t.areYouSure")} + + {t("settings.preferenceDeleteConfirmation")} + + + + + + + ) +} diff --git a/sites/partners/src/components/settings/preferences-new/PreferenceEditDrawer.module.scss b/sites/partners/src/components/settings/preferences-new/PreferenceEditDrawer.module.scss new file mode 100644 index 0000000000..9b4b2f9dd7 --- /dev/null +++ b/sites/partners/src/components/settings/preferences-new/PreferenceEditDrawer.module.scss @@ -0,0 +1,4 @@ +.helperText { + font-size: var(--seeds-font-size-2xs); + color: var(--field-value-help-text-color); +} diff --git a/sites/partners/src/components/settings/preferences-new/PreferenceEditDrawer.tsx b/sites/partners/src/components/settings/preferences-new/PreferenceEditDrawer.tsx new file mode 100644 index 0000000000..195212a8b8 --- /dev/null +++ b/sites/partners/src/components/settings/preferences-new/PreferenceEditDrawer.tsx @@ -0,0 +1,996 @@ +import React, { useContext, useEffect, useMemo, useState } from "react" +import { + Field, + FieldGroup, + MinimalTable, + Select, + StandardTableData, + t, + Textarea, +} from "@bloom-housing/ui-components" +import { + Button, + Card, + Drawer, + FieldValue, + FormErrorMessage, + Grid, + Tag, +} from "@bloom-housing/ui-seeds" +import { AuthContext } from "@bloom-housing/shared-helpers" +import { useForm } from "react-hook-form" +import { + MultiselectOption, + MultiselectOptionCreate, + MultiselectQuestion, + MultiselectQuestionCreate, + MultiselectQuestionsApplicationSectionEnum, + MultiselectQuestionsStatusEnum, + MultiselectQuestionUpdate, + ValidationMethodEnum, + YesNoEnum, +} from "@bloom-housing/shared-helpers/src/types/backend-swagger" +import ManageIconSection from "../ManageIconSection" +import { DrawerType } from "./EditPreference" +import SectionWithGrid from "../../shared/SectionWithGrid" +import s from "./PreferenceEditDrawer.module.scss" +import { useMapLayersList } from "../../../lib/hooks" + +type PreferenceEditDrawerProps = { + drawerOpen: boolean + questionData: MultiselectQuestion + setQuestionData: React.Dispatch> + drawerType: DrawerType + onDrawerClose: () => void + saveQuestion: ( + formattedData: MultiselectQuestionCreate | MultiselectQuestionUpdate, + requestType: DrawerType + ) => void + copyQuestion: (data: MultiselectQuestionCreate) => void + setDeleteConfirmModalOpen: React.Dispatch> + isLoading: boolean +} + +type OptionForm = { + shouldCollectAddress: YesNoEnum + validationMethod?: ValidationMethodEnum + radiusSize?: string + shouldCollectRelationship?: YesNoEnum + shouldCollectName?: YesNoEnum + exclusiveQuestion: "exclusive" | "multiselect" + optionDescription: string + optionLinkTitle: string + optionTitle: string + optionUrl: string + canYouOptOut: YesNoEnum + mapLayerId?: string +} + +const alphaNumericPattern = /^[A-Z][a-zA-Z0-9 ']+$/ + +const PreferenceEditDrawer = ({ + drawerType, + questionData, + setQuestionData, + drawerOpen, + onDrawerClose, + saveQuestion, + copyQuestion, + setDeleteConfirmModalOpen, + isLoading, +}: PreferenceEditDrawerProps) => { + const [optionDrawerOpen, setOptionDrawerOpen] = useState(null) + const [optionData, setOptionData] = useState(null) + const [dragOrder, setDragOrder] = useState([]) + + const { profile } = useContext(AuthContext) + // eslint-disable-next-line @typescript-eslint/unbound-method + const { register, getValues, trigger, errors, clearErrors, setError, watch, formState } = + useForm() + + const { mapLayers } = useMapLayersList(watch("jurisdictionId")) + + const isAdditionalDetailsEnabled = profile?.jurisdictions?.some( + (jurisdiction) => jurisdiction.enableGeocodingPreferences + ) + + const shouldCollectAddressExpand = + ((optionData?.shouldCollectAddress && watch("shouldCollectAddress") === undefined) || + watch("shouldCollectAddress") === YesNoEnum.yes) && + isAdditionalDetailsEnabled + const isValidationRadiusVisible = + profile?.jurisdictions.find((juris) => juris.id === watch("jurisdictionId")) + ?.enableGeocodingRadiusMethod || + profile?.jurisdictions.every((juris) => juris.enableGeocodingRadiusMethod) + const radiusExpand = + (optionData?.validationMethod === ValidationMethodEnum.radius && + watch("validationMethod") === undefined) || + watch("validationMethod") === ValidationMethodEnum.radius + + const mapExpand = + (optionData?.validationMethod === ValidationMethodEnum.map && + watch("validationMethod") === undefined) || + watch("validationMethod") === ValidationMethodEnum.map + + // Update local state with dragged state + useEffect(() => { + if (questionData?.multiselectOptions?.length > 0 && dragOrder?.length > 0) { + const newDragOrder = [] + dragOrder.forEach((item, index) => { + newDragOrder.push({ + ...questionData?.multiselectOptions?.filter( + (draftItem) => draftItem.name === item.name.content + )[0], + ordinal: index + 1, + }) + }) + setQuestionData({ ...questionData, multiselectOptions: newDragOrder }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dragOrder]) + + const draggableTableData: StandardTableData = useMemo( + () => + questionData?.multiselectOptions + ?.sort((a, b) => (a.ordinal < b.ordinal ? -1 : 1)) + .map((item) => ({ + name: { content: item.name }, + description: { content: item.description }, + action: { + content: ( + { + const draftOptions = [...questionData.multiselectOptions] + draftOptions.push({ + ...item, + ordinal: questionData.multiselectOptions.length + 1, + }) + setQuestionData({ ...questionData, multiselectOptions: draftOptions }) + }} + copyTestId={`option-copy-icon: ${item.name}`} + onEdit={() => { + setOptionData(item) + setOptionDrawerOpen("edit") + }} + editTestId={`option-edit-icon: ${item.name}`} + onDelete={() => { + setQuestionData({ + ...questionData, + multiselectOptions: questionData.multiselectOptions + .filter((option) => option.ordinal !== item.ordinal) + .map((option, index) => { + return { ...option, ordinal: index + 1 } + }), + }) + }} + /> + ), + }, + })), + [questionData, setQuestionData] + ) + + const drawerTitle = + drawerType === "add" ? t("settings.preferenceAdd") : t("settings.preferenceEdit") + + const selectDrawerTitle = optionData + ? t("settings.preferenceEditOption") + : t("settings.preferenceAddOption") + + let variant = null + switch (questionData?.status) { + case MultiselectQuestionsStatusEnum.active: + variant = "success" + break + case MultiselectQuestionsStatusEnum.toRetire: + case MultiselectQuestionsStatusEnum.retired: + variant = "highlight-warm" + break + } + const statusText = `${questionData?.status + ?.charAt(0) + ?.toUpperCase()}${questionData?.status?.slice(1)}` + const statusTag = questionData?.status ? {statusText} : undefined + + let validationMethodsFields = [ + { + label: t("settings.preferenceValidatingAddress.checkWithinRadius"), + value: ValidationMethodEnum.radius, + defaultChecked: optionData?.validationMethod === ValidationMethodEnum.radius, + id: "validationMethodRadius", + dataTestId: "validation-method-radius", + inputProps: { + onChange: () => { + clearErrors("validationMethod") + }, + }, + }, + { + label: t("settings.preferenceValidatingAddress.checkWithArcGisMap"), + value: ValidationMethodEnum.map, + defaultChecked: optionData?.validationMethod === ValidationMethodEnum.map, + id: "validationMethodMap", + dataTestId: "validation-method-map", + inputProps: { + onChange: () => { + clearErrors("validationMethod") + }, + }, + }, + { + label: t("settings.preferenceValidatingAddress.checkManually"), + value: ValidationMethodEnum.none, + defaultChecked: optionData?.validationMethod === ValidationMethodEnum.none, + id: "validationMethodNone", + dataTestId: "validation-method-none", + inputProps: { + onChange: () => { + clearErrors("validationMethod") + }, + }, + }, + ] + + if (!isValidationRadiusVisible) { + validationMethodsFields = validationMethodsFields.filter( + (field) => field.id !== "validationMethodRadius" + ) + } + + /** + * Saves the preference and the associated options + */ + const savePreference = async (forceToVisible: boolean = null) => { + const validation = await trigger() + if (!questionData || !questionData?.multiselectOptions?.length) { + setError("questions", { message: t("errors.requiredFieldError") }) + return + } + if (!validation) return + const formValues = getValues() + if (!isValidationRadiusVisible) { + questionData.multiselectOptions = questionData?.multiselectOptions.map((option) => + option.validationMethod === ValidationMethodEnum.radius + ? { + ...option, + validationMethod: ValidationMethodEnum.none, + radiusSize: undefined, + } + : option + ) + } + + let newStatus = questionData?.status ?? MultiselectQuestionsStatusEnum.draft + if (newStatus === MultiselectQuestionsStatusEnum.draft && forceToVisible === true) { + newStatus = MultiselectQuestionsStatusEnum.visible + } else if (newStatus === MultiselectQuestionsStatusEnum.visible && forceToVisible === false) { + newStatus = MultiselectQuestionsStatusEnum.draft + } + + const formattedQuestionData: MultiselectQuestionUpdate | MultiselectQuestionCreate = { + applicationSection: MultiselectQuestionsApplicationSectionEnum.preferences, + description: formValues.description.trim(), + hideFromListing: formValues.showOnListingQuestion === YesNoEnum.no, + jurisdictions: [], // TODO: remove this when V2 schema is default + jurisdiction: profile.jurisdictions.find((juris) => juris.id === formValues.jurisdictionId), + links: formValues.preferenceUrl + ? [{ title: formValues.preferenceLinkTitle.trim(), url: formValues.preferenceUrl.trim() }] + : [], + isExclusive: formValues.exclusiveQuestion === "exclusive", + multiselectOptions: questionData?.multiselectOptions?.map((option) => { + option.text = "" // TODO: remove this when V2 schema is default + return option + }), + status: newStatus, + name: formValues.name.trim(), + text: "", // TODO: remove this when V2 schema is default + } + clearErrors() + clearErrors("questions") + saveQuestion(formattedQuestionData, drawerType) + } + + const toggleVisibility = () => { + void savePreference( + !questionData || questionData?.status === MultiselectQuestionsStatusEnum.draft + ) + } + + return ( + <> + { + clearErrors() + clearErrors("questions") + onDrawerClose() + }} + ariaLabelledBy="preference-drawer-header" + > + + {drawerTitle} {statusTag} + + + + + + + + clearErrors("name"), + }} + /> + + + + +