Skip to content

Commit

Permalink
Merge pull request #35446 from abzokhattab/migrate-settings-profile-t…
Browse files Browse the repository at this point in the history
…o-ts

Migrate 'SettingsProfile' page to TypeScript
  • Loading branch information
tgolen authored Mar 13, 2024
2 parents 37a2dbb + f128c27 commit c8fd623
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 315 deletions.
10 changes: 3 additions & 7 deletions src/components/Form/FormProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import lodashIsEqual from 'lodash/isEqual';
import type {ForwardedRef, MutableRefObject, ReactNode} from 'react';
import type {ForwardedRef, MutableRefObject, ReactNode, RefAttributes} from 'react';
import React, {createRef, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import type {NativeSyntheticEvent, StyleProp, TextInputSubmitEditingEventData, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
Expand All @@ -18,7 +18,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {RegisterInput} from './FormContext';
import FormContext from './FormContext';
import FormWrapper from './FormWrapper';
import type {FormInputErrors, FormOnyxValues, FormProps, InputComponentBaseProps, InputRefs, ValueTypeKey} from './types';
import type {FormInputErrors, FormOnyxValues, FormProps, FormRef, InputComponentBaseProps, InputRefs, ValueTypeKey} from './types';

// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web.
// 200ms delay was chosen as a result of empirical testing.
Expand Down Expand Up @@ -73,10 +73,6 @@ type FormProviderProps<TFormID extends OnyxFormKey = OnyxFormKey> = FormProvider
submitFlexEnabled?: boolean;
};

type FormRef<TFormID extends OnyxFormKey = OnyxFormKey> = {
resetForm: (optionalValue: FormOnyxValues<TFormID>) => void;
};

function FormProvider(
{
formID,
Expand Down Expand Up @@ -393,6 +389,6 @@ export default withOnyx<FormProviderProps, FormProviderOnyxProps>({
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
key: (props) => `${props.formID}Draft` as any,
},
})(forwardRef(FormProvider)) as <TFormID extends OnyxFormKey>(props: Omit<FormProviderProps<TFormID>, keyof FormProviderOnyxProps>) => ReactNode;
})(forwardRef(FormProvider)) as <TFormID extends OnyxFormKey>(props: Omit<FormProviderProps<TFormID> & RefAttributes<FormRef>, keyof FormProviderOnyxProps>) => ReactNode;

export type {FormProviderProps};
19 changes: 18 additions & 1 deletion src/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,25 @@ type FormProps<TFormID extends OnyxFormKey = OnyxFormKey> = {
disablePressOnEnter?: boolean;
};

type FormRef<TFormID extends OnyxFormKey = OnyxFormKey> = {
resetForm: (optionalValue: FormOnyxValues<TFormID>) => void;
};

type InputRefs = Record<string, MutableRefObject<InputComponentBaseProps>>;

type FormInputErrors<TFormID extends OnyxFormKey = OnyxFormKey> = Partial<Record<FormOnyxKeys<TFormID>, MaybePhraseKey>>;

