-
Notifications
You must be signed in to change notification settings - Fork 0
feat(tax-servicing-countries) - create custom component #505
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,218 @@ | ||
| import { useFormFields } from '@/src/context'; | ||
| import { $TSFixMe, Components, JSFField } from '@/src/types/remoteFlows'; | ||
| import { useFormContext } from 'react-hook-form'; | ||
| import { | ||
| FormControl, | ||
| FormDescription, | ||
| FormField, | ||
| FormItem, | ||
| FormLabel, | ||
| FormMessage, | ||
| } from '@/src/components/ui/form'; | ||
| import { MultiSelect } from '@/src/components/ui/multi-select'; | ||
| import { useState, useCallback, useMemo } from 'react'; | ||
|
|
||
| type TaxCountriesFieldProps = JSFField & { | ||
| placeholder?: string; | ||
| options?: Array<{ value: string; label: string }>; | ||
| className?: string; | ||
| onChange?: (value: $TSFixMe) => void; | ||
| component?: Components['multi-select']; | ||
| $meta?: { | ||
| regions?: Record<string, string[]>; | ||
| subregions?: Record<string, string[]>; | ||
| }; | ||
| }; | ||
|
|
||
| // Constants for tax servicing countries logic | ||
| const GLOBAL_OPTION = { label: 'Global', value: 'GLOBAL', type: 'global' }; | ||
| const AREA_TYPE = { | ||
| REGIONS: 'regions', | ||
| SUBREGIONS: 'subregions', | ||
| COUNTRIES: 'countries', | ||
| }; | ||
|
|
||
| export function TaxServicingCountriesField({ | ||
| label, | ||
| name, | ||
| options, | ||
| defaultValue, | ||
| description, | ||
| onChange, | ||
| component, | ||
| $meta, | ||
| ...rest | ||
| }: TaxCountriesFieldProps) { | ||
| const { control } = useFormContext(); | ||
| const { components } = useFormFields(); | ||
| const [selected, setSelected] = useState<$TSFixMe[]>(); | ||
|
|
||
| // Enhanced options with regions/subregions/countries | ||
| const enhancedOptions = useMemo(() => { | ||
| const { regions = {}, subregions = {} } = $meta || {}; | ||
|
|
||
| const regionsOptions = [ | ||
| GLOBAL_OPTION, | ||
| ...Object.keys(regions).map((region) => ({ | ||
| label: region, | ||
| value: region, | ||
| type: AREA_TYPE.REGIONS, | ||
| category: 'Regions', | ||
| })), | ||
| ]; | ||
|
|
||
| const subregionsOptions = Object.keys(subregions).map((subregion) => ({ | ||
| label: subregion, | ||
| value: subregion, | ||
| type: AREA_TYPE.SUBREGIONS, | ||
| category: 'Subregions', | ||
| })); | ||
|
|
||
| const countriesOptions = | ||
| options?.map((option) => ({ | ||
| ...option, | ||
| type: AREA_TYPE.COUNTRIES, | ||
| category: 'Countries', | ||
| })) || []; | ||
|
|
||
| return [...regionsOptions, ...subregionsOptions, ...countriesOptions]; | ||
| }, [options, $meta]); | ||
|
|
||
| // Enhanced change handler with regions/subregions logic | ||
| const handleChange = useCallback( | ||
| (rawValues: $TSFixMe[]) => { | ||
| const { regions = {}, subregions = {} } = $meta || {}; | ||
|
|
||
| if (!rawValues?.length) { | ||
| onChange?.([]); | ||
| setSelected([]); | ||
| return; | ||
| } | ||
|
|
||
| // Check if Global is being selected | ||
| const isGlobalSelected = rawValues.some( | ||
| (option) => option.value === GLOBAL_OPTION.value, | ||
| ); | ||
|
|
||
| // If Global is selected, clear all other selections and only keep Global | ||
| if (isGlobalSelected) { | ||
| // If Global was already selected and user is trying to add more options, ignore the new selections | ||
| if (selected?.some((option) => option.value === GLOBAL_OPTION.value)) { | ||
| return; // Don't change anything if Global is already selected | ||
| } | ||
|
|
||
| // If Global is being selected for the first time, clear everything else | ||
| onChange?.([GLOBAL_OPTION.value]); | ||
| setSelected([GLOBAL_OPTION]); | ||
| return; | ||
| } | ||
|
|
||
| // If Global is currently selected and user is trying to select other options, ignore them | ||
| if (selected?.some((option) => option.value === GLOBAL_OPTION.value)) { | ||
| return; // Don't allow other selections when Global is selected | ||
| } | ||
|
|
||
| // Track countries to prevent duplicates when multiple overlapping areas are selected | ||
| const existingCountries = new Set(); | ||
| const updatedSelected = rawValues.flatMap((option) => { | ||
| // If the selection is a country, add it directly | ||
| if (option.type === AREA_TYPE.COUNTRIES) { | ||
| existingCountries.add(option.value); | ||
| return [option]; | ||
| } | ||
|
|
||
| // For regions/subregions, look up their countries | ||
| const lookupLabel = option.originalLabel || option.label; | ||
| const areaCountries = | ||
| option.type === AREA_TYPE.REGIONS | ||
| ? regions[lookupLabel] || [] | ||
| : subregions[lookupLabel] || []; | ||
|
|
||
| // Convert each country name into a country option object | ||
| return areaCountries | ||
| .filter((country) => !existingCountries.has(country)) | ||
| .map((country) => { | ||
| existingCountries.add(country); | ||
| return { | ||
| label: country, | ||
| value: country, | ||
| type: AREA_TYPE.COUNTRIES, | ||
| category: 'Countries', | ||
| }; | ||
| }); | ||
| }); | ||
|
|
||
| // Extract just the country values for the form | ||
| const countryValues = updatedSelected | ||
| .filter((option) => option.type === AREA_TYPE.COUNTRIES) | ||
| .map((option) => option.value); | ||
|
|
||
| onChange?.(countryValues); | ||
| setSelected(updatedSelected); | ||
| }, | ||
| [$meta, onChange, selected], // Added selected to dependencies | ||
| ); | ||
|
|
||
| return ( | ||
| <FormField | ||
| defaultValue={defaultValue} | ||
| control={control} | ||
| name={name} | ||
| render={({ field, fieldState }) => { | ||
| const CustomSelectField = component || components?.['multi-select']; | ||
| if (CustomSelectField) { | ||
| const customSelectFieldProps = { | ||
| label, | ||
| name, | ||
| options: enhancedOptions, | ||
| defaultValue, | ||
| description, | ||
| onChange: handleChange, | ||
| $meta, | ||
| ...rest, | ||
| }; | ||
| return ( | ||
| <CustomSelectField | ||
| field={{ | ||
| ...field, | ||
| onChange: (value: $TSFixMe) => { | ||
| field.onChange(value); | ||
| handleChange(value); | ||
| }, | ||
| }} | ||
| fieldState={fieldState} | ||
| fieldData={customSelectFieldProps} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| const selectedOptions = | ||
| selected || | ||
| enhancedOptions.filter((option) => | ||
| field.value?.includes(option.value), | ||
| ); | ||
|
|
||
| return ( | ||
| <FormItem | ||
| data-field={name} | ||
| className={`RemoteFlows__TaxCountriesField__Item__${name}`} | ||
| > | ||
| <FormLabel className='RemoteFlows__TaxCountriesField__Label'> | ||
| {label} | ||
| </FormLabel> | ||
| <FormControl> | ||
| <MultiSelect | ||
| options={enhancedOptions} | ||
| selected={selectedOptions} | ||
| onChange={handleChange} | ||
| {...rest} | ||
| /> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Form state not updated when using default MultiSelectHigh Severity The default |
||
| </FormControl> | ||
| {description && <FormDescription>{description}</FormDescription>} | ||
| {fieldState.error && <FormMessage />} | ||
| </FormItem> | ||
| ); | ||
| }} | ||
| /> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,7 @@ import { FlowOptions, JSFModify, JSONSchemaFormType } from '@/src/flows/types'; | |
| import { AnnualGrossSalary } from '@/src/flows/Onboarding/components/AnnualGrossSalary'; | ||
| import { $TSFixMe, JSFField, JSFFieldset, Meta } from '@/src/types/remoteFlows'; | ||
| import { EquityPriceDetails } from '@/src/flows/Onboarding/components/EquityPriceDetails'; | ||
| import { TaxServicingCountriesField } from '@/src/flows/Onboarding/components/TaxServicingCountries'; | ||
|
|
||
| type OnboardingHookProps = OnboardingFlowParams; | ||
|
|
||
|
|
@@ -277,13 +278,57 @@ export const useOnboarding = ({ | |
| Boolean(employmentId)), | ||
| ); | ||
|
|
||
| const taxServicingCountriesField = | ||
| options?.jsfModify?.basic_information?.fields?.tax_servicing_countries; | ||
| const taxServicingCountriesFieldPresentation = | ||
| taxServicingCountriesField && | ||
| typeof taxServicingCountriesField === 'object' && | ||
| 'presentation' in taxServicingCountriesField | ||
| ? ( | ||
| taxServicingCountriesField as { | ||
| presentation?: { | ||
| enableCustomTaxServicingComponent?: boolean; | ||
| }; | ||
| } | ||
| ).presentation | ||
| : undefined; | ||
|
|
||
| const customBasicInformationFields = useMemo(() => { | ||
| const enableCustomTaxServicingComponent = | ||
| taxServicingCountriesFieldPresentation?.enableCustomTaxServicingComponent !== | ||
| false; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Custom component enabled by default instead of opt-inMedium Severity The condition |
||
|
|
||
| return { | ||
| fields: { | ||
| tax_servicing_countries: { | ||
| ...taxServicingCountriesField, | ||
| presentation: { | ||
| ...taxServicingCountriesFieldPresentation, | ||
| enableCustomTaxServicingComponent, | ||
| ...(enableCustomTaxServicingComponent && { | ||
| Component: (props: JSFField) => { | ||
| return <TaxServicingCountriesField {...props} />; | ||
| }, | ||
| }), | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
| }, [taxServicingCountriesField, taxServicingCountriesFieldPresentation]); | ||
|
|
||
| const { | ||
| data: basicInformationForm, | ||
| isLoading: isLoadingBasicInformationForm, | ||
| } = useJSONSchema({ | ||
| form: 'employment_basic_information', | ||
| options: { | ||
| jsfModify: options?.jsfModify?.basic_information, | ||
| jsfModify: { | ||
| ...options?.jsfModify?.basic_information, | ||
| fields: { | ||
| ...options?.jsfModify?.basic_information?.fields, | ||
| ...customBasicInformationFields.fields, | ||
| }, | ||
| }, | ||
| queryOptions: { | ||
| enabled: isBasicInformationDetailsEnabled, | ||
| }, | ||
|
|
@@ -312,7 +357,7 @@ export const useOnboarding = ({ | |
| const equityCompensationField = | ||
| options?.jsfModify?.contract_details?.fields?.equity_compensation; | ||
|
|
||
| const customFields = useMemo( | ||
| const customContractDetailsFields = useMemo( | ||
| () => ({ | ||
| fields: { | ||
| annual_gross_salary: { | ||
|
|
@@ -380,7 +425,7 @@ export const useOnboarding = ({ | |
| ...options?.jsfModify?.contract_details, | ||
| fields: { | ||
| ...options?.jsfModify?.contract_details?.fields, | ||
| ...customFields.fields, | ||
| ...customContractDetailsFields.fields, | ||
| }, | ||
| }, | ||
| queryOptions: { | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Local state ignores external form value changes
Medium Severity
The
selectedstate is initialized asundefinedand only falls back tofield.valuewhile undefined. After any user interaction,handleChangecallssetSelected, setting it to an array. From then on,selectedOptionsalways uses the localselectedstate (since even[]is truthy), ignoring any external changes tofield.value. This meansform.reset()or programmatic value changes won't update the UI - users will see stale selections.Additional Locations (1)
src/flows/Onboarding/components/TaxServicingCountries.tsx#L47-L48