-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Expense rules phase 2 #79659
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
Merged
Merged
Expense rules phase 2 #79659
Changes from 6 commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
1c6f6fd
Expense rules phase 2
situchan 11c3849
Merge branch 'main' of https://github.com/situchan/Expensify into fea…
situchan 913bcbe
tax support, hash key for duplicated merchantToMatch
situchan defe53a
edit rule suport
situchan fe1a35c
Merge branch 'main' of https://github.com/situchan/Expensify into fea…
situchan d44315b
edit support fot category, tag, tax rate
situchan e93c87c
Merge branch 'main' of https://github.com/situchan/Expensify into fea…
situchan 4c49b61
minor fix
situchan c0fc0f0
add translations
situchan 26166d9
address feedback
situchan fc816ae
address feedback
situchan 56d2c6e
simplify form error logic
situchan a6388a0
fix exhaustive-deps lint
situchan 3fa0316
hide Expense Rules section on Account settings
situchan b30e6e4
auto save when select option
situchan 693d23b
fix typo
situchan 30b2b7f
Merge branch 'main' of https://github.com/situchan/Expensify into fea…
situchan 6beea65
address feedback
situchan 677eb6d
Merge branch 'main' of https://github.com/situchan/Expensify into fea…
situchan 2a47ac9
show not found page when rule changed
situchan a4b1f89
handle duplicated rules
situchan 041375c
Merge branch 'main' of https://github.com/situchan/Expensify into fea…
situchan 235b28e
add unit tests
situchan eb7f1d6
update section title style
situchan 9f35ddd
fix test
situchan b4ed717
remove "Then apply these updates:"
situchan 3c79aaa
fix not found page briefly shows
situchan a0eb317
Merge branch 'main' of https://github.com/situchan/Expensify into fea…
situchan 7215c1a
Revert "remove "Then apply these updates:""
situchan 521d175
update copies
situchan 7e4efdb
update translations
situchan f3b0dda
minor update
situchan 5a905f4
Merge branch 'main' of https://github.com/situchan/Expensify into fea…
situchan 4decaf6
remove isLoading
situchan cf826a9
handle encoding of special characters
situchan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import React, {useCallback, useEffect, useMemo, useState} from 'react'; | ||
| import {View} from 'react-native'; | ||
| import type {ValueOf} from 'type-fest'; | ||
| import FixedFooter from '@components/FixedFooter'; | ||
| import HeaderWithBackButton from '@components/HeaderWithBackButton'; | ||
| import ScreenWrapper from '@components/ScreenWrapper'; | ||
| import SearchFilterPageFooterButtons from '@components/Search/SearchFilterPageFooterButtons'; | ||
| import SelectionList from '@components/SelectionList'; | ||
| import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; | ||
| import type {ListItem} from '@components/SelectionList/ListItem/types'; | ||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| import usePrevious from '@hooks/usePrevious'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import {updateDraftRule} from '@libs/actions/User'; | ||
| import Navigation from '@libs/Navigation/Navigation'; | ||
| import CONST from '@src/CONST'; | ||
| import type {TranslationPaths} from '@src/languages/types'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import ROUTES from '@src/ROUTES'; | ||
| import type {InputID} from '@src/types/form/ExpenseRuleForm'; | ||
| import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; | ||
|
|
||
| type BooleanFilterItem = ListItem & { | ||
| value: ValueOf<typeof CONST.SEARCH.BOOLEAN>; | ||
| }; | ||
|
|
||
| type RuleBooleanBasePageProps = { | ||
| /** The key from boolean-based InputID */ | ||
| fieldID: InputID; | ||
|
|
||
| /** The translation key for the page title */ | ||
| titleKey: TranslationPaths; | ||
|
|
||
| /** The rule identifier */ | ||
| hash?: string; | ||
| }; | ||
|
|
||
| const booleanValues = Object.values(CONST.SEARCH.BOOLEAN); | ||
|
|
||
| function RuleBooleanBasePage({fieldID, titleKey, hash}: RuleBooleanBasePageProps) { | ||
| const {translate} = useLocalize(); | ||
| const [form, formMetadata] = useOnyx(ONYXKEYS.FORMS.EXPENSE_RULE_FORM, {canBeMissing: true}); | ||
| const styles = useThemeStyles(); | ||
|
|
||
| const isLoading = isLoadingOnyxValue(formMetadata); | ||
| const prevIsLoading = usePrevious(isLoading); | ||
|
|
||
| // Called only first time when Onyx loading finished | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
|
situchan marked this conversation as resolved.
Outdated
|
||
| const getInitialValue = () => | ||
| booleanValues.find((value) => { | ||
| if (!form?.[fieldID]) { | ||
| return false; | ||
| } | ||
| const booleanValue = form[fieldID] === 'true' ? CONST.SEARCH.BOOLEAN.YES : CONST.SEARCH.BOOLEAN.NO; | ||
| return booleanValue === value; | ||
| }) ?? null; | ||
|
|
||
| const [selectedItem, setSelectedItem] = useState<string | null>(getInitialValue); | ||
|
|
||
|
situchan marked this conversation as resolved.
Outdated
|
||
| useEffect(() => { | ||
| if (isLoading || !prevIsLoading) { | ||
| return; | ||
| } | ||
| // eslint-disable-next-line react-hooks/set-state-in-effect | ||
| setSelectedItem(getInitialValue()); | ||
|
JS00001 marked this conversation as resolved.
Outdated
|
||
| }, [isLoading, prevIsLoading, getInitialValue]); | ||
|
|
||
| const items = useMemo(() => { | ||
|
JS00001 marked this conversation as resolved.
Outdated
|
||
| return booleanValues.map((value) => ({ | ||
| value, | ||
| keyForList: value, | ||
| text: translate(`common.${value}`), | ||
| isSelected: selectedItem === value, | ||
| })); | ||
| }, [selectedItem, translate]); | ||
|
|
||
| const onSelectItem = useCallback((selectedValue: BooleanFilterItem) => { | ||
| const newValue = selectedValue.isSelected ? null : selectedValue.value; | ||
| setSelectedItem(newValue); | ||
| }, []); | ||
|
|
||
| const goBack = () => { | ||
| Navigation.goBack(hash ? ROUTES.SETTINGS_RULES_EDIT.getRoute(hash) : ROUTES.SETTINGS_RULES_ADD.getRoute()); | ||
| }; | ||
|
|
||
| const applyChanges = () => { | ||
| updateDraftRule({[fieldID]: selectedItem === CONST.SEARCH.BOOLEAN.YES ? 'true' : 'false'}); | ||
| goBack(); | ||
|
situchan marked this conversation as resolved.
Outdated
|
||
| }; | ||
|
|
||
| return ( | ||
| <ScreenWrapper | ||
| testID="RuleBooleanBasePage" | ||
| shouldShowOfflineIndicatorInWideScreen | ||
| offlineIndicatorStyle={styles.mtAuto} | ||
| includeSafeAreaPaddingBottom | ||
| shouldEnableMaxHeight | ||
| > | ||
| <HeaderWithBackButton | ||
| title={translate(titleKey)} | ||
| onBackButtonPress={goBack} | ||
| /> | ||
| <View style={[styles.flex1]}> | ||
| <SelectionList | ||
| shouldSingleExecuteRowSelect | ||
| data={items} | ||
| ListItem={SingleSelectListItem} | ||
| onSelectRow={onSelectItem} | ||
| /> | ||
| </View> | ||
| <FixedFooter style={styles.mtAuto}> | ||
| <SearchFilterPageFooterButtons applyChanges={applyChanges} /> | ||
| </FixedFooter> | ||
| </ScreenWrapper> | ||
| ); | ||
| } | ||
|
|
||
| export default RuleBooleanBasePage; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import React from 'react'; | ||
| import type {FormOnyxValues} from '@components/Form/types'; | ||
| import HeaderWithBackButton from '@components/HeaderWithBackButton'; | ||
| import ScreenWrapper from '@components/ScreenWrapper'; | ||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import {updateDraftRule} from '@libs/actions/User'; | ||
| import Navigation from '@libs/Navigation/Navigation'; | ||
| import CONST from '@src/CONST'; | ||
| import type {TranslationPaths} from '@src/languages/types'; | ||
| import type ONYXKEYS from '@src/ONYXKEYS'; | ||
| import ROUTES from '@src/ROUTES'; | ||
| import type {InputID} from '@src/types/form/ExpenseRuleForm'; | ||
| import TextBase from './TextBase'; | ||
|
|
||
| // Text-based field IDs that accept string input | ||
| type RuleTextBaseProps = { | ||
| /** The key from text-based InputID */ | ||
| fieldID: InputID; | ||
|
|
||
| /** The translation key for the page title and input label if labelKey is missing */ | ||
| titleKey: TranslationPaths; | ||
|
|
||
| /** The translation key for the input label */ | ||
| labelKey?: TranslationPaths; | ||
|
|
||
| /** Test ID for the screen wrapper */ | ||
| testID: string; | ||
|
|
||
| /** The translation key for the hint text to display below the TextInput */ | ||
| hintKey?: TranslationPaths; | ||
|
|
||
| /** Whether this field is required */ | ||
| isRequired?: boolean; | ||
|
|
||
| /** The character limit for the input */ | ||
| characterLimit?: number; | ||
|
|
||
| /** The rule identifier */ | ||
| hash?: string; | ||
| }; | ||
|
|
||
| function RuleTextBase({fieldID, hintKey, isRequired, titleKey, labelKey, testID, hash, characterLimit = CONST.MERCHANT_NAME_MAX_BYTES}: RuleTextBaseProps) { | ||
|
situchan marked this conversation as resolved.
|
||
| const {translate} = useLocalize(); | ||
| const styles = useThemeStyles(); | ||
|
|
||
| const goBack = () => { | ||
| Navigation.goBack(hash ? ROUTES.SETTINGS_RULES_EDIT.getRoute(hash) : ROUTES.SETTINGS_RULES_ADD.getRoute()); | ||
| }; | ||
|
|
||
| const onSave = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.EXPENSE_RULE_FORM>) => { | ||
| updateDraftRule(values); | ||
| goBack(); | ||
| }; | ||
|
|
||
| return ( | ||
| <ScreenWrapper | ||
| testID={testID} | ||
| shouldShowOfflineIndicatorInWideScreen | ||
| offlineIndicatorStyle={styles.mtAuto} | ||
| includeSafeAreaPaddingBottom | ||
| shouldEnableMaxHeight | ||
| > | ||
| <HeaderWithBackButton | ||
| title={translate(titleKey)} | ||
| onBackButtonPress={goBack} | ||
| /> | ||
| <TextBase | ||
| fieldID={fieldID} | ||
| hint={hintKey ? translate(hintKey) : undefined} | ||
| isRequired={isRequired} | ||
| label={translate(labelKey ?? titleKey)} | ||
| onSubmit={onSave} | ||
| title={translate(titleKey)} | ||
| characterLimit={characterLimit} | ||
| /> | ||
| </ScreenWrapper> | ||
| ); | ||
| } | ||
|
|
||
| export default RuleTextBase; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import React from 'react'; | ||
| import {View} from 'react-native'; | ||
| import FormProvider from '@components/Form/FormProvider'; | ||
| import InputWrapper from '@components/Form/InputWrapper'; | ||
| import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; | ||
| import TextInput from '@components/TextInput'; | ||
| import useAutoFocusInput from '@hooks/useAutoFocusInput'; | ||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import {isRequiredFulfilled, isValidInputLength} from '@libs/ValidationUtils'; | ||
| import CONST from '@src/CONST'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import type {InputID} from '@src/types/form/ExpenseRuleForm'; | ||
| import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; | ||
|
|
||
| type TextBaseProps = { | ||
| fieldID: InputID; | ||
| hint?: string; | ||
| isRequired?: boolean; | ||
| title: string; | ||
| label: string; | ||
| characterLimit?: number; | ||
| onSubmit: (values: FormOnyxValues<typeof ONYXKEYS.FORMS.EXPENSE_RULE_FORM>) => void; | ||
| }; | ||
|
|
||
| function TextBase({fieldID, hint, isRequired, title, label, onSubmit, characterLimit = CONST.MERCHANT_NAME_MAX_BYTES}: TextBaseProps) { | ||
| const {translate} = useLocalize(); | ||
| const [form, formMetadata] = useOnyx(ONYXKEYS.FORMS.EXPENSE_RULE_FORM, {canBeMissing: true}); | ||
| const styles = useThemeStyles(); | ||
|
|
||
| const isLoading = isLoadingOnyxValue(formMetadata); | ||
|
|
||
| const currentValue = form?.[fieldID] ?? ''; | ||
| const {inputCallbackRef} = useAutoFocusInput(); | ||
|
|
||
| const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.EXPENSE_RULE_FORM>) => { | ||
| const errors: FormInputErrors<typeof ONYXKEYS.FORMS.EXPENSE_RULE_FORM> = {}; | ||
| const fieldValue = values[fieldID] ?? ''; | ||
|
|
||
| if (typeof fieldValue !== 'string') { | ||
| return errors; | ||
| } | ||
|
|
||
| const trimmedValue = fieldValue.trim(); | ||
|
|
||
| if (isRequired && !isRequiredFulfilled(fieldValue)) { | ||
| errors[fieldID] = translate('common.error.fieldRequired'); | ||
| } else { | ||
| const {isValid, byteLength} = isValidInputLength(trimmedValue, characterLimit); | ||
|
|
||
| if (!isValid) { | ||
| errors[fieldID] = translate('common.error.characterLimitExceedCounter', byteLength, characterLimit); | ||
| } | ||
| } | ||
|
|
||
| return errors; | ||
| }; | ||
|
|
||
| if (isLoading) { | ||
| return null; | ||
|
situchan marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| return ( | ||
| <FormProvider | ||
| style={[styles.flex1, styles.ph5]} | ||
| formID={ONYXKEYS.FORMS.EXPENSE_RULE_FORM} | ||
| validate={validate} | ||
| onSubmit={onSubmit} | ||
| submitButtonText={translate('common.save')} | ||
| enabledWhenOffline | ||
| > | ||
| <View style={styles.mb5}> | ||
| <InputWrapper | ||
| hint={hint} | ||
| InputComponent={TextInput} | ||
| inputID={fieldID} | ||
| name={fieldID} | ||
| defaultValue={typeof currentValue === 'string' ? currentValue : undefined} | ||
| label={label} | ||
| accessibilityLabel={title} | ||
| role={CONST.ROLE.PRESENTATION} | ||
| ref={inputCallbackRef} | ||
| /> | ||
| </View> | ||
| </FormProvider> | ||
| ); | ||
| } | ||
|
|
||
| export default TextBase; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.