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

feat: Step 3 UI #51667

Merged
merged 30 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bd49f5c
feat: step 3
MrMuzyk Oct 28, 2024
321b51b
Merge branch 'main' of github.com:Expensify/App into feat/step-3-ui
MrMuzyk Oct 28, 2024
22d5afd
Merge branch 'main' of github.com:Expensify/App into feat/step-3-ui
MrMuzyk Oct 29, 2024
0467eb2
Merge branch 'main' of github.com:Expensify/App into feat/step-3-ui
MrMuzyk Oct 29, 2024
d66962c
feat: step 3 pt 2
MrMuzyk Oct 29, 2024
a7dca3c
feat: remove mocked provinces and add comment to remaining mock
MrMuzyk Oct 29, 2024
7e86dc5
Merge branch 'main' of github.com:Expensify/App into feat/step-3-ui
MrMuzyk Oct 29, 2024
d97c3f3
fix: prettier
MrMuzyk Oct 29, 2024
6d1a23f
fix: linter, ts
MrMuzyk Oct 29, 2024
5438548
Merge branch 'main' of github.com:Expensify/App into feat/step-3-ui
MrMuzyk Oct 30, 2024
618b560
fix: simplify PushRowWithModal
MrMuzyk Oct 30, 2024
9cd5c5e
fix: polishing fixes
MrMuzyk Oct 31, 2024
a50dac3
fix: validation fixes
MrMuzyk Oct 31, 2024
4a2e40f
fix: replace substring
MrMuzyk Oct 31, 2024
7bc7df5
Merge branch 'main' of github.com:Expensify/App into feat/step-3-ui
MrMuzyk Oct 31, 2024
e077889
fix: AddressBusiness
MrMuzyk Oct 31, 2024
3ae86bc
fix: change resetting so autocomplete works
MrMuzyk Oct 31, 2024
bc4e6d0
fix: move lists outside of component
MrMuzyk Oct 31, 2024
789f0af
fix: cr fixes and translations
MrMuzyk Nov 4, 2024
c38f54e
fix: typings
MrMuzyk Nov 4, 2024
3c1509b
Merge branch 'main' of github.com:Expensify/App into feat/step-3-ui
MrMuzyk Nov 4, 2024
f0fe0b2
Merge branch 'main' of github.com:Expensify/App into feat/step-3-ui
MrMuzyk Nov 5, 2024
9fcafe6
fix: fix crash
MrMuzyk Nov 5, 2024
b20e4e0
fix: minor correction
MrMuzyk Nov 5, 2024
8e089e2
fix: Improve phone number validation
MrMuzyk Nov 5, 2024
a0e96f1
fix: fix state displaying and validation
MrMuzyk Nov 5, 2024
f9c2db5
Merge branch 'main' of github.com:Expensify/App into feat/step-3-ui
MrMuzyk Nov 5, 2024
bff7aaf
fix: reset state errors whenever stepField change
MrMuzyk Nov 6, 2024
370b0b3
Merge branch 'main' of github.com:Expensify/App into feat/step-3-ui
MrMuzyk Nov 6, 2024
ce274cb
fix: tighten phone number validation
MrMuzyk Nov 6, 2024
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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
"date-fns-tz": "^3.2.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
"expensify-common": "2.0.101",
"expensify-common": "2.0.106",
"expo": "51.0.31",
"expo-av": "14.0.7",
"expo-image": "1.12.15",
Expand Down
8 changes: 4 additions & 4 deletions src/components/CountryPicker/CountrySelectorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import RadioListItem from '@components/SelectionList/RadioListItem';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import searchCountryOptions from '@libs/searchCountryOptions';
import type {CountryData} from '@libs/searchCountryOptions';
import searchOptions from '@libs/searchOptions';
import type {Option} from '@libs/searchOptions';
import StringUtils from '@libs/StringUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
Expand All @@ -27,7 +27,7 @@ type CountrySelectorModalProps = {
currentCountry: string;

/** Function to call when the user selects a country */
onCountrySelected: (value: CountryData) => void;
onCountrySelected: (value: Option) => void;

/** Function to call when the user presses on the modal backdrop */
onBackdropPress?: () => void;
Expand All @@ -52,7 +52,7 @@ function CountrySelectorModal({isVisible, currentCountry, onCountrySelected, onC
[translate, currentCountry],
);

const searchResults = searchCountryOptions(debouncedSearchValue, countries);
const searchResults = searchOptions(debouncedSearchValue, countries);
const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';

const styles = useThemeStyles();
Expand Down
4 changes: 2 additions & 2 deletions src/components/CountryPicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, {useState} from 'react';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
import type {CountryData} from '@libs/searchCountryOptions';
import type {Option} from '@libs/searchOptions';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import CountrySelectorModal from './CountrySelectorModal';
Expand All @@ -26,7 +26,7 @@ function CountryPicker({value, errorText, onInputChange = () => {}}: CountryPick
setIsPickerVisible(false);
};

const updateInput = (item: CountryData) => {
const updateInput = (item: Option) => {
onInputChange?.(item.value);
hidePickerModal();
};
Expand Down
12 changes: 12 additions & 0 deletions src/components/Form/FormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import CONST from '@src/CONST';
import type {OnyxFormDraftKey, OnyxFormKey} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Form} from '@src/types/form';
import type {Errors} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {RegisterInput} from './FormContext';
import FormContext from './FormContext';
Expand Down Expand Up @@ -244,9 +245,20 @@ function FormProvider(
setErrors({});
}, [formID]);

const resetFormFieldError = useCallback(
(inputID: keyof Form) => {
const newErrors = {...errors};
delete newErrors[inputID];
FormActions.setErrors(formID, newErrors as Errors);
setErrors(newErrors);
},
[errors, formID],
);

useImperativeHandle(forwardedRef, () => ({
resetForm,
resetErrors,
resetFormFieldError,
}));

const registerInput = useCallback<RegisterInput>(
Expand Down
2 changes: 2 additions & 0 deletions src/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type NetSuiteCustomListPicker from '@pages/workspace/accounting/netsuite/
import type NetSuiteMenuWithTopDescriptionForm from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm';
import type {Country} from '@src/CONST';
import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS';
import type {Form} from '@src/types/form';
import type {BaseForm} from '@src/types/form/Form';

/**
Expand Down Expand Up @@ -164,6 +165,7 @@ type FormProps<TFormID extends OnyxFormKey = OnyxFormKey> = {
type FormRef<TFormID extends OnyxFormKey = OnyxFormKey> = {
resetForm: (optionalValue: FormOnyxValues<TFormID>) => void;
resetErrors: () => void;
resetFormFieldError: (fieldID: keyof Form) => void;
};

type InputRefs = Record<string, MutableRefObject<InputComponentBaseProps>>;
Expand Down
61 changes: 24 additions & 37 deletions src/components/PushRowWithModal/PushRowModal.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React, {useEffect, useState} from 'react';
import React, {useMemo} from 'react';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import searchOptions from '@libs/searchOptions';
import StringUtils from '@libs/StringUtils';
import CONST from '@src/CONST';

type PushRowModalProps = {
Expand Down Expand Up @@ -40,44 +43,28 @@ type ListItemType = {
function PushRowModal({isVisible, selectedOption, onOptionChange, onClose, optionsList, headerTitle, searchInputTitle}: PushRowModalProps) {
const {translate} = useLocalize();

const allOptions = Object.entries(optionsList).map(([key, value]) => ({
value: key,
text: value,
keyForList: key,
isSelected: key === selectedOption,
}));
const [searchbarInputText, setSearchbarInputText] = useState('');
const [optionListItems, setOptionListItems] = useState(allOptions);

useEffect(() => {
setOptionListItems((prevOptionListItems) =>
prevOptionListItems.map((option) => ({
...option,
isSelected: option.value === selectedOption,
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');

const options = useMemo(
() =>
Object.entries(optionsList).map(([key, value]) => ({
value: key,
text: value,
keyForList: key,
isSelected: key === selectedOption,
searchValue: StringUtils.sanitizeString(value),
})),
);
}, [selectedOption]);

const filterShownOptions = (searchText: string) => {
setSearchbarInputText(searchText);
const searchWords = searchText.toLowerCase().match(/[a-z0-9]+/g) ?? [];
setOptionListItems(
allOptions.filter((option) =>
searchWords.every((word) =>
option.text
.toLowerCase()
.replace(/[^a-z0-9]/g, ' ')
.includes(word),
),
),
);
};
[optionsList, selectedOption],
);

const handleSelectRow = (option: ListItemType) => {
onOptionChange(option.value);
onClose();
};

const searchResults = searchOptions(debouncedSearchValue, options);
const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';

return (
<Modal
onClose={onClose}
Expand All @@ -97,13 +84,13 @@ function PushRowModal({isVisible, selectedOption, onOptionChange, onClose, optio
onBackButtonPress={onClose}
/>
<SelectionList
headerMessage={searchbarInputText.trim().length && !optionListItems.length ? translate('common.noResultsFound') : ''}
headerMessage={headerMessage}
textInputLabel={searchInputTitle}
textInputValue={searchbarInputText}
onChangeText={filterShownOptions}
textInputValue={searchValue}
onChangeText={setSearchValue}
onSelectRow={handleSelectRow}
sections={[{data: optionListItems}]}
initiallyFocusedOptionKey={optionListItems.find((option) => option.value === selectedOption)?.keyForList}
sections={[{data: searchResults}]}
initiallyFocusedOptionKey={selectedOption}
showScrollIndicator
shouldShowTooltips={false}
ListItem={RadioListItem}
Expand Down
29 changes: 16 additions & 13 deletions src/components/PushRowWithModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ type PushRowWithModalProps = {
/** The list of options that we want to display where key is option code and value is option name */
optionsList: Record<string, string>;

/** The currently selected option */
selectedOption: string;
/** Current value of the selected item */
value?: string;

/** Function to call when the user selects an option */
onOptionChange: (option: string) => void;
/** Function called whenever list item is selected */
onInputChange?: (value: string, key?: string) => void;

/** Additional styles to apply to container */
wrapperStyles?: StyleProp<ViewStyle>;
Expand All @@ -32,13 +32,12 @@ type PushRowWithModalProps = {
/** Text to display on error message */
errorText?: string;

/** Function called whenever option changes */
onInputChange?: (value: string) => void;
/** The ID of the input that should be reset when the value changes */
stateInputIDToReset?: string;
};

function PushRowWithModal({
selectedOption,
onOptionChange,
value,
optionsList,
wrapperStyles,
description,
Expand All @@ -47,6 +46,7 @@ function PushRowWithModal({
shouldAllowChange = true,
errorText,
onInputChange = () => {},
stateInputIDToReset,
}: PushRowWithModalProps) {
const [isModalVisible, setIsModalVisible] = useState(false);

Expand All @@ -58,16 +58,19 @@ function PushRowWithModal({
setIsModalVisible(true);
};

const handleOptionChange = (value: string) => {
onOptionChange(value);
onInputChange(value);
const handleOptionChange = (optionValue: string) => {
onInputChange(optionValue);

if (stateInputIDToReset) {
onInputChange('', stateInputIDToReset);
}
};

return (
<>
<MenuItemWithTopDescription
description={description}
title={optionsList[selectedOption]}
title={value ? optionsList[value] : ''}
shouldShowRightIcon={shouldAllowChange}
onPress={handleModalOpen}
wrapperStyle={wrapperStyles}
Expand All @@ -77,7 +80,7 @@ function PushRowWithModal({
/>
<PushRowModal
isVisible={isModalVisible}
selectedOption={selectedOption}
selectedOption={value ?? ''}
onOptionChange={handleOptionChange}
onClose={handleModalClose}
optionsList={optionsList}
Expand Down
8 changes: 4 additions & 4 deletions src/components/StatePicker/StateSelectorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import RadioListItem from '@components/SelectionList/RadioListItem';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import searchCountryOptions from '@libs/searchCountryOptions';
import type {CountryData} from '@libs/searchCountryOptions';
import searchOptions from '@libs/searchOptions';
import type {Option} from '@libs/searchOptions';
import StringUtils from '@libs/StringUtils';
import CONST from '@src/CONST';

Expand All @@ -29,7 +29,7 @@ type StateSelectorModalProps = {
currentState: string;

/** Function to call when the user selects a state */
onStateSelected: (value: CountryData) => void;
onStateSelected: (value: Option) => void;

/** Function to call when the user presses on the modal backdrop */
onBackdropPress?: () => void;
Expand All @@ -56,7 +56,7 @@ function StateSelectorModal({isVisible, currentState, onStateSelected, onClose,
[translate, currentState],
);

const searchResults = searchCountryOptions(debouncedSearchValue, countryStates);
const searchResults = searchOptions(debouncedSearchValue, countryStates);
const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';

const styles = useThemeStyles();
Expand Down
4 changes: 2 additions & 2 deletions src/components/StatePicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, {useState} from 'react';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
import type {CountryData} from '@libs/searchCountryOptions';
import type {Option} from '@libs/searchOptions';
import CONST from '@src/CONST';
import StateSelectorModal from './StateSelectorModal';

Expand All @@ -28,7 +28,7 @@ function StatePicker({value, errorText, onInputChange = () => {}}: StatePickerPr
setIsPickerVisible(false);
};

const updateInput = (item: CountryData) => {
const updateInput = (item: Option) => {
onInputChange?.(item.value);
hidePickerModal();
};
Expand Down
Loading
Loading