[offers][feat] Add form validation for typeaheads (#508)

pull/509/head
Ai Ling 2 years ago committed by GitHub
parent 35a06c1185
commit 32bbb45f4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,37 @@
import type { ComponentProps } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import CitiesTypeahead from '~/components/shared/CitiesTypeahead';
type Props = Omit<
ComponentProps<typeof CitiesTypeahead>,
'onSelect' | 'value'
> & {
names: { label: string; value: string };
};
export default function FormCitiesTypeahead({ names, ...props }: Props) {
const { setValue } = useFormContext();
const watchCityId = useWatch({
name: names.value,
});
const watchCityName = useWatch({
name: names.label,
});
return (
<CitiesTypeahead
label="Location"
{...props}
value={{
id: watchCityId,
label: watchCityName,
value: watchCityId,
}}
onSelect={(option) => {
setValue(names.value, option?.value);
setValue(names.label, option?.value);
}}
/>
);
}

@ -0,0 +1,36 @@
import type { ComponentProps } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
type Props = Omit<
ComponentProps<typeof CompaniesTypeahead>,
'onSelect' | 'value'
> & {
names: { label: string; value: string };
};
export default function FormCompaniesTypeahead({ names, ...props }: Props) {
const { setValue } = useFormContext();
const watchCompanyId = useWatch({
name: names.value,
});
const watchCompanyName = useWatch({
name: names.label,
});
return (
<CompaniesTypeahead
{...props}
value={{
id: watchCompanyId,
label: watchCompanyName,
value: watchCompanyId,
}}
onSelect={(option) => {
setValue(names.value, option?.value);
setValue(names.label, option?.value);
}}
/>
);
}

@ -0,0 +1,34 @@
import type { ComponentProps } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import type { JobTitleType } from '~/components/shared/JobTitles';
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
type Props = Omit<
ComponentProps<typeof JobTitlesTypeahead>,
'onSelect' | 'value'
> & {
name: string;
};
export default function FormJobTitlesTypeahead({ name, ...props }: Props) {
const { setValue } = useFormContext();
const watchJobTitle = useWatch({
name,
});
return (
<JobTitlesTypeahead
{...props}
value={{
id: watchJobTitle,
label: getLabelForJobTitleType(watchJobTitle as JobTitleType),
value: watchJobTitle,
}}
onSelect={(option) => {
setValue(name, option?.value);
}}
/>
);
}

@ -33,13 +33,13 @@ const defaultOfferValues = {
cityId: '', cityId: '',
comments: '', comments: '',
companyId: '', companyId: '',
jobTitle: '',
jobType: JobType.FULLTIME, jobType: JobType.FULLTIME,
monthYearReceived: { monthYearReceived: {
month: getCurrentMonth() as Month, month: getCurrentMonth() as Month,
year: getCurrentYear(), year: getCurrentYear(),
}, },
negotiationStrategy: '', negotiationStrategy: '',
title: '',
}; };
export const defaultFullTimeOfferValues = { export const defaultFullTimeOfferValues = {
@ -108,6 +108,7 @@ export default function OffersSubmissionForm({
const pageRef = useRef<HTMLDivElement>(null); const pageRef = useRef<HTMLDivElement>(null);
const scrollToTop = () => const scrollToTop = () =>
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 }); pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
const formMethods = useForm<OffersProfileFormData>({ const formMethods = useForm<OffersProfileFormData>({
defaultValues: initialOfferProfileValues, defaultValues: initialOfferProfileValues,
mode: 'all', mode: 'all',

@ -4,11 +4,6 @@ import { Collapsible, RadioList } from '@tih/ui';
import { FieldError } from '~/components/offers/constants'; import { FieldError } from '~/components/offers/constants';
import type { BackgroundPostData } from '~/components/offers/types'; import type { BackgroundPostData } from '~/components/offers/types';
import CitiesTypeahead from '~/components/shared/CitiesTypeahead';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import type { JobTitleType } from '~/components/shared/JobTitles';
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
import { import {
Currency, Currency,
@ -17,6 +12,9 @@ import {
import { EducationFieldOptions } from '../../EducationFields'; import { EducationFieldOptions } from '../../EducationFields';
import { EducationLevelOptions } from '../../EducationLevels'; import { EducationLevelOptions } from '../../EducationLevels';
import FormCitiesTypeahead from '../../forms/FormCitiesTypeahead';
import FormCompaniesTypeahead from '../../forms/FormCompaniesTypeahead';
import FormJobTitlesTypeahead from '../../forms/FormJobTitlesTypeahead';
import FormRadioList from '../../forms/FormRadioList'; import FormRadioList from '../../forms/FormRadioList';
import FormSection from '../../forms/FormSection'; import FormSection from '../../forms/FormSection';
import FormSelect from '../../forms/FormSelect'; import FormSelect from '../../forms/FormSelect';
@ -85,56 +83,19 @@ function YoeSection() {
} }
function FullTimeJobFields() { function FullTimeJobFields() {
const { register, setValue, formState } = useFormContext<{ const { register, formState } = useFormContext<{
background: BackgroundPostData; background: BackgroundPostData;
}>(); }>();
const experiencesField = formState.errors.background?.experiences?.[0]; const experiencesField = formState.errors.background?.experiences?.[0];
const watchJobTitle = useWatch({
name: 'background.experiences.0.title',
});
const watchCompanyId = useWatch({
name: 'background.experiences.0.companyId',
});
const watchCompanyName = useWatch({
name: 'background.experiences.0.companyName',
});
const watchCityId = useWatch({
name: 'background.experiences.0.cityId',
});
const watchCityName = useWatch({
name: 'background.experiences.0.cityName',
});
return ( return (
<> <>
<div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6"> <div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6">
<JobTitlesTypeahead <FormJobTitlesTypeahead name="background.experiences.0.title" />
value={{ <FormCompaniesTypeahead
id: watchJobTitle, names={{
label: getLabelForJobTitleType(watchJobTitle as JobTitleType), label: 'background.experiences.0.companyName',
value: watchJobTitle, value: 'background.experiences.0.companyId',
}}
onSelect={(option) => {
if (option) {
setValue('background.experiences.0.title', option.value);
}
}}
/>
<CompaniesTypeahead
value={{
id: watchCompanyId,
label: watchCompanyName,
value: watchCompanyId,
}}
onSelect={(option) => {
if (option) {
setValue('background.experiences.0.companyId', option.value);
setValue('background.experiences.0.companyName', option.label);
} else {
setValue('background.experiences.0.companyId', '');
setValue('background.experiences.0.companyName', '');
}
}} }}
/> />
</div> </div>
@ -172,21 +133,10 @@ function FullTimeJobFields() {
placeholder="e.g. L4, Junior" placeholder="e.g. L4, Junior"
{...register(`background.experiences.0.level`)} {...register(`background.experiences.0.level`)}
/> />
<CitiesTypeahead <FormCitiesTypeahead
label="Location" names={{
value={{ label: 'background.experiences.0.cityName',
id: watchCityId, value: 'background.experiences.0.cityId',
label: watchCityName,
value: watchCityId,
}}
onSelect={(option) => {
if (option) {
setValue('background.experiences.0.cityId', option.value);
setValue('background.experiences.0.cityName', option.label);
} else {
setValue('background.experiences.0.cityId', '');
setValue('background.experiences.0.cityName', '');
}
}} }}
/> />
<FormTextInput <FormTextInput
@ -205,53 +155,19 @@ function FullTimeJobFields() {
} }
function InternshipJobFields() { function InternshipJobFields() {
const { register, setValue, formState } = useFormContext<{ const { register, formState } = useFormContext<{
background: BackgroundPostData; background: BackgroundPostData;
}>(); }>();
const experiencesField = formState.errors.background?.experiences?.[0]; const experiencesField = formState.errors.background?.experiences?.[0];
const watchJobTitle = useWatch({
name: 'background.experiences.0.title',
});
const watchCompanyId = useWatch({
name: 'background.experiences.0.companyId',
});
const watchCompanyName = useWatch({
name: 'background.experiences.0.companyName',
});
const watchCityId = useWatch({
name: 'background.experiences.0.cityId',
});
const watchCityName = useWatch({
name: 'background.experiences.0.cityName',
});
return ( return (
<> <>
<div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6"> <div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6">
<JobTitlesTypeahead <FormJobTitlesTypeahead name="background.experiences.0.title" />
value={{ <FormCompaniesTypeahead
id: watchJobTitle, names={{
label: getLabelForJobTitleType(watchJobTitle as JobTitleType), label: 'background.experiences.0.companyName',
value: watchJobTitle, value: 'background.experiences.0.companyId',
}}
onSelect={(option) => {
if (option) {
setValue('background.experiences.0.title', option.value);
}
}}
/>
<CompaniesTypeahead
value={{
id: watchCompanyId,
label: watchCompanyName,
value: watchCompanyId,
}}
onSelect={(option) => {
if (option) {
setValue('background.experiences.0.companyId', option.value);
setValue('background.experiences.0.companyName', option.label);
}
}} }}
/> />
</div> </div>
@ -280,21 +196,10 @@ function InternshipJobFields() {
/> />
<Collapsible label="Add more details"> <Collapsible label="Add more details">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<CitiesTypeahead <FormCitiesTypeahead
label="Location" names={{
value={{ label: 'background.experiences.0.cityName',
id: watchCityId, value: 'background.experiences.0.cityId',
label: watchCityName,
value: watchCityId,
}}
onSelect={(option) => {
if (option) {
setValue('background.experiences.0.cityId', option.value);
setValue('background.experiences.0.cityName', option.label);
} else {
setValue('background.experiences.0.cityId', '');
setValue('background.experiences.0.cityName', '');
}
}} }}
/> />
<FormTextInput <FormTextInput
@ -388,4 +293,4 @@ export default function BackgroundForm() {
</div> </div>
</div> </div>
); );
} }

@ -4,6 +4,7 @@ import type {
UseFieldArrayRemove, UseFieldArrayRemove,
UseFieldArrayReturn, UseFieldArrayReturn,
} from 'react-hook-form'; } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { useWatch } from 'react-hook-form'; import { useWatch } from 'react-hook-form';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { useFieldArray } from 'react-hook-form'; import { useFieldArray } from 'react-hook-form';
@ -12,17 +13,14 @@ import { TrashIcon } from '@heroicons/react/24/outline';
import { JobType } from '@prisma/client'; import { JobType } from '@prisma/client';
import { Button, Dialog, HorizontalDivider } from '@tih/ui'; import { Button, Dialog, HorizontalDivider } from '@tih/ui';
import CitiesTypeahead from '~/components/shared/CitiesTypeahead';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import type { JobTitleType } from '~/components/shared/JobTitles';
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
import { import {
defaultFullTimeOfferValues, defaultFullTimeOfferValues,
defaultInternshipOfferValues, defaultInternshipOfferValues,
} from '../OffersSubmissionForm'; } from '../OffersSubmissionForm';
import { FieldError, JobTypeLabel } from '../../constants'; import { FieldError, JobTypeLabel } from '../../constants';
import FormCitiesTypeahead from '../../forms/FormCitiesTypeahead';
import FormCompaniesTypeahead from '../../forms/FormCompaniesTypeahead';
import FormJobTitlesTypeahead from '../../forms/FormJobTitlesTypeahead';
import FormMonthYearPicker from '../../forms/FormMonthYearPicker'; import FormMonthYearPicker from '../../forms/FormMonthYearPicker';
import FormSection from '../../forms/FormSection'; import FormSection from '../../forms/FormSection';
import FormSelect from '../../forms/FormSelect'; import FormSelect from '../../forms/FormSelect';
@ -46,26 +44,11 @@ function FullTimeOfferDetailsForm({
index, index,
remove, remove,
}: FullTimeOfferDetailsFormProps) { }: FullTimeOfferDetailsFormProps) {
const { register, formState, setValue } = useFormContext<{ const { register, formState, setValue, control } = useFormContext<{
offers: Array<OfferFormData>; offers: Array<OfferFormData>;
}>(); }>();
const offerFields = formState.errors.offers?.[index]; const offerFields = formState.errors.offers?.[index];
const watchJobTitle = useWatch({
name: `offers.${index}.offersFullTime.title`,
});
const watchCompanyId = useWatch({
name: `offers.${index}.companyId`,
});
const watchCompanyName = useWatch({
name: `offers.${index}.companyName`,
});
const watchCityId = useWatch({
name: `offers.${index}.cityId`,
});
const watchCityName = useWatch({
name: `offers.${index}.cityName`,
});
const watchCurrency = useWatch({ const watchCurrency = useWatch({
name: `offers.${index}.offersFullTime.totalCompensation.currency`, name: `offers.${index}.offersFullTime.totalCompensation.currency`,
}); });
@ -83,18 +66,17 @@ function FullTimeOfferDetailsForm({
<div className="space-y-8 rounded-lg border border-slate-200 p-6 sm:space-y-16 sm:p-8"> <div className="space-y-8 rounded-lg border border-slate-200 p-6 sm:space-y-16 sm:p-8">
<FormSection title="Company & Title Information"> <FormSection title="Company & Title Information">
<div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6"> <div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6">
<JobTitlesTypeahead <Controller
required={true} control={control}
value={{ name={`offers.${index}.offersFullTime.title`}
id: watchJobTitle, render={() => (
label: getLabelForJobTitleType(watchJobTitle as JobTitleType), <FormJobTitlesTypeahead
value: watchJobTitle, errorMessage={offerFields?.offersFullTime?.title?.message}
}} name={`offers.${index}.offersFullTime.title`}
onSelect={(option) => { required={true}
if (option) { />
setValue(`offers.${index}.offersFullTime.title`, option.value); )}
} rules={{ required: true }}
}}
/> />
<FormTextInput <FormTextInput
errorMessage={offerFields?.offersFullTime?.level?.message} errorMessage={offerFields?.offersFullTime?.level?.message}
@ -107,37 +89,35 @@ function FullTimeOfferDetailsForm({
/> />
</div> </div>
<div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6"> <div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6">
<CompaniesTypeahead <Controller
required={true} control={control}
value={{ name={`offers.${index}.companyId`}
id: watchCompanyId, render={() => (
label: watchCompanyName, <FormCompaniesTypeahead
value: watchCompanyId, errorMessage={offerFields?.companyId?.message}
}} names={{
onSelect={(option) => { label: `offers.${index}.companyName`,
if (option) { value: `offers.${index}.companyId`,
setValue(`offers.${index}.companyId`, option.value); }}
setValue(`offers.${index}.companyName`, option.label); required={true}
} />
}} )}
rules={{ required: true }}
/> />
<CitiesTypeahead <Controller
label="Location" control={control}
required={true} name={`offers.${index}.cityId`}
value={{ render={() => (
id: watchCityId, <FormCitiesTypeahead
label: watchCityName, errorMessage={offerFields?.cityId?.message}
value: watchCityId, names={{
}} label: `offers.${index}.cityName`,
onSelect={(option) => { value: `offers.${index}.companyId`,
if (option) { }}
setValue(`offers.${index}.cityId`, option.value); required={true}
setValue(`offers.${index}.cityName`, option.label); />
} else { )}
setValue(`offers.${index}.cityId`, ''); rules={{ required: true }}
setValue(`offers.${index}.cityName`, '');
}
}}
/> />
</div> </div>
</FormSection> </FormSection>
@ -303,76 +283,56 @@ function InternshipOfferDetailsForm({
index, index,
remove, remove,
}: InternshipOfferDetailsFormProps) { }: InternshipOfferDetailsFormProps) {
const { register, formState, setValue } = useFormContext<{ const { register, formState, control } = useFormContext<{
offers: Array<OfferFormData>; offers: Array<OfferFormData>;
}>(); }>();
const offerFields = formState.errors.offers?.[index]; const offerFields = formState.errors.offers?.[index];
const watchJobTitle = useWatch({
name: `offers.${index}.offersIntern.title`,
});
const watchCompanyId = useWatch({
name: `offers.${index}.companyId`,
});
const watchCompanyName = useWatch({
name: `offers.${index}.companyName`,
});
const watchCityId = useWatch({
name: `offers.${index}.cityId`,
});
const watchCityName = useWatch({
name: `offers.${index}.cityName`,
});
return ( return (
<div className="space-y-8 rounded-lg border border-slate-200 p-6 sm:space-y-16 sm:p-8"> <div className="space-y-8 rounded-lg border border-slate-200 p-6 sm:space-y-16 sm:p-8">
<FormSection title="Company & Title Information"> <FormSection title="Company & Title Information">
<JobTitlesTypeahead <Controller
required={true} control={control}
value={{ name={`offers.${index}.offersIntern.title`}
id: watchJobTitle, render={() => (
label: getLabelForJobTitleType(watchJobTitle as JobTitleType), <FormJobTitlesTypeahead
value: watchJobTitle, errorMessage={offerFields?.offersIntern?.title?.message}
}} name={`offers.${index}.offersIntern.title`}
onSelect={(option) => { required={true}
if (option) { />
setValue(`offers.${index}.offersIntern.title`, option.value); )}
} rules={{ required: true }}
}}
/> />
<div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6"> <div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6">
<CompaniesTypeahead <Controller
required={true} control={control}
value={{ name={`offers.${index}.companyId`}
id: watchCompanyId, render={() => (
label: watchCompanyName, <FormCompaniesTypeahead
value: watchCompanyId, errorMessage={offerFields?.companyId?.message}
}} names={{
onSelect={(option) => { label: `offers.${index}.companyName`,
if (option) { value: `offers.${index}.companyId`,
setValue(`offers.${index}.companyId`, option.value); }}
setValue(`offers.${index}.companyName`, option.label); required={true}
} />
}} )}
rules={{ required: true }}
/> />
<CitiesTypeahead <Controller
label="Location" control={control}
required={true} name={`offers.${index}.cityId`}
value={{ render={() => (
id: watchCityId, <FormCitiesTypeahead
label: watchCityName, errorMessage={offerFields?.cityId?.message}
value: watchCityId, names={{
}} label: `offers.${index}.cityName`,
onSelect={(option) => { value: `offers.${index}.companyId`,
if (option) { }}
setValue(`offers.${index}.cityId`, option.value); required={true}
setValue(`offers.${index}.cityName`, option.label); />
} else { )}
setValue(`offers.${index}.cityId`, ''); rules={{ required: true }}
setValue(`offers.${index}.cityName`, '');
}
}}
/> />
</div> </div>
<div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6"> <div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6">

Loading…
Cancel
Save