Skip to content

Commit

Permalink
Merge pull request #962 from CruGlobal/person-modal-show-invalid-date
Browse files Browse the repository at this point in the history
Helpscout - Show invalid date in person modal with error to fix
  • Loading branch information
dr-bizz authored Jun 20, 2024
2 parents 31f7a54 + 360fac1 commit eb907de
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import CakeIcon from '@mui/icons-material/Cake';
import { FormikProps } from 'formik';
import { DateTime } from 'luxon';
Expand All @@ -8,9 +8,12 @@ import {
PersonCreateInput,
PersonUpdateInput,
} from 'src/graphql/types.generated';
import { useLocale } from 'src/hooks/useLocale';
import { validateAndFormatInvalidDate } from 'src/lib/intlFormat';
import { ModalSectionContainer } from '../ModalSectionContainer/ModalSectionContainer';
import { ModalSectionIcon } from '../ModalSectionIcon/ModalSectionIcon';
import { NewSocial } from '../PersonModal';
import { buildDate } from '../personModalHelper';

interface PersonBirthdayProps {
formikProps: FormikProps<(PersonUpdateInput | PersonCreateInput) & NewSocial>;
Expand All @@ -20,28 +23,54 @@ export const PersonBirthday: React.FC<PersonBirthdayProps> = ({
formikProps,
}) => {
const { t } = useTranslation();
const locale = useLocale();
const [birthdayDateIsInvalid, setBirthdayDateIsInvalid] = useState(false);
const [backupBirthdayDate, setBackupBirthdayDate] =
useState<DateTime<boolean> | null>(null);

const {
values: { birthdayDay, birthdayMonth, birthdayYear },
setFieldValue,
} = formikProps;

useEffect(() => {
if (typeof birthdayMonth !== 'number' || typeof birthdayDay !== 'number') {
return;
}

const date = validateAndFormatInvalidDate(
birthdayYear,
birthdayMonth,
birthdayDay,
locale,
);

setBackupBirthdayDate(
date.formattedInvalidDate as unknown as DateTime<boolean>,
);
if (date.dateTime.invalidExplanation) {
setBirthdayDateIsInvalid(true);
}
}, [birthdayMonth, birthdayDay, birthdayYear]);

const handleDateChange = (date: DateTime | null) => {
setFieldValue('birthdayDay', date?.day || null);
setFieldValue('birthdayMonth', date?.month || null);
setFieldValue('birthdayYear', date?.year || null);
setFieldValue('birthdayDay', date?.day ?? null);
setFieldValue('birthdayMonth', date?.month ?? null);
setFieldValue('birthdayYear', date?.year ?? null);
};

const birthdayDate = useMemo(
() => buildDate(birthdayMonth, birthdayDay, birthdayYear),
[birthdayMonth, birthdayDay, birthdayYear],
);

return (
<ModalSectionContainer>
<ModalSectionIcon transform="translateY(-100%)" icon={<CakeIcon />} />
<CustomDateField
label={t('Birthday')}
value={
birthdayMonth && birthdayDay
? DateTime.local(birthdayYear ?? 1900, birthdayMonth, birthdayDay)
: null
}
invalidDate={birthdayDateIsInvalid}
value={birthdayDateIsInvalid ? backupBirthdayDate : birthdayDate}
onChange={(date) => handleDateChange(date)}
/>
</ModalSectionContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,47 @@ describe('PersonModal', () => {
expect(queryByText('Show Less')).not.toBeInTheDocument(),
);
});

it('should show invalid dates and highlight them as errors', async () => {
const { getByText, getByRole, queryAllByText } = render(
<SnackbarProvider>
<LocalizationProvider dateAdapter={AdapterLuxon}>
<ThemeProvider theme={theme}>
<GqlMockedProvider>
<ContactDetailProvider>
<PersonModal
contactId={contactId}
accountListId={accountListId}
handleClose={handleClose}
person={{
...mockPerson,
anniversaryDay: 0,
anniversaryMonth: 0,
anniversaryYear: 2000,
birthdayDay: 0,
birthdayMonth: 0,
birthdayYear: 2000,
}}
/>
</ContactDetailProvider>
</GqlMockedProvider>
</ThemeProvider>
</LocalizationProvider>
</SnackbarProvider>,
);

const birthdayInput = getByRole('textbox', { name: 'Birthday' });
expect(birthdayInput).toHaveValue('0/0/2000');
expect(birthdayInput.parentElement).toHaveClass('Mui-error');

userEvent.click(queryAllByText('Show More')[0]);
await waitFor(() => expect(getByText('Show Less')).toBeInTheDocument());

const anniversaryInput = getByRole('textbox', { name: 'Anniversary' });
expect(anniversaryInput).toHaveValue('0/0/2000');
expect(anniversaryInput.parentElement).toHaveClass('Mui-error');
});

describe('Updating', () => {
const createObjectURL = jest
.fn()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import BusinessIcon from '@mui/icons-material/Business';
import SchoolIcon from '@mui/icons-material/School';
import {
Expand All @@ -20,11 +20,14 @@ import {
PersonCreateInput,
PersonUpdateInput,
} from 'src/graphql/types.generated';
import { useLocale } from 'src/hooks/useLocale';
import { validateAndFormatInvalidDate } from 'src/lib/intlFormat';
import { RingIcon } from '../../../../../../RingIcon';
import { ModalSectionContainer } from '../ModalSectionContainer/ModalSectionContainer';
import { ModalSectionIcon } from '../ModalSectionIcon/ModalSectionIcon';
import { NewSocial } from '../PersonModal';
import { PersonSocial } from '../PersonSocials/PersonSocials';
import { buildDate } from '../personModalHelper';

const DeceasedLabel = styled(FormControlLabel)(() => ({
margin: 'none',
Expand All @@ -40,6 +43,11 @@ export const PersonShowMore: React.FC<PersonShowMoreProps> = ({
showDeceased = true,
}) => {
const { t } = useTranslation();
const locale = useLocale();
const [anniversaryDateIsInvalid, setAnniversaryDateIsInvalid] =
useState(false);
const [backupAnniversaryDate, setBackupAnniversaryDate] =
useState<DateTime<boolean> | null>(null);

const {
values: {
Expand All @@ -58,11 +66,40 @@ export const PersonShowMore: React.FC<PersonShowMoreProps> = ({
setFieldValue,
} = formikProps;

useEffect(() => {
if (
typeof anniversaryMonth !== 'number' ||
typeof anniversaryDay !== 'number'
) {
return;
}

const date = validateAndFormatInvalidDate(
anniversaryYear,
anniversaryMonth,
anniversaryDay,
locale,
);

setBackupAnniversaryDate(
date.formattedInvalidDate as unknown as DateTime<boolean>,
);
if (date.dateTime.invalidExplanation) {
setAnniversaryDateIsInvalid(true);
}
}, [anniversaryMonth, anniversaryDay, anniversaryYear]);

const handleDateChange = (date: DateTime | null) => {
setFieldValue('anniversaryDay', date?.day || null);
setFieldValue('anniversaryMonth', date?.month || null);
setFieldValue('anniversaryYear', date?.year || null);
setFieldValue('anniversaryDay', date?.day ?? null);
setFieldValue('anniversaryMonth', date?.month ?? null);
setFieldValue('anniversaryYear', date?.year ?? null);
};

const anniversaryDate = useMemo(
() => buildDate(anniversaryMonth, anniversaryDay, anniversaryYear),
[anniversaryMonth, anniversaryDay, anniversaryYear],
);

return (
<>
{/* Legal First Name and Gender Section */}
Expand Down Expand Up @@ -142,14 +179,11 @@ export const PersonShowMore: React.FC<PersonShowMoreProps> = ({
<Grid item xs={12} sm={6}>
<CustomDateField
label={t('Anniversary')}
invalidDate={anniversaryDateIsInvalid}
value={
anniversaryMonth && anniversaryDay
? DateTime.local(
anniversaryYear ?? 1900,
anniversaryMonth,
anniversaryDay,
)
: null
anniversaryDateIsInvalid
? backupAnniversaryDate
: anniversaryDate
}
onChange={(date) => date && handleDateChange(date)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DateTime } from 'luxon';
import { TFunction } from 'react-i18next';
import * as yup from 'yup';
import {
Expand Down Expand Up @@ -263,3 +264,7 @@ export const formatSubmittedFields = (
),
};
};

export const buildDate = (month, day, year) => {
return month && day ? DateTime.local(year ?? 1900, month, day) : null;
};
5 changes: 3 additions & 2 deletions src/components/common/DateTimePickers/CustomDateField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ export const CustomDateField: React.FC<DesktopDateFieldProps> = (props) => {
const isDesktop = useMediaQuery(DEFAULT_DESKTOP_MODE_MEDIA_QUERY, {
defaultMatches: true,
});
const { label, value, invalidDate, onChange, ...textFieldProps } = props;

if (isDesktop) {
// If value is not valid render desktop input as it can render invalid value
if (isDesktop || invalidDate) {
return <DesktopDateField {...props} />;
}

const { label, value, onChange, ...textFieldProps } = props;
return (
<MobileDatePicker<DateTime>
label={label}
Expand Down
33 changes: 32 additions & 1 deletion src/components/common/DateTimePickers/DesktopDateField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,24 @@ import { DesktopDateField } from './DesktopDateField';
interface TestComponentProps {
value?: DateTime | null;
locale?: string;
invalidDate?: boolean;
}

const onChange = jest.fn();

const TestComponent: React.FC<TestComponentProps> = ({
value = DateTime.local(2024, 1, 2, 3, 4, 5),
locale = 'en-US',
invalidDate = false,
}) => (
<UserPreferenceContext.Provider value={{ locale, userId: 'userId' }}>
<LocalizationProvider dateAdapter={AdapterLuxon} adapterLocale={locale}>
<DesktopDateField value={value} onChange={onChange} label="Date" />
<DesktopDateField
value={value}
onChange={onChange}
label="Date"
invalidDate={invalidDate}
/>
</LocalizationProvider>
</UserPreferenceContext.Provider>
);
Expand All @@ -40,6 +47,30 @@ describe('DesktopDateField', () => {
expect(getByRole('textbox')).toHaveValue('');
});

it('shows invalid date when invalidDate prop is TRUE', () => {
const { getByRole, rerender } = render(<TestComponent />);
rerender(
<TestComponent
value={'0/0/2000' as unknown as DateTime}
invalidDate={true}
/>,
);

expect(getByRole('textbox')).toHaveValue('0/0/2000');
});

it('shows default date with a invalid date when invalidDate prop is FALSE', () => {
const { getByRole, rerender } = render(<TestComponent />);
rerender(
<TestComponent
value={'0/0/2000' as unknown as DateTime}
invalidDate={false}
/>,
);

expect(getByRole('textbox')).toHaveValue('1/2/2024');
});

it('the formatted value', () => {
const { getByRole } = render(<TestComponent />);

Expand Down
4 changes: 4 additions & 0 deletions src/components/common/DateTimePickers/DesktopDateField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ export interface DesktopDateFieldProps
extends Omit<StandardTextFieldProps, 'onChange'> {
value: DateTime | null;
onChange: (date: DateTime | null) => void;
invalidDate?: boolean;
}

export const DesktopDateField: React.FC<DesktopDateFieldProps> = ({
value,
onChange,
invalidDate,
...props
}) => {
const locale = useLocale();
Expand All @@ -30,6 +32,8 @@ export const DesktopDateField: React.FC<DesktopDateFieldProps> = ({
setRawDate('');
} else if (value.isValid) {
setRawDate(value.toFormat('D', options));
} else if (invalidDate) {
setRawDate(value as unknown as string);
}
}, [locale, value]);

Expand Down
33 changes: 32 additions & 1 deletion src/lib/intlFormat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
monthYearFormat,
numberFormat,
percentageFormat,
validateAndFormatInvalidDate,
} from './intlFormat';

describe('intlFormat', () => {
Expand Down Expand Up @@ -186,14 +187,44 @@ describe('intlFormat', () => {
expect(date).toBeNull();
});

it('returns if month is null', () => {
it('handle an invalid date', () => {
const date = dateFromParts(2000, 0, 0, locale);

expect(date).toBe('0/0/2000 - Invalid Date, please fix.');
});

it('handle an invalid date without a year', () => {
const date = dateFromParts(null, 0, 0, locale);

expect(date).toBe('0/0/2020 - Invalid Date, please fix.');
});

it('handle an invalid date where we can not format the invalid date', () => {
const date = dateFromParts(0, 0, 2000, locale);

expect(date).toBe(
'Invalid Date - you specified 0 (of type number) as a month, which is invalid',
);
});
});

describe('validateAndFormatInvalidDate', () => {
const locale = 'en-US';
it('returns invalid date en-US formatted', () => {
const date = validateAndFormatInvalidDate(2000, 0, 0, locale);
expect(date.formattedInvalidDate).toBe('0/0/2000');
});

it('returns invalid date en-UK formatted', () => {
const date = validateAndFormatInvalidDate(2000, 0, 0, 'en-UK');
expect(date.formattedInvalidDate).toBe('0/00/2000');
});

it('returns invalid date de formatted', () => {
const date = validateAndFormatInvalidDate(2000, 0, 0, 'de');
expect(date.formattedInvalidDate).toBe('0.0.2000');
});
});
//this test often fails locally. It passes on github.
describe('dateTimeFormat', () => {
const locale = 'en-US';
Expand Down
Loading

0 comments on commit eb907de

Please sign in to comment.