diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx index 542b16b5c342..2017856e8fec 100644 --- a/src/components/MoneyRequestAmountInput.tsx +++ b/src/components/MoneyRequestAmountInput.tsx @@ -99,6 +99,9 @@ type MoneyRequestAmountInputProps = { /** Whether to allow direct negative input (for split amounts where value is already negative) */ allowNegativeInput?: boolean; + /** Style for the negative symbol */ + negativeSymbolStyle?: StyleProp; + /** The testID of the input. Used to locate this view in end-to-end tests. */ testID?: string; @@ -181,6 +184,7 @@ function MoneyRequestAmountInput({ isNegative = false, allowFlippingAmount = false, allowNegativeInput = false, + negativeSymbolStyle, toggleNegative, clearNegative, ref, @@ -274,6 +278,7 @@ function MoneyRequestAmountInput({ touchableInputWrapperStyle={props.touchableInputWrapperStyle} contentWidth={contentWidth} isNegative={isNegative} + negativeSymbolStyle={negativeSymbolStyle} testID={testID} errorText={props.errorText} footer={props.footer} diff --git a/src/components/NumberWithSymbolForm.tsx b/src/components/NumberWithSymbolForm.tsx index fc816fe924ba..3ba24c3f2c60 100644 --- a/src/components/NumberWithSymbolForm.tsx +++ b/src/components/NumberWithSymbolForm.tsx @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import type {ForwardedRef} from 'react'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import type {KeyboardTypeOptions, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; +import type {KeyboardTypeOptions, NativeSyntheticEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -90,6 +90,9 @@ type NumberWithSymbolFormProps = { /** Whether to allow direct negative input (for split amounts where value is already negative) */ allowNegativeInput?: boolean; + /** Style for the negative symbol */ + negativeSymbolStyle?: StyleProp; + /** Whether to use dynamic font size for the amount input */ shouldUseDynamicFontSize?: boolean; @@ -182,6 +185,7 @@ function NumberWithSymbolForm({ isNegative = false, allowFlippingAmount = false, allowNegativeInput = false, + negativeSymbolStyle, toggleNegative, clearNegative, ref, @@ -577,6 +581,7 @@ function NumberWithSymbolForm({ prefixContainerStyle={props.prefixContainerStyle} touchableInputWrapperStyle={props.touchableInputWrapperStyle} isNegative={isNegative} + negativeSymbolStyle={negativeSymbolStyle} toggleNegative={toggleNegative} onFocus={props.onFocus} accessibilityLabel={props.accessibilityLabel} diff --git a/src/components/Table/EditableCell/EditableCell.tsx b/src/components/Table/EditableCell/EditableCell.tsx index d773aa0b0b08..5c822a769e01 100644 --- a/src/components/Table/EditableCell/EditableCell.tsx +++ b/src/components/Table/EditableCell/EditableCell.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useId} from 'react'; import type {ReactNode, RefObject} from 'react'; import {View} from 'react-native'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import {useEditingCellActions} from './EditingCellContext'; @@ -47,8 +47,8 @@ type EditableCellProps = { */ function EditableCell({children, editContent, popoverContent, isEditing, canEdit, onStartEditing, anchorRef}: EditableCellProps) { const styles = useThemeStyles(); - const {isLargeScreenWidth} = useResponsiveLayout(); - const isEditable = isLargeScreenWidth; + const {isLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP(); + const isEditable = isLargeScreenWidth && !shouldUseNarrowLayout; const cellId = useId(); const {setIsEditingCell, setFocusedCellId} = useEditingCellActions(); diff --git a/src/components/TextInputWithSymbol/BaseTextInputWithSymbol.tsx b/src/components/TextInputWithSymbol/BaseTextInputWithSymbol.tsx index a5827b9c9602..2346e77e6b73 100644 --- a/src/components/TextInputWithSymbol/BaseTextInputWithSymbol.tsx +++ b/src/components/TextInputWithSymbol/BaseTextInputWithSymbol.tsx @@ -24,6 +24,7 @@ function BaseTextInputWithSymbol({ style, symbolTextStyle, isNegative = false, + negativeSymbolStyle, rightHandSideComponent, ref, disabled, @@ -42,7 +43,7 @@ function BaseTextInputWithSymbol({ onChangeAmount(newAmount); }; - const negativeSymbol = -; + const negativeSymbol = -; return ( <> diff --git a/src/components/TextInputWithSymbol/types.ts b/src/components/TextInputWithSymbol/types.ts index 6f5dc80cb359..df88419b6467 100644 --- a/src/components/TextInputWithSymbol/types.ts +++ b/src/components/TextInputWithSymbol/types.ts @@ -87,6 +87,9 @@ type BaseTextInputWithSymbolProps = { /** Function to toggle the amount to negative */ toggleNegative?: () => void; + /** Style for the negative symbol */ + negativeSymbolStyle?: StyleProp; + /** The test ID of TextInput. Used to locate the view in end-to-end tests. */ testID?: string; diff --git a/src/components/TransactionItemRow/DataCells/TotalCell.tsx b/src/components/TransactionItemRow/DataCells/TotalCell.tsx index 347298cd2ab6..4e8ef923e59d 100644 --- a/src/components/TransactionItemRow/DataCells/TotalCell.tsx +++ b/src/components/TransactionItemRow/DataCells/TotalCell.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useRef} from 'react'; +import React, {useMemo, useRef, useState} from 'react'; import MoneyRequestAmountInput from '@components/MoneyRequestAmountInput'; import {EditableCell, useInlineEditState} from '@components/Table/EditableCell'; import type {EditableProps} from '@components/Table/EditableCell'; @@ -32,12 +32,14 @@ function TotalCell({shouldShowTooltip, transactionItem, canEdit, onSave}: TotalC } const absoluteAmount = Math.abs(amount ?? 0); + const [isNegative, setIsNegative] = useState((amount ?? 0) < 0); const handleAmountSave = (amountString: string) => { const parsedValue = parseFloatAnyLocale(amountString); if (!Number.isNaN(parsedValue) && parsedValue >= 0) { const normalizedValue = roundToTwoDecimalPlaces(parsedValue); - onSave?.(convertToBackendAmount(normalizedValue)); + const finalAmount = isNegative ? -normalizedValue : normalizedValue; + onSave?.(convertToBackendAmount(finalAmount)); } }; @@ -56,6 +58,11 @@ function TotalCell({shouldShowTooltip, transactionItem, canEdit, onSave}: TotalC ref?.focus(); }; + const handleStartEditing = () => { + setIsNegative((amount ?? 0) < 0); + startEditing(); + }; + const handleAmountChange = (amountString: string) => { setLocalValue(amountString); }; @@ -65,6 +72,10 @@ function TotalCell({shouldShowTooltip, transactionItem, canEdit, onSave}: TotalC return convertToFrontendAmountAsString(amountAsInt, decimals); }; + const toggleNegative = () => setIsNegative((prev) => !prev); + + const clearNegative = () => setIsNegative(false); + const handleEscape = () => { cancelEditing(); inputRef.current?.blur(); @@ -99,7 +110,7 @@ function TotalCell({shouldShowTooltip, transactionItem, canEdit, onSave}: TotalC } > diff --git a/src/libs/actions/TransactionInlineEdit.ts b/src/libs/actions/TransactionInlineEdit.ts index 0563cf5e77e9..da930ae44fa3 100644 --- a/src/libs/actions/TransactionInlineEdit.ts +++ b/src/libs/actions/TransactionInlineEdit.ts @@ -14,7 +14,8 @@ import {getTagLists, isMultiLevelTags} from '@libs/PolicyUtils'; import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {canEditFieldOfMoneyRequest, canEditMoneyRequest, canUserPerformWriteAction, isArchivedReport, isReportInGroupPolicy} from '@libs/ReportUtils'; import {hasEnabledTags} from '@libs/TagsOptionsListUtils'; -import {calculateTaxAmount, getCurrency, getOriginalTransactionWithSplitInfo, getTaxValue, isExpenseUnreported} from '@libs/TransactionUtils'; +import {calculateTaxAmount, getCurrency, getOriginalTransactionWithSplitInfo, getTaxValue, isDistanceRequest, isExpenseUnreported} from '@libs/TransactionUtils'; +import {isInvalidMerchantValue} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type { @@ -190,9 +191,7 @@ function editTransactionDateInline(params: TransactionInlineEditParams, newDate: /** Updates the merchant of an expense from the Search results table or the Expense Report page. */ function editTransactionMerchantInline(params: TransactionInlineEditParams, newMerchant: string) { - // Merchant must be a non-empty string. An empty merchant is not a valid - // state and the IOU action would save it as a blank row label. - if (!newMerchant.trim()) { + if (isInvalidMerchantValue(newMerchant)) { return; } const iouParams = getIouParamsForTransaction(params); @@ -329,6 +328,33 @@ function getTransactionEditPermissions({ } } + if (field === CONST.EDIT_REQUEST_FIELD.MERCHANT) { + // Distance expenses cannot have their merchant edited + if (isDistanceRequest(transaction)) { + return false; + } + } + + if (field === CONST.EDIT_REQUEST_FIELD.CATEGORY) { + // Matches MoneyRequestView's shouldShowCategory logic + // For policy expenses, check if there's a category or enabled options + if (isPolicyExpenseChat) { + return !!categoryForDisplay || hasEnabledOptions(policyCategories ?? {}); + } + // For unreported expenses, allow if no policy or if categories are enabled with options + if (isUnreported) { + return !policy || hasEnabledOptions(policyCategories ?? {}); + } + } + + if (field === CONST.EDIT_REQUEST_FIELD.TAG) { + // Single-level tags only (multi-level needs a picker UI not available inline) + if (isMultiLevelTags(policyTags)) { + return false; + } + return !!transaction?.tag || hasEnabledTags(policyTagLists); + } + return ( isUnreported || canEditFieldOfMoneyRequest({ @@ -347,12 +373,9 @@ function getTransactionEditPermissions({ canEditMerchant: canEditRestricted(CONST.EDIT_REQUEST_FIELD.MERCHANT), // Non-restricted; always editable when canEdit is true canEditDescription: true, - // Matches MoneyRequestView's shouldShowCategory logic - canEditCategory: - (isPolicyExpenseChat && (!!categoryForDisplay || hasEnabledOptions(policyCategories ?? {}))) || (isUnreported && (!policy || hasEnabledOptions(policyCategories ?? {}))), + canEditCategory: canEditRestricted(CONST.EDIT_REQUEST_FIELD.CATEGORY), canEditAmount: canEditRestricted(CONST.EDIT_REQUEST_FIELD.AMOUNT), - // single-level tags only (multi-level needs a picker UI not available inline). - canEditTag: !isMultiLevelTags(policyTags) && (!!transaction?.tag || hasEnabledTags(policyTagLists)), + canEditTag: canEditRestricted(CONST.EDIT_REQUEST_FIELD.TAG), }; } diff --git a/src/styles/index.ts b/src/styles/index.ts index 5a740b894ac7..d6c7b3b8ed94 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1168,6 +1168,7 @@ const staticStyles = (theme: ThemeColors) => ...FontUtils.fontFamily.platform.EXP_NEUE, color: theme.textSupporting, fontSize: variables.fontSizeNormal, + lineHeight: variables.fontSizeNormalHeight, }, borderColorFocus: {