Skip to content
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

Helpscout - Show invalid date in person modal with error to fix #962

Merged
merged 3 commits into from
Jun 20, 2024
Merged
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
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 @@
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 @@
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 @@
setFieldValue,
} = formikProps;

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

Check warning on line 74 in src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/PersonShowMore/PersonShowMore.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/PersonShowMore/PersonShowMore.tsx#L74

Added line #L74 was not covered by tests
}

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 @@
<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
Loading