export type {FormProps, ValidInputs, InputComponentValueProps, FormValue, ValueTypeKey, FormOnyxValues, FormOnyxKeys, FormInputErrors, InputRefs, InputComponentBaseProps, ValueTypeMap};
export type {
FormProps,
ValidInputs,
InputComponentValueProps,
FormValue,
ValueTypeKey,
FormOnyxValues,
FormOnyxKeys,
FormInputErrors,
InputRefs,
InputComponentBaseProps,
ValueTypeMap,
FormRef,
};
4 changes: 2 additions & 2 deletions src/libs/actions/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import playSoundExcludingMobile from '@libs/Sound/playSoundExcludingMobile';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {BlockedFromConcierge, FrequentlyUsedEmoji, Policy} from '@src/types/onyx';
import type {BlockedFromConcierge, CustomStatusDraft, FrequentlyUsedEmoji, Policy} from '@src/types/onyx';
import type Login from '@src/types/onyx/Login';
import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer';
import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails';
Expand Down Expand Up @@ -945,7 +945,7 @@ function clearCustomStatus() {
* @param status.emojiCode
* @param status.clearAfter - ISO 8601 format string, which represents the time when the status should be cleared
*/
function updateDraftCustomStatus(status: Status) {
function updateDraftCustomStatus(status: CustomStatusDraft) {
Onyx.merge(ONYXKEYS.CUSTOM_STATUS_DRAFT, status);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import lodashGet from 'lodash/get';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {InteractionManager, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import EmojiPickerButtonDropdown from '@components/EmojiPicker/EmojiPickerButtonDropdown';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormInputErrors, FormOnyxValues, FormRef} from '@components/Form/types';
import HeaderPageLayout from '@components/HeaderPageLayout';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
Expand All @@ -13,55 +15,57 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import DateUtils from '@libs/DateUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/SettingsStatusSetForm';
import type {CustomStatusDraft} from '@src/types/onyx';

const INPUT_IDS = {
EMOJI_CODE: 'emojiCode',
STATUS_TEXT: 'statusText',
type StatusPageOnyxProps = {
draftStatus: OnyxEntry<CustomStatusDraft>;
};

const propTypes = {
...withCurrentUserPersonalDetailsPropTypes,
};
type StatusPageProps = StatusPageOnyxProps & WithCurrentUserPersonalDetailsProps;

const initialEmoji = '💬';

function StatusPage({draftStatus, currentUserPersonalDetails}) {
function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const formRef = useRef(null);
const [brickRoadIndicator, setBrickRoadIndicator] = useState('');
const currentUserEmojiCode = lodashGet(currentUserPersonalDetails, 'status.emojiCode', '');
const currentUserStatusText = lodashGet(currentUserPersonalDetails, 'status.text', '');
const currentUserClearAfter = lodashGet(currentUserPersonalDetails, 'status.clearAfter', '');
const draftEmojiCode = lodashGet(draftStatus, 'emojiCode');
const draftText = lodashGet(draftStatus, 'text');
const draftClearAfter = lodashGet(draftStatus, 'clearAfter');

const formRef = useRef<FormRef>(null);
const [brickRoadIndicator, setBrickRoadIndicator] = useState<ValueOf<typeof CONST.BRICK_ROAD_INDICATOR_STATUS>>();
const currentUserEmojiCode = currentUserPersonalDetails?.status?.emojiCode ?? '';
const currentUserStatusText = currentUserPersonalDetails?.status?.text ?? '';
const currentUserClearAfter = currentUserPersonalDetails?.status?.clearAfter ?? '';
const draftEmojiCode = draftStatus?.emojiCode;
const draftText = draftStatus?.text;
const draftClearAfter = draftStatus?.clearAfter;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const defaultEmoji = draftEmojiCode || currentUserEmojiCode;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const defaultText = draftText || currentUserStatusText;

const customClearAfter = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const dataToShow = draftClearAfter || currentUserClearAfter;
return DateUtils.getLocalizedTimePeriodDescription(dataToShow);
}, [draftClearAfter, currentUserClearAfter]);

const isValidClearAfterDate = useCallback(() => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const clearAfterTime = draftClearAfter || currentUserClearAfter;
if (clearAfterTime === CONST.CUSTOM_STATUS_TYPES.NEVER || clearAfterTime === '') {
return true;
Expand All @@ -72,19 +76,19 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {

const navigateBackToPreviousScreen = useCallback(() => Navigation.goBack(), []);
const updateStatus = useCallback(
({emojiCode, statusText}) => {
({emojiCode, statusText}: FormOnyxValues<typeof ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM>) => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const clearAfterTime = draftClearAfter || currentUserClearAfter || CONST.CUSTOM_STATUS_TYPES.NEVER;
const isValid = DateUtils.isTimeAtLeastOneMinuteInFuture({dateTimeString: clearAfterTime});
if (!isValid && clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER) {
setBrickRoadIndicator(isValidClearAfterDate() ? null : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR);
setBrickRoadIndicator(isValidClearAfterDate() ? undefined : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR);
return;
}
User.updateCustomStatus({
text: statusText,
emojiCode: !emojiCode && statusText ? initialEmoji : emojiCode,
clearAfter: clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER ? clearAfterTime : '',
});

User.clearDraftCustomStatus();
InteractionManager.runAfterInteractions(() => {
navigateBackToPreviousScreen();
Expand All @@ -100,13 +104,14 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
emojiCode: '',
clearAfter: DateUtils.getEndOfToday(),
});
formRef.current.resetForm({[INPUT_IDS.EMOJI_CODE]: ''});
formRef.current?.resetForm({[INPUT_IDS.EMOJI_CODE]: ''});

InteractionManager.runAfterInteractions(() => {
navigateBackToPreviousScreen();
});
};

useEffect(() => setBrickRoadIndicator(isValidClearAfterDate() ? null : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR), [isValidClearAfterDate]);
useEffect(() => setBrickRoadIndicator(isValidClearAfterDate() ? undefined : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR), [isValidClearAfterDate]);

useEffect(() => {
if (!currentUserEmojiCode && !currentUserClearAfter && !draftClearAfter) {
Expand All @@ -119,7 +124,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const validateForm = useCallback(() => {
const validateForm = useCallback((): FormInputErrors<typeof ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM> => {
if (brickRoadIndicator) {
return {clearAfter: ''};
}
Expand Down Expand Up @@ -157,16 +162,17 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
<InputWrapper
InputComponent={EmojiPickerButtonDropdown}
inputID={INPUT_IDS.EMOJI_CODE}
// @ts-expect-error TODO: Remove ts-expect-error when EmojiPickerButtonDropdown migration is done
accessibilityLabel={INPUT_IDS.EMOJI_CODE}
role={CONST.ACCESSIBILITY_ROLE.TEXT}
role={CONST.ROLE.PRESENTATION}
defaultValue={defaultEmoji}
style={styles.mb3}
/>
<InputWrapper
InputComponent={TextInput}
ref={inputCallbackRef}
inputID={INPUT_IDS.STATUS_TEXT}
role={CONST.ACCESSIBILITY_ROLE.TEXT}
role={CONST.ROLE.PRESENTATION}
label={translate('statusPage.message')}
accessibilityLabel={INPUT_IDS.STATUS_TEXT}
defaultValue={defaultText}
Expand Down Expand Up @@ -198,13 +204,11 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
}

StatusPage.displayName = 'StatusPage';
StatusPage.propTypes = propTypes;

export default compose(
withCurrentUserPersonalDetails,
withOnyx({
export default withCurrentUserPersonalDetails(
withOnyx<StatusPageProps, StatusPageOnyxProps>({
draftStatus: {
key: () => ONYXKEYS.CUSTOM_STATUS_DRAFT,
},
}),
)(StatusPage);
})(StatusPage),
);
Loading

0 comments on commit c8fd623

Please sign in to comment.