From 6a458b7522aea7bc14eb6074af1accf82306371e Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 29 Aug 2023 17:01:01 +0300 Subject: [PATCH 001/134] Allow zIndex to be overridden --- src/components/PopoverWithoutOverlay/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index 778f65349969..15924d97467c 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -50,7 +50,7 @@ function Popover(props) { return ( From c2aa62fdf2ca857136e2fb23c5897877474595aa Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 29 Aug 2023 17:17:02 +0300 Subject: [PATCH 002/134] Allow overriding some modal styles from HeaderWithBackButton --- src/components/AttachmentModal.js | 29 ++++++++++++------- .../headerWithBackButtonPropTypes.js | 3 ++ src/components/HeaderWithBackButton/index.js | 2 ++ src/components/PopoverMenu/index.js | 5 ++++ src/components/ThreeDotsMenu/index.js | 7 ++++- src/styles/styles.js | 8 +++-- 6 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index c07a4474a68b..56eb0de7c871 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -18,6 +18,7 @@ import compose from '../libs/compose'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; import Button from './Button'; import HeaderWithBackButton from './HeaderWithBackButton'; +import ReceiptAttachmentHeader from './ReceiptAttachmentHeader'; import fileDownload from '../libs/fileDownload'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import ConfirmModal from './ConfirmModal'; @@ -336,16 +337,24 @@ function AttachmentModal(props) { propagateSwipe > {props.isSmallScreenWidth && } - downloadAttachment(source)} - shouldShowCloseButton={!props.isSmallScreenWidth} - shouldShowBackButton={props.isSmallScreenWidth} - onBackButtonPress={closeModal} - onCloseButtonPress={closeModal} - /> + {isAttachmentReceipt ? ( + + ) : ( + downloadAttachment(source)} + shouldShowCloseButton={!props.isSmallScreenWidth} + shouldShowBackButton={props.isSmallScreenWidth} + onBackButtonPress={closeModal} + onCloseButtonPress={closeModal} + /> + )} {!_.isEmpty(props.report) ? ( )} {shouldShowCloseButton && ( diff --git a/src/components/PopoverMenu/index.js b/src/components/PopoverMenu/index.js index 67b9a0406aef..6484d24f2870 100644 --- a/src/components/PopoverMenu/index.js +++ b/src/components/PopoverMenu/index.js @@ -27,6 +27,9 @@ const propTypes = { /** Ref of the anchor */ anchorRef: refPropTypes, + /** Outer style of popover that passes down to Modal */ + outerStyle: PropTypes.oneOf([PropTypes.array, PropTypes.object]), + /** Where the popover should be positioned relative to the anchor points. */ anchorAlignment: PropTypes.shape({ horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)), @@ -42,6 +45,7 @@ const defaultProps = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }, + outerStyle: {}, anchorRef: () => {}, withoutOverlay: false, }; @@ -89,6 +93,7 @@ function PopoverMenu(props) { disableAnimation={props.disableAnimation} fromSidebarMediumScreen={props.fromSidebarMediumScreen} withoutOverlay={props.withoutOverlay} + outerStyle={props.outerStyle} > {!_.isEmpty(props.headerText) && {props.headerText}} diff --git a/src/components/ThreeDotsMenu/index.js b/src/components/ThreeDotsMenu/index.js index b5637a4f3879..4d44f9eb6d7f 100644 --- a/src/components/ThreeDotsMenu/index.js +++ b/src/components/ThreeDotsMenu/index.js @@ -29,6 +29,9 @@ const propTypes = { /** Function to call on icon press */ onIconPress: PropTypes.func, + /** Outer styles that get passed down to Modal */ + outerStyle: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), + /** menuItems that'll show up on toggle of the popup menu */ menuItems: ThreeDotsMenuItemPropTypes.isRequired, @@ -53,13 +56,14 @@ const defaultProps = { iconStyles: [], icon: Expensicons.ThreeDots, onIconPress: () => {}, + outerStyle: {}, anchorAlignment: { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, // we assume that popover menu opens below the button, anchor is at TOP }, }; -function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, menuItems, anchorPosition, anchorAlignment}) { +function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, outerStyle, menuItems, anchorPosition, anchorAlignment}) { const [isPopupMenuVisible, setPopupMenuVisible] = useState(false); const buttonRef = useRef(null); const {translate} = useLocalize(); @@ -108,6 +112,7 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me menuItems={menuItems} withoutOverlay anchorRef={buttonRef} + outerStyle={outerStyle} /> ); diff --git a/src/styles/styles.js b/src/styles/styles.js index d6258da62cbc..1eb02a79f955 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -2148,6 +2148,10 @@ const styles = { width: '100%', }, + receiptAttachmentHeaderThreeDotsMenuStyles: { + zIndex: variables.popoverModalzIndex, + }, + defaultAttachmentView: { backgroundColor: themeColors.sidebar, borderRadius: variables.componentBorderRadiusNormal, @@ -3102,8 +3106,8 @@ const styles = { flex: 1, }, - threeDotsPopoverOffset: (windowWidth) => ({ - ...getPopOverVerticalOffset(60), + threeDotsPopoverOffset: (windowWidth, isWithinFullScreenModal) => ({ + ...getPopOverVerticalOffset(60 + (isWithinFullScreenModal ? 20 : 0)), horizontal: windowWidth - 60, }), From bfe63488873ea36356412e145e10520c1d461392 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 29 Aug 2023 17:17:28 +0300 Subject: [PATCH 003/134] New attachment view header component --- src/components/ReceiptAttachmentHeader.js | 86 +++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/components/ReceiptAttachmentHeader.js diff --git a/src/components/ReceiptAttachmentHeader.js b/src/components/ReceiptAttachmentHeader.js new file mode 100644 index 000000000000..11943e7a7fc7 --- /dev/null +++ b/src/components/ReceiptAttachmentHeader.js @@ -0,0 +1,86 @@ +import React, {useState, useCallback} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import {View} from 'react-native'; +import HeaderWithBackButton from './HeaderWithBackButton'; +import iouReportPropTypes from '../pages/iouReportPropTypes'; +import * as Expensicons from './Icon/Expensicons'; +import styles from '../styles/styles'; +import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; +import compose from '../libs/compose'; +import ONYXKEYS from '../ONYXKEYS'; +import ConfirmModal from './ConfirmModal'; +import useLocalize from '../hooks/useLocalize'; + +const propTypes = { + /** The report currently being looked at */ + report: iouReportPropTypes.isRequired, + + /** The expense report or iou report (only will have a value if this is a transaction thread) */ + parentReport: iouReportPropTypes, + + ...windowDimensionsPropTypes, +}; + +const defaultProps = { + parentReport: {}, +}; + +function ReceiptAttachmentHeader(props) { + const {translate} = useLocalize(); + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + + const deleteReceipt = useCallback(() => { + // IOU.deleteMoneyRequest(parentReportAction.originalMessage.IOUTransactionID, parentReportAction, true); + setIsDeleteModalVisible(false); + }, [props.parentReport, setIsDeleteModalVisible]); + + return ( + <> + + setIsDeleteModalVisible(true), + }, + ]} + threeDotsAnchorPosition={styles.threeDotsPopoverOffset(props.windowWidth, true)} + outerThreeDotsMenuStyle={styles.receiptAttachmentHeaderThreeDotsMenuStyles} + shouldShowBackButton={props.isSmallScreenWidth} + onBackButtonPress={props.onBackButtonPress} + onCloseButtonPress={props.onCloseButtonPress} + /> + + setIsDeleteModalVisible(false)} + prompt={translate('receipt.deleteConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + + ); +} + +ReceiptAttachmentHeader.displayName = 'ReceiptAttachmentHeader'; +ReceiptAttachmentHeader.propTypes = propTypes; +ReceiptAttachmentHeader.defaultProps = defaultProps; + +export default compose( + withWindowDimensions, + withOnyx({ + parentReport: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, + }, + }), +)(ReceiptAttachmentHeader); From 93738713966c5f4d4243dfe7cb62be1a8fb69ea8 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 29 Aug 2023 17:17:45 +0300 Subject: [PATCH 004/134] Forgot to commit new style var --- src/styles/variables.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/variables.ts b/src/styles/variables.ts index f584e657c693..ae4fe6e4ee26 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -82,6 +82,7 @@ export default { sideBarWidth: 375, pdfPageMaxWidth: 992, tooltipzIndex: 10050, + popoverModalzIndex: 9999, gutterWidth: 12, popoverMenuShadow: '0px 4px 12px 0px rgba(0, 0, 0, 0.06)', optionRowHeight: 64, From 73ba5b668e5879eff61ddcc127962806608f0b4d Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 29 Aug 2023 18:22:31 +0300 Subject: [PATCH 005/134] Finish transaction-get logic --- src/components/ReceiptAttachmentHeader.js | 30 ++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/ReceiptAttachmentHeader.js b/src/components/ReceiptAttachmentHeader.js index 11943e7a7fc7..37532d669095 100644 --- a/src/components/ReceiptAttachmentHeader.js +++ b/src/components/ReceiptAttachmentHeader.js @@ -1,22 +1,24 @@ import React, {useState, useCallback} from 'react'; -import {withOnyx} from 'react-native-onyx'; +import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; import {View} from 'react-native'; import HeaderWithBackButton from './HeaderWithBackButton'; -import iouReportPropTypes from '../pages/iouReportPropTypes'; import * as Expensicons from './Icon/Expensicons'; import styles from '../styles/styles'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; import compose from '../libs/compose'; -import ONYXKEYS from '../ONYXKEYS'; import ConfirmModal from './ConfirmModal'; import useLocalize from '../hooks/useLocalize'; +import ReceiptActions from '../libs/actions/Receipt'; +import * as ReportActionsUtils from '../libs/ReportActionsUtils'; +import _ from 'lodash'; const propTypes = { /** The report currently being looked at */ - report: iouReportPropTypes.isRequired, - - /** The expense report or iou report (only will have a value if this is a transaction thread) */ - parentReport: iouReportPropTypes, + report: PropTypes.shape({ + parentReportID: PropTypes.string.isRequired, + parentReportActionID: PropTypes.string.isRequired, + }).isRequired, ...windowDimensionsPropTypes, }; @@ -30,9 +32,14 @@ function ReceiptAttachmentHeader(props) { const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const deleteReceipt = useCallback(() => { - // IOU.deleteMoneyRequest(parentReportAction.originalMessage.IOUTransactionID, parentReportAction, true); + // Get receipt attachment transaction ID + const parentReportAction = ReportActionsUtils.getParentReportAction(props.report); + const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], 0); + + // Detatch receipt & clear modal open state + ReceiptActions.detachReceipt(transactionID); setIsDeleteModalVisible(false); - }, [props.parentReport, setIsDeleteModalVisible]); + }, [props.receiptTransactions, setIsDeleteModalVisible]); return ( <> @@ -78,9 +85,4 @@ ReceiptAttachmentHeader.defaultProps = defaultProps; export default compose( withWindowDimensions, - withOnyx({ - parentReport: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, - }, - }), )(ReceiptAttachmentHeader); From fe013075d6fe4aa6bdc39a250ac7c5fdc8db459c Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 29 Aug 2023 18:50:25 +0300 Subject: [PATCH 006/134] Add optimistic and failure data --- src/libs/actions/Receipt.js | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/libs/actions/Receipt.js b/src/libs/actions/Receipt.js index fbe9c22faaa2..dc25e28b22cc 100644 --- a/src/libs/actions/Receipt.js +++ b/src/libs/actions/Receipt.js @@ -1,5 +1,20 @@ import Onyx from 'react-native-onyx'; +import lodashGet from 'lodash/get'; import ONYXKEYS from '../../ONYXKEYS'; +import * as API from '../API'; +import * as CollectionUtils from '../CollectionUtils'; + +const allTransactionData = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + callback: (data, key) => { + if (!key || !data) { + return; + } + const transactionID = CollectionUtils.extractCollectionItemID(key); + allTransactionData[transactionID] = data; + }, +}); /** * Sets the upload receipt error modal content when an invalid receipt is uploaded @@ -27,7 +42,36 @@ function clearUploadReceiptError() { }); } +/** + * Detaches the receipt from a transaction + * + * @param {String} transactionID + */ +function detachReceipt(transactionID) { + API.write('DetachReceipt', {transactionID}, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + receipt: {}, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + receipt: lodashGet(allTransactionData, [transactionID, 'receipt'], {}), + }, + }, + ], + }); +} + export default { setUploadReceiptError, clearUploadReceiptError, + detachReceipt, }; From c0f11989f5f0055822e031fe1f0a92c0558a66b4 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 29 Aug 2023 18:59:15 +0300 Subject: [PATCH 007/134] Fixing up some lint errors --- src/components/ReceiptAttachmentHeader.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/ReceiptAttachmentHeader.js b/src/components/ReceiptAttachmentHeader.js index 37532d669095..e1af8473ff51 100644 --- a/src/components/ReceiptAttachmentHeader.js +++ b/src/components/ReceiptAttachmentHeader.js @@ -11,7 +11,6 @@ import ConfirmModal from './ConfirmModal'; import useLocalize from '../hooks/useLocalize'; import ReceiptActions from '../libs/actions/Receipt'; import * as ReportActionsUtils from '../libs/ReportActionsUtils'; -import _ from 'lodash'; const propTypes = { /** The report currently being looked at */ @@ -23,10 +22,6 @@ const propTypes = { ...windowDimensionsPropTypes, }; -const defaultProps = { - parentReport: {}, -}; - function ReceiptAttachmentHeader(props) { const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); @@ -39,7 +34,7 @@ function ReceiptAttachmentHeader(props) { // Detatch receipt & clear modal open state ReceiptActions.detachReceipt(transactionID); setIsDeleteModalVisible(false); - }, [props.receiptTransactions, setIsDeleteModalVisible]); + }, [props.report, setIsDeleteModalVisible]); return ( <> @@ -60,7 +55,6 @@ function ReceiptAttachmentHeader(props) { ]} threeDotsAnchorPosition={styles.threeDotsPopoverOffset(props.windowWidth, true)} outerThreeDotsMenuStyle={styles.receiptAttachmentHeaderThreeDotsMenuStyles} - shouldShowBackButton={props.isSmallScreenWidth} onBackButtonPress={props.onBackButtonPress} onCloseButtonPress={props.onCloseButtonPress} /> @@ -83,6 +77,4 @@ ReceiptAttachmentHeader.displayName = 'ReceiptAttachmentHeader'; ReceiptAttachmentHeader.propTypes = propTypes; ReceiptAttachmentHeader.defaultProps = defaultProps; -export default compose( - withWindowDimensions, -)(ReceiptAttachmentHeader); +export default withWindowDimensions(ReceiptAttachmentHeader); From e46307c6153e86f0fbe3ec892751845c3f7babbb Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 29 Aug 2023 19:08:13 +0300 Subject: [PATCH 008/134] Navigate user back to receipt report after detaching --- src/components/ReceiptAttachmentHeader.js | 3 +-- src/libs/actions/Receipt.js | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/ReceiptAttachmentHeader.js b/src/components/ReceiptAttachmentHeader.js index e1af8473ff51..da81aaa514e0 100644 --- a/src/components/ReceiptAttachmentHeader.js +++ b/src/components/ReceiptAttachmentHeader.js @@ -32,7 +32,7 @@ function ReceiptAttachmentHeader(props) { const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], 0); // Detatch receipt & clear modal open state - ReceiptActions.detachReceipt(transactionID); + ReceiptActions.detachReceipt(transactionID, props.report.reportID); setIsDeleteModalVisible(false); }, [props.report, setIsDeleteModalVisible]); @@ -75,6 +75,5 @@ function ReceiptAttachmentHeader(props) { ReceiptAttachmentHeader.displayName = 'ReceiptAttachmentHeader'; ReceiptAttachmentHeader.propTypes = propTypes; -ReceiptAttachmentHeader.defaultProps = defaultProps; export default withWindowDimensions(ReceiptAttachmentHeader); diff --git a/src/libs/actions/Receipt.js b/src/libs/actions/Receipt.js index dc25e28b22cc..db81a67bb521 100644 --- a/src/libs/actions/Receipt.js +++ b/src/libs/actions/Receipt.js @@ -3,6 +3,8 @@ import lodashGet from 'lodash/get'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; import * as CollectionUtils from '../CollectionUtils'; +import Navigation from '../Navigation/Navigation'; +import ROUTES from '../../ROUTES'; const allTransactionData = {}; Onyx.connect({ @@ -47,7 +49,7 @@ function clearUploadReceiptError() { * * @param {String} transactionID */ -function detachReceipt(transactionID) { +function detachReceipt(transactionID, reportID) { API.write('DetachReceipt', {transactionID}, { optimisticData: [ { @@ -68,6 +70,7 @@ function detachReceipt(transactionID) { }, ], }); + Navigation.navigate(ROUTES.getReportRoute(reportID)); } export default { From 802af8d1cbc8801cba5104b7040739662739084d Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 29 Aug 2023 19:09:08 +0300 Subject: [PATCH 009/134] Add translations --- src/languages/en.js | 3 ++- src/languages/es.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index fa8ea84c141b..cd01c2680ccd 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -393,6 +393,8 @@ export default { flash: 'flash', shutter: 'shutter', gallery: 'gallery', + deleteReceipt: 'Delete receipt', + deleteConfirmation: 'Are you sure you want to delete this receipt?', }, iou: { amount: 'Amount', @@ -408,7 +410,6 @@ export default { pay: 'Pay', viewDetails: 'View details', pending: 'Pending', - deleteReceipt: 'Delete receipt', receiptScanning: 'Receipt scan in progress…', receiptStatusTitle: 'Scanning…', receiptStatusText: "Only you can see this receipt when it's scanning. Check back later or enter the details now.", diff --git a/src/languages/es.js b/src/languages/es.js index fd6fcd9b767f..6d37b5d0f89b 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -392,6 +392,8 @@ export default { flash: 'flash', shutter: 'obturador', gallery: 'galería', + deleteReceipt: 'Eliminar recibo', + deleteConfirmation: '¿Estás seguro de que quieres borrar este recibo?', }, iou: { amount: 'Importe', @@ -407,7 +409,6 @@ export default { pay: 'Pagar', viewDetails: 'Ver detalles', pending: 'Pendiente', - deleteReceipt: 'Eliminar recibo', receiptScanning: 'Escaneo de recibo en curso…', receiptStatusTitle: 'Escaneando…', receiptStatusText: 'Solo tú puedes ver este recibo cuando se está escaneando. Vuelve más tarde o introduce los detalles ahora.', From 6491074ea5fd5adc3f54fcb0c8ff0813060fea8c Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 29 Aug 2023 19:38:18 +0300 Subject: [PATCH 010/134] lint & prettify --- src/components/ReceiptAttachmentHeader.js | 1 - src/libs/actions/Receipt.js | 41 +++++++++++++---------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/components/ReceiptAttachmentHeader.js b/src/components/ReceiptAttachmentHeader.js index da81aaa514e0..7e66f784fdd0 100644 --- a/src/components/ReceiptAttachmentHeader.js +++ b/src/components/ReceiptAttachmentHeader.js @@ -6,7 +6,6 @@ import HeaderWithBackButton from './HeaderWithBackButton'; import * as Expensicons from './Icon/Expensicons'; import styles from '../styles/styles'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; -import compose from '../libs/compose'; import ConfirmModal from './ConfirmModal'; import useLocalize from '../hooks/useLocalize'; import ReceiptActions from '../libs/actions/Receipt'; diff --git a/src/libs/actions/Receipt.js b/src/libs/actions/Receipt.js index db81a67bb521..2222b8ff9caf 100644 --- a/src/libs/actions/Receipt.js +++ b/src/libs/actions/Receipt.js @@ -48,28 +48,33 @@ function clearUploadReceiptError() { * Detaches the receipt from a transaction * * @param {String} transactionID + * @param {String} reportID */ function detachReceipt(transactionID, reportID) { - API.write('DetachReceipt', {transactionID}, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: { - receipt: {}, + API.write( + 'DetachReceipt', + {transactionID}, + { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + receipt: {}, + }, }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: { - receipt: lodashGet(allTransactionData, [transactionID, 'receipt'], {}), + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + receipt: lodashGet(allTransactionData, [transactionID, 'receipt'], {}), + }, }, - }, - ], - }); + ], + }, + ); Navigation.navigate(ROUTES.getReportRoute(reportID)); } From 776513ea65d424b2b0d022ac86cd554cd4829d6e Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Fri, 1 Sep 2023 18:56:05 +0300 Subject: [PATCH 011/134] Prop types fixes --- src/components/PopoverMenu/index.js | 2 +- src/components/ReceiptAttachmentHeader.js | 7 +++++++ src/components/ThreeDotsMenu/index.js | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/PopoverMenu/index.js b/src/components/PopoverMenu/index.js index 6484d24f2870..3e3fa247fe6e 100644 --- a/src/components/PopoverMenu/index.js +++ b/src/components/PopoverMenu/index.js @@ -28,7 +28,7 @@ const propTypes = { anchorRef: refPropTypes, /** Outer style of popover that passes down to Modal */ - outerStyle: PropTypes.oneOf([PropTypes.array, PropTypes.object]), + outerStyle: PropTypes.object, /** Where the popover should be positioned relative to the anchor points. */ anchorAlignment: PropTypes.shape({ diff --git a/src/components/ReceiptAttachmentHeader.js b/src/components/ReceiptAttachmentHeader.js index 7e66f784fdd0..4927c3fe458e 100644 --- a/src/components/ReceiptAttachmentHeader.js +++ b/src/components/ReceiptAttachmentHeader.js @@ -14,10 +14,17 @@ import * as ReportActionsUtils from '../libs/ReportActionsUtils'; const propTypes = { /** The report currently being looked at */ report: PropTypes.shape({ + reportID: PropTypes.string.isRequired, parentReportID: PropTypes.string.isRequired, parentReportActionID: PropTypes.string.isRequired, }).isRequired, + /** Method to trigger when pressing back button of the header */ + onBackButtonPress: PropTypes.func.isRequired, + + /** Method to trigger when pressing close button of the header */ + onCloseButtonPress: PropTypes.func.isRequired, + ...windowDimensionsPropTypes, }; diff --git a/src/components/ThreeDotsMenu/index.js b/src/components/ThreeDotsMenu/index.js index 4d44f9eb6d7f..93850b08a3bb 100644 --- a/src/components/ThreeDotsMenu/index.js +++ b/src/components/ThreeDotsMenu/index.js @@ -30,7 +30,7 @@ const propTypes = { onIconPress: PropTypes.func, /** Outer styles that get passed down to Modal */ - outerStyle: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), + outerStyle: PropTypes.object, /** menuItems that'll show up on toggle of the popup menu */ menuItems: ThreeDotsMenuItemPropTypes.isRequired, From d0898636d15a3bd685685a7f5d9a7ac89b2205a0 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 19 Sep 2023 13:00:46 +0800 Subject: [PATCH 012/134] Fix types --- src/libs/actions/Receipt.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/Receipt.ts b/src/libs/actions/Receipt.ts index 13553e0b0d7d..2d3c347240e4 100644 --- a/src/libs/actions/Receipt.ts +++ b/src/libs/actions/Receipt.ts @@ -5,8 +5,14 @@ import * as API from '../API'; import * as CollectionUtils from '../CollectionUtils'; import Navigation from '../Navigation/Navigation'; import ROUTES from '../../ROUTES'; +import * as OnyxTypes from '../../types/onyx'; +import CONST from '../../CONST'; -const allTransactionData = {}; +type TransactionMap = { + [key: string]: OnyxTypes.Transaction; +}; + +const allTransactionData: TransactionMap = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION, callback: (data, key) => { @@ -42,11 +48,8 @@ function clearUploadReceiptError() { /** * Detaches the receipt from a transaction - * - * @param {String} transactionID - * @param {String} reportID */ -function detachReceipt(transactionID, reportID) { +function detachReceipt(transactionID: string, reportID: string) { API.write( 'DetachReceipt', {transactionID}, @@ -71,7 +74,7 @@ function detachReceipt(transactionID, reportID) { ], }, ); - Navigation.navigate(ROUTES.getReportRoute(reportID)); + Navigation.navigate(ROUTES.getReportRoute(reportID), CONST.NAVIGATION.TYPE.UP); } export default { From f3087eeb74b2c727a229f85d713c5f199880682f Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Wed, 20 Sep 2023 16:51:39 +0800 Subject: [PATCH 013/134] Remove old version of custom header --- src/components/AttachmentModal.js | 1 - src/components/ReceiptAttachmentHeader.js | 85 ----------------------- src/styles/styles.js | 4 -- 3 files changed, 90 deletions(-) delete mode 100644 src/components/ReceiptAttachmentHeader.js diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index eacf8780b6e2..946b5e2ddec9 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -18,7 +18,6 @@ import compose from '../libs/compose'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; import Button from './Button'; import HeaderWithBackButton from './HeaderWithBackButton'; -import ReceiptAttachmentHeader from './ReceiptAttachmentHeader'; import fileDownload from '../libs/fileDownload'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import ConfirmModal from './ConfirmModal'; diff --git a/src/components/ReceiptAttachmentHeader.js b/src/components/ReceiptAttachmentHeader.js deleted file mode 100644 index 4927c3fe458e..000000000000 --- a/src/components/ReceiptAttachmentHeader.js +++ /dev/null @@ -1,85 +0,0 @@ -import React, {useState, useCallback} from 'react'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import {View} from 'react-native'; -import HeaderWithBackButton from './HeaderWithBackButton'; -import * as Expensicons from './Icon/Expensicons'; -import styles from '../styles/styles'; -import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; -import ConfirmModal from './ConfirmModal'; -import useLocalize from '../hooks/useLocalize'; -import ReceiptActions from '../libs/actions/Receipt'; -import * as ReportActionsUtils from '../libs/ReportActionsUtils'; - -const propTypes = { - /** The report currently being looked at */ - report: PropTypes.shape({ - reportID: PropTypes.string.isRequired, - parentReportID: PropTypes.string.isRequired, - parentReportActionID: PropTypes.string.isRequired, - }).isRequired, - - /** Method to trigger when pressing back button of the header */ - onBackButtonPress: PropTypes.func.isRequired, - - /** Method to trigger when pressing close button of the header */ - onCloseButtonPress: PropTypes.func.isRequired, - - ...windowDimensionsPropTypes, -}; - -function ReceiptAttachmentHeader(props) { - const {translate} = useLocalize(); - const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); - - const deleteReceipt = useCallback(() => { - // Get receipt attachment transaction ID - const parentReportAction = ReportActionsUtils.getParentReportAction(props.report); - const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], 0); - - // Detatch receipt & clear modal open state - ReceiptActions.detachReceipt(transactionID, props.report.reportID); - setIsDeleteModalVisible(false); - }, [props.report, setIsDeleteModalVisible]); - - return ( - <> - - setIsDeleteModalVisible(true), - }, - ]} - threeDotsAnchorPosition={styles.threeDotsPopoverOffset(props.windowWidth, true)} - outerThreeDotsMenuStyle={styles.receiptAttachmentHeaderThreeDotsMenuStyles} - onBackButtonPress={props.onBackButtonPress} - onCloseButtonPress={props.onCloseButtonPress} - /> - - setIsDeleteModalVisible(false)} - prompt={translate('receipt.deleteConfirmation')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - - ); -} - -ReceiptAttachmentHeader.displayName = 'ReceiptAttachmentHeader'; -ReceiptAttachmentHeader.propTypes = propTypes; - -export default withWindowDimensions(ReceiptAttachmentHeader); diff --git a/src/styles/styles.js b/src/styles/styles.js index fac4ce864cba..8b5d2b052cc7 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -2197,10 +2197,6 @@ const styles = (theme) => ({ width: '100%', }, - receiptAttachmentHeaderThreeDotsMenuStyles: { - zIndex: variables.popoverModalzIndex, - }, - defaultAttachmentView: { backgroundColor: theme.sidebar, borderRadius: variables.componentBorderRadiusNormal, From 7c83e8c7d59cdba6e60542949dbd427d995ead9b Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Wed, 20 Sep 2023 16:53:57 +0800 Subject: [PATCH 014/134] Revert a few style updates --- .../HeaderWithBackButton/headerWithBackButtonPropTypes.js | 3 --- src/components/HeaderWithBackButton/index.js | 1 - src/components/PopoverMenu/index.js | 5 ----- src/components/ThreeDotsMenu/index.js | 7 +------ src/styles/styles.js | 4 ++-- 5 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js b/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js index b3248f2447eb..d2cdc5b29898 100644 --- a/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js +++ b/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js @@ -48,9 +48,6 @@ const propTypes = { left: PropTypes.number, }), - /** Outer style that gets passed down to Modal */ - outerThreeDotsMenuStyle: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), - /** Whether we should show a close button */ shouldShowCloseButton: PropTypes.bool, diff --git a/src/components/HeaderWithBackButton/index.js b/src/components/HeaderWithBackButton/index.js index 27b4a9f2408b..cf61a4cf4eb7 100755 --- a/src/components/HeaderWithBackButton/index.js +++ b/src/components/HeaderWithBackButton/index.js @@ -46,7 +46,6 @@ function HeaderWithBackButton({ horizontal: 0, }, threeDotsMenuItems = [], - outerThreeDotsMenuStyle = {}, children = null, shouldOverlay = false, }) { diff --git a/src/components/PopoverMenu/index.js b/src/components/PopoverMenu/index.js index bd913d3f0a9b..5fabf73547ea 100644 --- a/src/components/PopoverMenu/index.js +++ b/src/components/PopoverMenu/index.js @@ -27,9 +27,6 @@ const propTypes = { /** Ref of the anchor */ anchorRef: refPropTypes, - /** Outer style of popover that passes down to Modal */ - outerStyle: PropTypes.object, - /** Where the popover should be positioned relative to the anchor points. */ anchorAlignment: PropTypes.shape({ horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)), @@ -45,7 +42,6 @@ const defaultProps = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }, - outerStyle: {}, anchorRef: () => {}, withoutOverlay: false, }; @@ -93,7 +89,6 @@ function PopoverMenu(props) { disableAnimation={props.disableAnimation} fromSidebarMediumScreen={props.fromSidebarMediumScreen} withoutOverlay={props.withoutOverlay} - outerStyle={props.outerStyle} > {!_.isEmpty(props.headerText) && {props.headerText}} diff --git a/src/components/ThreeDotsMenu/index.js b/src/components/ThreeDotsMenu/index.js index efef3bb6a8c8..f0cee6fdea2f 100644 --- a/src/components/ThreeDotsMenu/index.js +++ b/src/components/ThreeDotsMenu/index.js @@ -29,9 +29,6 @@ const propTypes = { /** Function to call on icon press */ onIconPress: PropTypes.func, - /** Outer styles that get passed down to Modal */ - outerStyle: PropTypes.object, - /** menuItems that'll show up on toggle of the popup menu */ menuItems: ThreeDotsMenuItemPropTypes.isRequired, @@ -59,7 +56,6 @@ const defaultProps = { iconStyles: [], icon: Expensicons.ThreeDots, onIconPress: () => {}, - outerStyle: {}, anchorAlignment: { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, // we assume that popover menu opens below the button, anchor is at TOP @@ -67,7 +63,7 @@ const defaultProps = { shouldOverlay: false, }; -function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, outerStyle, menuItems, anchorPosition, anchorAlignment, shouldOverlay}) { +function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, menuItems, anchorPosition, anchorAlignment, shouldOverlay}) { const [isPopupMenuVisible, setPopupMenuVisible] = useState(false); const buttonRef = useRef(null); const {translate} = useLocalize(); @@ -116,7 +112,6 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, ou menuItems={menuItems} withoutOverlay={!shouldOverlay} anchorRef={buttonRef} - outerStyle={outerStyle} /> ); diff --git a/src/styles/styles.js b/src/styles/styles.js index 8b5d2b052cc7..01689a43952a 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3177,8 +3177,8 @@ const styles = (theme) => ({ flex: 1, }, - threeDotsPopoverOffset: (windowWidth, isWithinFullScreenModal) => ({ - ...getPopOverVerticalOffset(60 + (isWithinFullScreenModal ? 20 : 0)), + threeDotsPopoverOffset: (windowWidth) => ({ + ...getPopOverVerticalOffset(60), horizontal: windowWidth - 60, }), From eaa6671829478be268f9820185241417806588ae Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Wed, 20 Sep 2023 16:54:34 +0800 Subject: [PATCH 015/134] Revert unused var --- src/styles/variables.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 0c1183e00768..6291323cef0b 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -83,7 +83,6 @@ export default { sideBarWidth: 375, pdfPageMaxWidth: 992, tooltipzIndex: 10050, - popoverModalzIndex: 9999, gutterWidth: 12, popoverMenuShadow: '0px 4px 12px 0px rgba(0, 0, 0, 0.06)', optionRowHeight: 64, From ca054cfb9ec65b07f49e8656cc916ee873b35ca4 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Wed, 20 Sep 2023 18:04:52 +0800 Subject: [PATCH 016/134] Connect detachReceipt with transactionID --- src/components/AttachmentModal.js | 35 ++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 946b5e2ddec9..68cee61bbc9b 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -1,6 +1,7 @@ import React, {useState, useCallback, useRef} from 'react'; import PropTypes from 'prop-types'; import {View, Animated, Keyboard} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import lodashExtend from 'lodash/extend'; @@ -30,6 +31,8 @@ import useWindowDimensions from '../hooks/useWindowDimensions'; import Navigation from '../libs/Navigation/Navigation'; import ROUTES from '../ROUTES'; import useNativeDriver from '../libs/useNativeDriver'; +import Receipt from '../libs/actions/Receipt'; +import ONYXKEYS from '../ONYXKEYS'; /** * Modal render prop component that exposes modal launching triggers that can be used @@ -79,6 +82,13 @@ const propTypes = { /** Denotes whether it is a workspace avatar or not */ isWorkspaceAvatar: PropTypes.bool, + + /* Onyx Props */ + /** The parent report */ + parentReport: reportPropTypes, + + /** The report action this report is tied to from the parent report */ + parentReportAction: PropTypes.shape(reportActionPropTypes), }; const defaultProps = { @@ -95,6 +105,8 @@ const defaultProps = { onModalHide: () => {}, onCarouselAttachmentChange: () => {}, isWorkspaceAvatar: false, + parentReport: {}, + parentReportAction: {}, }; function AttachmentModal(props) { @@ -372,6 +384,14 @@ function AttachmentModal(props) { text: props.translate('common.download'), onSelected: () => downloadAttachment(source), }, + { + icon: Expensicons.Trashcan, + text: props.translate('receipt.deleteReceipt'), + onSelected: () => { + const transactionID = lodashGet(props.parentReportAction, 'originalMessage.IOUTransactionID', ''); + Receipt.detachReceipt(transactionID, props.report.reportID) + }, + }, ]} shouldOverlay /> @@ -441,4 +461,17 @@ function AttachmentModal(props) { AttachmentModal.propTypes = propTypes; AttachmentModal.defaultProps = defaultProps; AttachmentModal.displayName = 'AttachmentModal'; -export default compose(withWindowDimensions, withLocalize)(AttachmentModal); +export default compose( + withWindowDimensions, + withLocalize, + withOnyx({ + parentReport: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, + }, + parentReportAction: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${(report.parentReportID, report.parentReportActionID)}`, + selector: (reportActions, props) => props && props.parentReport && reportActions && reportActions[props.parentReport.parentReportActionID], + canEvict: false, + }, + }), +)(AttachmentModal); From 9b4818eeb00c3cf99d18e6e97fa8b0d8d743b8b4 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Wed, 20 Sep 2023 13:51:32 +0100 Subject: [PATCH 017/134] feat(workspace-settings): currency selector push to page --- src/ROUTES.ts | 2 + .../AppNavigator/ModalStackNavigators.js | 7 ++ src/libs/Navigation/linkingConfig.js | 3 + .../WorkspaceSettingsCurrencyPage.js | 106 ++++++++++++++++++ src/pages/workspace/WorkspaceSettingsPage.js | 40 +++---- 5 files changed, 136 insertions(+), 22 deletions(-) create mode 100644 src/pages/workspace/WorkspaceSettingsCurrencyPage.js diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2c37116db395..8c3f60802ec3 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -190,6 +190,7 @@ export default { WORKSPACE_INVITE: 'workspace/:policyID/invite', WORKSPACE_INVITE_MESSAGE: 'workspace/:policyID/invite-message', WORKSPACE_SETTINGS: 'workspace/:policyID/settings', + WORKSPACE_SETTINGS_CURRENCY: 'workspace/:policyID/settings/currency', WORKSPACE_CARD: 'workspace/:policyID/card', WORKSPACE_REIMBURSE: 'workspace/:policyID/reimburse', WORKSPACE_RATE_AND_UNIT: 'workspace/:policyID/rateandunit', @@ -202,6 +203,7 @@ export default { getWorkspaceInviteRoute: (policyID: string) => `workspace/${policyID}/invite`, getWorkspaceInviteMessageRoute: (policyID: string) => `workspace/${policyID}/invite-message`, getWorkspaceSettingsRoute: (policyID: string) => `workspace/${policyID}/settings`, + getWorkspaceSettingsCurrencyRoute: (policyID: string) => `workspace/${policyID}/settings/currency`, getWorkspaceCardRoute: (policyID: string) => `workspace/${policyID}/card`, getWorkspaceReimburseRoute: (policyID: string) => `workspace/${policyID}/reimburse`, getWorkspaceRateAndUnitRoute: (policyID: string) => `workspace/${policyID}/rateandunit`, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 392781a777db..eabce0686e14 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -599,6 +599,13 @@ const SettingsModalStackNavigator = createModalStackNavigator([ }, name: 'Workspace_Settings', }, + { + getComponent: () => { + const WorkspaceSettingsCurrencyPage = require('../../../pages/workspace/WorkspaceSettingsCurrencyPage').default; + return WorkspaceSettingsCurrencyPage; + }, + name: 'Workspace_Settings_Currency', + }, { getComponent: () => { const WorkspaceCardPage = require('../../../pages/workspace/card/WorkspaceCardPage').default; diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 11d21d6d005c..925d786cc6b4 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -173,6 +173,9 @@ export default { Workspace_Settings: { path: ROUTES.WORKSPACE_SETTINGS, }, + Workspace_Settings_Currency: { + path: ROUTES.WORKSPACE_SETTINGS_CURRENCY, + }, Workspace_Card: { path: ROUTES.WORKSPACE_CARD, }, diff --git a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js new file mode 100644 index 000000000000..de554d95860a --- /dev/null +++ b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js @@ -0,0 +1,106 @@ +import React, {useState, useMemo, useCallback} from 'react'; +import _ from 'underscore'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import useLocalize from '../../hooks/useLocalize'; +import ScreenWrapper from '../../components/ScreenWrapper'; +import HeaderWithBackButton from '../../components/HeaderWithBackButton'; +import SelectionList from '../../components/SelectionList'; +import Navigation from '../../libs/Navigation/Navigation'; +import ROUTES from '../../ROUTES'; +import compose from '../../libs/compose'; +import ONYXKEYS from '../../ONYXKEYS'; +import withPolicy, {policyDefaultProps, policyPropTypes} from './withPolicy'; +import * as Policy from '../../libs/actions/Policy'; + +const propTypes = { + // List of available currencies + currencyList: PropTypes.objectOf( + PropTypes.shape({ + // Symbol for the currency + symbol: PropTypes.string, + }), + ), + ...policyPropTypes, +}; + +const defaultProps = { + currencyList: {}, + ...policyDefaultProps, +}; + +function WorkspaceSettingsCurrencyPage({currencyList, policy}) { + const {translate} = useLocalize(); + const [searchText, setSearchText] = useState(''); + + const getDisplayText = useCallback((currencyCode, currencySymbol) => `${currencyCode} - ${currencySymbol}`, []); + + const {sections, initiallyFocusedOptionKey} = useMemo(() => { + const trimmedText = searchText.trim().toLowerCase(); + const currencyListKeys = _.keys(currencyList); + + const filteredItems = _.filter(currencyListKeys, (currencyCode) => { + const currency = currencyList[currencyCode]; + return getDisplayText(currencyCode, currency.symbol).toLowerCase().includes(trimmedText); + }); + + let selectedCurrencyCode; + + const currencyItems = _.map(filteredItems, (currencyCode) => { + const currency = currencyList[currencyCode]; + const isSelected = policy.outputCurrency === currencyCode; + + if (isSelected) { + selectedCurrencyCode = currencyCode; + } + + return { + text: getDisplayText(currencyCode, currency.symbol), + keyForList: currencyCode, + isSelected, + }; + }); + + return { + sections: [{data: currencyItems, indexOffset: 0}], + initiallyFocusedOptionKey: selectedCurrencyCode, + }; + }, [getDisplayText, currencyList, policy.outputCurrency, searchText]); + + const headerMessage = Boolean(searchText.trim()) && !sections[0].data.length ? translate('common.noResultsFound') : ''; + + const onSelectCurrency = (item) => { + Policy.updateGeneralSettings(policy.id, policy.name, item.keyForList); + Navigation.goBack(ROUTES.getWorkspaceSettingsRoute(policy.id)); + }; + + return ( + + Navigation.goBack(ROUTES.getWorkspaceSettingsRoute(policy.id))} + /> + + + + ); +} + +WorkspaceSettingsCurrencyPage.propTypes = propTypes; +WorkspaceSettingsCurrencyPage.defaultProps = defaultProps; + +export default compose( + withPolicy, + withOnyx({ + currencyList: {key: ONYXKEYS.CURRENCY_LIST}, + }), +)(WorkspaceSettingsCurrencyPage); diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index 2a9576d5d8d3..00dcb99a92dc 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -1,8 +1,7 @@ -import React, {useCallback, useMemo} from 'react'; +import React, {useCallback} from 'react'; import {Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; -import _ from 'underscore'; import lodashGet from 'lodash/get'; import ONYXKEYS from '../../ONYXKEYS'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; @@ -12,7 +11,6 @@ import * as Policy from '../../libs/actions/Policy'; import * as Expensicons from '../../components/Icon/Expensicons'; import AvatarWithImagePicker from '../../components/AvatarWithImagePicker'; import CONST from '../../CONST'; -import Picker from '../../components/Picker'; import TextInput from '../../components/TextInput'; import WorkspacePageWithSections from './WorkspacePageWithSections'; import withPolicy, {policyPropTypes, policyDefaultProps} from './withPolicy'; @@ -25,6 +23,8 @@ import Avatar from '../../components/Avatar'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions'; +import MenuItemWithTopDescription from '../../components/MenuItemWithTopDescription'; +import Text from '../../components/Text'; const propTypes = { // The currency list constant object from Onyx @@ -45,25 +45,19 @@ const defaultProps = { }; function WorkspaceSettingsPage(props) { - const currencyItems = useMemo(() => { - const currencyListKeys = _.keys(props.currencyList); - return _.map(currencyListKeys, (currencyCode) => ({ - value: currencyCode, - label: `${currencyCode} - ${props.currencyList[currencyCode].symbol}`, - })); - }, [props.currencyList]); + const formattedCurrency = `${props.policy.outputCurrency} - ${props.currencyList[props.policy.outputCurrency].symbol}`; const submit = useCallback( (values) => { if (props.policy.isPolicyUpdating) { return; } - const outputCurrency = values.currency; - Policy.updateGeneralSettings(props.policy.id, values.name.trim(), outputCurrency); + + Policy.updateGeneralSettings(props.policy.id, values.name.trim(), props.policy.outputCurrency); Keyboard.dismiss(); Navigation.goBack(ROUTES.getWorkspaceInitialRoute(props.policy.id)); }, - [props.policy.id, props.policy.isPolicyUpdating], + [props.policy.id, props.policy.isPolicyUpdating, props.policy.outputCurrency], ); const validate = useCallback((values) => { @@ -93,7 +87,7 @@ function WorkspaceSettingsPage(props) {
- Navigation.navigate(ROUTES.getWorkspaceSettingsCurrencyRoute(props.policy.id))} /> + + {hasVBA ? props.translate('workspace.editor.currencyInputDisabledText') : props.translate('workspace.editor.currencyInputHelpText')} +
From 4847ec46bb5c67000fd09ad98b3de4f7820e117a Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Wed, 20 Sep 2023 14:05:21 +0100 Subject: [PATCH 018/134] chore: cleanup --- src/pages/workspace/WorkspaceSettingsCurrencyPage.js | 6 +++--- src/pages/workspace/WorkspaceSettingsPage.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js index de554d95860a..aa5e1701f8ce 100644 --- a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js +++ b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js @@ -14,10 +14,10 @@ import withPolicy, {policyDefaultProps, policyPropTypes} from './withPolicy'; import * as Policy from '../../libs/actions/Policy'; const propTypes = { - // List of available currencies + /** Constant, list of available currencies */ currencyList: PropTypes.objectOf( PropTypes.shape({ - // Symbol for the currency + /** Symbol of the currency */ symbol: PropTypes.string, }), ), @@ -88,8 +88,8 @@ function WorkspaceSettingsCurrencyPage({currencyList, policy}) { onChangeText={setSearchText} onSelectRow={onSelectCurrency} headerMessage={headerMessage} - showScrollIndicator initiallyFocusedOptionKey={initiallyFocusedOptionKey} + showScrollIndicator /> ); diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index 00dcb99a92dc..d680d6f38625 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -27,10 +27,10 @@ import MenuItemWithTopDescription from '../../components/MenuItemWithTopDescript import Text from '../../components/Text'; const propTypes = { - // The currency list constant object from Onyx + /** Constant, list of available currencies */ currencyList: PropTypes.objectOf( PropTypes.shape({ - // Symbol for the currency + /** Symbol of the currency */ symbol: PropTypes.string, }), ), From 2f2286d0573f280deca2ccee06d2c7612a5fd684 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Wed, 20 Sep 2023 15:28:09 +0100 Subject: [PATCH 019/134] chore: address pr comments --- .../WorkspaceSettingsCurrencyPage.js | 56 +++++++++---------- src/pages/workspace/WorkspaceSettingsPage.js | 6 +- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js index aa5e1701f8ce..649943cdbf56 100644 --- a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js +++ b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js @@ -18,7 +18,7 @@ const propTypes = { currencyList: PropTypes.objectOf( PropTypes.shape({ /** Symbol of the currency */ - symbol: PropTypes.string, + symbol: PropTypes.string.isRequired, }), ), ...policyPropTypes, @@ -29,45 +29,41 @@ const defaultProps = { ...policyDefaultProps, }; +const getDisplayText = (currencyCode, currencySymbol) => `${currencyCode} - ${currencySymbol}`; + function WorkspaceSettingsCurrencyPage({currencyList, policy}) { const {translate} = useLocalize(); const [searchText, setSearchText] = useState(''); + const trimmedText = searchText.trim().toLowerCase(); + const currencyListKeys = _.keys(currencyList); - const getDisplayText = useCallback((currencyCode, currencySymbol) => `${currencyCode} - ${currencySymbol}`, []); - - const {sections, initiallyFocusedOptionKey} = useMemo(() => { - const trimmedText = searchText.trim().toLowerCase(); - const currencyListKeys = _.keys(currencyList); - - const filteredItems = _.filter(currencyListKeys, (currencyCode) => { - const currency = currencyList[currencyCode]; - return getDisplayText(currencyCode, currency.symbol).toLowerCase().includes(trimmedText); - }); + const filteredItems = _.filter(currencyListKeys, (currencyCode) => { + const currency = currencyList[currencyCode]; + return getDisplayText(currencyCode, currency.symbol).toLowerCase().includes(trimmedText); + }); - let selectedCurrencyCode; + let initiallyFocusedOptionKey; - const currencyItems = _.map(filteredItems, (currencyCode) => { - const currency = currencyList[currencyCode]; - const isSelected = policy.outputCurrency === currencyCode; + const currencyItems = _.map(filteredItems, (currencyCode) => { + const currency = currencyList[currencyCode]; + const isSelected = policy.outputCurrency === currencyCode; - if (isSelected) { - selectedCurrencyCode = currencyCode; - } - - return { - text: getDisplayText(currencyCode, currency.symbol), - keyForList: currencyCode, - isSelected, - }; - }); + if (isSelected) { + initiallyFocusedOptionKey = currencyCode; + } return { - sections: [{data: currencyItems, indexOffset: 0}], - initiallyFocusedOptionKey: selectedCurrencyCode, + text: getDisplayText(currencyCode, currency.symbol), + keyForList: currencyCode, + isSelected, }; - }, [getDisplayText, currencyList, policy.outputCurrency, searchText]); + }); + + const sections = [{data: currencyItems, indexOffset: 0}]; + + const headerMessage = searchText.trim() && !sections[0].data.length ? translate('common.noResultsFound') : ''; - const headerMessage = Boolean(searchText.trim()) && !sections[0].data.length ? translate('common.noResultsFound') : ''; + const onBackButtonPress = useCallback(() => Navigation.goBack(ROUTES.getWorkspaceSettingsRoute(policy.id)), [policy.id]); const onSelectCurrency = (item) => { Policy.updateGeneralSettings(policy.id, policy.name, item.keyForList); @@ -78,7 +74,7 @@ function WorkspaceSettingsCurrencyPage({currencyList, policy}) { Navigation.goBack(ROUTES.getWorkspaceSettingsRoute(policy.id))} + onBackButtonPress={onBackButtonPress} /> { @@ -87,7 +87,7 @@ function WorkspaceSettingsPage(props) {
Date: Wed, 20 Sep 2023 15:46:56 +0100 Subject: [PATCH 020/134] chore: cleanup --- src/pages/workspace/WorkspaceSettingsCurrencyPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js index 649943cdbf56..63457f491951 100644 --- a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js +++ b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js @@ -1,4 +1,4 @@ -import React, {useState, useMemo, useCallback} from 'react'; +import React, {useState, useCallback} from 'react'; import _ from 'underscore'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; From 55392425d06d0b9edcc2fc61e7878d9a0705a2af Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Wed, 20 Sep 2023 18:16:59 +0100 Subject: [PATCH 021/134] chore: address pr comments --- .../WorkspaceSettingsCurrencyPage.js | 2 +- src/pages/workspace/WorkspaceSettingsPage.js | 77 +++++++++++-------- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js index 63457f491951..a7b3739edfb2 100644 --- a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js +++ b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js @@ -61,7 +61,7 @@ function WorkspaceSettingsCurrencyPage({currencyList, policy}) { const sections = [{data: currencyItems, indexOffset: 0}]; - const headerMessage = searchText.trim() && !sections[0].data.length ? translate('common.noResultsFound') : ''; + const headerMessage = searchText.trim() && !currencyItems.length ? translate('common.noResultsFound') : ''; const onBackButtonPress = useCallback(() => Navigation.goBack(ROUTES.getWorkspaceSettingsRoute(policy.id)), [policy.id]); diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index d6dc5150d0cb..c472bfbfd7a6 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -4,7 +4,6 @@ import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import lodashGet from 'lodash/get'; import ONYXKEYS from '../../ONYXKEYS'; -import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import styles from '../../styles/styles'; import compose from '../../libs/compose'; import * as Policy from '../../libs/actions/Policy'; @@ -25,6 +24,7 @@ import ROUTES from '../../ROUTES'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions'; import MenuItemWithTopDescription from '../../components/MenuItemWithTopDescription'; import Text from '../../components/Text'; +import useLocalize from '../../hooks/useLocalize'; const propTypes = { /** Constant, list of available currencies */ @@ -34,8 +34,17 @@ const propTypes = { symbol: PropTypes.string.isRequired, }), ), + + /** The route object passed to this page from the navigator */ + route: PropTypes.shape({ + /** Each parameter passed via the URL */ + params: PropTypes.shape({ + /** The policyID that is being configured */ + policyID: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + ...policyPropTypes, - ...withLocalizePropTypes, ...windowDimensionsPropTypes, }; @@ -44,20 +53,21 @@ const defaultProps = { ...policyDefaultProps, }; -function WorkspaceSettingsPage(props) { - const formattedCurrency = props.policy ? `${props.policy.outputCurrency} - ${props.currencyList[props.policy.outputCurrency].symbol}` : ''; +function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { + const {translate} = useLocalize(); + const formattedCurrency = policy ? `${policy.outputCurrency} - ${currencyList[policy.outputCurrency].symbol}` : ''; const submit = useCallback( (values) => { - if (props.policy.isPolicyUpdating) { + if (policy.isPolicyUpdating) { return; } - Policy.updateGeneralSettings(props.policy.id, values.name.trim(), props.policy.outputCurrency); + Policy.updateGeneralSettings(policy.id, values.name.trim(), policy.outputCurrency); Keyboard.dismiss(); - Navigation.goBack(ROUTES.getWorkspaceInitialRoute(props.policy.id)); + Navigation.goBack(ROUTES.getWorkspaceInitialRoute(policy.id)); }, - [props.policy.id, props.policy.isPolicyUpdating, props.policy.outputCurrency], + [policy.id, policy.isPolicyUpdating, policy.outputCurrency], ); const validate = useCallback((values) => { @@ -75,18 +85,20 @@ function WorkspaceSettingsPage(props) { return errors; }, []); - const policyName = lodashGet(props.policy, 'name', ''); + const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.getWorkspaceSettingsCurrencyRoute(policy.id)), [policy.id]); + + const policyName = lodashGet(policy, 'name', ''); return ( {(hasVBA) => ( ( Policy.updateWorkspaceAvatar(lodashGet(props.policy, 'id', ''), file)} - onImageRemoved={() => Policy.deleteWorkspaceAvatar(lodashGet(props.policy, 'id', ''))} + isUsingDefaultAvatar={!lodashGet(policy, 'avatar', null)} + onImageSelected={(file) => Policy.updateWorkspaceAvatar(lodashGet(policy, 'id', ''), file)} + onImageRemoved={() => Policy.deleteWorkspaceAvatar(lodashGet(policy, 'id', ''))} editorMaskImage={Expensicons.ImageCropSquareMask} - pendingAction={lodashGet(props.policy, 'pendingFields.avatar', null)} - errors={lodashGet(props.policy, 'errorFields.avatar', null)} - onErrorClose={() => Policy.clearAvatarErrors(props.policy.id)} - previewSource={UserUtils.getFullSizeAvatar(props.policy.avatar, '')} - headerTitle={props.translate('workspace.common.workspaceAvatar')} - originalFileName={props.policy.originalFileName} + pendingAction={lodashGet(policy, 'pendingFields.avatar', null)} + errors={lodashGet(policy, 'errorFields.avatar', null)} + onErrorClose={() => Policy.clearAvatarErrors(policy.id)} + previewSource={UserUtils.getFullSizeAvatar(policy.avatar, '')} + headerTitle={translate('workspace.common.workspaceAvatar')} + originalFileName={policy.originalFileName} /> - + Navigation.navigate(ROUTES.getWorkspaceSettingsCurrencyRoute(props.policy.id))} + onPress={onPressCurrency} /> - {hasVBA ? props.translate('workspace.editor.currencyInputDisabledText') : props.translate('workspace.editor.currencyInputHelpText')} + {hasVBA ? translate('workspace.editor.currencyInputDisabledText') : translate('workspace.editor.currencyInputHelpText')} @@ -164,6 +176,5 @@ export default compose( withOnyx({ currencyList: {key: ONYXKEYS.CURRENCY_LIST}, }), - withLocalize, withNetwork(), )(WorkspaceSettingsPage); From cf3979a2e6bd9f848a862a6cfb932513a7f45209 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Thu, 21 Sep 2023 18:30:52 +0800 Subject: [PATCH 022/134] Fix up logic --- src/components/AttachmentModal.js | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 68cee61bbc9b..ebfcf5f0f3a3 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -1,7 +1,6 @@ import React, {useState, useCallback, useRef} from 'react'; import PropTypes from 'prop-types'; import {View, Animated, Keyboard} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import lodashExtend from 'lodash/extend'; @@ -32,7 +31,7 @@ import Navigation from '../libs/Navigation/Navigation'; import ROUTES from '../ROUTES'; import useNativeDriver from '../libs/useNativeDriver'; import Receipt from '../libs/actions/Receipt'; -import ONYXKEYS from '../ONYXKEYS'; +import * as ReportActionsUtils from '../libs/ReportActionsUtils'; /** * Modal render prop component that exposes modal launching triggers that can be used @@ -82,13 +81,6 @@ const propTypes = { /** Denotes whether it is a workspace avatar or not */ isWorkspaceAvatar: PropTypes.bool, - - /* Onyx Props */ - /** The parent report */ - parentReport: reportPropTypes, - - /** The report action this report is tied to from the parent report */ - parentReportAction: PropTypes.shape(reportActionPropTypes), }; const defaultProps = { @@ -388,7 +380,8 @@ function AttachmentModal(props) { icon: Expensicons.Trashcan, text: props.translate('receipt.deleteReceipt'), onSelected: () => { - const transactionID = lodashGet(props.parentReportAction, 'originalMessage.IOUTransactionID', ''); + const parentReportAction = ReportActionsUtils.getReportAction(props.report.parentReportID, props.report.parentReportActionID); + const transactionID = lodashGet(parentReportAction, 'originalMessage.IOUTransactionID', ''); Receipt.detachReceipt(transactionID, props.report.reportID) }, }, @@ -464,14 +457,4 @@ AttachmentModal.displayName = 'AttachmentModal'; export default compose( withWindowDimensions, withLocalize, - withOnyx({ - parentReport: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, - }, - parentReportAction: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${(report.parentReportID, report.parentReportActionID)}`, - selector: (reportActions, props) => props && props.parentReport && reportActions && reportActions[props.parentReport.parentReportActionID], - canEvict: false, - }, - }), )(AttachmentModal); From 64a4f68e5f9ab3ebe098d2a1d9a1f1657cc8c9f4 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Thu, 21 Sep 2023 18:35:49 +0800 Subject: [PATCH 023/134] revert a few old changes --- src/components/AttachmentModal.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index ebfcf5f0f3a3..4fbe0d49d7b8 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -97,8 +97,6 @@ const defaultProps = { onModalHide: () => {}, onCarouselAttachmentChange: () => {}, isWorkspaceAvatar: false, - parentReport: {}, - parentReportAction: {}, }; function AttachmentModal(props) { @@ -454,7 +452,4 @@ function AttachmentModal(props) { AttachmentModal.propTypes = propTypes; AttachmentModal.defaultProps = defaultProps; AttachmentModal.displayName = 'AttachmentModal'; -export default compose( - withWindowDimensions, - withLocalize, -)(AttachmentModal); +export default compose(withWindowDimensions, withLocalize)(AttachmentModal); From 43140ea100b2c7737db7fc2f487913a2fa37d3b6 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Thu, 21 Sep 2023 12:07:58 +0100 Subject: [PATCH 024/134] chore: fix submit button style --- src/components/Form.js | 7 ++++++- src/pages/workspace/WorkspaceSettingsPage.js | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/Form.js b/src/components/Form.js index ef6c3ea10474..be73f448548b 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -76,6 +76,10 @@ const propTypes = { /** Container styles */ style: stylePropTypes, + /** Submit button container styles */ + // eslint-disable-next-line react/forbid-prop-types + submitButtonStyles: PropTypes.arrayOf(PropTypes.object), + /** Custom content to display in the footer after submit button */ footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), @@ -98,6 +102,7 @@ const defaultProps = { shouldValidateOnBlur: true, footerContent: null, style: [], + submitButtonStyles: [], validate: () => ({}), }; @@ -447,7 +452,7 @@ function Form(props) { focusInput.focus(); } }} - containerStyles={[styles.mh0, styles.mt5, styles.flex1]} + containerStyles={[styles.mh0, styles.mt5, styles.flex1, ...props.submitButtonStyles]} enabledWhenOffline={props.enabledWhenOffline} isSubmitActionDangerous={props.isSubmitActionDangerous} disablePressOnEnter diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index c472bfbfd7a6..2ff1491c7ed9 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -100,6 +100,7 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { formID={ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM} submitButtonText={translate('workspace.editor.save')} style={styles.flexGrow1} + submitButtonStyles={[styles.mh5]} scrollContextEnabled validate={validate} onSubmit={submit} From 86b650f4aabc0223a436b4d760909a9636f32980 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Thu, 21 Sep 2023 12:14:07 +0100 Subject: [PATCH 025/134] chore: cleanup --- src/components/Form.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Form.js b/src/components/Form.js index be73f448548b..7d62d936c159 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -477,6 +477,7 @@ function Form(props) { props.isSubmitActionDangerous, props.isSubmitButtonVisible, props.submitButtonText, + props.submitButtonStyles, ], ); From b319defb7906e9e86cdcbd1bd458566af53b52ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Fri, 22 Sep 2023 17:33:58 +0200 Subject: [PATCH 026/134] initial commit --- package-lock.json | 1 + package.json | 7 +- src/libs/fileDownload/FileUtils.js | 52 +++++- .../ReceiptSelector/NavigationAwareCamera.js | 65 ++----- .../NavigationAwareCamera.native.js | 77 ++++++++ src/pages/iou/ReceiptSelector/index.js | 166 +++++++++++++++++- 6 files changed, 307 insertions(+), 61 deletions(-) create mode 100644 src/pages/iou/ReceiptSelector/NavigationAwareCamera.native.js diff --git a/package-lock.json b/package-lock.json index ff0500eb385b..85a2d05fd26a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,6 +114,7 @@ "react-pdf": "^6.2.2", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", + "react-webcam": "^7.1.1", "react-window": "^1.8.9", "save": "^2.4.0", "semver": "^7.5.2", diff --git a/package.json b/package.json index 44b936c8c588..6a9f852ca3ec 100644 --- a/package.json +++ b/package.json @@ -61,9 +61,9 @@ "@formatjs/intl-pluralrules": "^5.2.2", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", - "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", "@kie/act-js": "^2.0.1", "@kie/mock-github": "^1.0.0", + "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", "@onfido/react-native-sdk": "7.4.0", "@react-native-async-storage/async-storage": "^1.17.10", "@react-native-camera-roll/camera-roll": "5.4.0", @@ -81,9 +81,9 @@ "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.0.11", + "@types/node": "^18.14.0", "@ua/react-native-airship": "^15.2.6", "awesome-phonenumber": "^5.4.0", - "@types/node": "^18.14.0", "babel-plugin-transform-remove-console": "^6.9.4", "babel-polyfill": "^6.26.0", "canvas-size": "^1.2.6", @@ -111,8 +111,8 @@ "react-collapse": "^5.1.0", "react-content-loader": "^6.1.0", "react-dom": "18.1.0", - "react-map-gl": "^7.1.3", "react-error-boundary": "^4.0.11", + "react-map-gl": "^7.1.3", "react-native": "0.72.4", "react-native-blob-util": "^0.17.3", "react-native-collapsible": "^1.6.0", @@ -156,6 +156,7 @@ "react-pdf": "^6.2.2", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", + "react-webcam": "^7.1.1", "react-window": "^1.8.9", "save": "^2.4.0", "semver": "^7.5.2", diff --git a/src/libs/fileDownload/FileUtils.js b/src/libs/fileDownload/FileUtils.js index cee2b8877ef6..ca784ff95b4e 100644 --- a/src/libs/fileDownload/FileUtils.js +++ b/src/libs/fileDownload/FileUtils.js @@ -170,4 +170,54 @@ const readFileAsync = (path, fileName) => }); }); -export {showGeneralErrorAlert, showSuccessAlert, showPermissionErrorAlert, splitExtensionFromFileName, getAttachmentName, getFileType, cleanFileName, appendTimeToFileName, readFileAsync}; +/** + * Converts a base64 encoded image string to a File instance. + * Adds a `uri` property to the File instance for accessing the blob as a URI. + * + * @param {string} base64 - The base64 encoded image string. + * @param {string} filename - Desired filename for the File instance. + * @returns {File} The File instance created from the base64 string with an additional `uri` property. + * + * @example + * const base64Image = "data:image/png;base64,..."; // your base64 encoded image + * const imageFile = base64ToFile(base64Image, "example.png"); + * console.log(imageFile.uri); // Blob URI + */ +function base64ToFile(base64, filename) { + // Decode the base64 string + const byteString = atob(base64.split(',')[1]); + + // Get the mime type from the base64 string + const mimeString = base64.split(',')[0].split(':')[1].split(';')[0]; + + // Convert byte string to Uint8Array + const arrayBuffer = new ArrayBuffer(byteString.length); + const uint8Array = new Uint8Array(arrayBuffer); + for (let i = 0; i < byteString.length; i++) { + uint8Array[i] = byteString.charCodeAt(i); + } + + // Create a blob from the Uint8Array + const blob = new Blob([uint8Array], {type: mimeString}); + + // Create a File instance from the Blob + const file = new File([blob], filename, {type: mimeString, lastModified: Date.now()}); + + // Add a uri property to the File instance for accessing the blob as a URI + file.uri = URL.createObjectURL(blob); + + return file; +} + +export { + showGeneralErrorAlert, + showSuccessAlert, + showPermissionErrorAlert, + splitExtensionFromFileName, + getAttachmentName, + getFileType, + cleanFileName, + appendTimeToFileName, + readFileAsync, + base64ToFile, +}; diff --git a/src/pages/iou/ReceiptSelector/NavigationAwareCamera.js b/src/pages/iou/ReceiptSelector/NavigationAwareCamera.js index 8fe00c7a65b3..95ed1f6c046b 100644 --- a/src/pages/iou/ReceiptSelector/NavigationAwareCamera.js +++ b/src/pages/iou/ReceiptSelector/NavigationAwareCamera.js @@ -1,7 +1,6 @@ import React, {useEffect, useState} from 'react'; -import {Camera} from 'react-native-vision-camera'; -import {useTabAnimation} from '@react-navigation/material-top-tabs'; -import {useNavigation} from '@react-navigation/native'; +import Webcam from 'react-webcam'; +import {useIsFocused} from '@react-navigation/native'; import PropTypes from 'prop-types'; import refPropTypes from '../../../components/refPropTypes'; @@ -14,53 +13,19 @@ const propTypes = { }; // Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused. -function NavigationAwareCamera({cameraTabIndex, forwardedRef, ...props}) { - // Get navigation to get initial isFocused value (only needed once during init!) - const navigation = useNavigation(); - const [isCameraActive, setIsCameraActive] = useState(navigation.isFocused()); - - // Get the animation value from the tab navigator. Its a value between 0 and the - // number of pages we render in the tab navigator. When we even just slightly start to scroll to the camera page, - // (value is e.g. 0.001 on animation start) we want to activate the camera, so its as fast as possible active. - const tabPositionAnimation = useTabAnimation(); - - useEffect(() => { - const listenerId = tabPositionAnimation.addListener(({value}) => { - // Activate camera as soon the index is animating towards the `cameraTabIndex` - setIsCameraActive(value > cameraTabIndex - 1 && value < cameraTabIndex + 1); - }); - - return () => { - tabPositionAnimation.removeListener(listenerId); - }; - }, [cameraTabIndex, tabPositionAnimation]); - - // Note: The useEffect can be removed once VisionCamera V3 is used. - // Its only needed for android, because there is a native cameraX android bug. With out this flow would break the camera: - // 1. Open camera tab - // 2. Take a picture - // 3. Go back from the opened screen - // 4. The camera is not working anymore - useEffect(() => { - const removeBlurListener = navigation.addListener('blur', () => { - setIsCameraActive(false); - }); - const removeFocusListener = navigation.addListener('focus', () => { - setIsCameraActive(true); - }); - - return () => { - removeBlurListener(); - removeFocusListener(); - }; - }, [navigation]); +function NavigationAwareCamera({cameraTabIndex, ...props}, ref) { + const isCameraActive = useIsFocused(); + if (!isCameraActive) { + return null; + } return ( - ); } @@ -68,10 +33,4 @@ function NavigationAwareCamera({cameraTabIndex, forwardedRef, ...props}) { NavigationAwareCamera.propTypes = propTypes; NavigationAwareCamera.displayName = 'NavigationAwareCamera'; -export default React.forwardRef((props, ref) => ( - -)); +export default React.forwardRef(NavigationAwareCamera); diff --git a/src/pages/iou/ReceiptSelector/NavigationAwareCamera.native.js b/src/pages/iou/ReceiptSelector/NavigationAwareCamera.native.js new file mode 100644 index 000000000000..8fe00c7a65b3 --- /dev/null +++ b/src/pages/iou/ReceiptSelector/NavigationAwareCamera.native.js @@ -0,0 +1,77 @@ +import React, {useEffect, useState} from 'react'; +import {Camera} from 'react-native-vision-camera'; +import {useTabAnimation} from '@react-navigation/material-top-tabs'; +import {useNavigation} from '@react-navigation/native'; +import PropTypes from 'prop-types'; +import refPropTypes from '../../../components/refPropTypes'; + +const propTypes = { + /* The index of the tab that contains this camera */ + cameraTabIndex: PropTypes.number.isRequired, + + /* Forwarded ref */ + forwardedRef: refPropTypes.isRequired, +}; + +// Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused. +function NavigationAwareCamera({cameraTabIndex, forwardedRef, ...props}) { + // Get navigation to get initial isFocused value (only needed once during init!) + const navigation = useNavigation(); + const [isCameraActive, setIsCameraActive] = useState(navigation.isFocused()); + + // Get the animation value from the tab navigator. Its a value between 0 and the + // number of pages we render in the tab navigator. When we even just slightly start to scroll to the camera page, + // (value is e.g. 0.001 on animation start) we want to activate the camera, so its as fast as possible active. + const tabPositionAnimation = useTabAnimation(); + + useEffect(() => { + const listenerId = tabPositionAnimation.addListener(({value}) => { + // Activate camera as soon the index is animating towards the `cameraTabIndex` + setIsCameraActive(value > cameraTabIndex - 1 && value < cameraTabIndex + 1); + }); + + return () => { + tabPositionAnimation.removeListener(listenerId); + }; + }, [cameraTabIndex, tabPositionAnimation]); + + // Note: The useEffect can be removed once VisionCamera V3 is used. + // Its only needed for android, because there is a native cameraX android bug. With out this flow would break the camera: + // 1. Open camera tab + // 2. Take a picture + // 3. Go back from the opened screen + // 4. The camera is not working anymore + useEffect(() => { + const removeBlurListener = navigation.addListener('blur', () => { + setIsCameraActive(false); + }); + const removeFocusListener = navigation.addListener('focus', () => { + setIsCameraActive(true); + }); + + return () => { + removeBlurListener(); + removeFocusListener(); + }; + }, [navigation]); + + return ( + + ); +} + +NavigationAwareCamera.propTypes = propTypes; +NavigationAwareCamera.displayName = 'NavigationAwareCamera'; + +export default React.forwardRef((props, ref) => ( + +)); diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index eb6e2328afd2..246fb52c455f 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -1,5 +1,5 @@ import {View, Text, PixelRatio} from 'react-native'; -import React, {useContext, useState} from 'react'; +import React, {useCallback, useContext, useRef, useState} from 'react'; import lodashGet from 'lodash/get'; import _ from 'underscore'; import PropTypes from 'prop-types'; @@ -22,6 +22,12 @@ import {DragAndDropContext} from '../../../components/DragAndDrop/Provider'; import {iouPropTypes, iouDefaultProps} from '../propTypes'; import * as FileUtils from '../../../libs/fileDownload/FileUtils'; import Navigation from '../../../libs/Navigation/Navigation'; +import * as Expensicons from '../../../components/Icon/Expensicons'; +import Icon from '../../../components/Icon'; +import themeColors from '../../../styles/themes/default'; +import Shutter from '../../../../assets/images/shutter.svg'; +import NavigationAwareCamera from './NavigationAwareCamera'; +import * as Browser from '../../../libs/Browser'; const propTypes = { /** The report on which the request is initiated on */ @@ -62,6 +68,8 @@ function ReceiptSelector(props) { const {isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); const {isDraggingOver} = useContext(DragAndDropContext); + const [cameraPermissionState, setCameraPermissionState] = useState('prompt'); + const cameraRef = useRef(null); const hideReciptModal = () => { setIsAttachmentInvalid(false); @@ -122,9 +130,93 @@ function ReceiptSelector(props) { IOU.navigateToNextPage(iou, iouType, reportID, report); }; + const capturePhoto = useCallback(() => { + if (!cameraRef.current.getScreenshot) { + return; + } + const imageB64 = cameraRef.current.getScreenshot(); + const filename = `receipt_${Date.now()}.png`; + const imageFile = FileUtils.base64ToFile(imageB64, filename); + const filePath = URL.createObjectURL(imageFile); + IOU.setMoneyRequestReceipt(filePath, imageFile.name); + + if (props.transactionID) { + IOU.replaceReceipt(props.transactionID, imageFile, filePath); + Navigation.dismissModal(); + return; + } + + IOU.navigateToNextPage(props.iou, iouType, reportID, props.report); + }, [cameraRef, props.iou, props.report, reportID, iouType, props.transactionID]); + return ( - - {!isDraggingOver ? ( + + {!isDraggingOver && Browser.isMobile() && ( + <> + + {(cameraPermissionState === 'prompt' || cameraPermissionState === 'unknown') && Give permissions} + {cameraPermissionState === 'denied' && Turn on permissions or else} + setCameraPermissionState('granted')} + onUserMediaError={() => setCameraPermissionState('denied')} + style={{objectFit: 'cover', width: '100%', height: '100%'}} + ref={cameraRef} + screenshotFormat="image/png" + /> + + + + + {({openPicker}) => ( + { + openPicker({ + onPicked: (file) => { + setReceiptAndNavigate(file, props.iou, props.report); + }, + }); + }} + > + + + )} + + + + + alert('set flash')} + > + + + + + )} + {!isDraggingOver && !Browser.isMobile() && ( <> { @@ -168,7 +260,7 @@ function ReceiptSelector(props) { )} - ) : null} + )} { const file = lodashGet(e, ['dataTransfer', 'files', 0]); @@ -187,6 +279,72 @@ function ReceiptSelector(props) { /> ); + + // return ( + // + // {!isDraggingOver ? ( + // <> + // { + // setReceiptImageTopPosition(PixelRatio.roundToNearestPixel(nativeEvent.layout.top)); + // }} + // > + // + // + // {translate('receipt.upload')} + // + // {isSmallScreenWidth ? translate('receipt.chooseReceipt') : translate('receipt.dragReceiptBeforeEmail')} + // + // {isSmallScreenWidth ? null : translate('receipt.dragReceiptAfterEmail')} + // + // + // {({openPicker}) => ( + // + //