From 0230d2351e64d4ecbfd6c0f7d53d134ea81eb6c6 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 12 Jan 2024 17:22:20 +0100 Subject: [PATCH 01/65] add a new report flag --- src/types/onyx/Report.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index f6af87038d00..0c2970f4d3f6 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -160,6 +160,9 @@ type Report = { /** If the report contains reportFields, save the field id and its value */ reportFields?: Record; + + /** Whether the user can do self approve or submit of an expense report */ + isPreventSelfApprovalEnabled?: boolean; }; export default Report; From c2f14f83e5fe41318b054dee0f582865403905f1 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 12 Jan 2024 17:22:42 +0100 Subject: [PATCH 02/65] draft implementation of buildNextStep --- src/libs/NextStepUtils.ts | 129 ++++++++++++- tests/unit/NextStepUtilsTest.ts | 315 ++++++++++++++++++++++++++++++++ 2 files changed, 442 insertions(+), 2 deletions(-) create mode 100644 tests/unit/NextStepUtilsTest.ts diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index a76a7d3c75c4..c45ccec43e24 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -1,6 +1,23 @@ import Str from 'expensify-common/lib/str'; +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report, ReportNextStep} from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportNextStep'; import EmailUtils from './EmailUtils'; +import * as ReportUtils from './ReportUtils'; + +let currentUserAccountID: number | undefined; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (value) => { + if (!value) { + return; + } + + currentUserAccountID = value.accountID; + }, +}); function parseMessage(messages: Message[] | undefined) { let nextStepHTML = ''; @@ -27,5 +44,113 @@ function parseMessage(messages: Message[] | undefined) { return `${formattedHtml}`; } -// eslint-disable-next-line import/prefer-default-export -export {parseMessage}; +/** + * + * @param report + * @param isPaidWithWallet - Whether a report has been paid with wallet or outside of Expensify + * @returns next step + */ +function buildNextStep(report: Report, isPaidWithWallet = false): ReportNextStep | null { + const { + statusNum = CONST.REPORT.STATUS.OPEN, + // TODO: Clarify default value + isPreventSelfApprovalEnabled = false, + } = report; + const policy = ReportUtils.getPolicy(report.policyID ?? ''); + const isSelfApproval = policy.submitsTo === currentUserAccountID; + const submitterDisplayName = isSelfApproval ? 'you' : ReportUtils.getDisplayNameForParticipant(policy.submitsTo, true) ?? ''; + const type: ReportNextStep['type'] = 'neutral'; + let optimisticNextStep: ReportNextStep | null; + + switch (statusNum) { + case CONST.REPORT.STATUS.OPEN: { + const message = [ + { + text: 'Waiting for', + }, + { + text: submitterDisplayName, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: 'these expenses.', + }, + ]; + const preventedSelfApprovalMessage = [ + { + text: "Oops! Looks like you're submitting to ", + }, + { + text: 'yourself', + type: 'strong', + }, + { + text: '. Approving your own reports is ', + }, + { + text: 'forbidden', + type: 'strong', + }, + { + text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.', + }, + ]; + + optimisticNextStep = { + type, + title: 'Next Steps:', + message: isPreventSelfApprovalEnabled && isSelfApproval ? preventedSelfApprovalMessage : message, + }; + break; + } + + case CONST.REPORT.STATUS.SUBMITTED: + optimisticNextStep = { + type, + title: 'Next Steps:', + message: [{text: 'Waiting for'}, {text: submitterDisplayName, type: 'strong'}, {text: 'to'}, {text: 'review', type: 'strong'}, {text: ' %expenses.'}], + }; + break; + + case CONST.REPORT.STATUS.APPROVED: { + const message = [ + { + text: isSelfApproval ? Str.recapitalize(submitterDisplayName) : submitterDisplayName, + type: 'strong', + }, + { + text: 'have marked these expenses as', + }, + { + text: 'paid', + type: 'strong', + }, + ]; + + if (!isPaidWithWallet) { + message.push({text: 'outside of Expensify.'}); + } + + optimisticNextStep = { + type, + title: 'Finished!', + message, + }; + break; + } + + default: + optimisticNextStep = null; + } + + return optimisticNextStep; +} + +export {parseMessage, buildNextStep}; diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts new file mode 100644 index 000000000000..5de218911375 --- /dev/null +++ b/tests/unit/NextStepUtilsTest.ts @@ -0,0 +1,315 @@ +import Str from 'expensify-common/lib/str'; +import CONST from '@src/CONST'; +import type {ReportNextStep} from '@src/types/onyx'; +import * as NextStepUtils from '../../src/libs/NextStepUtils'; +import * as ReportUtils from '../../src/libs/ReportUtils'; + +describe('libs/NextStepUtils', () => { + describe('buildNextStep', () => { + const fakeSubmitterEmail = 'submitter@expensify.com'; + const fakeSelfSubmitterEmail = 'you'; + const fakeChatReportID = '1'; + const fakePolicyID = '2'; + const fakePayeeAccountID = 3; + const report = ReportUtils.buildOptimisticExpenseReport(fakeChatReportID, fakePolicyID, fakePayeeAccountID, -500, CONST.CURRENCY.USD); + + const optimisticNextStep: ReportNextStep = { + type: 'neutral', + title: '', + message: [], + }; + + beforeEach(() => { + report.statusNum = CONST.REPORT.STATUS.OPEN; + optimisticNextStep.title = ''; + optimisticNextStep.message = []; + }); + + it('generates an optimistic nextStep once a report has been opened', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for', + }, + { + text: fakeSubmitterEmail, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: 'these expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been self opened', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for', + }, + { + text: fakeSelfSubmitterEmail, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: 'these expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been opened with prevented self submitting', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: "Oops! Looks like you're submitting to ", + }, + { + text: 'yourself', + type: 'strong', + }, + { + text: '. Approving your own reports is ', + }, + { + text: 'forbidden', + type: 'strong', + }, + { + text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been submitted', () => { + report.statusNum = CONST.REPORT.STATUS.SUBMITTED; + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for', + }, + { + text: fakeSubmitterEmail, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been self submitted', () => { + report.statusNum = CONST.REPORT.STATUS.SUBMITTED; + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for', + }, + { + text: fakeSelfSubmitterEmail, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been approved', () => { + report.statusNum = CONST.REPORT.STATUS.APPROVED; + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for', + }, + { + text: fakeSubmitterEmail, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been self approved', () => { + report.statusNum = CONST.REPORT.STATUS.APPROVED; + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for', + }, + { + text: fakeSelfSubmitterEmail, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been paid with wallet', () => { + report.statusNum = CONST.REPORT.STATUS.REIMBURSED; + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: fakeSubmitterEmail, + type: 'strong', + }, + { + text: 'have marked these expenses as', + }, + { + text: 'paid', + type: 'strong', + }, + ]; + + const result = NextStepUtils.buildNextStep(report, true); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been self paid with wallet', () => { + report.statusNum = CONST.REPORT.STATUS.REIMBURSED; + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: Str.recapitalize(fakeSelfSubmitterEmail), + type: 'strong', + }, + { + text: 'have marked these expenses as', + }, + { + text: 'paid', + type: 'strong', + }, + ]; + + const result = NextStepUtils.buildNextStep(report, true); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been paid outside of Expensify', () => { + report.statusNum = CONST.REPORT.STATUS.REIMBURSED; + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: fakeSubmitterEmail, + type: 'strong', + }, + { + text: 'have marked these expenses as', + }, + { + text: 'paid', + type: 'strong', + }, + { + text: 'outside of Expensify.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been paid self outside of Expensify', () => { + report.statusNum = CONST.REPORT.STATUS.REIMBURSED; + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: Str.recapitalize(fakeSelfSubmitterEmail), + type: 'strong', + }, + { + text: 'have marked these expenses as', + }, + { + text: 'paid', + type: 'strong', + }, + { + text: 'outside of Expensify.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); +}); From fe878dc037d442d763c526060eb82c16e5de2158 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 13 Jan 2024 18:28:06 +0530 Subject: [PATCH 03/65] fix submit behavior on native devices --- src/components/Form/FormProvider.js | 20 ++++++++++++++++++-- src/components/Form/FormWrapper.js | 8 +++----- src/components/Form/InputWrapper.js | 11 ++++++++++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 50b24e368fc6..361a0f096f8d 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -238,7 +238,7 @@ const FormProvider = forwardRef( })); const registerInput = useCallback( - (inputID, propsToParse = {}) => { + (inputID, shouldSubmitEdit, propsToParse = {}) => { const newRef = inputRefs.current[inputID] || propsToParse.ref || createRef(); if (inputRefs.current[inputID] !== newRef) { inputRefs.current[inputID] = newRef; @@ -256,6 +256,20 @@ const FormProvider = forwardRef( inputValues[inputID] = _.isUndefined(propsToParse.defaultValue) ? getInitialValueByType(propsToParse.valueType) : propsToParse.defaultValue; } + // If the input is a submit editing input, we need to set the onSubmitEditing prop + // to the submit function of the form + const onSubmitEditingObject = shouldSubmitEdit + ? { + onSubmitEditing: (e) => { + submit(); + if (!propsToParse.onSubmitEditing) { + return; + } + propsToParse.onSubmitEditing(e); + }, + } + : {}; + const errorFields = lodashGet(formState, 'errorFields', {}); const fieldErrorMessage = _.chain(errorFields[inputID]) @@ -268,6 +282,8 @@ const FormProvider = forwardRef( return { ...propsToParse, + returnKeyType: shouldSubmitEdit ? 'go' : propsToParse.returnKeyType, + ...onSubmitEditingObject, ref: typeof propsToParse.ref === 'function' ? (node) => { @@ -365,7 +381,7 @@ const FormProvider = forwardRef( }, }; }, - [draftValues, formID, errors, formState, hasServerError, inputValues, onValidate, setTouchedInput, shouldValidateOnBlur, shouldValidateOnChange], + [draftValues, inputValues, formState, errors, submit, setTouchedInput, shouldValidateOnBlur, onValidate, hasServerError, shouldValidateOnChange, formID], ); const value = useMemo(() => ({registerInput}), [registerInput]); diff --git a/src/components/Form/FormWrapper.js b/src/components/Form/FormWrapper.js index da34262a8af8..af31e0b6f70a 100644 --- a/src/components/Form/FormWrapper.js +++ b/src/components/Form/FormWrapper.js @@ -1,10 +1,9 @@ import PropTypes from 'prop-types'; import React, {useCallback, useMemo, useRef} from 'react'; -import {Keyboard, ScrollView, StyleSheet} from 'react-native'; +import {Keyboard, ScrollView, StyleSheet, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; -import FormSubmit from '@components/FormSubmit'; import refPropTypes from '@components/refPropTypes'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; @@ -107,11 +106,10 @@ function FormWrapper(props) { const scrollViewContent = useCallback( (safeAreaPaddingBottomStyle) => ( - {children} {isSubmitButtonVisible && ( @@ -155,7 +153,7 @@ function FormWrapper(props) { disablePressOnEnter /> )} - + ), [ children, diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index 9a31210195c4..7f49660478ff 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -16,8 +16,17 @@ const defaultProps = { valueType: 'string', }; +const canUseSubmitEditing = (multiline, autoGrowHeight, submitOnEnter) => { + const isMultiline = multiline || autoGrowHeight; + if (!isMultiline) { + return true; + } + return Boolean(submitOnEnter); +}; + function InputWrapper(props) { const {InputComponent, inputID, forwardedRef, ...rest} = props; + const shouldSubmitEdit = canUseSubmitEditing(rest.multiline, rest.autoGrowHeight, rest.submitOnEnter); const {registerInput} = useContext(FormContext); // There are inputs that dont have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were @@ -25,7 +34,7 @@ function InputWrapper(props) { // For now this side effect happened only in `TextInput` components. const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } InputWrapper.propTypes = propTypes; From 36301d1fa489e2965b0dd09c55b6bcc01c3a66f6 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 13 Jan 2024 18:32:14 +0530 Subject: [PATCH 04/65] remove unused logic --- src/components/FormSubmit/index.native.tsx | 18 ---- src/components/FormSubmit/index.tsx | 86 ------------------- src/components/FormSubmit/types.ts | 13 --- .../TextInput/BaseTextInput/index.native.tsx | 4 - .../TextInput/BaseTextInput/index.tsx | 4 - 5 files changed, 125 deletions(-) delete mode 100644 src/components/FormSubmit/index.native.tsx delete mode 100644 src/components/FormSubmit/index.tsx delete mode 100644 src/components/FormSubmit/types.ts diff --git a/src/components/FormSubmit/index.native.tsx b/src/components/FormSubmit/index.native.tsx deleted file mode 100644 index 5eae7b51d988..000000000000 --- a/src/components/FormSubmit/index.native.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import type {FormSubmitProps, FormSubmitRef} from './types'; - -function FormSubmit({style, children}: FormSubmitProps, ref: FormSubmitRef) { - return ( - - {children} - - ); -} - -FormSubmit.displayName = 'FormSubmit'; - -export default React.forwardRef(FormSubmit); diff --git a/src/components/FormSubmit/index.tsx b/src/components/FormSubmit/index.tsx deleted file mode 100644 index 2ccd006bf322..000000000000 --- a/src/components/FormSubmit/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import type {KeyboardEvent} from 'react'; -import React, {useEffect} from 'react'; -import {View} from 'react-native'; -import * as ComponentUtils from '@libs/ComponentUtils'; -import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; -import CONST from '@src/CONST'; -import type {FormSubmitProps, FormSubmitRef} from './types'; - -function FormSubmit({children, onSubmit, style}: FormSubmitProps, ref: FormSubmitRef) { - /** - * Calls the submit callback when ENTER is pressed on a form element. - */ - const submitForm = (event: KeyboardEvent) => { - // ENTER is pressed with modifier key or during text composition, do not submit the form - if (event.shiftKey || event.key !== CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey || isEnterWhileComposition(event)) { - return; - } - - const eventTarget = event.target as HTMLElement; - - const tagName = eventTarget?.tagName ?? ''; - - // ENTER is pressed on INPUT or SELECT element, call the submit callback. - if (tagName === 'INPUT' || tagName === 'SELECT') { - onSubmit(); - return; - } - - // Pressing Enter on TEXTAREA element adds a new line. When `dataset.submitOnEnter` prop is passed, call the submit callback. - if (tagName === 'TEXTAREA' && (eventTarget?.dataset?.submitOnEnter ?? 'false') === 'true') { - event.preventDefault(); - onSubmit(); - return; - } - - // ENTER is pressed on checkbox element, call the submit callback. - if (eventTarget?.role === 'checkbox') { - onSubmit(); - } - }; - - const preventDefaultFormBehavior = (e: SubmitEvent) => e.preventDefault(); - - useEffect(() => { - if (!(ref && 'current' in ref)) { - return; - } - - const form = ref.current as HTMLFormElement | null; - - if (!form) { - return; - } - - // Prevent the browser from applying its own validation, which affects the email input - form.setAttribute('novalidate', ''); - - form.addEventListener('submit', preventDefaultFormBehavior); - - return () => { - if (!form) { - return; - } - - form.removeEventListener('submit', preventDefaultFormBehavior); - }; - }, [ref]); - - return ( - // React-native-web prevents event bubbling on TextInput for key presses - // https://github.com/necolas/react-native-web/blob/fa47f80d34ee6cde2536b2a2241e326f84b633c4/packages/react-native-web/src/exports/TextInput/index.js#L272 - // Thus use capture phase. - - {children} - - ); -} - -FormSubmit.displayName = 'FormSubmitWithRef'; - -export default React.forwardRef(FormSubmit); diff --git a/src/components/FormSubmit/types.ts b/src/components/FormSubmit/types.ts deleted file mode 100644 index 722a3fbf746e..000000000000 --- a/src/components/FormSubmit/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type {ForwardedRef} from 'react'; -import type React from 'react'; -import type {StyleProp, View, ViewStyle} from 'react-native'; - -type FormSubmitProps = { - children: React.ReactNode; - onSubmit: () => void; - style?: StyleProp; -}; - -type FormSubmitRef = ForwardedRef; - -export type {FormSubmitProps, FormSubmitRef}; diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index d19d835d68bb..a31aa0073bdc 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -50,7 +50,6 @@ function BaseTextInput( hint = '', onInputChange = () => {}, shouldDelayFocus = false, - submitOnEnter = false, multiline = false, shouldInterceptSwipe = false, autoCorrect = true, @@ -363,9 +362,6 @@ function BaseTextInput( selection={inputProps.selection} readOnly={isReadOnly} defaultValue={defaultValue} - // FormSubmit Enter key handler does not have access to direct props. - // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. - dataSet={{submitOnEnter: isMultiline && submitOnEnter}} /> {inputProps.isLoading && ( {}, shouldDelayFocus = false, - submitOnEnter = false, multiline = false, shouldInterceptSwipe = false, autoCorrect = true, @@ -383,9 +382,6 @@ function BaseTextInput( selection={inputProps.selection} readOnly={isReadOnly} defaultValue={defaultValue} - // FormSubmit Enter key handler does not have access to direct props. - // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. - dataSet={{submitOnEnter: isMultiline && submitOnEnter}} /> {inputProps.isLoading && ( Date: Sat, 13 Jan 2024 18:46:28 +0530 Subject: [PATCH 05/65] small bug fix --- src/components/Form/FormProvider.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 361a0f096f8d..6ffe63892a01 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -270,6 +270,8 @@ const FormProvider = forwardRef( } : {}; + const isMultiline = propsToParse.multiline || propsToParse.autoGrowHeight; + const errorFields = lodashGet(formState, 'errorFields', {}); const fieldErrorMessage = _.chain(errorFields[inputID]) @@ -283,6 +285,7 @@ const FormProvider = forwardRef( return { ...propsToParse, returnKeyType: shouldSubmitEdit ? 'go' : propsToParse.returnKeyType, + blurOnSubmit: (isMultiline && shouldSubmitEdit) || propsToParse.blurOnSubmit, ...onSubmitEditingObject, ref: typeof propsToParse.ref === 'function' From 8d51fa2ff527f1d494e224f62aae59af3034c7af Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 16 Jan 2024 18:18:04 +0530 Subject: [PATCH 06/65] make submitOnEnter use consistent --- src/pages/EditRequestDescriptionPage.js | 4 ++-- src/pages/iou/MoneyRequestDescriptionPage.js | 4 ++-- src/pages/iou/request/step/IOURequestStepDescription.js | 4 ++-- src/pages/tasks/NewTaskDescriptionPage.js | 4 ++-- src/pages/tasks/NewTaskDetailsPage.js | 4 ++-- src/pages/tasks/TaskDescriptionPage.js | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pages/EditRequestDescriptionPage.js b/src/pages/EditRequestDescriptionPage.js index 9b2a9e465746..b459c17a3ee3 100644 --- a/src/pages/EditRequestDescriptionPage.js +++ b/src/pages/EditRequestDescriptionPage.js @@ -9,7 +9,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Browser from '@libs/Browser'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -77,7 +77,7 @@ function EditRequestDescriptionPage({defaultDescription, onSubmit}) { }} autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} - submitOnEnter={!Browser.isMobile()} + submitOnEnter={!canUseTouchScreen()} /> diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index fe3100b8c3bd..d98eed9c7727 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -12,8 +12,8 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as IOU from '@libs/actions/IOU'; -import * as Browser from '@libs/Browser'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; @@ -142,7 +142,7 @@ function MoneyRequestDescriptionPage({iou, route, selectedTab}) { }} autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} - submitOnEnter={!Browser.isMobile()} + submitOnEnter={!canUseTouchScreen()} /> diff --git a/src/pages/iou/request/step/IOURequestStepDescription.js b/src/pages/iou/request/step/IOURequestStepDescription.js index 849f3276667e..8eb060c925ca 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.js +++ b/src/pages/iou/request/step/IOURequestStepDescription.js @@ -8,7 +8,7 @@ import TextInput from '@components/TextInput'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Browser from '@libs/Browser'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; @@ -107,7 +107,7 @@ function IOURequestStepDescription({ autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} inputStyle={[styles.verticalAlignTop]} - submitOnEnter={!Browser.isMobile()} + submitOnEnter={!canUseTouchScreen()} /> diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.js index b11e7c163755..06c8ed97bb48 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.js +++ b/src/pages/tasks/NewTaskDescriptionPage.js @@ -11,7 +11,7 @@ import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Browser from '@libs/Browser'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; @@ -79,7 +79,7 @@ function NewTaskDescriptionPage(props) { updateMultilineInputRange(el); }} autoGrowHeight - submitOnEnter={!Browser.isMobile()} + submitOnEnter={!canUseTouchScreen()} containerStyles={[styles.autoGrowHeightMultilineInput]} /> diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js index 3dab58dfad04..b2a76eb07ce3 100644 --- a/src/pages/tasks/NewTaskDetailsPage.js +++ b/src/pages/tasks/NewTaskDetailsPage.js @@ -11,7 +11,7 @@ import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Browser from '@libs/Browser'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -111,7 +111,7 @@ function NewTaskDetailsPage(props) { label={props.translate('newTaskPage.descriptionOptional')} accessibilityLabel={props.translate('newTaskPage.descriptionOptional')} autoGrowHeight - submitOnEnter={!Browser.isMobile()} + submitOnEnter={!canUseTouchScreen()} containerStyles={[styles.autoGrowHeightMultilineInput]} defaultValue={parser.htmlToMarkdown(parser.replace(taskDescription))} value={taskDescription} diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js index 3a6999d4408a..10205c65eacf 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.js @@ -15,7 +15,7 @@ import TextInput from '@components/TextInput'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Browser from '@libs/Browser'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; @@ -126,7 +126,7 @@ function TaskDescriptionPage(props) { updateMultilineInputRange(inputRef.current); }} autoGrowHeight - submitOnEnter={!Browser.isMobile()} + submitOnEnter={!canUseTouchScreen()} containerStyles={[styles.autoGrowHeightMultilineInput]} /> From 3854a9a7d0e78fd4969768ac5d2f805b096089d4 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 16 Jan 2024 18:19:05 +0530 Subject: [PATCH 07/65] fix prettier --- src/pages/iou/MoneyRequestDescriptionPage.js | 2 +- src/pages/iou/request/step/IOURequestStepDescription.js | 2 +- src/pages/tasks/NewTaskDescriptionPage.js | 2 +- src/pages/tasks/NewTaskDetailsPage.js | 2 +- src/pages/tasks/TaskDescriptionPage.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index d98eed9c7727..7d3c31ca12ba 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -12,8 +12,8 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as IOU from '@libs/actions/IOU'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; diff --git a/src/pages/iou/request/step/IOURequestStepDescription.js b/src/pages/iou/request/step/IOURequestStepDescription.js index 8eb060c925ca..7473239d92b5 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.js +++ b/src/pages/iou/request/step/IOURequestStepDescription.js @@ -8,8 +8,8 @@ import TextInput from '@components/TextInput'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import * as IOU from '@userActions/IOU'; diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.js index 06c8ed97bb48..7620afaf2dc8 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.js +++ b/src/pages/tasks/NewTaskDescriptionPage.js @@ -11,8 +11,8 @@ import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import * as Task from '@userActions/Task'; diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js index b2a76eb07ce3..adf26820f7b9 100644 --- a/src/pages/tasks/NewTaskDetailsPage.js +++ b/src/pages/tasks/NewTaskDetailsPage.js @@ -11,8 +11,8 @@ import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as Task from '@userActions/Task'; diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js index 10205c65eacf..db8974632ab7 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.js @@ -15,8 +15,8 @@ import TextInput from '@components/TextInput'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; From 04d2c446e47d8fb86d86690a06b5df12ad3a80a5 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 16 Jan 2024 18:45:31 +0100 Subject: [PATCH 08/65] cover all basic scenarios --- src/libs/NextStepUtils.ts | 254 +++++++++---- tests/unit/NextStepUtilsTest.ts | 612 +++++++++++++++++--------------- 2 files changed, 512 insertions(+), 354 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index c45ccec43e24..901f1ffee96e 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -5,6 +5,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Report, ReportNextStep} from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportNextStep'; import EmailUtils from './EmailUtils'; +import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as ReportUtils from './ReportUtils'; let currentUserAccountID: number | undefined; @@ -44,108 +45,229 @@ function parseMessage(messages: Message[] | undefined) { return `${formattedHtml}`; } +type BuildNextStepParameters = { + isPaidWithWallet?: boolean; +}; + /** + * Generates an optimistic nextStep based on a current report status and other properties. * * @param report - * @param isPaidWithWallet - Whether a report has been paid with wallet or outside of Expensify - * @returns next step + * @param parameters + * @param parameters.isPaidWithWallet - Whether a report has been paid with wallet or outside of Expensify + * @returns nextStep */ -function buildNextStep(report: Report, isPaidWithWallet = false): ReportNextStep | null { +function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { const { statusNum = CONST.REPORT.STATUS.OPEN, // TODO: Clarify default value isPreventSelfApprovalEnabled = false, + ownerAccountID = -1, } = report; const policy = ReportUtils.getPolicy(report.policyID ?? ''); - const isSelfApproval = policy.submitsTo === currentUserAccountID; + const isOwner = currentUserAccountID === ownerAccountID; + const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? ''; + const isSelfApproval = currentUserAccountID === policy.submitsTo; const submitterDisplayName = isSelfApproval ? 'you' : ReportUtils.getDisplayNameForParticipant(policy.submitsTo, true) ?? ''; const type: ReportNextStep['type'] = 'neutral'; let optimisticNextStep: ReportNextStep | null; switch (statusNum) { - case CONST.REPORT.STATUS.OPEN: { - const message = [ - { - text: 'Waiting for', - }, - { - text: submitterDisplayName, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'submit', - type: 'strong', - }, - { - text: 'these expenses.', - }, - ]; - const preventedSelfApprovalMessage = [ - { - text: "Oops! Looks like you're submitting to ", - }, - { - text: 'yourself', - type: 'strong', - }, - { - text: '. Approving your own reports is ', - }, - { - text: 'forbidden', - type: 'strong', - }, - { - text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.', - }, - ]; - + // Generates an optimistic nextStep once a report has been opened + case CONST.REPORT.STATUS.OPEN: + // Self review optimisticNextStep = { type, title: 'Next Steps:', - message: isPreventSelfApprovalEnabled && isSelfApproval ? preventedSelfApprovalMessage : message, + message: [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: ' these expenses. This report may be selected at random for manual approval.', + }, + ], }; + + // TODO: Clarify date + // Scheduled submit enabled + if (policy.isHarvestingEnabled) { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + } + + // Prevented self submitting + if (isPreventSelfApprovalEnabled && isSelfApproval) { + optimisticNextStep.message = [ + { + text: "Oops! Looks like you're submitting to ", + }, + { + text: 'yourself', + type: 'strong', + }, + { + text: '. Approving your own reports is ', + }, + { + text: 'forbidden', + type: 'strong', + }, + { + text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.', + }, + ]; + } + break; - } + // Generates an optimistic nextStep once a report has been submitted case CONST.REPORT.STATUS.SUBMITTED: + // Self review & another reviewer optimisticNextStep = { type, title: 'Next Steps:', - message: [{text: 'Waiting for'}, {text: submitterDisplayName, type: 'strong'}, {text: 'to'}, {text: 'review', type: 'strong'}, {text: ' %expenses.'}], + message: [ + { + text: 'Waiting for ', + }, + { + text: submitterDisplayName, + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ], }; + + // Another owner + if (!isOwner) { + optimisticNextStep.message = [ + { + text: ownerLogin, + type: 'strong', + }, + { + text: ' is waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' these %expenses.', + }, + ]; + } + break; - case CONST.REPORT.STATUS.APPROVED: { - const message = [ - { - text: isSelfApproval ? Str.recapitalize(submitterDisplayName) : submitterDisplayName, - type: 'strong', - }, - { - text: 'have marked these expenses as', - }, - { - text: 'paid', - type: 'strong', - }, - ]; - - if (!isPaidWithWallet) { - message.push({text: 'outside of Expensify.'}); + // Generates an optimistic nextStep once a report has been approved + case CONST.REPORT.STATUS.APPROVED: + // Self review + optimisticNextStep = { + type, + title: 'Next Steps:', + message: [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ], + }; + + // Another owner + if (!isOwner) { + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: 'No further action required!', + }, + ]; } + break; + + // Generates an optimistic nextStep once a report has been paid + case CONST.REPORT.STATUS.REIMBURSED: + // Paid with wallet optimisticNextStep = { type, title: 'Finished!', - message, + message: [ + { + text: 'You', + type: 'strong', + }, + { + text: ' have marked these expenses as ', + }, + { + text: 'paid', + type: 'strong', + }, + ], }; + + // Paid outside of Expensify + if (typeof isPaidWithWallet === 'boolean' && !isPaidWithWallet) { + optimisticNextStep.message?.push({text: ' outside of Expensify'}); + } + + optimisticNextStep.message?.push({text: '.'}); + break; - } + // Resets a nextStep default: optimisticNextStep = null; } diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 5de218911375..6fe5d1dd31f1 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -1,17 +1,35 @@ -import Str from 'expensify-common/lib/str'; +import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; -import type {ReportNextStep} from '@src/types/onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report, ReportNextStep} from '@src/types/onyx'; import * as NextStepUtils from '../../src/libs/NextStepUtils'; import * as ReportUtils from '../../src/libs/ReportUtils'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +Onyx.init({keys: ONYXKEYS}); describe('libs/NextStepUtils', () => { describe('buildNextStep', () => { - const fakeSubmitterEmail = 'submitter@expensify.com'; - const fakeSelfSubmitterEmail = 'you'; - const fakeChatReportID = '1'; - const fakePolicyID = '2'; - const fakePayeeAccountID = 3; - const report = ReportUtils.buildOptimisticExpenseReport(fakeChatReportID, fakePolicyID, fakePayeeAccountID, -500, CONST.CURRENCY.USD); + const currentUserEmail = 'current-user@expensify.com'; + const currentUserAccountID = 37; + const strangeEmail = 'stranger@expensify.com'; + const strangeAccountID = 50; + const policyID = '1'; + const policy: Policy = { + // Important props + id: policyID, + owner: currentUserEmail, + submitsTo: currentUserAccountID, + isHarvestingEnabled: false, + // Required props + name: 'Policy', + role: 'admin', + type: 'team', + outputCurrency: CONST.CURRENCY.USD, + areChatRoomsEnabled: true, + isPolicyExpenseChatEnabled: true, + }; + const report = ReportUtils.buildOptimisticExpenseReport('fake-chat-report-id-1', policyID, 1, -500, CONST.CURRENCY.USD) as Report; const optimisticNextStep: ReportNextStep = { type: 'neutral', @@ -19,297 +37,315 @@ describe('libs/NextStepUtils', () => { message: [], }; + beforeAll(() => { + // @ts-expect-error Preset necessary values + Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: currentUserEmail, accountID: currentUserAccountID}, + [`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]: policy, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: { + [strangeAccountID]: { + accountID: strangeAccountID, + login: strangeEmail, + avatar: '', + }, + }, + }).then(waitForBatchedUpdates); + }); + beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS.OPEN; + report.ownerAccountID = currentUserAccountID; optimisticNextStep.title = ''; optimisticNextStep.message = []; }); - it('generates an optimistic nextStep once a report has been opened', () => { - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for', - }, - { - text: fakeSubmitterEmail, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'submit', - type: 'strong', - }, - { - text: 'these expenses.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); + describe('it generates an optimistic nextStep once a report has been opened', () => { + beforeEach(() => { + report.statusNum = CONST.REPORT.STATUS.OPEN; + }); + + test('self review', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: ' these expenses. This report may be selected at random for manual approval.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + // TODO: Clarify date + test('scheduled submit enabled', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('prevented self submitting', () => { + report.isPreventSelfApprovalEnabled = true; + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: "Oops! Looks like you're submitting to ", + }, + { + text: 'yourself', + type: 'strong', + }, + { + text: '. Approving your own reports is ', + }, + { + text: 'forbidden', + type: 'strong', + }, + { + text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.', + }, + ]; + + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + submitsTo: currentUserAccountID, + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); }); - it('generates an optimistic nextStep once a report has been self opened', () => { - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for', - }, - { - text: fakeSelfSubmitterEmail, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'submit', - type: 'strong', - }, - { - text: 'these expenses.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); + describe('it generates an optimistic nextStep once a report has been submitted', () => { + beforeEach(() => { + report.statusNum = CONST.REPORT.STATUS.SUBMITTED; + }); + + test('self review', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + test('another reviewer', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for ', + }, + { + text: strangeEmail, + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + submitsTo: strangeAccountID, + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('another owner', () => { + report.ownerAccountID = strangeAccountID; + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: strangeEmail, + type: 'strong', + }, + { + text: ' is waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' these %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); }); - it('generates an optimistic nextStep once a report has been opened with prevented self submitting', () => { - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: "Oops! Looks like you're submitting to ", - }, - { - text: 'yourself', - type: 'strong', - }, - { - text: '. Approving your own reports is ', - }, - { - text: 'forbidden', - type: 'strong', - }, - { - text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been submitted', () => { - report.statusNum = CONST.REPORT.STATUS.SUBMITTED; - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for', - }, - { - text: fakeSubmitterEmail, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'review', - type: 'strong', - }, - { - text: ' %expenses.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); + describe('it generates an optimistic nextStep once a report has been approved', () => { + beforeEach(() => { + report.statusNum = CONST.REPORT.STATUS.APPROVED; + }); + + test('self review', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + test('another owner', () => { + report.ownerAccountID = strangeAccountID; + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: 'No further action required!', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); }); - it('generates an optimistic nextStep once a report has been self submitted', () => { - report.statusNum = CONST.REPORT.STATUS.SUBMITTED; - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for', - }, - { - text: fakeSelfSubmitterEmail, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'review', - type: 'strong', - }, - { - text: ' %expenses.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been approved', () => { - report.statusNum = CONST.REPORT.STATUS.APPROVED; - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for', - }, - { - text: fakeSubmitterEmail, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'review', - type: 'strong', - }, - { - text: ' %expenses.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been self approved', () => { - report.statusNum = CONST.REPORT.STATUS.APPROVED; - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for', - }, - { - text: fakeSelfSubmitterEmail, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'review', - type: 'strong', - }, - { - text: ' %expenses.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been paid with wallet', () => { - report.statusNum = CONST.REPORT.STATUS.REIMBURSED; - optimisticNextStep.title = 'Finished!'; - optimisticNextStep.message = [ - { - text: fakeSubmitterEmail, - type: 'strong', - }, - { - text: 'have marked these expenses as', - }, - { - text: 'paid', - type: 'strong', - }, - ]; - - const result = NextStepUtils.buildNextStep(report, true); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been self paid with wallet', () => { - report.statusNum = CONST.REPORT.STATUS.REIMBURSED; - optimisticNextStep.title = 'Finished!'; - optimisticNextStep.message = [ - { - text: Str.recapitalize(fakeSelfSubmitterEmail), - type: 'strong', - }, - { - text: 'have marked these expenses as', - }, - { - text: 'paid', - type: 'strong', - }, - ]; - - const result = NextStepUtils.buildNextStep(report, true); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been paid outside of Expensify', () => { - report.statusNum = CONST.REPORT.STATUS.REIMBURSED; - optimisticNextStep.title = 'Finished!'; - optimisticNextStep.message = [ - { - text: fakeSubmitterEmail, - type: 'strong', - }, - { - text: 'have marked these expenses as', - }, - { - text: 'paid', - type: 'strong', - }, - { - text: 'outside of Expensify.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been paid self outside of Expensify', () => { - report.statusNum = CONST.REPORT.STATUS.REIMBURSED; - optimisticNextStep.title = 'Finished!'; - optimisticNextStep.message = [ - { - text: Str.recapitalize(fakeSelfSubmitterEmail), - type: 'strong', - }, - { - text: 'have marked these expenses as', - }, - { - text: 'paid', - type: 'strong', - }, - { - text: 'outside of Expensify.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); + describe('it generates an optimistic nextStep once a report has been paid', () => { + beforeEach(() => { + report.statusNum = CONST.REPORT.STATUS.REIMBURSED; + }); + + test('paid with wallet', () => { + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: 'You', + type: 'strong', + }, + { + text: ' have marked these expenses as ', + }, + { + text: 'paid', + type: 'strong', + }, + { + text: '.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report, {isPaidWithWallet: true}); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + test('paid outside of Expensify', () => { + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: 'You', + type: 'strong', + }, + { + text: ' have marked these expenses as ', + }, + { + text: 'paid', + type: 'strong', + }, + { + text: ' outside of Expensify', + }, + { + text: '.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report, {isPaidWithWallet: false}); + + expect(result).toStrictEqual(optimisticNextStep); + }); }); }); }); From a32e22e830f8592f22025242d7ac4eb386f95212 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Jan 2024 12:14:17 +0100 Subject: [PATCH 09/65] rename const --- src/libs/NextStepUtils.ts | 10 +++++----- tests/unit/NextStepUtilsTest.ts | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 901f1ffee96e..e5a3afad4966 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -59,7 +59,7 @@ type BuildNextStepParameters = { */ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { const { - statusNum = CONST.REPORT.STATUS.OPEN, + statusNum = CONST.REPORT.STATUS_NUM.OPEN, // TODO: Clarify default value isPreventSelfApprovalEnabled = false, ownerAccountID = -1, @@ -74,7 +74,7 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete switch (statusNum) { // Generates an optimistic nextStep once a report has been opened - case CONST.REPORT.STATUS.OPEN: + case CONST.REPORT.STATUS_NUM.OPEN: // Self review optimisticNextStep = { type, @@ -143,7 +143,7 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete break; // Generates an optimistic nextStep once a report has been submitted - case CONST.REPORT.STATUS.SUBMITTED: + case CONST.REPORT.STATUS_NUM.SUBMITTED: // Self review & another reviewer optimisticNextStep = { type, @@ -199,7 +199,7 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete break; // Generates an optimistic nextStep once a report has been approved - case CONST.REPORT.STATUS.APPROVED: + case CONST.REPORT.STATUS_NUM.APPROVED: // Self review optimisticNextStep = { type, @@ -238,7 +238,7 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete break; // Generates an optimistic nextStep once a report has been paid - case CONST.REPORT.STATUS.REIMBURSED: + case CONST.REPORT.STATUS_NUM.REIMBURSED: // Paid with wallet optimisticNextStep = { type, diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 6fe5d1dd31f1..e2637a3bdb85 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -60,7 +60,7 @@ describe('libs/NextStepUtils', () => { describe('it generates an optimistic nextStep once a report has been opened', () => { beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS.OPEN; + report.statusNum = CONST.REPORT.STATUS_NUM.OPEN; }); test('self review', () => { @@ -150,7 +150,7 @@ describe('libs/NextStepUtils', () => { describe('it generates an optimistic nextStep once a report has been submitted', () => { beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS.SUBMITTED; + report.statusNum = CONST.REPORT.STATUS_NUM.SUBMITTED; }); test('self review', () => { @@ -246,7 +246,7 @@ describe('libs/NextStepUtils', () => { describe('it generates an optimistic nextStep once a report has been approved', () => { beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS.APPROVED; + report.statusNum = CONST.REPORT.STATUS_NUM.APPROVED; }); test('self review', () => { @@ -293,7 +293,7 @@ describe('libs/NextStepUtils', () => { describe('it generates an optimistic nextStep once a report has been paid', () => { beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS.REIMBURSED; + report.statusNum = CONST.REPORT.STATUS_NUM.REIMBURSED; }); test('paid with wallet', () => { From bbe860da7e34094a932aa4538109f2234ffbce74 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Jan 2024 12:14:37 +0100 Subject: [PATCH 10/65] clear todo --- src/libs/NextStepUtils.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index e5a3afad4966..13ae204c0296 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -58,12 +58,7 @@ type BuildNextStepParameters = { * @returns nextStep */ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { - const { - statusNum = CONST.REPORT.STATUS_NUM.OPEN, - // TODO: Clarify default value - isPreventSelfApprovalEnabled = false, - ownerAccountID = -1, - } = report; + const {statusNum = CONST.REPORT.STATUS_NUM.OPEN, isPreventSelfApprovalEnabled = false, ownerAccountID = -1} = report; const policy = ReportUtils.getPolicy(report.policyID ?? ''); const isOwner = currentUserAccountID === ownerAccountID; const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? ''; From b0ad3787dfd3a8f9b8f74f23f20d6a91fcd854ba Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 17 Jan 2024 18:50:20 +0530 Subject: [PATCH 11/65] added props to custom components --- src/components/AddressSearch/index.tsx | 4 ++++ src/components/AddressSearch/types.ts | 6 ++++++ src/components/RoomNameInput/index.js | 4 +++- src/components/RoomNameInput/index.native.js | 4 +++- src/components/RoomNameInput/roomNameInputPropTypes.js | 8 ++++++++ 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 89e87eeebe54..ddaecf94dbbd 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -42,6 +42,8 @@ function AddressSearch( onBlur, onInputChange, onPress, + onSubmitEditing, + returnKeyType, predefinedPlaces = [], preferredLocale, renamedInputKeys = { @@ -380,6 +382,8 @@ function AddressSearch( defaultValue, inputID, shouldSaveDraft, + returnKeyType, + onSubmitEditing, onFocus: () => { setIsFocused(true); }, diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index 8016f1b2ea39..75d6464cd992 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -63,6 +63,12 @@ type AddressSearchProps = { /** A callback function when an address has been auto-selected */ onPress?: (props: OnPressProps) => void; + /** On submit editing handler provided by the FormProvider */ + onSubmitEditing?: () => void; + + /** Return key type provided to the TextInput */ + returnKeyType?: string; + /** Customize the TextInput container */ containerStyles?: StyleProp; diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js index 61f004a47b96..e3c5a86ff945 100644 --- a/src/components/RoomNameInput/index.js +++ b/src/components/RoomNameInput/index.js @@ -6,7 +6,7 @@ import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) { +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, onSubmitEditing, returnKeyType, shouldDelayFocus}) { const {translate} = useLocalize(); const [selection, setSelection] = useState(); @@ -52,6 +52,8 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value={value.substring(1)} // Since the room name always starts with a prefix, we omit the first character to avoid displaying it twice. selection={selection} onSelectionChange={(event) => setSelection(event.nativeEvent.selection)} + onSubmitEditing={onSubmitEditing} + returnKeyType={returnKeyType} errorText={errorText} autoCapitalize="none" onBlur={(event) => isFocused && onBlur(event)} diff --git a/src/components/RoomNameInput/index.native.js b/src/components/RoomNameInput/index.native.js index a2c09996ad34..bae347fca3d2 100644 --- a/src/components/RoomNameInput/index.native.js +++ b/src/components/RoomNameInput/index.native.js @@ -7,7 +7,7 @@ import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) { +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, onSubmitEditing, returnKeyType, shouldDelayFocus}) { const {translate} = useLocalize(); /** @@ -42,6 +42,8 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 onBlur={(event) => isFocused && onBlur(event)} + onSubmitEditing={onSubmitEditing} + returnKeyType={returnKeyType} autoFocus={isFocused && autoFocus} autoCapitalize="none" shouldDelayFocus={shouldDelayFocus} diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 60be8430b056..6d88c0e62793 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -17,6 +17,12 @@ const propTypes = { /** A ref forwarded to the TextInput */ forwardedRef: refPropTypes, + /** On submit editing handler provided by the FormProvider */ + onSubmitEditing: PropTypes.func, + + /** Return key type provided to the TextInput */ + returnKeyType: PropTypes.string, + /** The ID used to uniquely identify the input in a Form */ inputID: PropTypes.string, @@ -39,6 +45,8 @@ const defaultProps = { disabled: false, errorText: '', forwardedRef: () => {}, + onSubmitEditing: () => {}, + returnKeyType: undefined, inputID: undefined, onBlur: () => {}, From 9387b10f451560091d7655a97dcac1af87163c5a Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Jan 2024 17:35:13 +0100 Subject: [PATCH 12/65] integrate timestamp handling --- src/libs/NextStepUtils.ts | 57 +++++++-- src/types/onyx/Policy.ts | 3 + tests/unit/NextStepUtilsTest.ts | 200 ++++++++++++++++++++++++++++---- 3 files changed, 231 insertions(+), 29 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 13ae204c0296..8c44b84ed734 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -1,3 +1,4 @@ +import {format, lastDayOfMonth} from 'date-fns'; import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; @@ -90,28 +91,66 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete type: 'strong', }, { - text: ' these expenses. This report may be selected at random for manual approval.', + text: ' these expenses.', }, ], }; - // TODO: Clarify date // Scheduled submit enabled if (policy.isHarvestingEnabled) { optimisticNextStep.message = [ { text: 'These expenses are scheduled to ', }, - { - text: 'automatically submit!', - type: 'strong', - }, - { - text: ' No further action required!', - }, ]; + + if (policy.autoReportingFrequency) { + const currentDate = new Date(); + + let autoSubmissionDate: string; + + if (policy.autoReportingOffset === 'lastDayOfMonth') { + const currentDateWithLastDayOfMonth = lastDayOfMonth(currentDate); + + autoSubmissionDate = format(currentDateWithLastDayOfMonth, 'do'); + } else if (policy.autoReportingOffset === 'lastBusinessDayOfMonth') { + // TODO: Get from the backend + // const currentLastBusinessDayOfMonth = + autoSubmissionDate = ''; + } else if (Number.isNaN(Number(policy.autoReportingOffset))) { + autoSubmissionDate = ''; + } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + autoSubmissionDate = format(currentDate.setDate(+policy.autoReportingOffset!), 'do'); + } + + const harvestingSuffixes = { + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE]: 'later today', + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY]: 'on Sunday', + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY]: 'on the 1st and 16th of each month', + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY]: `on the ${autoSubmissionDate} of each month`, + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP]: 'at the end of your trip', + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL]: '', + }; + + optimisticNextStep.message.push({ + text: `automatically submit ${harvestingSuffixes[policy.autoReportingFrequency]}!`, + type: 'strong', + }); + } else { + optimisticNextStep.message.push({ + text: `automatically submit!`, + type: 'strong', + }); + } + + optimisticNextStep.message.push({ + text: ' No further action required!', + }); } + // TODO add "This report may be selected at random for manual approval." + // Prevented self submitting if (isPreventSelfApprovalEnabled && isSelfApproval) { optimisticNextStep.message = [ diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index da4522487a7a..0be879fbfd68 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -74,6 +74,9 @@ type Policy = { /** The scheduled submit frequency set up on the this policy */ autoReportingFrequency?: ValueOf; + /** The scheduled submission date */ + autoReportingOffset?: string; + /** Whether the scheduled submit is enabled */ isHarvestingEnabled?: boolean; diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index e2637a3bdb85..33089b26ea0e 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -1,3 +1,4 @@ +import {format, lastDayOfMonth} from 'date-fns'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -81,7 +82,7 @@ describe('libs/NextStepUtils', () => { type: 'strong', }, { - text: ' these expenses. This report may be selected at random for manual approval.', + text: ' these expenses.', }, ]; @@ -90,28 +91,187 @@ describe('libs/NextStepUtils', () => { expect(result).toStrictEqual(optimisticNextStep); }); - // TODO: Clarify date - test('scheduled submit enabled', () => { + describe('scheduled submit enabled', () => { optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'These expenses are scheduled to ', - }, - { - text: 'automatically submit!', - type: 'strong', - }, - { - text: ' No further action required!', - }, - ]; - return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, - }).then(() => { - const result = NextStepUtils.buildNextStep(report); + test('daily', () => { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit later today!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'immediate', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); - expect(result).toStrictEqual(optimisticNextStep); + test('weekly', () => { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit on Sunday!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'weekly', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('twice a month', () => { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit on the 1st and 16th of each month!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'semimonthly', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('monthly on the 2nd', () => { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit on the 2nd of each month!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'monthly', + autoReportingOffset: '2', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('monthly on the last day', () => { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: `automatically submit on the ${format(lastDayOfMonth(new Date()), 'do')} of each month!`, + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'monthly', + autoReportingOffset: 'lastDayOfMonth', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('trip', () => { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit at the end of your trip!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'trip', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('manual', () => { + optimisticNextStep.message = [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: ' these expenses.', + }, + { + text: ' This report may be selected at random for manual approval.', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'manual', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); }); }); From 090d8ff545547c63a2940b9e83af02ceb8ec7cba Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Jan 2024 17:42:31 +0100 Subject: [PATCH 13/65] add a case with random for manual approval --- src/libs/NextStepUtils.ts | 6 +++++- src/types/onyx/Policy.ts | 3 +++ tests/unit/NextStepUtilsTest.ts | 35 +++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 8c44b84ed734..4014bcd549fc 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -149,7 +149,11 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete }); } - // TODO add "This report may be selected at random for manual approval." + if (isOwner && policy.isAutoApprovalEnabled) { + optimisticNextStep.message?.push({ + text: ' This report may be selected at random for manual approval.', + }); + } // Prevented self submitting if (isPreventSelfApprovalEnabled && isSelfApproval) { diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 0be879fbfd68..08a13fa12e38 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -65,6 +65,9 @@ type Policy = { /** Whether chat rooms can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ areChatRoomsEnabled: boolean; + /** Whether auto approval enabled */ + isAutoApprovalEnabled?: boolean; + /** Whether policy expense chats can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ isPolicyExpenseChatEnabled: boolean; diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 33089b26ea0e..9eb3e0a14555 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -22,6 +22,7 @@ describe('libs/NextStepUtils', () => { owner: currentUserEmail, submitsTo: currentUserAccountID, isHarvestingEnabled: false, + isAutoApprovalEnabled: false, // Required props name: 'Policy', role: 'admin', @@ -91,6 +92,40 @@ describe('libs/NextStepUtils', () => { expect(result).toStrictEqual(optimisticNextStep); }); + test('self review and auto approval enabled', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: ' these expenses.', + }, + { + text: ' This report may be selected at random for manual approval.', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isAutoApprovalEnabled: true, + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + describe('scheduled submit enabled', () => { optimisticNextStep.title = 'Next Steps:'; From f77cea193bd5775f668dfd8e67a9852a7dc6df40 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Jan 2024 17:48:16 +0100 Subject: [PATCH 14/65] add comment --- src/libs/NextStepUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 4014bcd549fc..a0880c8a96e6 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -149,6 +149,7 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete }); } + // Self review and auto approval enabled if (isOwner && policy.isAutoApprovalEnabled) { optimisticNextStep.message?.push({ text: ' This report may be selected at random for manual approval.', From 58baa0afd7821999d980a71c471ebc1ee20b2a06 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Jan 2024 13:54:40 +0100 Subject: [PATCH 15/65] integrate predictedNextStatus argument --- src/libs/NextStepUtils.ts | 9 +++--- tests/unit/NextStepUtilsTest.ts | 53 +++++++++++---------------------- 2 files changed, 23 insertions(+), 39 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index a0880c8a96e6..4cebc1325b27 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -1,6 +1,7 @@ import {format, lastDayOfMonth} from 'date-fns'; import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report, ReportNextStep} from '@src/types/onyx'; @@ -54,12 +55,12 @@ type BuildNextStepParameters = { * Generates an optimistic nextStep based on a current report status and other properties. * * @param report - * @param parameters + * @param predictedNextStatus - a next expected status of the report * @param parameters.isPaidWithWallet - Whether a report has been paid with wallet or outside of Expensify * @returns nextStep */ -function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { - const {statusNum = CONST.REPORT.STATUS_NUM.OPEN, isPreventSelfApprovalEnabled = false, ownerAccountID = -1} = report; +function buildNextStep(report: Report, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { + const {isPreventSelfApprovalEnabled = false, ownerAccountID = -1} = report; const policy = ReportUtils.getPolicy(report.policyID ?? ''); const isOwner = currentUserAccountID === ownerAccountID; const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? ''; @@ -68,7 +69,7 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete const type: ReportNextStep['type'] = 'neutral'; let optimisticNextStep: ReportNextStep | null; - switch (statusNum) { + switch (predictedNextStatus) { // Generates an optimistic nextStep once a report has been opened case CONST.REPORT.STATUS_NUM.OPEN: // Self review diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 9eb3e0a14555..bd4c7c9a0834 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -31,13 +31,12 @@ describe('libs/NextStepUtils', () => { areChatRoomsEnabled: true, isPolicyExpenseChatEnabled: true, }; - const report = ReportUtils.buildOptimisticExpenseReport('fake-chat-report-id-1', policyID, 1, -500, CONST.CURRENCY.USD) as Report; - const optimisticNextStep: ReportNextStep = { type: 'neutral', title: '', message: [], }; + const report = ReportUtils.buildOptimisticExpenseReport('fake-chat-report-id-1', policyID, 1, -500, CONST.CURRENCY.USD) as Report; beforeAll(() => { // @ts-expect-error Preset necessary values @@ -61,10 +60,6 @@ describe('libs/NextStepUtils', () => { }); describe('it generates an optimistic nextStep once a report has been opened', () => { - beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS_NUM.OPEN; - }); - test('self review', () => { optimisticNextStep.title = 'Next Steps:'; optimisticNextStep.message = [ @@ -87,7 +82,7 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -120,7 +115,7 @@ describe('libs/NextStepUtils', () => { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isAutoApprovalEnabled: true, }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -147,7 +142,7 @@ describe('libs/NextStepUtils', () => { isHarvestingEnabled: true, autoReportingFrequency: 'immediate', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -171,7 +166,7 @@ describe('libs/NextStepUtils', () => { isHarvestingEnabled: true, autoReportingFrequency: 'weekly', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -195,7 +190,7 @@ describe('libs/NextStepUtils', () => { isHarvestingEnabled: true, autoReportingFrequency: 'semimonthly', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -220,7 +215,7 @@ describe('libs/NextStepUtils', () => { autoReportingFrequency: 'monthly', autoReportingOffset: '2', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -245,7 +240,7 @@ describe('libs/NextStepUtils', () => { autoReportingFrequency: 'monthly', autoReportingOffset: 'lastDayOfMonth', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -269,7 +264,7 @@ describe('libs/NextStepUtils', () => { isHarvestingEnabled: true, autoReportingFrequency: 'trip', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -303,7 +298,7 @@ describe('libs/NextStepUtils', () => { isHarvestingEnabled: true, autoReportingFrequency: 'manual', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -336,7 +331,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { submitsTo: currentUserAccountID, }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -344,10 +339,6 @@ describe('libs/NextStepUtils', () => { }); describe('it generates an optimistic nextStep once a report has been submitted', () => { - beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS_NUM.SUBMITTED; - }); - test('self review', () => { optimisticNextStep.title = 'Next Steps:'; optimisticNextStep.message = [ @@ -370,7 +361,7 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED); expect(result).toStrictEqual(optimisticNextStep); }); @@ -400,7 +391,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { submitsTo: strangeAccountID, }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.SUBMITTED); expect(result).toStrictEqual(optimisticNextStep); }); @@ -433,17 +424,13 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.SUBMITTED); expect(result).toStrictEqual(optimisticNextStep); }); }); describe('it generates an optimistic nextStep once a report has been approved', () => { - beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS_NUM.APPROVED; - }); - test('self review', () => { optimisticNextStep.title = 'Next Steps:'; optimisticNextStep.message = [ @@ -466,7 +453,7 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED); expect(result).toStrictEqual(optimisticNextStep); }); @@ -480,17 +467,13 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED); expect(result).toStrictEqual(optimisticNextStep); }); }); describe('it generates an optimistic nextStep once a report has been paid', () => { - beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS_NUM.REIMBURSED; - }); - test('paid with wallet', () => { optimisticNextStep.title = 'Finished!'; optimisticNextStep.message = [ @@ -510,7 +493,7 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report, {isPaidWithWallet: true}); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithWallet: true}); expect(result).toStrictEqual(optimisticNextStep); }); @@ -537,7 +520,7 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report, {isPaidWithWallet: false}); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithWallet: false}); expect(result).toStrictEqual(optimisticNextStep); }); From 13d2d7f83bf0843b1368b38e5e554b00a110aafd Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Jan 2024 14:55:51 +0100 Subject: [PATCH 16/65] replace review with approve once submit --- src/libs/NextStepUtils.ts | 4 ++-- tests/unit/NextStepUtilsTest.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 4cebc1325b27..321d72187545 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -200,7 +200,7 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { text: ' to ', }, { - text: 'review', + text: 'approve', type: 'strong', }, { @@ -380,7 +380,7 @@ describe('libs/NextStepUtils', () => { text: ' to ', }, { - text: 'review', + text: 'approve', type: 'strong', }, { @@ -416,7 +416,7 @@ describe('libs/NextStepUtils', () => { text: ' to ', }, { - text: 'review', + text: 'approve', type: 'strong', }, { From 81616da30daeb78c9a77067812cf0206459a2c22 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Jan 2024 17:05:07 +0100 Subject: [PATCH 17/65] handle manager case --- src/libs/NextStepUtils.ts | 12 ++++++++---- tests/unit/NextStepUtilsTest.ts | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 321d72187545..cdc7c625b189 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -60,8 +60,9 @@ type BuildNextStepParameters = { * @returns nextStep */ function buildNextStep(report: Report, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { - const {isPreventSelfApprovalEnabled = false, ownerAccountID = -1} = report; + const {isPreventSelfApprovalEnabled = false, ownerAccountID = -1, managerID} = report; const policy = ReportUtils.getPolicy(report.policyID ?? ''); + const isManager = currentUserAccountID === managerID; const isOwner = currentUserAccountID === ownerAccountID; const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? ''; const isSelfApproval = currentUserAccountID === policy.submitsTo; @@ -183,7 +184,9 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { beforeEach(() => { report.ownerAccountID = currentUserAccountID; + report.managerID = currentUserAccountID; optimisticNextStep.title = ''; optimisticNextStep.message = []; }); @@ -353,7 +354,7 @@ describe('libs/NextStepUtils', () => { text: ' to ', }, { - text: 'approve', + text: 'review', type: 'strong', }, { @@ -367,6 +368,7 @@ describe('libs/NextStepUtils', () => { }); test('another reviewer', () => { + report.managerID = strangeAccountID; optimisticNextStep.title = 'Next Steps:'; optimisticNextStep.message = [ { @@ -416,7 +418,7 @@ describe('libs/NextStepUtils', () => { text: ' to ', }, { - text: 'approve', + text: 'review', type: 'strong', }, { From 003d387200ff20b92e897bae0796e603591e802c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Jan 2024 18:05:10 +0100 Subject: [PATCH 18/65] improve adding the random for manual approval message --- src/libs/NextStepUtils.ts | 14 +++++++------- tests/unit/NextStepUtilsTest.ts | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index cdc7c625b189..43569f4c8106 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -151,13 +151,6 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { report.managerID = currentUserAccountID; optimisticNextStep.title = ''; optimisticNextStep.message = []; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policy).then(waitForBatchedUpdates); }); describe('it generates an optimistic nextStep once a report has been opened', () => { From 85731fbdaadb584ddc1b8ac8f20a25bcec4d2075 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Jan 2024 18:31:33 +0100 Subject: [PATCH 19/65] integrate optimistic next step generation --- src/libs/actions/IOU.js | 107 +++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 41 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 430d88b98569..66e804cf76ab 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -14,6 +14,7 @@ import * as IOUUtils from '@libs/IOUUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import * as Localize from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; +import * as NextStepUtils from '@libs/NextStepUtils'; import * as NumberUtils from '@libs/NumberUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Permissions from '@libs/Permissions'; @@ -330,6 +331,7 @@ function getReceiptError(receipt, filename, isScanRequest = true) { * @param {Array} policyTags * @param {Array} policyCategories * @param {Boolean} hasOutstandingChildRequest + * @param {Object} [optimisticNextStep] * @returns {Array} - An array containing the optimistic data, success data, and failure data. */ function buildOnyxDataForMoneyRequest( @@ -349,6 +351,7 @@ function buildOnyxDataForMoneyRequest( policyTags, policyCategories, hasOutstandingChildRequest = false, + optimisticNextStep, ) { const isScanRequest = TransactionUtils.isScanRequest(transaction); const optimisticData = [ @@ -431,6 +434,14 @@ function buildOnyxDataForMoneyRequest( }); } + if (!_.isEmpty(optimisticNextStep)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, + value: optimisticNextStep, + }); + } + const successData = [ ...(isNewChatReport ? [ @@ -810,6 +821,10 @@ function getMoneyRequestInformation( // so the employee has to submit to their manager manually. const hasOutstandingChildRequest = isPolicyExpenseChat && needsToBeManuallySubmitted; + const optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.OPEN); + // eslint-disable-next-line no-console + console.log('optimisticNextStep', optimisticNextStep); + // STEP 5: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest( chatReport, @@ -828,6 +843,7 @@ function getMoneyRequestInformation( policyTags, policyCategories, hasOutstandingChildRequest, + optimisticNextStep, ); return { @@ -3024,6 +3040,7 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho } const currentNextStep = lodashGet(allNextSteps, `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, null); + const optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithWallet: paymentMethodType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY}); const optimisticData = [ { @@ -3065,6 +3082,11 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, value: {[iouReport.policyID]: paymentMethodType}, }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, + value: optimisticNextStep, + }, ]; const successData = [ @@ -3099,20 +3121,12 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, value: chatReport, }, - ]; - - if (!_.isNull(currentNextStep)) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, - value: null, - }); - failureData.push({ + { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, value: currentNextStep, - }); - } + }, + ]; // In case the report preview action is loaded locally, let's update it. if (optimisticReportPreviewAction) { @@ -3184,9 +3198,9 @@ function sendMoneyWithWallet(report, amount, currency, comment, managerID, recip } function approveMoneyRequest(expenseReport) { - const currentNextStep = lodashGet(allNextSteps, `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, null); - const optimisticApprovedReportAction = ReportUtils.buildOptimisticApprovedReportAction(expenseReport.total, expenseReport.currency, expenseReport.reportID); + const currentNextStep = lodashGet(allNextSteps, `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, null); + const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.APPROVED); const optimisticReportActionsData = { onyxMethod: Onyx.METHOD.MERGE, @@ -3209,7 +3223,15 @@ function approveMoneyRequest(expenseReport) { statusNum: CONST.REPORT.STATUS_NUM.APPROVED, }, }; - const optimisticData = [optimisticIOUReportData, optimisticReportActionsData]; + const optimisticData = [ + optimisticIOUReportData, + optimisticReportActionsData, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: optimisticNextStep, + }, + ]; const successData = [ { @@ -3233,20 +3255,12 @@ function approveMoneyRequest(expenseReport) { }, }, }, - ]; - - if (!_.isNull(currentNextStep)) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, - value: null, - }); - failureData.push({ + { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, value: currentNextStep, - }); - } + }, + ]; API.write('ApproveMoneyRequest', {reportID: expenseReport.reportID, approvedReportActionID: optimisticApprovedReportAction.reportActionID}, {optimisticData, successData, failureData}); } @@ -3255,11 +3269,11 @@ function approveMoneyRequest(expenseReport) { * @param {Object} expenseReport */ function submitReport(expenseReport) { - const currentNextStep = lodashGet(allNextSteps, `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, null); - const optimisticSubmittedReportAction = ReportUtils.buildOptimisticSubmittedReportAction(expenseReport.total, expenseReport.currency, expenseReport.reportID); const parentReport = ReportUtils.getReport(expenseReport.parentReportID); const isCurrentUserManager = currentUserPersonalDetails.accountID === expenseReport.managerID; + const currentNextStep = lodashGet(allNextSteps, `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, null); + const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.SUBMITTED); const optimisticData = [ { @@ -3283,6 +3297,11 @@ function submitReport(expenseReport) { statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }, }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: optimisticNextStep, + }, ...(parentReport.reportID ? [ { @@ -3330,6 +3349,11 @@ function submitReport(expenseReport) { stateNum: CONST.REPORT.STATE_NUM.OPEN, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: currentNextStep, + }, ...(parentReport.reportID ? [ { @@ -3344,19 +3368,6 @@ function submitReport(expenseReport) { : []), ]; - if (!_.isNull(currentNextStep)) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, - value: null, - }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, - value: currentNextStep, - }); - } - API.write( 'SubmitReport', { @@ -3376,6 +3387,10 @@ function cancelPayment(expenseReport, chatReport) { const optimisticReportAction = ReportUtils.buildOptimisticCancelPaymentReportAction(expenseReport.reportID); const policy = ReportUtils.getPolicy(chatReport.policyID); const isFree = policy && policy.type === CONST.POLICY.TYPE.FREE; + const statusNum = isFree ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.OPEN; + const currentNextStep = lodashGet(allNextSteps, `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, null); + const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, statusNum); + const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -3395,9 +3410,14 @@ function cancelPayment(expenseReport, chatReport) { lastMessageText: lodashGet(optimisticReportAction, 'message.0.text', ''), lastMessageHtml: lodashGet(optimisticReportAction, 'message.0.html', ''), stateNum: isFree ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.OPEN, - statusNum: isFree ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.OPEN, + statusNum, }, }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: optimisticNextStep, + }, ...(chatReport.reportID ? [ { @@ -3442,6 +3462,11 @@ function cancelPayment(expenseReport, chatReport) { statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: currentNextStep, + }, ...(chatReport.reportID ? [ { From fa2c8fb084c825c2af6c34cf0d10e10d265fdc17 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 19 Jan 2024 14:00:09 +0100 Subject: [PATCH 20/65] minor improvements --- src/CONST.ts | 1 + src/libs/NextStepUtils.ts | 36 ++++++++++++++++----------------- src/types/onyx/Policy.ts | 3 --- tests/unit/NextStepUtilsTest.ts | 2 +- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index e3cce4b613af..5de9b9f29a28 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -182,6 +182,7 @@ const CONST = { UNIX_EPOCH: '1970-01-01 00:00:00.000', MAX_DATE: '9999-12-31', MIN_DATE: '0001-01-01', + ORDINAL_DAY_OF_MONTH: 'do', }, SMS: { DOMAIN: '@expensify.sms', diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 43569f4c8106..df477a887926 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -105,47 +105,45 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf; - /** The scheduled submission date */ - autoReportingOffset?: string; - /** Whether the scheduled submit is enabled */ isHarvestingEnabled?: boolean; diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 6b6b046fd427..820ffb937450 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -216,7 +216,7 @@ describe('libs/NextStepUtils', () => { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'monthly', - autoReportingOffset: '2', + autoReportingOffset: 2, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); From dfec8f60dbce63a758a5fa5ab3171e2aa0fca8c2 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 19 Jan 2024 14:01:25 +0100 Subject: [PATCH 21/65] remove log --- src/libs/actions/IOU.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index ba6227512f7a..f5858a234823 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -822,8 +822,6 @@ function getMoneyRequestInformation( const hasOutstandingChildRequest = isPolicyExpenseChat && needsToBeManuallySubmitted; const optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.OPEN); - // eslint-disable-next-line no-console - console.log('optimisticNextStep', optimisticNextStep); // STEP 5: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest( From 59a036dede1a9d8e7d0e91898ec3d0312b326061 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 19 Jan 2024 15:03:01 +0100 Subject: [PATCH 22/65] improve and fix tests --- src/libs/NextStepUtils.ts | 3 +- tests/unit/NextStepUtilsTest.ts | 65 ++++++++++++++++++--------------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index df477a887926..371ef7cda8ec 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -99,7 +99,7 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); test('self review and auto approval enabled', () => { @@ -115,17 +115,19 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isAutoApprovalEnabled: true, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); describe('scheduled submit enabled', () => { - optimisticNextStep.title = 'Next Steps:'; + beforeEach(() => { + optimisticNextStep.title = 'Next Steps:'; + }); test('daily', () => { optimisticNextStep.message = [ @@ -141,13 +143,13 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'immediate', }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -165,13 +167,13 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'weekly', }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -189,13 +191,13 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'semimonthly', }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -213,14 +215,14 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'monthly', autoReportingOffset: 2, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -238,14 +240,14 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'monthly', autoReportingOffset: 'lastDayOfMonth', }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -263,13 +265,13 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'trip', }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -292,18 +294,15 @@ describe('libs/NextStepUtils', () => { { text: ' these expenses.', }, - { - text: ' This report may be selected at random for manual approval.', - }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'manual', }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); }); @@ -336,7 +335,7 @@ describe('libs/NextStepUtils', () => { }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); }); @@ -366,7 +365,7 @@ describe('libs/NextStepUtils', () => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); test('another reviewer', () => { @@ -397,7 +396,7 @@ describe('libs/NextStepUtils', () => { }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.SUBMITTED); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -430,7 +429,7 @@ describe('libs/NextStepUtils', () => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.SUBMITTED); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -459,7 +458,7 @@ describe('libs/NextStepUtils', () => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); test('another owner', () => { @@ -473,7 +472,7 @@ describe('libs/NextStepUtils', () => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -499,7 +498,7 @@ describe('libs/NextStepUtils', () => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithWallet: true}); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); test('paid outside of Expensify', () => { @@ -526,7 +525,15 @@ describe('libs/NextStepUtils', () => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithWallet: false}); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); + }); + }); + + describe('it generates a nullable optimistic nextStep', () => { + test('closed status', () => { + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.CLOSED); + + expect(result).toBeNull(); }); }); }); From 93f21dae4c3a1d936e302fe30eea49e361f38c98 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 12:29:03 +0530 Subject: [PATCH 23/65] switch to form element instead of view --- src/components/Form/FormWrapper.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Form/FormWrapper.js b/src/components/Form/FormWrapper.js index 0d468dbcd0d2..f95b7f7eb801 100644 --- a/src/components/Form/FormWrapper.js +++ b/src/components/Form/FormWrapper.js @@ -1,9 +1,10 @@ import PropTypes from 'prop-types'; import React, {useCallback, useMemo, useRef} from 'react'; -import {Keyboard, ScrollView, StyleSheet, View} from 'react-native'; +import {Keyboard, ScrollView, StyleSheet} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; +import FormElement from '@components/FormElement'; import refPropTypes from '@components/refPropTypes'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; @@ -110,7 +111,7 @@ function FormWrapper(props) { const scrollViewContent = useCallback( (safeAreaPaddingBottomStyle) => ( - )} - + ), [ children, From 4848e99953ac4c0fcdbfc278cdbf657a5a29f4f6 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 19:13:09 +0530 Subject: [PATCH 24/65] adjustment according to recommendations --- src/components/Form/FormProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index f4118912c8f9..246b19dcd932 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -271,6 +271,7 @@ const FormProvider = forwardRef( } propsToParse.onSubmitEditing(e); }, + returnKeyType: 'go', } : {}; @@ -288,7 +289,6 @@ const FormProvider = forwardRef( return { ...propsToParse, - returnKeyType: shouldSubmitEdit ? 'go' : propsToParse.returnKeyType, blurOnSubmit: (isMultiline && shouldSubmitEdit) || propsToParse.blurOnSubmit, ...onSubmitEditingObject, ref: From 55bb4d2220a16229788ee154f67ac638af88775e Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 23 Jan 2024 19:52:38 +0530 Subject: [PATCH 25/65] changes according to recommendation --- src/components/Form/InputWrapper.js | 16 ++++++++++++---- src/pages/EditRequestDescriptionPage.js | 3 +-- src/pages/iou/MoneyRequestDescriptionPage.js | 3 +-- .../request/step/IOURequestStepDescription.js | 3 +-- src/pages/tasks/NewTaskDescriptionPage.js | 3 +-- src/pages/tasks/NewTaskDetailsPage.js | 3 +-- src/pages/tasks/TaskDescriptionPage.js | 3 +-- 7 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index 7f49660478ff..71f9a3273164 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React, {forwardRef, useContext} from 'react'; import refPropTypes from '@components/refPropTypes'; import TextInput from '@components/TextInput'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import FormContext from './FormContext'; const propTypes = { @@ -9,24 +10,31 @@ const propTypes = { inputID: PropTypes.string.isRequired, valueType: PropTypes.string, forwardedRef: refPropTypes, + + /** Whether the input allows the form to be submitted when the user presses enter. + * This is useful for inputs that are not multiline and don't have a submit button by default. + * This property is ignored on mobile devices as they don't have a shift + enter key to create a newline. + */ + inputAllowsSubmit: PropTypes.bool, }; const defaultProps = { forwardedRef: undefined, valueType: 'string', + inputAllowsSubmit: false, }; -const canUseSubmitEditing = (multiline, autoGrowHeight, submitOnEnter) => { +const canUseSubmitEditing = (multiline, autoGrowHeight, inputAllowsSubmit) => { const isMultiline = multiline || autoGrowHeight; if (!isMultiline) { return true; } - return Boolean(submitOnEnter); + return Boolean(inputAllowsSubmit) && !canUseTouchScreen(); }; function InputWrapper(props) { - const {InputComponent, inputID, forwardedRef, ...rest} = props; - const shouldSubmitEdit = canUseSubmitEditing(rest.multiline, rest.autoGrowHeight, rest.submitOnEnter); + const {InputComponent, inputID, forwardedRef, inputAllowsSubmit, ...rest} = props; + const shouldSubmitEdit = canUseSubmitEditing(rest.multiline, rest.autoGrowHeight, inputAllowsSubmit); const {registerInput} = useContext(FormContext); // There are inputs that dont have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were diff --git a/src/pages/EditRequestDescriptionPage.js b/src/pages/EditRequestDescriptionPage.js index b459c17a3ee3..d64cee3c5dc3 100644 --- a/src/pages/EditRequestDescriptionPage.js +++ b/src/pages/EditRequestDescriptionPage.js @@ -9,7 +9,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -77,7 +76,7 @@ function EditRequestDescriptionPage({defaultDescription, onSubmit}) { }} autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} - submitOnEnter={!canUseTouchScreen()} + inputAllowsSubmit /> diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index 7d3c31ca12ba..74fe27af9dfd 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -13,7 +13,6 @@ import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as IOU from '@libs/actions/IOU'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; @@ -142,7 +141,7 @@ function MoneyRequestDescriptionPage({iou, route, selectedTab}) { }} autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} - submitOnEnter={!canUseTouchScreen()} + inputAllowsSubmit /> diff --git a/src/pages/iou/request/step/IOURequestStepDescription.js b/src/pages/iou/request/step/IOURequestStepDescription.js index 7473239d92b5..fe0f65f9ea3b 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.js +++ b/src/pages/iou/request/step/IOURequestStepDescription.js @@ -9,7 +9,6 @@ import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import * as IOU from '@userActions/IOU'; @@ -107,7 +106,7 @@ function IOURequestStepDescription({ autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} inputStyle={[styles.verticalAlignTop]} - submitOnEnter={!canUseTouchScreen()} + inputAllowsSubmit /> diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.js index 7620afaf2dc8..e1d8945fe58f 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.js +++ b/src/pages/tasks/NewTaskDescriptionPage.js @@ -12,7 +12,6 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import * as Task from '@userActions/Task'; @@ -79,7 +78,7 @@ function NewTaskDescriptionPage(props) { updateMultilineInputRange(el); }} autoGrowHeight - submitOnEnter={!canUseTouchScreen()} + inputAllowsSubmit containerStyles={[styles.autoGrowHeightMultilineInput]} /> diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js index adf26820f7b9..29d84ad60b8b 100644 --- a/src/pages/tasks/NewTaskDetailsPage.js +++ b/src/pages/tasks/NewTaskDetailsPage.js @@ -12,7 +12,6 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as Task from '@userActions/Task'; @@ -111,7 +110,7 @@ function NewTaskDetailsPage(props) { label={props.translate('newTaskPage.descriptionOptional')} accessibilityLabel={props.translate('newTaskPage.descriptionOptional')} autoGrowHeight - submitOnEnter={!canUseTouchScreen()} + inputAllowsSubmit containerStyles={[styles.autoGrowHeightMultilineInput]} defaultValue={parser.htmlToMarkdown(parser.replace(taskDescription))} value={taskDescription} diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js index db8974632ab7..453f55e5a89d 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.js @@ -16,7 +16,6 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; @@ -126,7 +125,7 @@ function TaskDescriptionPage(props) { updateMultilineInputRange(inputRef.current); }} autoGrowHeight - submitOnEnter={!canUseTouchScreen()} + inputAllowsSubmit containerStyles={[styles.autoGrowHeightMultilineInput]} /> From 8481f664890c039efd7ee81011848f5a49399011 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 24 Jan 2024 17:18:30 +0100 Subject: [PATCH 26/65] integrate last business day calculation --- src/libs/NextStepUtils.ts | 11 +++++------ tests/unit/NextStepUtilsTest.ts | 29 ++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 371ef7cda8ec..9a5ec66db948 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -1,4 +1,4 @@ -import {format, lastDayOfMonth} from 'date-fns'; +import {format, lastDayOfMonth, setDate} from 'date-fns'; import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -6,6 +6,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report, ReportNextStep} from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportNextStep'; +import DateUtils from './DateUtils'; import EmailUtils from './EmailUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as ReportUtils from './ReportUtils'; @@ -113,12 +114,10 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { text: 'These expenses are scheduled to ', }, { - text: `automatically submit on the ${format(lastDayOfMonth(new Date()), 'do')} of each month!`, + text: `automatically submit on the ${format(lastDayOfMonth(new Date()), CONST.DATE.ORDINAL_DAY_OF_MONTH)} of each month!`, type: 'strong', }, { @@ -251,6 +252,32 @@ describe('libs/NextStepUtils', () => { }); }); + test('monthly on the last business day', () => { + const lastBusinessDayOfMonth = DateUtils.getLastBusinessDayOfMonth(new Date()); + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: `automatically submit on the ${format(new Date().setDate(lastBusinessDayOfMonth), CONST.DATE.ORDINAL_DAY_OF_MONTH)} of each month!`, + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'monthly', + autoReportingOffset: 'lastBusinessDayOfMonth', + }).then(() => { + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); + + expect(result).toMatchObject(optimisticNextStep); + }); + }); + test('trip', () => { optimisticNextStep.message = [ { From 4ce1443b839da2a75ada8193ecbbf6aea46958dd Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 25 Jan 2024 15:48:02 +0100 Subject: [PATCH 27/65] ref: move ReportDetailsPage to TS --- src/components/DisplayNames/types.ts | 2 +- src/libs/PolicyUtils.ts | 2 +- src/libs/ReportUtils.ts | 2 +- ...rtDetailsPage.js => ReportDetailsPage.tsx} | 182 +++++++++--------- .../home/report/withReportOrNotFound.tsx | 8 +- 5 files changed, 94 insertions(+), 102 deletions(-) rename src/pages/{ReportDetailsPage.js => ReportDetailsPage.tsx} (65%) diff --git a/src/components/DisplayNames/types.ts b/src/components/DisplayNames/types.ts index 2e6f36d5cc07..7da1819c9f01 100644 --- a/src/components/DisplayNames/types.ts +++ b/src/components/DisplayNames/types.ts @@ -20,7 +20,7 @@ type DisplayNamesProps = { fullTitle: string; /** Array of objects that map display names to their corresponding tooltip */ - displayNamesWithTooltips: DisplayNameWithTooltip[]; + displayNamesWithTooltips?: DisplayNameWithTooltip[]; /** Number of lines before wrapping */ numberOfLines: number; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index b8ed62f93082..47916dc474ba 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -112,7 +112,7 @@ function isExpensifyGuideTeam(email: string): boolean { */ const isPolicyAdmin = (policy: OnyxEntry): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; -const isPolicyMember = (policyID: string, policies: Record): boolean => Object.values(policies).some((policy) => policy?.id === policyID); +const isPolicyMember = (policyID: string, policies: OnyxCollection): boolean => Object.values(policies ?? {}).some((policy) => policy?.id === policyID); /** * Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID. diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e9c3b1710cc0..fb15feb38d59 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4540,7 +4540,7 @@ function shouldAutoFocusOnKeyPress(event: KeyboardEvent): boolean { /** * Navigates to the appropriate screen based on the presence of a private note for the current user. */ -function navigateToPrivateNotes(report: Report, session: Session) { +function navigateToPrivateNotes(report: OnyxEntry, session: OnyxEntry) { if (isEmpty(report) || isEmpty(session) || !session.accountID) { return; } diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.tsx similarity index 65% rename from src/pages/ReportDetailsPage.js rename to src/pages/ReportDetailsPage.tsx index 3e682d592370..9a9ca49c68eb 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.tsx @@ -1,103 +1,99 @@ -import PropTypes from 'prop-types'; +import type {FC} from 'react'; import React, {useEffect, useMemo} from 'react'; import {ScrollView, View} from 'react-native'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {SvgProps} from 'react-native-svg'; +import type {ValueOf} from 'type-fest'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DisplayNames from '@components/DisplayNames'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import MultipleAvatars from '@components/MultipleAvatars'; -import {withNetwork} from '@components/OnyxProvider'; import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle'; -import participantPropTypes from '@components/participantPropTypes'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import RoomHeaderAvatars from '@components/RoomHeaderAvatars'; import ScreenWrapper from '@components/ScreenWrapper'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound'; import withReportOrNotFound from './home/report/withReportOrNotFound'; -import reportPropTypes from './reportPropTypes'; -const propTypes = { - ...withLocalizePropTypes, - - /** The report currently being looked at */ - report: reportPropTypes.isRequired, - - /** The policies which the user has access to and which the report could be tied to */ - policies: PropTypes.shape({ - /** Name of the policy */ - name: PropTypes.string, - }), - - /** Route params */ - route: PropTypes.shape({ - params: PropTypes.shape({ - /** Report ID passed via route r/:reportID/details */ - reportID: PropTypes.string, - }), - }).isRequired, - - /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), +type ReportDetailsPageMenuItem = { + key: DeepValueOf; + translationKey: TranslationPaths; + icon: FC; + isAnonymousAction: boolean; + action: () => void; + brickRoadIndicator?: ValueOf; + subtitle?: number; }; -const defaultProps = { - policies: {}, - personalDetails: {}, +type ReportDetailsPageOnyxProps = { + personalDetails: OnyxCollection; + session: OnyxEntry; }; +type ReportDetailsPageProps = { + report: OnyxEntry; +} & ReportDetailsPageOnyxProps & + WithReportOrNotFoundProps; -function ReportDetailsPage(props) { +function ReportDetailsPage({policies, report, session, personalDetails}: ReportDetailsPageProps) { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); const styles = useThemeStyles(); - const policy = useMemo(() => props.policies[`${ONYXKEYS.COLLECTION.POLICY}${props.report.policyID}`], [props.policies, props.report.policyID]); - const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy), [policy]); - const isPolicyMember = useMemo(() => PolicyUtils.isPolicyMember(props.report.policyID, props.policies), [props.report.policyID, props.policies]); - const shouldUseFullTitle = useMemo(() => ReportUtils.shouldUseFullTitleToDisplay(props.report), [props.report]); - const isChatRoom = useMemo(() => ReportUtils.isChatRoom(props.report), [props.report]); - const isThread = useMemo(() => ReportUtils.isChatThread(props.report), [props.report]); - const isUserCreatedPolicyRoom = useMemo(() => ReportUtils.isUserCreatedPolicyRoom(props.report), [props.report]); - const isArchivedRoom = useMemo(() => ReportUtils.isArchivedRoom(props.report), [props.report]); - const isMoneyRequestReport = useMemo(() => ReportUtils.isMoneyRequestReport(props.report), [props.report]); + const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`], [policies, report?.policyID]); + const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy ?? null), [policy]); + const isPolicyMember = useMemo(() => PolicyUtils.isPolicyMember(report?.policyID ?? '', policies), [report?.policyID, policies]); + const shouldUseFullTitle = useMemo(() => ReportUtils.shouldUseFullTitleToDisplay(report), [report]); + const isChatRoom = useMemo(() => ReportUtils.isChatRoom(report), [report]); + const isThread = useMemo(() => ReportUtils.isChatThread(report), [report]); + const isUserCreatedPolicyRoom = useMemo(() => ReportUtils.isUserCreatedPolicyRoom(report), [report]); + const isArchivedRoom = useMemo(() => ReportUtils.isArchivedRoom(report), [report]); + const isMoneyRequestReport = useMemo(() => ReportUtils.isMoneyRequestReport(report), [report]); // eslint-disable-next-line react-hooks/exhaustive-deps -- policy is a dependency because `getChatRoomSubtitle` calls `getPolicyName` which in turn retrieves the value from the `policy` value stored in Onyx - const chatRoomSubtitle = useMemo(() => ReportUtils.getChatRoomSubtitle(props.report), [props.report, policy]); - const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(props.report); - const participants = useMemo(() => ReportUtils.getVisibleMemberIDs(props.report), [props.report]); + const chatRoomSubtitle = useMemo(() => ReportUtils.getChatRoomSubtitle(report), [report, policy]); + const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); + const participants = useMemo(() => ReportUtils.getVisibleMemberIDs(report), [report]); - const isGroupDMChat = useMemo(() => ReportUtils.isDM(props.report) && participants.length > 1, [props.report, participants.length]); + const isGroupDMChat = useMemo(() => ReportUtils.isDM(report) && participants.length > 1, [report, participants.length]); - const isPrivateNotesFetchTriggered = !_.isUndefined(props.report.isLoadingPrivateNotes); + const isPrivateNotesFetchTriggered = report?.isLoadingPrivateNotes !== undefined; useEffect(() => { // Do not fetch private notes if isLoadingPrivateNotes is already defined, or if network is offline. - if (isPrivateNotesFetchTriggered || props.network.isOffline) { + if (isPrivateNotesFetchTriggered || isOffline) { return; } - Report.getReportPrivateNote(props.report.reportID); - }, [props.report.reportID, props.network.isOffline, isPrivateNotesFetchTriggered]); + Report.getReportPrivateNote(report?.reportID ?? ''); + }, [report?.reportID, isOffline, isPrivateNotesFetchTriggered]); - const menuItems = useMemo(() => { + const menuItems: ReportDetailsPageMenuItem[] = useMemo(() => { const items = []; if (!isGroupDMChat) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.SHARE_CODE, - translationKey: 'common.shareCode', + translationKey: 'common.shareCode' as const, icon: Expensicons.QrCode, isAnonymousAction: true, - action: () => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.getRoute(props.report.reportID)), + action: () => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.getRoute(report?.reportID ?? '')), }); } @@ -111,61 +107,62 @@ function ReportDetailsPage(props) { if ((!isUserCreatedPolicyRoom && participants.length) || (isUserCreatedPolicyRoom && isPolicyMember)) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.MEMBERS, - translationKey: 'common.members', + translationKey: 'common.members' as const, icon: Expensicons.Users, subtitle: participants.length, isAnonymousAction: false, action: () => { - if (isUserCreatedPolicyRoom && !props.report.parentReportID) { - Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(props.report.reportID)); + if (isUserCreatedPolicyRoom && !report?.parentReportID) { + Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(report?.reportID ?? '')); } else { - Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID)); + Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report?.reportID ?? '')); } }, }); - } else if (isUserCreatedPolicyRoom && (!participants.length || !isPolicyMember) && !props.report.parentReportID) { + } else if (isUserCreatedPolicyRoom && (!participants.length || !isPolicyMember) && !report?.parentReportID) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.INVITE, - translationKey: 'common.invite', + translationKey: 'common.invite' as const, icon: Expensicons.Users, isAnonymousAction: false, action: () => { - Navigation.navigate(ROUTES.ROOM_INVITE.getRoute(props.report.reportID)); + Navigation.navigate(ROUTES.ROOM_INVITE.getRoute(report?.reportID ?? '')); }, }); } items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.SETTINGS, - translationKey: 'common.settings', + translationKey: 'common.settings' as const, icon: Expensicons.Gear, isAnonymousAction: false, action: () => { - Navigation.navigate(ROUTES.REPORT_SETTINGS.getRoute(props.report.reportID)); + Navigation.navigate(ROUTES.REPORT_SETTINGS.getRoute(report?.reportID ?? '')); }, }); // Prevent displaying private notes option for threads and task reports - if (!isThread && !isMoneyRequestReport && !ReportUtils.isTaskReport(props.report)) { + if (!isThread && !isMoneyRequestReport && !ReportUtils.isTaskReport(report)) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.PRIVATE_NOTES, - translationKey: 'privateNotes.title', + translationKey: 'privateNotes.title' as const, icon: Expensicons.Pencil, isAnonymousAction: false, - action: () => ReportUtils.navigateToPrivateNotes(props.report, props.session), - brickRoadIndicator: Report.hasErrorInPrivateNotes(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', + action: () => ReportUtils.navigateToPrivateNotes(report, session), + brickRoadIndicator: Report.hasErrorInPrivateNotes(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }); } return items; - }, [isArchivedRoom, participants.length, isThread, isMoneyRequestReport, props.report, isGroupDMChat, isPolicyMember, isUserCreatedPolicyRoom, props.session]); + }, [isArchivedRoom, participants.length, isThread, isMoneyRequestReport, report, isGroupDMChat, isPolicyMember, isUserCreatedPolicyRoom, session]); const displayNamesWithTooltips = useMemo(() => { const hasMultipleParticipants = participants.length > 1; - return ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participants, props.personalDetails), hasMultipleParticipants); - }, [participants, props.personalDetails]); + // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. + return ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails), hasMultipleParticipants); + }, [participants, personalDetails]); - const icons = useMemo(() => ReportUtils.getIcons(props.report, props.personalDetails, props.policies), [props.report, props.personalDetails, props.policies]); + const icons = useMemo(() => ReportUtils.getIcons(report, personalDetails, policies), [report, personalDetails, policies]); const chatRoomSubtitleText = chatRoomSubtitle ? ( - + { Navigation.goBack(); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.report.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report?.reportID ?? '')); }} /> @@ -199,14 +196,14 @@ function ReportDetailsPage(props) { ) : ( )} { - Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(props.report.policyID)); + Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(report?.policyID ?? '')); }} > {chatRoomSubtitleText} @@ -229,28 +227,28 @@ function ReportDetailsPage(props) { ) : ( chatRoomSubtitleText )} - {!_.isEmpty(parentNavigationSubtitleData) && isMoneyRequestReport && ( + {!isEmptyObject(parentNavigationSubtitleData) && isMoneyRequestReport && ( )} - {_.map(menuItems, (item) => { + {menuItems.map((item) => { const brickRoadIndicator = - ReportUtils.hasReportNameError(props.report) && item.key === CONST.REPORT_DETAILS_MENU_ITEM.SETTINGS ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + ReportUtils.hasReportNameError(report) && item.key === CONST.REPORT_DETAILS_MENU_ITEM.SETTINGS ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined; return ( ); })} @@ -261,22 +259,14 @@ function ReportDetailsPage(props) { } ReportDetailsPage.displayName = 'ReportDetailsPage'; -ReportDetailsPage.propTypes = propTypes; -ReportDetailsPage.defaultProps = defaultProps; -export default compose( - withLocalize, - withReportOrNotFound(), - withNetwork(), - withOnyx({ +export default withReportOrNotFound()( + withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - }, session: { key: ONYXKEYS.SESSION, }, - }), -)(ReportDetailsPage); + })(ReportDetailsPage), +); diff --git a/src/pages/home/report/withReportOrNotFound.tsx b/src/pages/home/report/withReportOrNotFound.tsx index 7613bafeacdc..a8facc3e1c76 100644 --- a/src/pages/home/report/withReportOrNotFound.tsx +++ b/src/pages/home/report/withReportOrNotFound.tsx @@ -22,16 +22,16 @@ type OnyxProps = { isLoadingReportData: OnyxEntry; }; -type ComponentProps = OnyxProps & { +type WithReportOrNotFoundProps = OnyxProps & { route: RouteProp<{params: {reportID: string}}>; }; export default function ( shouldRequireReportID = true, -): ( +): ( WrappedComponent: React.ComponentType>, ) => React.ComponentType, keyof OnyxProps>> { - return function (WrappedComponent: ComponentType>) { + return function (WrappedComponent: ComponentType>) { function WithReportOrNotFound(props: TProps, ref: ForwardedRef) { const contentShown = React.useRef(false); @@ -89,3 +89,5 @@ export default function ( })(React.forwardRef(WithReportOrNotFound)); }; } + +export type {WithReportOrNotFoundProps}; From 8bf49b65d002766aa90e71ff7a03eba6c01a854d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 25 Jan 2024 15:53:52 +0100 Subject: [PATCH 28/65] fix: typecheck --- src/components/DisplayNames/DisplayNamesWithTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx index ce0ae7ddcf4f..1cacb0e20c5d 100644 --- a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx +++ b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx @@ -56,7 +56,7 @@ function DisplayNamesWithToolTip({shouldUseFullTitle, fullTitle, displayNamesWit > {shouldUseFullTitle ? ReportUtils.formatReportLastMessageText(fullTitle) - : displayNamesWithTooltips.map(({displayName, accountID, avatar, login}, index) => ( + : displayNamesWithTooltips?.map(({displayName, accountID, avatar, login}, index) => ( // eslint-disable-next-line react/no-array-index-key Date: Thu, 25 Jan 2024 15:59:50 +0100 Subject: [PATCH 29/65] fix: add comments to props --- src/pages/ReportDetailsPage.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 9a9ca49c68eb..bceaeddc0349 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -44,10 +44,14 @@ type ReportDetailsPageMenuItem = { }; type ReportDetailsPageOnyxProps = { + /** Personal details of all the users */ personalDetails: OnyxCollection; + + /** Session info for the currently logged in user. */ session: OnyxEntry; }; type ReportDetailsPageProps = { + /** The report currently being looked at */ report: OnyxEntry; } & ReportDetailsPageOnyxProps & WithReportOrNotFoundProps; From 7467e2c5a4fa037024c7a13b38898eca0d704436 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 25 Jan 2024 17:29:11 +0100 Subject: [PATCH 30/65] prettify codebase --- src/libs/NextStepUtils.ts | 64 +++++++++++++++++---------------- src/types/onyx/Report.ts | 3 -- tests/unit/NextStepUtilsTest.ts | 26 +++++++------- 3 files changed, 47 insertions(+), 46 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 9a5ec66db948..f606afbc6702 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -57,17 +57,18 @@ type BuildNextStepParameters = { * * @param report * @param predictedNextStatus - a next expected status of the report - * @param parameters.isPaidWithWallet - Whether a report has been paid with wallet or outside of Expensify + * @param parameters.isPaidWithWallet - Whether a report has been paid with the wallet or outside of Expensify * @returns nextStep */ function buildNextStep(report: Report, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { - const {isPreventSelfApprovalEnabled = false, ownerAccountID = -1, managerID} = report; const policy = ReportUtils.getPolicy(report.policyID ?? ''); - const isManager = currentUserAccountID === managerID; + const {submitsTo, isHarvestingEnabled, isPreventSelfApprovalEnabled, isAutoApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; + const {ownerAccountID = -1, managerID = -1} = report; const isOwner = currentUserAccountID === ownerAccountID; + const isManager = currentUserAccountID === managerID; + const isSelfApproval = currentUserAccountID === submitsTo; const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? ''; - const isSelfApproval = currentUserAccountID === policy.submitsTo; - const submitterDisplayName = isSelfApproval ? 'you' : ReportUtils.getDisplayNameForParticipant(policy.submitsTo, true) ?? ''; + const submitterDisplayName = isSelfApproval ? 'you' : ReportUtils.getDisplayNameForParticipant(submitsTo, true) ?? ''; const type: ReportNextStep['type'] = 'neutral'; let optimisticNextStep: ReportNextStep | null; @@ -100,7 +101,7 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf; - - /** Whether the user can do self approve or submit of an expense report */ - isPreventSelfApprovalEnabled?: boolean; }; export default Report; diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 5c6aef46a87f..5de6f539ea53 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -1,4 +1,4 @@ -import {format, lastDayOfMonth} from 'date-fns'; +import {format, lastDayOfMonth, setDate} from 'date-fns'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -146,7 +146,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'immediate', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -170,7 +170,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'weekly', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -194,7 +194,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'semimonthly', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -218,7 +218,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'monthly', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: 2, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -243,8 +243,8 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'monthly', - autoReportingOffset: 'lastDayOfMonth', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, + autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_DAY_OF_MONTH, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -259,7 +259,7 @@ describe('libs/NextStepUtils', () => { text: 'These expenses are scheduled to ', }, { - text: `automatically submit on the ${format(new Date().setDate(lastBusinessDayOfMonth), CONST.DATE.ORDINAL_DAY_OF_MONTH)} of each month!`, + text: `automatically submit on the ${format(setDate(new Date(), lastBusinessDayOfMonth), CONST.DATE.ORDINAL_DAY_OF_MONTH)} of each month!`, type: 'strong', }, { @@ -269,8 +269,8 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'monthly', - autoReportingOffset: 'lastBusinessDayOfMonth', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, + autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -294,7 +294,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'trip', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -325,7 +325,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'manual', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -335,7 +335,6 @@ describe('libs/NextStepUtils', () => { }); test('prevented self submitting', () => { - report.isPreventSelfApprovalEnabled = true; optimisticNextStep.title = 'Next Steps:'; optimisticNextStep.message = [ { @@ -359,6 +358,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { submitsTo: currentUserAccountID, + isPreventSelfApprovalEnabled: true, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); From 38bfb704fc7a94d0c3e15ef5b9bb89a576876619 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 25 Jan 2024 17:50:22 +0100 Subject: [PATCH 31/65] remove one case for now --- src/libs/NextStepUtils.ts | 9 +-------- src/types/onyx/Policy.ts | 3 --- tests/unit/NextStepUtilsTest.ts | 35 --------------------------------- 3 files changed, 1 insertion(+), 46 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index f606afbc6702..dec6acaecec2 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -62,7 +62,7 @@ type BuildNextStepParameters = { */ function buildNextStep(report: Report, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { const policy = ReportUtils.getPolicy(report.policyID ?? ''); - const {submitsTo, isHarvestingEnabled, isPreventSelfApprovalEnabled, isAutoApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; + const {submitsTo, isHarvestingEnabled, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; const {ownerAccountID = -1, managerID = -1} = report; const isOwner = currentUserAccountID === ownerAccountID; const isManager = currentUserAccountID === managerID; @@ -174,13 +174,6 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { owner: currentUserEmail, submitsTo: currentUserAccountID, isHarvestingEnabled: false, - isAutoApprovalEnabled: false, // Required props name: 'Policy', role: 'admin', @@ -91,40 +90,6 @@ describe('libs/NextStepUtils', () => { expect(result).toMatchObject(optimisticNextStep); }); - test('self review and auto approval enabled', () => { - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for ', - }, - { - text: 'you', - type: 'strong', - }, - { - text: ' to ', - }, - { - text: 'submit', - type: 'strong', - }, - { - text: ' these expenses.', - }, - { - text: ' This report may be selected at random for manual approval.', - }, - ]; - - return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isAutoApprovalEnabled: true, - }).then(() => { - const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - - expect(result).toMatchObject(optimisticNextStep); - }); - }); - describe('scheduled submit enabled', () => { beforeEach(() => { optimisticNextStep.title = 'Next Steps:'; From 888c5fc67d9fda330769dc094de74cebcffe7f9b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 30 Jan 2024 12:42:45 +0100 Subject: [PATCH 32/65] fix: resolved comments --- src/pages/ReportDetailsPage.tsx | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index bceaeddc0349..d858b47e9cc2 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,3 +1,4 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import type {FC} from 'react'; import React, {useEffect, useMemo} from 'react'; import {ScrollView, View} from 'react-native'; @@ -19,6 +20,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -27,6 +29,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -50,11 +53,7 @@ type ReportDetailsPageOnyxProps = { /** Session info for the currently logged in user. */ session: OnyxEntry; }; -type ReportDetailsPageProps = { - /** The report currently being looked at */ - report: OnyxEntry; -} & ReportDetailsPageOnyxProps & - WithReportOrNotFoundProps; +type ReportDetailsPageProps = ReportDetailsPageOnyxProps & WithReportOrNotFoundProps & StackScreenProps; function ReportDetailsPage({policies, report, session, personalDetails}: ReportDetailsPageProps) { const {translate} = useLocalize(); @@ -89,12 +88,12 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD }, [report?.reportID, isOffline, isPrivateNotesFetchTriggered]); const menuItems: ReportDetailsPageMenuItem[] = useMemo(() => { - const items = []; + const items: ReportDetailsPageMenuItem[] = []; if (!isGroupDMChat) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.SHARE_CODE, - translationKey: 'common.shareCode' as const, + translationKey: 'common.shareCode', icon: Expensicons.QrCode, isAnonymousAction: true, action: () => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.getRoute(report?.reportID ?? '')), @@ -111,7 +110,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD if ((!isUserCreatedPolicyRoom && participants.length) || (isUserCreatedPolicyRoom && isPolicyMember)) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.MEMBERS, - translationKey: 'common.members' as const, + translationKey: 'common.members', icon: Expensicons.Users, subtitle: participants.length, isAnonymousAction: false, @@ -126,7 +125,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD } else if (isUserCreatedPolicyRoom && (!participants.length || !isPolicyMember) && !report?.parentReportID) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.INVITE, - translationKey: 'common.invite' as const, + translationKey: 'common.invite', icon: Expensicons.Users, isAnonymousAction: false, action: () => { @@ -137,7 +136,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.SETTINGS, - translationKey: 'common.settings' as const, + translationKey: 'common.settings', icon: Expensicons.Gear, isAnonymousAction: false, action: () => { @@ -149,7 +148,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD if (!isThread && !isMoneyRequestReport && !ReportUtils.isTaskReport(report)) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.PRIVATE_NOTES, - translationKey: 'privateNotes.title' as const, + translationKey: 'privateNotes.title', icon: Expensicons.Pencil, isAnonymousAction: false, action: () => ReportUtils.navigateToPrivateNotes(report, session), From b2a5a009825a18f06cbdc19ef6a7dc6c337d4089 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 30 Jan 2024 12:48:10 +0100 Subject: [PATCH 33/65] fix: typecheck --- src/pages/ReportDetailsPage.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index d858b47e9cc2..fc905e35899e 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,10 +1,8 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import type {FC} from 'react'; import React, {useEffect, useMemo} from 'react'; import {ScrollView, View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import type {SvgProps} from 'react-native-svg'; import type {ValueOf} from 'type-fest'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DisplayNames from '@components/DisplayNames'; @@ -33,13 +31,14 @@ import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type IconAsset from '@src/types/utils/IconAsset'; import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound'; import withReportOrNotFound from './home/report/withReportOrNotFound'; type ReportDetailsPageMenuItem = { key: DeepValueOf; translationKey: TranslationPaths; - icon: FC; + icon: IconAsset; isAnonymousAction: boolean; action: () => void; brickRoadIndicator?: ValueOf; @@ -161,7 +160,6 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD const displayNamesWithTooltips = useMemo(() => { const hasMultipleParticipants = participants.length > 1; - // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. return ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails), hasMultipleParticipants); }, [participants, personalDetails]); From cf9fcfbdc671b8c3d9155a56747db761900bc244 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 30 Jan 2024 14:46:15 +0100 Subject: [PATCH 34/65] add new harvesting field of policy --- src/types/onyx/Policy.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index eca7e9d1ee06..ffa619291516 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -88,9 +88,14 @@ type Policy = { /** The scheduled submit frequency set up on the this policy */ autoReportingFrequency?: ValueOf; - /** Whether the scheduled submit is enabled */ + /** @deprecated Whether the scheduled submit is enabled */ isHarvestingEnabled?: boolean; + /** Whether the scheduled submit is enabled */ + harvesting?: { + enabled: boolean; + }; + /** Whether the scheduled submit is enabled */ isPreventSelfApprovalEnabled?: boolean; From 821ee65b43a8c2add86e75184b848c3403c03362 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 30 Jan 2024 14:47:17 +0100 Subject: [PATCH 35/65] clarify comments --- src/types/onyx/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index ffa619291516..719e0ba1fb9d 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -96,7 +96,7 @@ type Policy = { enabled: boolean; }; - /** Whether the scheduled submit is enabled */ + /** Whether the self approval or submitting is enabled */ isPreventSelfApprovalEnabled?: boolean; /** When the monthly scheduled submit should happen */ From 9a36327265853796e28ebb4caaa3c6c177a9d8fa Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 30 Jan 2024 14:59:12 +0100 Subject: [PATCH 36/65] integrate new harvesting field --- src/components/MoneyReportHeader.tsx | 4 +-- .../ReportActionItem/ReportPreview.tsx | 4 +-- src/libs/NextStepUtils.ts | 4 +-- src/libs/actions/IOU.js | 2 +- tests/unit/NextStepUtilsTest.ts | 36 ++++++++++++++----- tests/utils/LHNTestUtils.js | 4 ++- tests/utils/collections/policies.ts | 4 ++- 7 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 4b4e3915f969..c2e6ff341416 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -86,8 +86,8 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( - () => chatReport?.isOwnPolicyExpenseChat && !policy.isHarvestingEnabled, - [chatReport?.isOwnPolicyExpenseChat, policy.isHarvestingEnabled], + () => chatReport?.isOwnPolicyExpenseChat && !(policy.harvesting?.enabled ?? policy.isHarvestingEnabled), + [chatReport?.isOwnPolicyExpenseChat, policy.harvesting?.enabled, policy.isHarvestingEnabled], ); const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)]; diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index b2fece085f57..52e9d94eaefd 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -159,8 +159,8 @@ function ReportPreview({ // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( - () => chatReport?.isOwnPolicyExpenseChat && !policy?.isHarvestingEnabled, - [chatReport?.isOwnPolicyExpenseChat, policy?.isHarvestingEnabled], + () => chatReport?.isOwnPolicyExpenseChat && !(policy?.harvesting?.enabled ?? policy?.isHarvestingEnabled), + [chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled, policy?.isHarvestingEnabled], ); const getDisplayAmount = (): string => { diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index dec6acaecec2..85986e57057e 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -62,7 +62,7 @@ type BuildNextStepParameters = { */ function buildNextStep(report: Report, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { const policy = ReportUtils.getPolicy(report.policyID ?? ''); - const {submitsTo, isHarvestingEnabled, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; + const {submitsTo, harvesting, isHarvestingEnabled, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; const {ownerAccountID = -1, managerID = -1} = report; const isOwner = currentUserAccountID === ownerAccountID; const isManager = currentUserAccountID === managerID; @@ -101,7 +101,7 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { id: policyID, owner: currentUserEmail, submitsTo: currentUserAccountID, - isHarvestingEnabled: false, + harvesting: { + enabled: false, + }, // Required props name: 'Policy', role: 'admin', @@ -110,7 +112,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -134,7 +138,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -158,7 +164,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -182,7 +190,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: 2, }).then(() => { @@ -207,7 +217,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_DAY_OF_MONTH, }).then(() => { @@ -233,7 +245,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH, }).then(() => { @@ -258,7 +272,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -289,7 +305,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js index 6c72558e5df3..04246c1c438a 100644 --- a/tests/utils/LHNTestUtils.js +++ b/tests/utils/LHNTestUtils.js @@ -256,7 +256,9 @@ function getFakePolicy(id = 1, name = 'Workspace-Test-001') { lastModified: 1697323926777105, autoReporting: true, autoReportingFrequency: 'immediate', - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingOffset: 1, isPreventSelfApprovalEnabled: true, submitsTo: 123456, diff --git a/tests/utils/collections/policies.ts b/tests/utils/collections/policies.ts index 8547c171c7a7..4223c7e41941 100644 --- a/tests/utils/collections/policies.ts +++ b/tests/utils/collections/policies.ts @@ -11,7 +11,9 @@ export default function createRandomPolicy(index: number): Policy { autoReporting: randBoolean(), isPolicyExpenseChatEnabled: randBoolean(), autoReportingFrequency: rand(Object.values(CONST.POLICY.AUTO_REPORTING_FREQUENCIES)), - isHarvestingEnabled: randBoolean(), + harvesting: { + enabled: randBoolean(), + }, autoReportingOffset: 1, isPreventSelfApprovalEnabled: randBoolean(), submitsTo: index, From 2e6879c362f58bd757507dd58392ab68dfd1e52a Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 31 Jan 2024 10:33:39 +0100 Subject: [PATCH 37/65] Revert "integrate new harvesting field" This reverts commit 9a36327265853796e28ebb4caaa3c6c177a9d8fa. --- src/components/MoneyReportHeader.tsx | 4 +-- .../ReportActionItem/ReportPreview.tsx | 4 +-- src/libs/NextStepUtils.ts | 4 +-- src/libs/actions/IOU.js | 2 +- tests/unit/NextStepUtilsTest.ts | 36 +++++-------------- tests/utils/LHNTestUtils.js | 4 +-- tests/utils/collections/policies.ts | 4 +-- 7 files changed, 18 insertions(+), 40 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index c2e6ff341416..4b4e3915f969 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -86,8 +86,8 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( - () => chatReport?.isOwnPolicyExpenseChat && !(policy.harvesting?.enabled ?? policy.isHarvestingEnabled), - [chatReport?.isOwnPolicyExpenseChat, policy.harvesting?.enabled, policy.isHarvestingEnabled], + () => chatReport?.isOwnPolicyExpenseChat && !policy.isHarvestingEnabled, + [chatReport?.isOwnPolicyExpenseChat, policy.isHarvestingEnabled], ); const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)]; diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 52e9d94eaefd..b2fece085f57 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -159,8 +159,8 @@ function ReportPreview({ // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( - () => chatReport?.isOwnPolicyExpenseChat && !(policy?.harvesting?.enabled ?? policy?.isHarvestingEnabled), - [chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled, policy?.isHarvestingEnabled], + () => chatReport?.isOwnPolicyExpenseChat && !policy?.isHarvestingEnabled, + [chatReport?.isOwnPolicyExpenseChat, policy?.isHarvestingEnabled], ); const getDisplayAmount = (): string => { diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 85986e57057e..dec6acaecec2 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -62,7 +62,7 @@ type BuildNextStepParameters = { */ function buildNextStep(report: Report, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { const policy = ReportUtils.getPolicy(report.policyID ?? ''); - const {submitsTo, harvesting, isHarvestingEnabled, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; + const {submitsTo, isHarvestingEnabled, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; const {ownerAccountID = -1, managerID = -1} = report; const isOwner = currentUserAccountID === ownerAccountID; const isManager = currentUserAccountID === managerID; @@ -101,7 +101,7 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { id: policyID, owner: currentUserEmail, submitsTo: currentUserAccountID, - harvesting: { - enabled: false, - }, + isHarvestingEnabled: false, // Required props name: 'Policy', role: 'admin', @@ -112,9 +110,7 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - harvesting: { - enabled: true, - }, + isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -138,9 +134,7 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - harvesting: { - enabled: true, - }, + isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -164,9 +158,7 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - harvesting: { - enabled: true, - }, + isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -190,9 +182,7 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - harvesting: { - enabled: true, - }, + isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: 2, }).then(() => { @@ -217,9 +207,7 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - harvesting: { - enabled: true, - }, + isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_DAY_OF_MONTH, }).then(() => { @@ -245,9 +233,7 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - harvesting: { - enabled: true, - }, + isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH, }).then(() => { @@ -272,9 +258,7 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - harvesting: { - enabled: true, - }, + isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -305,9 +289,7 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - harvesting: { - enabled: true, - }, + isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js index 04246c1c438a..6c72558e5df3 100644 --- a/tests/utils/LHNTestUtils.js +++ b/tests/utils/LHNTestUtils.js @@ -256,9 +256,7 @@ function getFakePolicy(id = 1, name = 'Workspace-Test-001') { lastModified: 1697323926777105, autoReporting: true, autoReportingFrequency: 'immediate', - harvesting: { - enabled: true, - }, + isHarvestingEnabled: true, autoReportingOffset: 1, isPreventSelfApprovalEnabled: true, submitsTo: 123456, diff --git a/tests/utils/collections/policies.ts b/tests/utils/collections/policies.ts index 4223c7e41941..8547c171c7a7 100644 --- a/tests/utils/collections/policies.ts +++ b/tests/utils/collections/policies.ts @@ -11,9 +11,7 @@ export default function createRandomPolicy(index: number): Policy { autoReporting: randBoolean(), isPolicyExpenseChatEnabled: randBoolean(), autoReportingFrequency: rand(Object.values(CONST.POLICY.AUTO_REPORTING_FREQUENCIES)), - harvesting: { - enabled: randBoolean(), - }, + isHarvestingEnabled: randBoolean(), autoReportingOffset: 1, isPreventSelfApprovalEnabled: randBoolean(), submitsTo: index, From 45a4f7b006e622f8baf2785b5d108292fd5d6a02 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 31 Jan 2024 10:33:45 +0100 Subject: [PATCH 38/65] Revert "clarify comments" This reverts commit 821ee65b43a8c2add86e75184b848c3403c03362. --- src/types/onyx/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 719e0ba1fb9d..ffa619291516 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -96,7 +96,7 @@ type Policy = { enabled: boolean; }; - /** Whether the self approval or submitting is enabled */ + /** Whether the scheduled submit is enabled */ isPreventSelfApprovalEnabled?: boolean; /** When the monthly scheduled submit should happen */ From ad414b54f2576e0512a63d4e9f7c34cea74f5f5e Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 31 Jan 2024 10:33:49 +0100 Subject: [PATCH 39/65] Revert "add new harvesting field of policy" This reverts commit cf9fcfbdc671b8c3d9155a56747db761900bc244. --- src/types/onyx/Policy.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index ffa619291516..eca7e9d1ee06 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -88,13 +88,8 @@ type Policy = { /** The scheduled submit frequency set up on the this policy */ autoReportingFrequency?: ValueOf; - /** @deprecated Whether the scheduled submit is enabled */ - isHarvestingEnabled?: boolean; - /** Whether the scheduled submit is enabled */ - harvesting?: { - enabled: boolean; - }; + isHarvestingEnabled?: boolean; /** Whether the scheduled submit is enabled */ isPreventSelfApprovalEnabled?: boolean; From ae03b0997a1bafd38dd9b83f4076578ee61af1a8 Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Tue, 30 Jan 2024 12:29:58 +0100 Subject: [PATCH 40/65] Compute the component-specific input registration params --- src/components/Form/FormProvider.tsx | 31 +++----- src/components/Form/InputWrapper.tsx | 71 +++++++++++++++---- src/components/Form/types.ts | 12 ++-- src/pages/EditRequestDescriptionPage.js | 2 +- src/pages/iou/MoneyRequestDescriptionPage.js | 2 +- .../request/step/IOURequestStepDescription.js | 2 +- src/pages/tasks/NewTaskDescriptionPage.js | 2 +- src/pages/tasks/NewTaskDetailsPage.js | 2 +- src/pages/tasks/TaskDescriptionPage.js | 2 +- 9 files changed, 77 insertions(+), 49 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index ebe1102d52a4..e163c4a7d2d5 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -205,7 +205,7 @@ function FormProvider( })); const registerInput = useCallback( - (inputID: keyof Form, shouldSubmitEdit: boolean, inputProps: TInputProps): TInputProps => { + (inputID: keyof Form, shouldSubmitForm: boolean, inputProps: TInputProps): TInputProps => { const newRef: MutableRefObject = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); if (inputRefs.current[inputID] !== newRef) { inputRefs.current[inputID] = newRef; @@ -222,24 +222,6 @@ function FormProvider( inputValues[inputID] = inputProps.defaultValue ?? getInitialValueByType(inputProps.valueType); } - // If the input is a submit editing input, we need to set the onSubmitEditing prop - // to the submit function of the form - const onSubmitEditingObject = shouldSubmitEdit - ? { - onSubmitEditing: (event: NativeSyntheticEvent) => { - submit(); - if (!inputProps.onSubmitEditing) { - return; - } - inputProps.onSubmitEditing(event); - }, - returnKeyType: 'go', - } - : {}; - - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const isMultiline = inputProps.multiline || inputProps.autoGrowHeight; - const errorFields = formState?.errorFields?.[inputID] ?? {}; const fieldErrorMessage = Object.keys(errorFields) @@ -251,9 +233,14 @@ function FormProvider( return { ...inputProps, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - blurOnSubmit: (isMultiline && shouldSubmitEdit) || inputProps.blurOnSubmit, - ...onSubmitEditingObject, + ...(shouldSubmitForm && { + onSubmitEditing: (event: NativeSyntheticEvent) => { + submit(); + + inputProps.onSubmitEditing?.(event); + }, + returnKeyType: 'go', + }), ref: typeof inputRef === 'function' ? (node: BaseInputProps) => { diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 68c3f8639e5d..6266a799b128 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -6,27 +6,68 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import FormContext from './FormContext'; import type {InputWrapperProps, ValidInputs} from './types'; -const canUseSubmitEditing = (inputAllowsSubmit: boolean, multiline?: boolean, autoGrowHeight?: boolean) => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const isMultiline = multiline || autoGrowHeight; - if (!isMultiline) { - return true; +function computeComponentSpecificRegistrationParams({ + InputComponent, + shouldSubmitForm, + multiline, + autoGrowHeight, + blurOnSubmit +}: InputWrapperProps): { + readonly shouldSubmitForm: boolean, + readonly blurOnSubmit: boolean | undefined, + readonly shouldSetTouchedOnBlurOnly: boolean, +} { + if (InputComponent === TextInput) { + const isEffectivelyMultiline = Boolean(multiline ?? autoGrowHeight); + + // We calculate the effective requested value of `shouldSubmitForm`, assuming that the default value should be + // `true` for single-line inputs and `false` for multi-line inputs. + const shouldSubmitFormOrDefault = shouldSubmitForm ?? !isEffectivelyMultiline; + + // If the user can use the hardware keyboard, they have access to an alternative way of inserting a new line + // (like a Shift+Enter keyboard shortcut). For simplicity, we assume that when there's no touch screen, it's a + // desktop setup with a keyboard. + const canUseHardwareKeyboard = !canUseTouchScreen(); + + // We want to avoid a situation when the user can't insert a new line. For single line inputs, it's not a + // problem. For multi-line inputs, ensure there are alternative ways to add a newline. + const canInputSubmitForm = isEffectivelyMultiline ? canUseHardwareKeyboard : true; + + // We only honor the provided property if it's reasonable on this platform + const shouldReallySubmitForm = canInputSubmitForm && shouldSubmitFormOrDefault; + + return { + // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to + // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were + // calling some methods too early or twice, so we had to add this check to prevent that side effect. + // For now this side effect happened only in `TextInput` components. + shouldSetTouchedOnBlurOnly: true, + blurOnSubmit: (isEffectivelyMultiline && shouldReallySubmitForm) || blurOnSubmit, + shouldSubmitForm: shouldReallySubmitForm, + }; } - return inputAllowsSubmit && !canUseTouchScreen(); -}; -function InputWrapper({InputComponent, inputID, inputAllowsSubmit = false, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { + return { + shouldSetTouchedOnBlurOnly: false, + // Forward the originally provided value + blurOnSubmit, + shouldSubmitForm: false, + }; +} + +function InputWrapper(props: InputWrapperProps, ref: ForwardedRef) { + const {InputComponent, inputID, valueType = 'string', ...rest} = props; const {registerInput} = useContext(FormContext); - const shouldSubmitEdit = canUseSubmitEditing(inputAllowsSubmit, rest.multiline, rest.autoGrowHeight); - // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to - // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were - // calling some methods too early or twice, so we had to add this check to prevent that side effect. - // For now this side effect happened only in `TextInput` components. - const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; + + const { + shouldSetTouchedOnBlurOnly, + blurOnSubmit, + shouldSubmitForm, + } = computeComponentSpecificRegistrationParams(props); // TODO: Sometimes we return too many props with register input, so we need to consider if it's better to make the returned type more general and disregard the issue, or we would like to omit the unused props somehow. // eslint-disable-next-line react/jsx-props-no-spreading, @typescript-eslint/no-explicit-any - return ; + return ; } InputWrapper.displayName = 'InputWrapper'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 366759c7d85f..7377d06a1447 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -51,11 +51,11 @@ type InputWrapperProps = Omit InputComponent: TInput; inputID: string; - /** Whether the input allows the form to be submitted when the user presses enter. - * This is useful for inputs that are not multiline and don't have a submit button by default. - * This property is ignored on mobile devices as they don't have a shift + enter key to create a newline. - */ - inputAllowsSubmit?: boolean; + /** + * Should the containing form be submitted when this input is submitted itself? + * Currently, meaningful only for text inputs. + */ + shouldSubmitForm?: boolean; }; type ExcludeDraft = T extends `${string}Draft` ? never : T; @@ -96,7 +96,7 @@ type FormProps = { footerContent?: ReactNode; }; -type RegisterInput = (inputID: keyof Form, shouldSubmitEdit: boolean, inputProps: TInputProps) => TInputProps; +type RegisterInput = (inputID: keyof Form, shouldSubmitForm: boolean, inputProps: TInputProps) => TInputProps; type InputRefs = Record>; diff --git a/src/pages/EditRequestDescriptionPage.js b/src/pages/EditRequestDescriptionPage.js index d64cee3c5dc3..61cecaee929b 100644 --- a/src/pages/EditRequestDescriptionPage.js +++ b/src/pages/EditRequestDescriptionPage.js @@ -76,7 +76,7 @@ function EditRequestDescriptionPage({defaultDescription, onSubmit}) { }} autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} - inputAllowsSubmit + shouldSubmitForm /> diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index 74fe27af9dfd..a66ae8031a68 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -141,7 +141,7 @@ function MoneyRequestDescriptionPage({iou, route, selectedTab}) { }} autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} - inputAllowsSubmit + shouldSubmitForm /> diff --git a/src/pages/iou/request/step/IOURequestStepDescription.js b/src/pages/iou/request/step/IOURequestStepDescription.js index fe0f65f9ea3b..e5ef86f15285 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.js +++ b/src/pages/iou/request/step/IOURequestStepDescription.js @@ -106,7 +106,7 @@ function IOURequestStepDescription({ autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} inputStyle={[styles.verticalAlignTop]} - inputAllowsSubmit + shouldSubmitForm /> diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.js index e1d8945fe58f..4d84cac90537 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.js +++ b/src/pages/tasks/NewTaskDescriptionPage.js @@ -78,7 +78,7 @@ function NewTaskDescriptionPage(props) { updateMultilineInputRange(el); }} autoGrowHeight - inputAllowsSubmit + shouldSubmitForm containerStyles={[styles.autoGrowHeightMultilineInput]} /> diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js index 29d84ad60b8b..4f4f2560a0d9 100644 --- a/src/pages/tasks/NewTaskDetailsPage.js +++ b/src/pages/tasks/NewTaskDetailsPage.js @@ -110,7 +110,7 @@ function NewTaskDetailsPage(props) { label={props.translate('newTaskPage.descriptionOptional')} accessibilityLabel={props.translate('newTaskPage.descriptionOptional')} autoGrowHeight - inputAllowsSubmit + shouldSubmitForm containerStyles={[styles.autoGrowHeightMultilineInput]} defaultValue={parser.htmlToMarkdown(parser.replace(taskDescription))} value={taskDescription} diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js index 453f55e5a89d..3547827b173f 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.js @@ -125,7 +125,7 @@ function TaskDescriptionPage(props) { updateMultilineInputRange(inputRef.current); }} autoGrowHeight - inputAllowsSubmit + shouldSubmitForm containerStyles={[styles.autoGrowHeightMultilineInput]} /> From 23ee2949b732f5a46f756538d309940614b79768 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 31 Jan 2024 18:01:59 +0530 Subject: [PATCH 41/65] minor fixes to logic --- src/components/Form/InputWrapper.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 6266a799b128..e20d30fae87b 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -18,11 +18,13 @@ function computeComponentSpecificRegistrationParams( readonly shouldSetTouchedOnBlurOnly: boolean, } { if (InputComponent === TextInput) { - const isEffectivelyMultiline = Boolean(multiline ?? autoGrowHeight); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const isEffectivelyMultiline = Boolean(multiline || autoGrowHeight); // We calculate the effective requested value of `shouldSubmitForm`, assuming that the default value should be // `true` for single-line inputs and `false` for multi-line inputs. - const shouldSubmitFormOrDefault = shouldSubmitForm ?? !isEffectivelyMultiline; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const shouldSubmitFormOrDefault = shouldSubmitForm || !isEffectivelyMultiline; // If the user can use the hardware keyboard, they have access to an alternative way of inserting a new line // (like a Shift+Enter keyboard shortcut). For simplicity, we assume that when there's no touch screen, it's a From 18fa42a5b5a0ee8e7292eeaca93b4b894d9fe148 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 31 Jan 2024 18:15:26 +0530 Subject: [PATCH 42/65] adding custom component to the list --- src/components/Form/InputWrapper.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index e20d30fae87b..3cbb55aa8588 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -2,6 +2,8 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import TextInput from '@components/TextInput'; +import AddressSearch from '@components/AddressSearch'; +import RoomNameInput from '@components/RoomNameInput'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import FormContext from './FormContext'; import type {InputWrapperProps, ValidInputs} from './types'; @@ -17,7 +19,8 @@ function computeComponentSpecificRegistrationParams( readonly blurOnSubmit: boolean | undefined, readonly shouldSetTouchedOnBlurOnly: boolean, } { - if (InputComponent === TextInput) { + const validTextInputComponents = [TextInput, AddressSearch, RoomNameInput] as TInput[]; + if (validTextInputComponents.includes(InputComponent)) { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const isEffectivelyMultiline = Boolean(multiline || autoGrowHeight); From f6f53391cbf83e21acad594d441a564edbdc5843 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 31 Jan 2024 18:16:11 +0530 Subject: [PATCH 43/65] fix lint --- src/components/Form/FormProvider.tsx | 2 +- src/components/Form/InputWrapper.tsx | 18 +++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index e163c4a7d2d5..ba0f823fdbad 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -1,7 +1,7 @@ import lodashIsEqual from 'lodash/isEqual'; import type {ForwardedRef, MutableRefObject, ReactNode} from 'react'; -import type { NativeSyntheticEvent, TextInputSubmitEditingEventData } from 'react-native'; import React, {createRef, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import type {NativeSyntheticEvent, TextInputSubmitEditingEventData} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import * as ValidationUtils from '@libs/ValidationUtils'; diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 3cbb55aa8588..f7a3fbb0a2ac 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,9 +1,9 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; -import type {AnimatedTextInputRef} from '@components/RNTextInput'; -import TextInput from '@components/TextInput'; import AddressSearch from '@components/AddressSearch'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RoomNameInput from '@components/RoomNameInput'; +import TextInput from '@components/TextInput'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import FormContext from './FormContext'; import type {InputWrapperProps, ValidInputs} from './types'; @@ -13,11 +13,11 @@ function computeComponentSpecificRegistrationParams( shouldSubmitForm, multiline, autoGrowHeight, - blurOnSubmit + blurOnSubmit, }: InputWrapperProps): { - readonly shouldSubmitForm: boolean, - readonly blurOnSubmit: boolean | undefined, - readonly shouldSetTouchedOnBlurOnly: boolean, + readonly shouldSubmitForm: boolean; + readonly blurOnSubmit: boolean | undefined; + readonly shouldSetTouchedOnBlurOnly: boolean; } { const validTextInputComponents = [TextInput, AddressSearch, RoomNameInput] as TInput[]; if (validTextInputComponents.includes(InputComponent)) { @@ -64,11 +64,7 @@ function InputWrapper(props: InputWrapperProps Date: Wed, 31 Jan 2024 14:45:11 +0100 Subject: [PATCH 44/65] rename a var --- src/libs/NextStepUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index dec6acaecec2..5ba254779475 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -68,7 +68,7 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf Date: Wed, 31 Jan 2024 15:17:44 +0100 Subject: [PATCH 45/65] improve object keys --- src/libs/NextStepUtils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 5ba254779475..ed0e13b4f192 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -6,6 +6,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report, ReportNextStep} from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportNextStep'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; import DateUtils from './DateUtils'; import EmailUtils from './EmailUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; @@ -127,12 +128,14 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf, string> = { [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE]: 'later today', [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY]: 'on Sunday', [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY]: 'on the 1st and 16th of each month', [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY]: formattedDate ? `on the ${formattedDate} of each month` : '', [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP]: 'at the end of your trip', + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT]: '', + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL]: '', }; if (harvestingSuffixes[autoReportingFrequency]) { From 3f8f417a5c3081040d9b05fe31787059a547bf96 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 1 Feb 2024 14:57:45 +0530 Subject: [PATCH 46/65] minor fix to extra property passed --- src/components/Form/InputWrapper.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index f7a3fbb0a2ac..1fe648d026c6 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -21,8 +21,7 @@ function computeComponentSpecificRegistrationParams( } { const validTextInputComponents = [TextInput, AddressSearch, RoomNameInput] as TInput[]; if (validTextInputComponents.includes(InputComponent)) { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const isEffectivelyMultiline = Boolean(multiline || autoGrowHeight); + const isEffectivelyMultiline = Boolean(multiline) || Boolean(autoGrowHeight); // We calculate the effective requested value of `shouldSubmitForm`, assuming that the default value should be // `true` for single-line inputs and `false` for multi-line inputs. @@ -61,7 +60,7 @@ function computeComponentSpecificRegistrationParams( } function InputWrapper(props: InputWrapperProps, ref: ForwardedRef) { - const {InputComponent, inputID, valueType = 'string', ...rest} = props; + const {InputComponent, inputID, valueType = 'string', shouldSubmitForm: propShouldSubmitForm, ...rest} = props; const {registerInput} = useContext(FormContext); const {shouldSetTouchedOnBlurOnly, blurOnSubmit, shouldSubmitForm} = computeComponentSpecificRegistrationParams(props); From daf46917f77afe16384471f7fe3d8b7e833df51b Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 1 Feb 2024 15:55:58 +0530 Subject: [PATCH 47/65] adjusted according to recommendations --- src/components/Form/InputWrapper.tsx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 1fe648d026c6..4cfe6b3892a2 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -23,22 +23,15 @@ function computeComponentSpecificRegistrationParams( if (validTextInputComponents.includes(InputComponent)) { const isEffectivelyMultiline = Boolean(multiline) || Boolean(autoGrowHeight); - // We calculate the effective requested value of `shouldSubmitForm`, assuming that the default value should be - // `true` for single-line inputs and `false` for multi-line inputs. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const shouldSubmitFormOrDefault = shouldSubmitForm || !isEffectivelyMultiline; - // If the user can use the hardware keyboard, they have access to an alternative way of inserting a new line // (like a Shift+Enter keyboard shortcut). For simplicity, we assume that when there's no touch screen, it's a // desktop setup with a keyboard. const canUseHardwareKeyboard = !canUseTouchScreen(); - // We want to avoid a situation when the user can't insert a new line. For single line inputs, it's not a - // problem. For multi-line inputs, ensure there are alternative ways to add a newline. - const canInputSubmitForm = isEffectivelyMultiline ? canUseHardwareKeyboard : true; - - // We only honor the provided property if it's reasonable on this platform - const shouldReallySubmitForm = canInputSubmitForm && shouldSubmitFormOrDefault; + // We want to avoid a situation when the user can't insert a new line. For single-line inputs, it's not a problem and we + // force-enable form submission. For multi-line inputs, ensure that it was requested to enable form submission for this specific + // input and that alternative ways exist to add a new line. + const shouldReallySubmitForm = isEffectivelyMultiline ? Boolean(shouldSubmitForm) && canUseHardwareKeyboard : true; return { // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to From b12a5267ecc166f129ea3ab2a9ecb90d2e05309f Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 1 Feb 2024 15:56:46 +0530 Subject: [PATCH 48/65] minor adjustment --- src/components/Form/InputWrapper.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 4cfe6b3892a2..bd4bb6a1bf13 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -19,8 +19,9 @@ function computeComponentSpecificRegistrationParams( readonly blurOnSubmit: boolean | undefined; readonly shouldSetTouchedOnBlurOnly: boolean; } { - const validTextInputComponents = [TextInput, AddressSearch, RoomNameInput] as TInput[]; - if (validTextInputComponents.includes(InputComponent)) { + const textInputBasedComponents = [TextInput, AddressSearch, RoomNameInput] as TInput[]; + + if (textInputBasedComponents.includes(InputComponent)) { const isEffectivelyMultiline = Boolean(multiline) || Boolean(autoGrowHeight); // If the user can use the hardware keyboard, they have access to an alternative way of inserting a new line From b169516bf3f474eab5650eef8586a345542fa61a Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 1 Feb 2024 14:55:31 +0100 Subject: [PATCH 49/65] migrate netinfo mock to TypeScript --- __mocks__/@react-native-community/netinfo.js | 19 ------------ __mocks__/@react-native-community/netinfo.ts | 31 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 19 deletions(-) delete mode 100644 __mocks__/@react-native-community/netinfo.js create mode 100644 __mocks__/@react-native-community/netinfo.ts diff --git a/__mocks__/@react-native-community/netinfo.js b/__mocks__/@react-native-community/netinfo.js deleted file mode 100644 index 53a9282ea8db..000000000000 --- a/__mocks__/@react-native-community/netinfo.js +++ /dev/null @@ -1,19 +0,0 @@ -const defaultState = { - type: 'cellular', - isConnected: true, - isInternetReachable: true, - details: { - isConnectionExpensive: true, - cellularGeneration: '3g', - }, -}; - -const RNCNetInfoMock = { - configure: () => {}, - fetch: () => Promise.resolve(defaultState), - refresh: () => Promise.resolve(defaultState), - addEventListener: () => () => {}, - useNetInfo: () => {}, -}; - -export default RNCNetInfoMock; diff --git a/__mocks__/@react-native-community/netinfo.ts b/__mocks__/@react-native-community/netinfo.ts new file mode 100644 index 000000000000..0b7bdc9010a3 --- /dev/null +++ b/__mocks__/@react-native-community/netinfo.ts @@ -0,0 +1,31 @@ +import {NetInfoCellularGeneration, NetInfoStateType} from '@react-native-community/netinfo'; +import type {addEventListener, configure, fetch, NetInfoState, refresh, useNetInfo} from '@react-native-community/netinfo'; + +const defaultState: NetInfoState = { + type: NetInfoStateType.cellular, + isConnected: true, + isInternetReachable: true, + details: { + isConnectionExpensive: true, + cellularGeneration: NetInfoCellularGeneration['3g'], + carrier: 'T-Mobile', + }, +}; + +type NetInfoMock = { + configure: typeof configure; + fetch: typeof fetch; + refresh: typeof refresh; + addEventListener: typeof addEventListener; + useNetInfo: typeof useNetInfo; +}; + +const netInfoMock: NetInfoMock = { + configure: () => {}, + fetch: () => Promise.resolve(defaultState), + refresh: () => Promise.resolve(defaultState), + addEventListener: () => () => {}, + useNetInfo: () => defaultState, +}; + +export default netInfoMock; From 77f0019c1ecac5681ec96688f61ce9a457840f5a Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 6 Feb 2024 11:56:53 +0100 Subject: [PATCH 50/65] remove redundant prop --- tests/unit/NextStepUtilsTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index bec69d08a745..60b51e90cabc 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -28,7 +28,6 @@ describe('libs/NextStepUtils', () => { role: 'admin', type: 'team', outputCurrency: CONST.CURRENCY.USD, - areChatRoomsEnabled: true, isPolicyExpenseChatEnabled: true, }; const optimisticNextStep: ReportNextStep = { From 6aab7ff6173005b65e4f432ed427e8353b389df7 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 6 Feb 2024 11:57:05 +0100 Subject: [PATCH 51/65] use long name for manager --- src/libs/NextStepUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 3ef21a55b6a7..3becffb46a28 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -70,7 +70,7 @@ function buildNextStep(report: Report | EmptyObject, predictedNextStatus: ValueO const isManager = currentUserAccountID === managerID; const isSelfApproval = currentUserAccountID === submitsTo; const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? ''; - const managerDisplayName = isSelfApproval ? 'you' : ReportUtils.getDisplayNameForParticipant(submitsTo, true) ?? ''; + const managerDisplayName = isSelfApproval ? 'you' : ReportUtils.getDisplayNameForParticipant(submitsTo) ?? ''; const type: ReportNextStep['type'] = 'neutral'; let optimisticNextStep: ReportNextStep | null; From 4eece91f8e302822a0ad3aa1162ce1a5b10847f0 Mon Sep 17 00:00:00 2001 From: Github Date: Fri, 2 Feb 2024 16:24:38 +0100 Subject: [PATCH 52/65] ReportSceen perf test refactor --- tests/perf-test/ReportScreen.perf-test.js | 109 +++++++--------------- tests/utils/ReportTestUtils.js | 9 +- tests/utils/TestHelper.js | 2 + tests/utils/collections/reportActions.ts | 18 +++- 4 files changed, 59 insertions(+), 79 deletions(-) diff --git a/tests/perf-test/ReportScreen.perf-test.js b/tests/perf-test/ReportScreen.perf-test.js index 5a144e715f5b..e86d0bf4fa09 100644 --- a/tests/perf-test/ReportScreen.perf-test.js +++ b/tests/perf-test/ReportScreen.perf-test.js @@ -1,8 +1,7 @@ -import {act, fireEvent, screen} from '@testing-library/react-native'; +import {fireEvent, screen, waitFor} from '@testing-library/react-native'; import React from 'react'; import Onyx from 'react-native-onyx'; import {measurePerformance} from 'reassure'; -import _ from 'underscore'; import ComposeProviders from '../../src/components/ComposeProviders'; import DragAndDropProvider from '../../src/components/DragAndDrop/Provider'; import {LocaleContextProvider} from '../../src/components/LocaleContextProvider'; @@ -15,7 +14,10 @@ import * as Localize from '../../src/libs/Localize'; import ONYXKEYS from '../../src/ONYXKEYS'; import {ReportAttachmentsProvider} from '../../src/pages/home/report/ReportAttachmentsContext'; import ReportScreen from '../../src/pages/home/ReportScreen'; -import * as LHNTestUtils from '../utils/LHNTestUtils'; +import createCollection from '../utils/collections/createCollection'; +import createPersonalDetails from '../utils/collections/personalDetails'; +import createRandomPolicy from '../utils/collections/policies'; +import createRandomReport from '../utils/collections/reports'; import PusherHelper from '../utils/PusherHelper'; import * as ReportTestUtils from '../utils/ReportTestUtils'; import * as TestHelper from '../utils/TestHelper'; @@ -56,6 +58,7 @@ jest.mock('../../src/hooks/useEnvironment', () => jest.mock('../../src/libs/Permissions', () => ({ canUseLinkPreviews: jest.fn(() => true), + canUseDefaultRooms: jest.fn(() => true), })); jest.mock('../../src/hooks/usePermissions.ts'); @@ -103,32 +106,17 @@ afterEach(() => { PusherHelper.teardown(); }); -/** - * This is a helper function to create a mock for the addListener function of the react-navigation library. - * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate - * the transitionEnd event that is triggered when the screen transition animation is completed. - * - * P.S: This can't be moved to a utils file because Jest wants any external function to stay in the scope. - * - * @returns {Object} An object with two functions: triggerTransitionEnd and addListener - */ -const createAddListenerMock = () => { - const transitionEndListeners = []; - const triggerTransitionEnd = () => { - transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); - }; - - const addListener = jest.fn().mockImplementation((listener, callback) => { - if (listener === 'transitionEnd') { - transitionEndListeners.push(callback); - } - return () => { - _.filter(transitionEndListeners, (cb) => cb !== callback); - }; - }); +const policies = createCollection( + (item) => `${ONYXKEYS.COLLECTION.POLICY}${item.id}`, + (index) => createRandomPolicy(index), + 10, +); - return {triggerTransitionEnd, addListener}; -}; +const personalDetails = createCollection( + (item) => item.accountID, + (index) => createPersonalDetails(index), + 20, +); function ReportScreenWrapper(args) { return ( @@ -152,8 +140,12 @@ function ReportScreenWrapper(args) { ); } -test.skip('[ReportScreen] should render ReportScreen with composer interactions', () => { - const {triggerTransitionEnd, addListener} = createAddListenerMock(); +const report = {...createRandomReport(1), policyID: '1'}; +const reportActions = ReportTestUtils.getMockedReportActionsMap(500); +const mockRoute = {params: {reportID: '1'}}; + +test('[ReportScreen] should render ReportScreen with composer interactions', () => { + const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); const scenario = async () => { /** * First make sure ReportScreen is mounted, so that we can trigger @@ -163,11 +155,7 @@ test.skip('[ReportScreen] should render ReportScreen with composer interactions' * before the ReportScreen is mounted, and the test will fail. */ await screen.findByTestId('ReportScreen'); - - await act(triggerTransitionEnd); - - // Query for the report list - await screen.findByTestId('report-actions-list'); + await waitFor(triggerTransitionEnd); // Query for the composer const composer = await screen.findByTestId('composer'); @@ -189,15 +177,6 @@ test.skip('[ReportScreen] should render ReportScreen with composer interactions' await screen.findByLabelText(hintHeaderText); }; - const policy = { - policyID: 1, - name: 'Testing Policy', - }; - - const report = LHNTestUtils.getFakeReport(); - const reportActions = ReportTestUtils.getMockedReportActionsMap(1000); - const mockRoute = {params: {reportID: '1'}}; - const navigation = {addListener}; return waitForBatchedUpdates() @@ -206,12 +185,10 @@ test.skip('[ReportScreen] should render ReportScreen with composer interactions' [ONYXKEYS.IS_SIDEBAR_LOADED]: true, [`${ONYXKEYS.COLLECTION.REPORT}${mockRoute.params.reportID}`]: report, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${mockRoute.params.reportID}`]: reportActions, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails, [ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS], - [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, - [`${ONYXKEYS.COLLECTION.REPORT_METADATA}${mockRoute.params.reportID}`]: { - isLoadingReportActions: false, - }, + [`${ONYXKEYS.COLLECTION.POLICY}`]: policies, + [ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: true, }), ) .then(() => @@ -225,8 +202,8 @@ test.skip('[ReportScreen] should render ReportScreen with composer interactions' ); }); -test.skip('[ReportScreen] should press of the report item', () => { - const {triggerTransitionEnd, addListener} = createAddListenerMock(); +test('[ReportScreen] should press of the report item', () => { + const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); const scenario = async () => { /** * First make sure ReportScreen is mounted, so that we can trigger @@ -237,32 +214,19 @@ test.skip('[ReportScreen] should press of the report item', () => { */ await screen.findByTestId('ReportScreen'); - await act(triggerTransitionEnd); + await waitFor(triggerTransitionEnd); // Query for the report list await screen.findByTestId('report-actions-list'); - // Query for the composer - await screen.findByTestId('composer'); - - const hintReportPreviewText = Localize.translateLocal('iou.viewDetails'); - - // Query for report preview buttons - const reportPreviewButtons = await screen.findAllByLabelText(hintReportPreviewText); + const hintText = Localize.translateLocal('accessibilityHints.chatMessage'); - // click on the report preview button - fireEvent.press(reportPreviewButtons[0]); - }; + // Query for the list of items + const reportItems = await screen.findAllByLabelText(hintText); - const policy = { - policyID: 123, - name: 'Testing Policy', + fireEvent.press(reportItems[0], 'onLongPress'); }; - const report = LHNTestUtils.getFakeReport(); - const reportActions = ReportTestUtils.getMockedReportActionsMap(1000); - const mockRoute = {params: {reportID: '2'}}; - const navigation = {addListener}; return waitForBatchedUpdates() @@ -271,12 +235,9 @@ test.skip('[ReportScreen] should press of the report item', () => { [ONYXKEYS.IS_SIDEBAR_LOADED]: true, [`${ONYXKEYS.COLLECTION.REPORT}${mockRoute.params.reportID}`]: report, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${mockRoute.params.reportID}`]: reportActions, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails, [ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS], - [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, - [`${ONYXKEYS.COLLECTION.REPORT_METADATA}${mockRoute.params.reportID}`]: { - isLoadingReportActions: false, - }, + [`${ONYXKEYS.COLLECTION.POLICY}`]: policies, }), ) .then(() => diff --git a/tests/utils/ReportTestUtils.js b/tests/utils/ReportTestUtils.js index 910f2200876b..86899e4045f6 100644 --- a/tests/utils/ReportTestUtils.js +++ b/tests/utils/ReportTestUtils.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import createRandomReportAction from './collections/reportActions'; const actionNames = ['ADDCOMMENT', 'IOU', 'REPORTPREVIEW', 'CLOSED']; @@ -51,7 +52,13 @@ const getMockedReportActionsMap = (length = 100) => { const mockReports = Array.from({length}, (__, i) => { const reportID = i + 1; const actionName = i === 0 ? 'CREATED' : actionNames[i % actionNames.length]; - const reportAction = getFakeReportAction(reportID, actionName); + const reportAction = { + ...createRandomReportAction(reportID), + actionName, + originalMessage: { + linkedReportID: reportID.toString(), + }, + }; return {[reportID]: reportAction}; }); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index dd95ab4efb67..9059041afd19 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -218,6 +218,8 @@ function assertFormDataMatchesObject(formData, obj) { /** * This is a helper function to create a mock for the addListener function of the react-navigation library. + * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate + * the transitionEnd event that is triggered when the screen transition animation is completed. * * @returns {Object} An object with two functions: triggerTransitionEnd and addListener */ diff --git a/tests/utils/collections/reportActions.ts b/tests/utils/collections/reportActions.ts index cc258e89c041..bb14a2c7a41b 100644 --- a/tests/utils/collections/reportActions.ts +++ b/tests/utils/collections/reportActions.ts @@ -1,4 +1,5 @@ -import {rand, randAggregation, randBoolean, randPastDate, randWord} from '@ngneat/falso'; +import {rand, randAggregation, randBoolean, randWord} from '@ngneat/falso'; +import {format} from 'date-fns'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; @@ -17,6 +18,15 @@ const flattenActionNamesValues = (actionNames: any) => { return result; }; +const getRandomDate = (): string => { + const randomTimestamp = Math.random() * new Date().getTime(); + const randomDate = new Date(randomTimestamp); + + const formattedDate = format(randomDate, CONST.DATE.FNS_DB_FORMAT_STRING); + + return formattedDate; +}; + export default function createRandomReportAction(index: number): ReportAction { return { // we need to add any here because of the way we are generating random values @@ -32,7 +42,7 @@ export default function createRandomReportAction(index: number): ReportAction { text: randWord(), }, ], - created: randPastDate().toISOString(), + created: getRandomDate(), message: [ { type: randWord(), @@ -57,13 +67,13 @@ export default function createRandomReportAction(index: number): ReportAction { ], originalMessage: { html: randWord(), - type: rand(Object.values(CONST.IOU.REPORT_ACTION_TYPE)), + lastModified: getRandomDate(), }, whisperedToAccountIDs: randAggregation(), avatar: randWord(), automatic: randBoolean(), shouldShow: randBoolean(), - lastModified: randPastDate().toISOString(), + lastModified: getRandomDate(), pendingAction: rand(Object.values(CONST.RED_BRICK_ROAD_PENDING_ACTION)), delegateAccountID: index, errors: {}, From bebeea94afe9f3d72471eaae9e203c0941a023cf Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 8 Feb 2024 10:48:20 +0100 Subject: [PATCH 53/65] remove isHarvestingEnabled ans use another default value for accountID --- src/components/MoneyReportHeader.tsx | 4 ++-- src/components/ReportActionItem/ReportPreview.tsx | 4 ++-- src/libs/NextStepUtils.ts | 8 ++++---- src/libs/actions/IOU.ts | 2 +- src/types/onyx/Policy.ts | 3 --- tests/unit/NextStepUtilsTest.ts | 9 --------- 6 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 3f5a3c50f6cc..78e9357afd65 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -86,8 +86,8 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( - () => chatReport?.isOwnPolicyExpenseChat && !(policy.harvesting?.enabled ?? policy.isHarvestingEnabled), - [chatReport?.isOwnPolicyExpenseChat, policy.harvesting?.enabled, policy.isHarvestingEnabled], + () => chatReport?.isOwnPolicyExpenseChat && !policy.harvesting?.enabled, + [chatReport?.isOwnPolicyExpenseChat, policy.harvesting?.enabled], ); const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)]; diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index f12c6d9bea31..1a5f51f192f6 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -158,8 +158,8 @@ function ReportPreview({ // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( - () => chatReport?.isOwnPolicyExpenseChat && !(policy?.harvesting?.enabled ?? policy?.isHarvestingEnabled), - [chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled, policy?.isHarvestingEnabled], + () => chatReport?.isOwnPolicyExpenseChat && !policy?.harvesting?.enabled, + [chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled], ); const getDisplayAmount = (): string => { diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 3becffb46a28..e29d6c24c3a1 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -13,7 +13,7 @@ import EmailUtils from './EmailUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as ReportUtils from './ReportUtils'; -let currentUserAccountID: number | undefined; +let currentUserAccountID = -1; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (value) => { @@ -21,7 +21,7 @@ Onyx.connect({ return; } - currentUserAccountID = value.accountID; + currentUserAccountID = value?.accountID ?? -1; }, }); @@ -65,7 +65,7 @@ type BuildNextStepParameters = { function buildNextStep(report: Report | EmptyObject, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { const {policyID = '', ownerAccountID = -1, managerID = -1} = report; const policy = ReportUtils.getPolicy(policyID); - const {submitsTo, isHarvestingEnabled, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; + const {submitsTo, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; const isOwner = currentUserAccountID === ownerAccountID; const isManager = currentUserAccountID === managerID; const isSelfApproval = currentUserAccountID === submitsTo; @@ -103,7 +103,7 @@ function buildNextStep(report: Report | EmptyObject, predictedNextStatus: ValueO }; // Scheduled submit enabled - if (isHarvestingEnabled && autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL) { + if (autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL) { optimisticNextStep.message = [ { text: 'These expenses are scheduled to ', diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8ff9815e6ddd..94884eecc118 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -758,7 +758,7 @@ function getMoneyRequestInformation( isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy ?? null); // If the scheduled submit is turned off on the policy, user needs to manually submit the report which is indicated by GBR in LHN - needsToBeManuallySubmitted = isFromPaidPolicy && !(policy?.harvesting?.enabled ?? policy?.isHarvestingEnabled); + needsToBeManuallySubmitted = isFromPaidPolicy && !policy?.harvesting?.enabled; // If the linked expense report on paid policy is not draft, we need to create a new draft expense report if (iouReport && isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport)) { diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 784cd546a961..6b339d6e3ed4 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -85,9 +85,6 @@ type Policy = { /** The scheduled submit frequency set up on this policy */ autoReportingFrequency?: ValueOf; - /** @deprecated Whether the scheduled submit is enabled */ - isHarvestingEnabled?: boolean; - /** Whether the scheduled submit is enabled */ harvesting?: { enabled: boolean; diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 60b51e90cabc..325bf02030b6 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -22,7 +22,6 @@ describe('libs/NextStepUtils', () => { id: policyID, owner: currentUserEmail, submitsTo: currentUserAccountID, - isHarvestingEnabled: false, // Required props name: 'Policy', role: 'admin', @@ -109,7 +108,6 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -133,7 +131,6 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -157,7 +154,6 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -181,7 +177,6 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: 2, }).then(() => { @@ -206,7 +201,6 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_DAY_OF_MONTH, }).then(() => { @@ -232,7 +226,6 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH, }).then(() => { @@ -257,7 +250,6 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -288,7 +280,6 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); From 3124cb9bed3af28b75d300b75d6dd6961a0257f1 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 8 Feb 2024 15:23:29 +0530 Subject: [PATCH 54/65] removed the support of AddressSearch from keyboardsubmit --- src/components/AddressSearch/index.tsx | 4 ---- src/components/AddressSearch/types.ts | 6 ------ src/components/Form/InputWrapper.tsx | 3 +-- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index ddaecf94dbbd..89e87eeebe54 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -42,8 +42,6 @@ function AddressSearch( onBlur, onInputChange, onPress, - onSubmitEditing, - returnKeyType, predefinedPlaces = [], preferredLocale, renamedInputKeys = { @@ -382,8 +380,6 @@ function AddressSearch( defaultValue, inputID, shouldSaveDraft, - returnKeyType, - onSubmitEditing, onFocus: () => { setIsFocused(true); }, diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index 75d6464cd992..8016f1b2ea39 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -63,12 +63,6 @@ type AddressSearchProps = { /** A callback function when an address has been auto-selected */ onPress?: (props: OnPressProps) => void; - /** On submit editing handler provided by the FormProvider */ - onSubmitEditing?: () => void; - - /** Return key type provided to the TextInput */ - returnKeyType?: string; - /** Customize the TextInput container */ containerStyles?: StyleProp; diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index bd4bb6a1bf13..47fcdd52839e 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,6 +1,5 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; -import AddressSearch from '@components/AddressSearch'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RoomNameInput from '@components/RoomNameInput'; import TextInput from '@components/TextInput'; @@ -19,7 +18,7 @@ function computeComponentSpecificRegistrationParams( readonly blurOnSubmit: boolean | undefined; readonly shouldSetTouchedOnBlurOnly: boolean; } { - const textInputBasedComponents = [TextInput, AddressSearch, RoomNameInput] as TInput[]; + const textInputBasedComponents = [TextInput, RoomNameInput] as TInput[]; if (textInputBasedComponents.includes(InputComponent)) { const isEffectivelyMultiline = Boolean(multiline) || Boolean(autoGrowHeight); From b500950ced9cd0a38b79ae3b44f6c326167bedb2 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 8 Feb 2024 15:25:59 +0530 Subject: [PATCH 55/65] removed the old prop --- .../TextInput/BaseTextInput/baseTextInputPropTypes.js | 4 ---- src/components/TextInput/BaseTextInput/types.ts | 3 --- 2 files changed, 7 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js index 78f06b4075e0..ecd07499dec7 100644 --- a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js @@ -89,9 +89,6 @@ const propTypes = { /** Whether we should wait before focusing the TextInput, useful when using transitions */ shouldDelayFocus: PropTypes.bool, - /** Indicate whether pressing Enter on multiline input is allowed to submit the form. */ - submitOnEnter: PropTypes.bool, - /** Indicate whether input is multiline */ multiline: PropTypes.bool, @@ -132,7 +129,6 @@ const defaultProps = { prefixCharacter: '', onInputChange: () => {}, shouldDelayFocus: false, - submitOnEnter: false, icon: null, shouldUseDefaultValue: false, multiline: false, diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 01400adb0440..2e22485a2350 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -88,9 +88,6 @@ type CustomBaseTextInputProps = { /** Whether we should wait before focusing the TextInput, useful when using transitions */ shouldDelayFocus?: boolean; - /** Indicate whether pressing Enter on multiline input is allowed to submit the form. */ - submitOnEnter?: boolean; - /** Indicate whether input is multiline */ multiline?: boolean; From 29518878a201c2ed36c333bf662a0179961d201b Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 8 Feb 2024 15:38:45 +0530 Subject: [PATCH 56/65] adjusted type --- src/components/Form/InputWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 47fcdd52839e..621a448c5ac9 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -18,7 +18,7 @@ function computeComponentSpecificRegistrationParams( readonly blurOnSubmit: boolean | undefined; readonly shouldSetTouchedOnBlurOnly: boolean; } { - const textInputBasedComponents = [TextInput, RoomNameInput] as TInput[]; + const textInputBasedComponents: ValidInputs[] = [TextInput, RoomNameInput]; if (textInputBasedComponents.includes(InputComponent)) { const isEffectivelyMultiline = Boolean(multiline) || Boolean(autoGrowHeight); From 326cd6164ac78ef98af364d7c50d195037756842 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 8 Feb 2024 15:39:47 +0530 Subject: [PATCH 57/65] making textInputBasedComponents global to file --- src/components/Form/InputWrapper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 621a448c5ac9..fc9d1773c5d8 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -7,6 +7,8 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import FormContext from './FormContext'; import type {InputWrapperProps, ValidInputs} from './types'; +const textInputBasedComponents: ValidInputs[] = [TextInput, RoomNameInput]; + function computeComponentSpecificRegistrationParams({ InputComponent, shouldSubmitForm, @@ -18,8 +20,6 @@ function computeComponentSpecificRegistrationParams( readonly blurOnSubmit: boolean | undefined; readonly shouldSetTouchedOnBlurOnly: boolean; } { - const textInputBasedComponents: ValidInputs[] = [TextInput, RoomNameInput]; - if (textInputBasedComponents.includes(InputComponent)) { const isEffectivelyMultiline = Boolean(multiline) || Boolean(autoGrowHeight); From bbe1426c4d167afb7a0fd336868f4a813b669861 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 8 Feb 2024 13:06:10 +0100 Subject: [PATCH 58/65] integrate harvesting value --- src/libs/NextStepUtils.ts | 4 ++-- tests/unit/NextStepUtilsTest.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index e29d6c24c3a1..5922552150f6 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -65,7 +65,7 @@ type BuildNextStepParameters = { function buildNextStep(report: Report | EmptyObject, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { const {policyID = '', ownerAccountID = -1, managerID = -1} = report; const policy = ReportUtils.getPolicy(policyID); - const {submitsTo, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; + const {submitsTo, harvesting, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; const isOwner = currentUserAccountID === ownerAccountID; const isManager = currentUserAccountID === managerID; const isSelfApproval = currentUserAccountID === submitsTo; @@ -103,7 +103,7 @@ function buildNextStep(report: Report | EmptyObject, predictedNextStatus: ValueO }; // Scheduled submit enabled - if (autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL) { + if (harvesting?.enabled && autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL) { optimisticNextStep.message = [ { text: 'These expenses are scheduled to ', diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 325bf02030b6..568c641d2ac5 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -22,6 +22,9 @@ describe('libs/NextStepUtils', () => { id: policyID, owner: currentUserEmail, submitsTo: currentUserAccountID, + harvesting: { + enabled: false, + }, // Required props name: 'Policy', role: 'admin', @@ -109,6 +112,9 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE, + harvesting: { + enabled: true, + }, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -132,6 +138,9 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY, + harvesting: { + enabled: true, + }, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -155,6 +164,9 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY, + harvesting: { + enabled: true, + }, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -179,6 +191,9 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: 2, + harvesting: { + enabled: true, + }, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -203,6 +218,9 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_DAY_OF_MONTH, + harvesting: { + enabled: true, + }, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -228,6 +246,9 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH, + harvesting: { + enabled: true, + }, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -251,6 +272,9 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP, + harvesting: { + enabled: true, + }, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -281,6 +305,9 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL, + harvesting: { + enabled: true, + }, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); From d3541563c957768d426ba05b9d1aa8bdb6e64382 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Thu, 8 Feb 2024 21:24:27 +0530 Subject: [PATCH 59/65] Access property safely --- src/libs/OptionsListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index b6518b361381..15f4c1acf88a 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1001,7 +1001,7 @@ function getCategoryListSections( } const filteredRecentlyUsedCategories = recentlyUsedCategories - .filter((categoryName) => !selectedOptionNames.includes(categoryName) && categories[categoryName].enabled) + .filter((categoryName) => !selectedOptionNames.includes(categoryName) && categories[categoryName] && categories[categoryName].enabled) .map((categoryName) => ({ name: categoryName, enabled: categories[categoryName].enabled ?? false, From 1e47b6f6bfd849c17d463bd845847088dca7daf2 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Thu, 8 Feb 2024 21:39:19 +0530 Subject: [PATCH 60/65] Use chaining operator --- src/libs/OptionsListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 15f4c1acf88a..44449fecd517 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1001,7 +1001,7 @@ function getCategoryListSections( } const filteredRecentlyUsedCategories = recentlyUsedCategories - .filter((categoryName) => !selectedOptionNames.includes(categoryName) && categories[categoryName] && categories[categoryName].enabled) + .filter((categoryName) => !selectedOptionNames.includes(categoryName) && categories[categoryName]?.enabled) .map((categoryName) => ({ name: categoryName, enabled: categories[categoryName].enabled ?? false, From ca11d4c2b5fc744d330f4e54404f4e3136b289d1 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 9 Feb 2024 09:14:39 +0100 Subject: [PATCH 61/65] fix webpack config --- .storybook/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index 6717c1736f65..204f70344b18 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -25,7 +25,7 @@ module.exports = ({config}) => { config.resolve.alias = { 'react-native-config': 'react-web-config', 'react-native$': 'react-native-web', - '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.js'), + '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.ts'), '@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'), // Module alias support for storybook files, coping from `webpack.common.js` From 655d9deeaa07a90b480b75401ccf447af2f3c229 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 9 Feb 2024 13:48:54 +0100 Subject: [PATCH 62/65] use merge method --- src/libs/actions/IOU.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 4be5314fa0d6..09ca2a15a483 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -504,7 +504,7 @@ function buildOnyxDataForMoneyRequest( if (!isEmptyObject(optimisticNextStep)) { optimisticData.push({ - onyxMethod: Onyx.METHOD.SET, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, value: optimisticNextStep, }); @@ -3188,7 +3188,7 @@ function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxT value: {[iouReport.policyID ?? '']: paymentMethodType}, }, { - onyxMethod: Onyx.METHOD.SET, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, value: optimisticNextStep, }, @@ -3331,7 +3331,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject) { }, }; const optimisticNextStepData: OnyxUpdate = { - onyxMethod: Onyx.METHOD.SET, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, value: optimisticNextStep, }; @@ -3405,7 +3405,7 @@ function submitReport(expenseReport: OnyxTypes.Report) { }, }, { - onyxMethod: Onyx.METHOD.SET, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, value: optimisticNextStep, }, From e70ec81d5af44142a3b978bcdd45a1a5bd399e03 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 9 Feb 2024 13:49:24 +0100 Subject: [PATCH 63/65] check if a report created as submitted --- src/libs/actions/IOU.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 09ca2a15a483..05fd97f494b7 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -873,7 +873,8 @@ function getMoneyRequestInformation( } : {}; - const optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.OPEN); + const optimisticNextStep = NextStepUtils.buildNextStep(iouReport, needsToBeManuallySubmitted ? CONST.REPORT.STATUS_NUM.OPEN : CONST.REPORT.STATUS_NUM.SUBMITTED); + // STEP 5: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest( chatReport, From 12a28c12821a9c76db3038c64241e4f53101ceaf Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 9 Feb 2024 13:53:23 +0100 Subject: [PATCH 64/65] check if a report is expense --- src/libs/NextStepUtils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 5922552150f6..d75095315bd5 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -63,6 +63,10 @@ type BuildNextStepParameters = { * @returns nextStep */ function buildNextStep(report: Report | EmptyObject, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { + if (!ReportUtils.isExpenseReport(report)) { + return null; + } + const {policyID = '', ownerAccountID = -1, managerID = -1} = report; const policy = ReportUtils.getPolicy(policyID); const {submitsTo, harvesting, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; From b9ac4a3438739ff0f29853f5e31ade1c8ddbfb02 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 9 Feb 2024 13:58:47 +0100 Subject: [PATCH 65/65] flip a condition once submit --- src/libs/NextStepUtils.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index d75095315bd5..3b42382b10f9 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -188,16 +188,20 @@ function buildNextStep(report: Report | EmptyObject, predictedNextStatus: ValueO case CONST.REPORT.STATUS_NUM.SUBMITTED: { const verb = isManager ? 'review' : 'approve'; - // Self review & another reviewer + // Another owner optimisticNextStep = { type, title: 'Next Steps:', message: [ { - text: 'Waiting for ', + text: ownerLogin, + type: 'strong', }, { - text: managerDisplayName, + text: ' is waiting for ', + }, + { + text: 'you', type: 'strong', }, { @@ -208,23 +212,19 @@ function buildNextStep(report: Report | EmptyObject, predictedNextStatus: ValueO type: 'strong', }, { - text: ' %expenses.', + text: ' these %expenses.', }, ], }; - // Another owner - if (!isOwner) { + // Self review & another reviewer + if (isOwner) { optimisticNextStep.message = [ { - text: ownerLogin, - type: 'strong', - }, - { - text: ' is waiting for ', + text: 'Waiting for ', }, { - text: 'you', + text: managerDisplayName, type: 'strong', }, { @@ -235,7 +235,7 @@ function buildNextStep(report: Report | EmptyObject, predictedNextStatus: ValueO type: 'strong', }, { - text: ' these %expenses.', + text: ' %expenses.', }, ]; }