-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Create merchant rule #80545
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
Create merchant rule #80545
Changes from all commits
3eb1a09
c6599f3
05b79ef
f78eb95
971c5a3
60571ee
6d0d4ae
a82da08
18d62df
c7674f7
5adaf6d
f513c46
be25915
ccf233c
35cf978
f6a68fa
ffebc83
b3a5b7b
d85d81c
2b7c42c
d8fffb4
4c5ec75
1d41a3e
cb1f840
2b3a78b
faa093b
379450f
2a0e6a8
1cb2a25
f17d95c
31df6dd
5836397
5827e99
8296d9a
d4f8109
562e1b5
f10f523
ee66a66
17d70d6
e5fa4f4
a9cba1c
d45262f
174c531
2609db3
d269497
5ed5632
6f34508
6d7aa25
4509a80
db5111c
f64e88d
e47e5aa
c347e9d
fb2d8d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,45 +9,54 @@ import type {ListItem} from '@components/SelectionList/ListItem/types'; | |
| import useLocalize from '@hooks/useLocalize'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| 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 type {OnyxFormKey} from '@src/ONYXKEYS'; | ||
| import RuleNotFoundPageWrapper from './RuleNotFoundPageWrapper'; | ||
|
|
||
| type BooleanFilterItem = ListItem & { | ||
| value: ValueOf<typeof CONST.SEARCH.BOOLEAN>; | ||
| }; | ||
|
|
||
| type RuleBooleanBasePageProps = { | ||
| /** The key from boolean-based InputID */ | ||
| fieldID: InputID; | ||
| type RuleBooleanBaseProps = { | ||
| /** The field ID from the form */ | ||
| fieldID: string; | ||
|
|
||
| /** The translation key for the page title */ | ||
| titleKey: TranslationPaths; | ||
|
|
||
| /** The rule identifier */ | ||
| /** The form ID to read from Onyx */ | ||
| formID: OnyxFormKey; | ||
|
|
||
| /** Callback when a value is selected - receives boolean for merchant rules, string for personal rules */ | ||
| onSelect: (fieldID: string, value: boolean | 'true' | 'false' | null) => void; | ||
|
|
||
| /** Callback to go back */ | ||
| onBack: () => void; | ||
|
|
||
| /** Optional hash for rule not found validation */ | ||
| hash?: string; | ||
|
|
||
| /** Whether to use string values ('true'/'false') instead of boolean values (for ExpenseRuleForm compatibility) */ | ||
| useStringValues?: boolean; | ||
|
Comment on lines
+40
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any plan to use real boolean for personal rule? Or we always use "true", "false" string?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not right now because that'd involve migrating data we have stored, which is not a priority |
||
| }; | ||
|
|
||
| const booleanValues = Object.values(CONST.SEARCH.BOOLEAN); | ||
|
|
||
| function RuleBooleanBasePage({fieldID, titleKey, hash}: RuleBooleanBasePageProps) { | ||
| function RuleBooleanBase({fieldID, titleKey, formID, onSelect, onBack, hash, useStringValues = false}: RuleBooleanBaseProps) { | ||
| const {translate} = useLocalize(); | ||
| const [form] = useOnyx(ONYXKEYS.FORMS.EXPENSE_RULE_FORM, {canBeMissing: true}); | ||
| const [form] = useOnyx(formID, {canBeMissing: true}); | ||
| const styles = useThemeStyles(); | ||
|
|
||
| const selectedItem = | ||
| 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 formValue = (form as Record<string, boolean | string | undefined>)?.[fieldID]; | ||
|
|
||
| let selectedItem = null; | ||
| if (formValue !== undefined) { | ||
| // Handle both string ('true'/'false') and boolean (true/false) values | ||
| const isTruthy = useStringValues ? formValue === 'true' : formValue === true; | ||
| const booleanValue = isTruthy ? CONST.SEARCH.BOOLEAN.YES : CONST.SEARCH.BOOLEAN.NO; | ||
| selectedItem = booleanValues.find((value) => booleanValue === value) ?? null; | ||
| } | ||
|
|
||
| const items = booleanValues.map((value) => ({ | ||
| value, | ||
|
|
@@ -56,34 +65,34 @@ function RuleBooleanBasePage({fieldID, titleKey, hash}: RuleBooleanBasePageProps | |
| isSelected: selectedItem === value, | ||
| })); | ||
|
|
||
| const goBack = () => { | ||
| Navigation.goBack(hash ? ROUTES.SETTINGS_RULES_EDIT.getRoute(hash) : ROUTES.SETTINGS_RULES_ADD.getRoute()); | ||
| }; | ||
|
|
||
| const onSelectItem = (selectedValue: BooleanFilterItem) => { | ||
| const newValue = selectedValue.isSelected ? null : selectedValue.value; | ||
| let value = ''; | ||
| if (newValue === CONST.SEARCH.BOOLEAN.YES) { | ||
| value = 'true'; | ||
| } else if (newValue === CONST.SEARCH.BOOLEAN.NO) { | ||
| value = 'false'; | ||
| // If clicking on already-selected item, unselect it (set to undefined) | ||
| if (selectedValue.isSelected) { | ||
| onSelect(fieldID, null); | ||
| return; | ||
| } | ||
| const isYes = selectedValue.value === CONST.SEARCH.BOOLEAN.YES; | ||
| let value: boolean | 'true' | 'false'; | ||
| if (useStringValues) { | ||
| value = isYes ? 'true' : 'false'; | ||
| } else { | ||
| value = isYes; | ||
| } | ||
| updateDraftRule({[fieldID]: value}); | ||
| goBack(); | ||
| onSelect(fieldID, value); | ||
| }; | ||
|
|
||
| return ( | ||
| <RuleNotFoundPageWrapper hash={hash}> | ||
| <ScreenWrapper | ||
| testID="RuleBooleanBasePage" | ||
| testID="RuleBooleanBase" | ||
| shouldShowOfflineIndicatorInWideScreen | ||
| offlineIndicatorStyle={styles.mtAuto} | ||
| includeSafeAreaPaddingBottom | ||
| shouldEnableMaxHeight | ||
| > | ||
| <HeaderWithBackButton | ||
| title={translate(titleKey)} | ||
| onBackButtonPress={goBack} | ||
| onBackButtonPress={onBack} | ||
| /> | ||
| <View style={[styles.flex1]}> | ||
| <SelectionList | ||
|
|
@@ -98,4 +107,4 @@ function RuleBooleanBasePage({fieldID, titleKey, hash}: RuleBooleanBasePageProps | |
| ); | ||
| } | ||
|
|
||
| export default RuleBooleanBasePage; | ||
| export default RuleBooleanBase; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import React from 'react'; | ||
| import {View} from 'react-native'; | ||
| import HeaderWithBackButton from '@components/HeaderWithBackButton'; | ||
| import ScreenWrapper from '@components/ScreenWrapper'; | ||
| import SearchSingleSelectionPicker from '@components/Search/SearchSingleSelectionPicker'; | ||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import type {TranslationPaths} from '@src/languages/types'; | ||
| import type {Route} from '@src/ROUTES'; | ||
| import RuleNotFoundPageWrapper from './RuleNotFoundPageWrapper'; | ||
|
|
||
| type SelectionItem = { | ||
| name: string; | ||
| value: string; | ||
| }; | ||
|
|
||
| type RuleSelectionBaseProps = { | ||
| /** The translation key for the page title */ | ||
| titleKey: TranslationPaths; | ||
|
|
||
| /** Test ID for the screen wrapper */ | ||
| testID: string; | ||
|
|
||
| /** The currently selected item */ | ||
| selectedItem?: SelectionItem; | ||
|
|
||
| /** The list of items to display */ | ||
| items: SelectionItem[]; | ||
|
|
||
| /** Callback when a value is selected */ | ||
| onSave: (value?: string) => void; | ||
|
|
||
| /** Callback to go back */ | ||
| onBack: () => void; | ||
|
|
||
| /** The route to navigate back to */ | ||
| backToRoute: Route; | ||
|
|
||
| /** Optional hash for rule not found validation */ | ||
| hash?: string; | ||
| }; | ||
|
|
||
| function RuleSelectionBase({titleKey, testID, selectedItem, items, onSave, onBack, backToRoute, hash}: RuleSelectionBaseProps) { | ||
| const styles = useThemeStyles(); | ||
| const {translate} = useLocalize(); | ||
|
|
||
| return ( | ||
| <RuleNotFoundPageWrapper hash={hash}> | ||
| <ScreenWrapper | ||
| testID={testID} | ||
| shouldShowOfflineIndicatorInWideScreen | ||
| offlineIndicatorStyle={styles.mtAuto} | ||
| shouldEnableMaxHeight | ||
| > | ||
| <HeaderWithBackButton | ||
| title={translate(titleKey)} | ||
| onBackButtonPress={onBack} | ||
| /> | ||
| <View style={[styles.flex1]}> | ||
| <SearchSingleSelectionPicker | ||
| backToRoute={backToRoute} | ||
| initiallySelectedItem={selectedItem} | ||
| items={items} | ||
| onSaveSelection={onSave} | ||
| shouldAutoSave | ||
| /> | ||
| </View> | ||
| </ScreenWrapper> | ||
| </RuleNotFoundPageWrapper> | ||
| ); | ||
| } | ||
|
|
||
| export default RuleSelectionBase; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NAB: This is actually rule ID so original jsdoc
The rule identifieris also correct.Personal rule doesn't have its own id.