Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions example/src/Onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,19 @@ const OnboardingWithProps = ({
render={OnBoardingRender}
employmentId={employmentId}
externalId={externalId}
options={{
jsfModify: {
basic_information: {
fields: {
tax_servicing_countries: {
presentation: {
enableCustomTaxServicingComponent: true,
},
},
},
},
},
}}
/>
</RemoteFlows>
);
Expand Down
218 changes: 218 additions & 0 deletions src/flows/Onboarding/components/TaxServicingCountries.tsx
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}
/>
</FormControl>
{description && <FormDescription>{description}</FormDescription>}
{fieldState.error && <FormMessage />}
</FormItem>
);
}}
/>
);
}
51 changes: 48 additions & 3 deletions src/flows/Onboarding/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

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,
},
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -380,7 +425,7 @@ export const useOnboarding = ({
...options?.jsfModify?.contract_details,
fields: {
...options?.jsfModify?.contract_details?.fields,
...customFields.fields,
...customContractDetailsFields.fields,
},
},
queryOptions: {
Expand Down
Loading