diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d7da1da78f500..5eec9786d9d61 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1709,15 +1709,23 @@ const ROUTES = { }, WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM: { route: 'workspaces/:policyID/workflows/approvals/expenses-from', - - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/workflows/approvals/expenses-from` as const, backTo), + getRoute: (policyID: string) => `workspaces/${policyID}/workflows/approvals/expenses-from` as const, }, WORKSPACE_WORKFLOWS_APPROVALS_APPROVER: { route: 'workspaces/:policyID/workflows/approvals/approver', - getRoute: (policyID: string, approverIndex: number, backTo?: string) => - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getUrlWithBackToParam(`workspaces/${policyID}/workflows/approvals/approver?approverIndex=${approverIndex}` as const, backTo), + getRoute: (policyID: string, approverIndex: number) => `workspaces/${policyID}/workflows/approvals/approver?approverIndex=${approverIndex}` as const, + }, + WORKSPACE_WORKFLOWS_APPROVALS_APPROVER_CHANGE: { + route: 'workspaces/:policyID/workflows/approvals/approver-change', + getRoute: (policyID: string, approverIndex: number) => `workspaces/${policyID}/workflows/approvals/approver-change?approverIndex=${approverIndex}` as const, + }, + WORKSPACE_WORKFLOWS_APPROVALS_APPROVAL_LIMIT: { + route: 'workspaces/:policyID/workflows/approvals/approval-limit', + getRoute: (policyID: string, approverIndex: number) => `workspaces/${policyID}/workflows/approvals/approval-limit?approverIndex=${approverIndex}` as const, + }, + WORKSPACE_WORKFLOWS_APPROVALS_OVER_LIMIT_APPROVER: { + route: 'workspaces/:policyID/workflows/approvals/over-limit-approver', + getRoute: (policyID: string, approverIndex: number) => `workspaces/${policyID}/workflows/approvals/over-limit-approver?approverIndex=${approverIndex}` as const, }, WORKSPACE_WORKFLOWS_PAYER: { route: 'workspaces/:policyID/workflows/payer', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 199e494285515..4d8240896ff5b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -658,6 +658,9 @@ const SCREENS = { WORKFLOWS_APPROVALS_EDIT: 'Workspace_Approvals_Edit', WORKFLOWS_APPROVALS_EXPENSES_FROM: 'Workspace_Workflows_Approvals_Expenses_From', WORKFLOWS_APPROVALS_APPROVER: 'Workspace_Workflows_Approvals_Approver', + WORKFLOWS_APPROVALS_APPROVER_CHANGE: 'Workspace_Workflows_Approvals_Approver_Change', + WORKFLOWS_APPROVALS_APPROVAL_LIMIT: 'Workspace_Workflows_Approvals_Approval_Limit', + WORKFLOWS_APPROVALS_OVER_LIMIT_APPROVER: 'Workspace_Workflows_Approvals_Over_Limit_Approver', WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency', WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset', WORKFLOWS_CONNECT_EXISTING_BANK_ACCOUNT: 'Workspace_Workflows_Connect_Existing_Bank_Account', diff --git a/src/components/AmountForm.tsx b/src/components/AmountForm.tsx index 5d86de37fc21d..a9d15ec83897e 100644 --- a/src/components/AmountForm.tsx +++ b/src/components/AmountForm.tsx @@ -43,8 +43,14 @@ type AmountFormProps = { /** Whether to hide the currency symbol */ hideCurrencySymbol?: boolean; + /** Whether the input should be disabled */ + disabled?: boolean; + /** Reference to the outer element */ ref?: ForwardedRef; + + /** Callback when the user presses the submit key (Enter) */ + onSubmitEditing?: () => void; } & Pick; /** @@ -62,9 +68,11 @@ function AmountForm({ label, decimals: decimalsProp, hideCurrencySymbol = false, + disabled = false, autoFocus, autoGrowExtraSpace, autoGrowMarginSide, + onSubmitEditing, ref, }: AmountFormProps) { const styles = useThemeStyles(); @@ -99,6 +107,8 @@ function AmountForm({ autoFocus={autoFocus} autoGrowExtraSpace={autoGrowExtraSpace} autoGrowMarginSide={autoGrowMarginSide} + onSubmitEditing={onSubmitEditing} + disabled={disabled} /> ); } diff --git a/src/components/ApprovalWorkflowSection.tsx b/src/components/ApprovalWorkflowSection.tsx index 37c7fbbdae7a2..a037a60044307 100644 --- a/src/components/ApprovalWorkflowSection.tsx +++ b/src/components/ApprovalWorkflowSection.tsx @@ -1,12 +1,17 @@ import {Str} from 'expensify-common'; -import React, {useCallback, useMemo} from 'react'; +import React from 'react'; import {View} from 'react-native'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {sortAlphabetically} from '@libs/OptionsListUtils'; +import {getApprovalLimitDescription} from '@libs/WorkflowUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {personalDetailsByEmailSelector} from '@src/selectors/PersonalDetails'; import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; import Icon from './Icon'; import MenuItem from './MenuItem'; @@ -19,30 +24,30 @@ type ApprovalWorkflowSectionProps = { /** A function that is called when the section is pressed */ onPress: () => void; + + /** Currency used for formatting approval limits */ + currency?: string; }; -function ApprovalWorkflowSection({approvalWorkflow, onPress}: ApprovalWorkflowSectionProps) { +function ApprovalWorkflowSection({approvalWorkflow, onPress, currency = CONST.CURRENCY.USD}: ApprovalWorkflowSectionProps) { const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'Lightbulb', 'Users', 'UserCheck']); const styles = useThemeStyles(); const theme = useTheme(); const {translate, toLocaleOrdinal, localeCompare} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const [personalDetailsByEmail] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, { + canBeMissing: true, + selector: personalDetailsByEmailSelector, + }); - const approverTitle = useCallback( - (index: number) => - approvalWorkflow.approvers.length > 1 ? `${toLocaleOrdinal(index + 1, true)} ${translate('workflowsPage.approver').toLowerCase()}` : `${translate('workflowsPage.approver')}`, - [approvalWorkflow.approvers.length, toLocaleOrdinal, translate], - ); - - const members = useMemo(() => { - if (approvalWorkflow.isDefault) { - return translate('workspace.common.everyone'); - } + const approverTitle = (index: number) => + approvalWorkflow.approvers.length > 1 ? `${toLocaleOrdinal(index + 1, true)} ${translate('workflowsPage.approver').toLowerCase()}` : `${translate('workflowsPage.approver')}`; - return sortAlphabetically(approvalWorkflow.members, 'displayName', localeCompare) - .map((m) => Str.removeSMSDomain(m.displayName)) - .join(', '); - }, [approvalWorkflow.isDefault, approvalWorkflow.members, translate, localeCompare]); + const members = approvalWorkflow.isDefault + ? translate('workspace.common.everyone') + : sortAlphabetically(approvalWorkflow.members, 'displayName', localeCompare) + .map((m) => Str.removeSMSDomain(m.displayName)) + .join(', '); return ( ))} diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts index 10a0c7fada659..bb0c946b99804 100644 --- a/src/components/Icon/chunks/expensify-icons.chunk.ts +++ b/src/components/Icon/chunks/expensify-icons.chunk.ts @@ -164,6 +164,7 @@ import Phone from '@assets/images/phone.svg'; import Pin from '@assets/images/pin.svg'; import Plane from '@assets/images/plane.svg'; import Play from '@assets/images/play.svg'; +import PlusMinus from '@assets/images/plus-minus.svg'; import Plus from '@assets/images/plus.svg'; import Printer from '@assets/images/printer.svg'; import Profile from '@assets/images/profile.svg'; @@ -363,6 +364,7 @@ const Expensicons = { Pin, Play, Plus, + PlusMinus, Printer, Profile, QBOSquare, diff --git a/src/components/NumberWithSymbolForm.tsx b/src/components/NumberWithSymbolForm.tsx index 6c65ad9c04d71..3fdeeb99b4257 100644 --- a/src/components/NumberWithSymbolForm.tsx +++ b/src/components/NumberWithSymbolForm.tsx @@ -1,8 +1,9 @@ import {useIsFocused} from '@react-navigation/native'; import type {ForwardedRef} from 'react'; -import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; import type {NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import {useMouseContext} from '@hooks/useMouseContext'; import usePrevious from '@hooks/usePrevious'; @@ -25,7 +26,6 @@ import CONST from '@src/CONST'; import BigNumberPad from './BigNumberPad'; import Button from './Button'; import FormHelpMessage from './FormHelpMessage'; -import * as Expensicons from './Icon/Expensicons'; import ScrollView from './ScrollView'; import TextInput from './TextInput'; import isTextInputFocused from './TextInput/BaseTextInput/isTextInputFocused'; @@ -84,6 +84,9 @@ type NumberWithSymbolFormProps = { /** Reference to the outer element */ ref?: ForwardedRef; + + /** Callback when the user presses the submit key (Enter) */ + onSubmitEditing?: () => void; } & Omit; type NumberWithSymbolFormRef = { @@ -145,15 +148,29 @@ function NumberWithSymbolForm({ clearNegative, ref, disabled, + onSubmitEditing, ...props }: NumberWithSymbolFormProps) { const styles = useThemeStyles(); const {toLocaleDigit, numberFormat, translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['DownArrow', 'PlusMinus'] as const); const textInput = useRef(null); const numberRef = useRef(undefined); const [currentNumber, setCurrentNumber] = useState(typeof number === 'string' ? number : ''); + // sync currentNumber with number prop when it changes externally + useEffect(() => { + const newNumber = typeof number === 'string' ? number : ''; + + if (newNumber === currentNumber || (newNumber && currentNumber && Number(newNumber) === Number(currentNumber))) { + return; + } + + setCurrentNumber(newNumber); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [number]); + const [shouldUpdateSelection, setShouldUpdateSelection] = useState(true); const isFocused = useIsFocused(); @@ -178,9 +195,9 @@ function NumberWithSymbolForm({ setMouseUp(); }; - const clearSelection = useCallback(() => { + const clearSelection = () => { setSelection({start: selection.end, end: selection.end}); - }, [selection.end]); + }; /** * Event occurs when a user presses a mouse button over an DOM element. @@ -206,39 +223,36 @@ function NumberWithSymbolForm({ * Sets the selection and the number accordingly to the number passed to the input * @param newNumber - Changed number from user input */ - const setNewNumber = useCallback( - (newNumber: string) => { - // Remove spaces from the newNumber number because Safari on iOS adds spaces when pasting a copied number - // More info: https://github.com/Expensify/App/issues/16974 - const newNumberWithoutSpaces = stripSpacesFromAmount(newNumber); - const rawFinalNumber = newNumberWithoutSpaces.includes('.') ? stripCommaFromAmount(newNumberWithoutSpaces) : replaceCommasWithPeriod(newNumberWithoutSpaces); - - const finalNumber = handleNegativeAmountFlipping(rawFinalNumber, allowFlippingAmount, toggleNegative); - - // Use a shallow copy of selection to trigger setSelection - // More info: https://github.com/Expensify/App/issues/16385 - if (!validateAmount(finalNumber, decimals, maxLength)) { - setSelection((prevSelection) => ({...prevSelection})); - return; - } + const setNewNumber = (newNumber: string) => { + // Remove spaces from the newNumber number because Safari on iOS adds spaces when pasting a copied number + // More info: https://github.com/Expensify/App/issues/16974 + const newNumberWithoutSpaces = stripSpacesFromAmount(newNumber); + const rawFinalNumber = newNumberWithoutSpaces.includes('.') ? stripCommaFromAmount(newNumberWithoutSpaces) : replaceCommasWithPeriod(newNumberWithoutSpaces); - willSelectionBeUpdatedManually.current = true; - let hasSelectionBeenSet = false; - const strippedNumber = stripCommaFromAmount(finalNumber); - numberRef.current = strippedNumber; - setCurrentNumber((prevNumber) => { - const isForwardDelete = prevNumber.length > strippedNumber.length && forwardDeletePressedRef.current; - if (!hasSelectionBeenSet) { - hasSelectionBeenSet = true; - setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedNumber.length : prevNumber.length, strippedNumber.length)); - willSelectionBeUpdatedManually.current = false; - } - return strippedNumber; - }); - onInputChange?.(strippedNumber); - }, - [decimals, maxLength, onInputChange, allowFlippingAmount, toggleNegative], - ); + const finalNumber = handleNegativeAmountFlipping(rawFinalNumber, allowFlippingAmount, toggleNegative); + + // Use a shallow copy of selection to trigger setSelection + // More info: https://github.com/Expensify/App/issues/16385 + if (!validateAmount(finalNumber, decimals, maxLength)) { + setSelection((prevSelection) => ({...prevSelection})); + return; + } + + willSelectionBeUpdatedManually.current = true; + let hasSelectionBeenSet = false; + const strippedNumber = stripCommaFromAmount(finalNumber); + numberRef.current = strippedNumber; + setCurrentNumber((prevNumber) => { + const isForwardDelete = prevNumber.length > strippedNumber.length && forwardDeletePressedRef.current; + if (!hasSelectionBeenSet) { + hasSelectionBeenSet = true; + setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedNumber.length : prevNumber.length, strippedNumber.length)); + willSelectionBeUpdatedManually.current = false; + } + return strippedNumber; + }); + onInputChange?.(strippedNumber); + }; /** * Set a new number number properly formatted, used for the TextInput @@ -290,37 +304,34 @@ function NumberWithSymbolForm({ * Update number with number or Backspace pressed for BigNumberPad. * Validate new number with decimal number regex up to 6 digits and 2 decimal digit to enable Next button */ - const updateValueNumberPad = useCallback( - (key: string) => { - if (shouldUpdateSelection && !isTextInputFocused(textInput)) { - textInput.current?.focus(); - } - // Backspace button is pressed - if (key === '<' || key === 'Backspace') { - if (currentNumber.length > 0) { - const selectionStart = selection.start === selection.end ? selection.start - 1 : selection.start; - const newNumber = `${currentNumber.substring(0, selectionStart)}${currentNumber.substring(selection.end)}`; - setNewNumber(addLeadingZero(newNumber)); - } - return; + const updateValueNumberPad = (key: string) => { + if (shouldUpdateSelection && !isTextInputFocused(textInput)) { + textInput.current?.focus(); + } + // Backspace button is pressed + if (key === '<' || key === 'Backspace') { + if (currentNumber.length > 0) { + const selectionStart = selection.start === selection.end ? selection.start - 1 : selection.start; + const newNumber = `${currentNumber.substring(0, selectionStart)}${currentNumber.substring(selection.end)}`; + setNewNumber(addLeadingZero(newNumber)); } - const newNumber = addLeadingZero(`${currentNumber.substring(0, selection.start)}${key}${currentNumber.substring(selection.end)}`); - setNewNumber(newNumber); - }, - [currentNumber, selection.start, selection.end, shouldUpdateSelection, setNewNumber], - ); + return; + } + const newNumber = addLeadingZero(`${currentNumber.substring(0, selection.start)}${key}${currentNumber.substring(selection.end)}`); + setNewNumber(newNumber); + }; /** * Update long press number, to remove items pressing on < * * @param value - Changed text from user input */ - const updateLongPressHandlerState = useCallback((value: boolean) => { + const updateLongPressHandlerState = (value: boolean) => { setShouldUpdateSelection(!value); if (!value && !isTextInputFocused(textInput)) { textInput.current?.focus(); } - }, []); + }; /** * Input handler to check for a forward-delete key (or keyboard shortcut) press. @@ -386,6 +397,7 @@ function NumberWithSymbolForm({ autoFocus={props.autoFocus} autoGrowExtraSpace={props.autoGrowExtraSpace} autoGrowMarginSide={props.autoGrowMarginSide} + onSubmitEditing={onSubmitEditing} /> ); } @@ -472,7 +484,7 @@ function NumberWithSymbolForm({