diff --git a/android/app/build.gradle b/android/app/build.gradle
index 04d66b326184..f957ea7c5854 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -90,8 +90,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001037102
- versionName "1.3.71-2"
+ versionCode 1001037105
+ versionName "1.3.71-5"
}
flavorDimensions "default"
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index e0d3b2454561..07e1cfae80ac 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.3.71.2
+ 1.3.71.5
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index a5de0b39bdff..49d12cf93594 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.3.71.2
+ 1.3.71.5
diff --git a/package-lock.json b/package-lock.json
index 4b42a8a292e3..68ea17fc20b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.71-2",
+ "version": "1.3.71-5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.71-2",
+ "version": "1.3.71-5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -69,6 +69,7 @@
"react-collapse": "^5.1.0",
"react-content-loader": "^6.1.0",
"react-dom": "18.1.0",
+ "react-error-boundary": "^4.0.11",
"react-map-gl": "^7.1.3",
"react-native": "0.72.3",
"react-native-blob-util": "^0.17.3",
@@ -40187,6 +40188,17 @@
"react": "^18.1.0"
}
},
+ "node_modules/react-error-boundary": {
+ "version": "4.0.11",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz",
+ "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
"node_modules/react-freeze": {
"version": "1.0.3",
"license": "MIT",
@@ -75742,6 +75754,14 @@
"scheduler": "^0.22.0"
}
},
+ "react-error-boundary": {
+ "version": "4.0.11",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz",
+ "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==",
+ "requires": {
+ "@babel/runtime": "^7.12.5"
+ }
+ },
"react-freeze": {
"version": "1.0.3",
"requires": {}
diff --git a/package.json b/package.json
index 6b921644470b..72574455719b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.71-2",
+ "version": "1.3.71-5",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -112,6 +112,7 @@
"react-content-loader": "^6.1.0",
"react-dom": "18.1.0",
"react-map-gl": "^7.1.3",
+ "react-error-boundary": "^4.0.11",
"react-native": "0.72.3",
"react-native-blob-util": "^0.17.3",
"react-native-collapsible": "^1.6.0",
diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js
index aa50d6db574b..3a43ede5a8f4 100644
--- a/src/components/AvatarWithImagePicker.js
+++ b/src/components/AvatarWithImagePicker.js
@@ -23,6 +23,8 @@ import getImageResolution from '../libs/fileDownload/getImageResolution';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import DotIndicatorMessage from './DotIndicatorMessage';
import * as Browser from '../libs/Browser';
+import withNavigationFocus, {withNavigationFocusPropTypes} from './withNavigationFocus';
+import compose from '../libs/compose';
const propTypes = {
/** Avatar source to display */
@@ -80,6 +82,7 @@ const propTypes = {
errors: PropTypes.object,
...withLocalizePropTypes,
+ ...withNavigationFocusPropTypes,
};
const defaultProps = {
@@ -129,6 +132,9 @@ class AvatarWithImagePicker extends React.Component {
}
componentDidUpdate(prevProps) {
+ if (!prevProps.isFocused && this.props.isFocused) {
+ this.setError(null, {});
+ }
if (!prevProps.isUploading && this.props.isUploading) {
this.animation.start();
} else if (prevProps.isUploading && !this.props.isUploading) {
@@ -350,4 +356,4 @@ class AvatarWithImagePicker extends React.Component {
AvatarWithImagePicker.propTypes = propTypes;
AvatarWithImagePicker.defaultProps = defaultProps;
-export default withLocalize(AvatarWithImagePicker);
+export default compose(withLocalize, withNavigationFocus)(AvatarWithImagePicker);
diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.js b/src/components/ErrorBoundary/BaseErrorBoundary.js
index e479b04f7ade..d626442e81dd 100644
--- a/src/components/ErrorBoundary/BaseErrorBoundary.js
+++ b/src/components/ErrorBoundary/BaseErrorBoundary.js
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
+import {ErrorBoundary} from 'react-error-boundary';
import BootSplash from '../../libs/BootSplash';
import GenericErrorPage from '../../pages/ErrorPage/GenericErrorPage';
@@ -22,40 +23,27 @@ const defaultProps = {
* This component captures an error in the child component tree and logs it to the server
* It can be used to wrap the entire app as well as to wrap specific parts for more granularity
* @see {@link https://reactjs.org/docs/error-boundaries.html#where-to-place-error-boundaries}
+ * @return {React.Component}
*/
-class BaseErrorBoundary extends React.Component {
- constructor(props) {
- super(props);
- this.state = {hasError: false};
- this.clearError = this.clearError.bind(this);
- }
-
- static getDerivedStateFromError() {
- // Update state so the next render will show the fallback UI.
- return {hasError: true};
- }
-
- componentDidCatch(error, errorInfo) {
- this.props.logError(this.props.errorMessage, error, JSON.stringify(errorInfo));
-
+function BaseErrorBoundary({logError, errorMessage, children}) {
+ const catchError = (error, errorInfo) => {
+ logError(errorMessage, error, JSON.stringify(errorInfo));
// We hide the splash screen since the error might happened during app init
BootSplash.hide();
- }
-
- clearError() {
- this.setState({hasError: false});
- }
-
- render() {
- if (this.state.hasError) {
- return ;
- }
-
- return this.props.children;
- }
+ };
+
+ return (
+ }
+ onError={catchError}
+ >
+ {children}
+
+ );
}
BaseErrorBoundary.propTypes = propTypes;
BaseErrorBoundary.defaultProps = defaultProps;
+BaseErrorBoundary.displayName = 'BaseErrorBoundary';
export default BaseErrorBoundary;
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index cba2359eb257..6448a62639dc 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -5,6 +5,7 @@ import {format} from 'date-fns';
import _ from 'underscore';
import {View} from 'react-native';
import lodashGet from 'lodash/get';
+import Text from './Text';
import styles from '../styles/styles';
import * as ReportUtils from '../libs/ReportUtils';
import * as OptionsListUtils from '../libs/OptionsListUtils';
@@ -30,6 +31,7 @@ import Image from './Image';
import useLocalize from '../hooks/useLocalize';
import * as ReceiptUtils from '../libs/ReceiptUtils';
import categoryPropTypes from './categoryPropTypes';
+import Switch from './Switch';
import tagPropTypes from './tagPropTypes';
import ConfirmedRoute from './ConfirmedRoute';
import transactionPropTypes from './transactionPropTypes';
@@ -531,6 +533,16 @@ function MoneyRequestConfirmationList(props) {
disabled={didConfirm || props.isReadOnly}
/>
)}
+ {canUseTags && !lodashGet(props.policy, 'disabledFields.defaultBillable', true) && (
+
+ {translate('common.billable')}
+
+
+ )}
>
)}
@@ -562,5 +574,8 @@ export default compose(
transaction: {
key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
},
+ policy: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ },
}),
)(MoneyRequestConfirmationList);
diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js
index ca4103624440..d0181e3d736a 100644
--- a/src/components/ReportActionItem/TaskPreview.js
+++ b/src/components/ReportActionItem/TaskPreview.js
@@ -72,6 +72,11 @@ function TaskPreview(props) {
const assigneeDisplayName = lodashGet(props.personalDetailsList, [taskAssigneeAccountID, 'displayName'], '');
const taskAssignee = assigneeLogin || assigneeDisplayName;
const htmlForTaskPreview = taskAssignee ? `@${taskAssignee} ${taskTitle}` : `${taskTitle}`;
+ const isDeletedParentAction = ReportUtils.isCanceledTaskReport(props.taskReport, props.action);
+
+ if (isDeletedParentAction) {
+ return ${props.translate('parentReportAction.deletedTask')}`} />;
+ }
return (
diff --git a/src/languages/en.ts b/src/languages/en.ts
index c31e39319a98..25c31ac194d9 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -244,6 +244,7 @@ export default {
showMore: 'Show more',
merchant: 'Merchant',
category: 'Category',
+ billable: 'Billable',
tag: 'Tag',
receipt: 'Receipt',
replace: 'Replace',
@@ -1524,7 +1525,7 @@ export default {
completed: 'Completed',
messages: {
completed: 'completed task',
- canceled: 'canceled task',
+ canceled: 'deleted task',
reopened: 'reopened task',
error: 'You do not have the permission to do the requested action.',
},
@@ -1696,6 +1697,7 @@ export default {
parentReportAction: {
deletedMessage: '[Deleted message]',
deletedRequest: '[Deleted request]',
+ deletedTask: '[Deleted task]',
hiddenMessage: '[Hidden message]',
},
threads: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index d83104ff85e0..0f4e5807cdd7 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -234,6 +234,7 @@ export default {
showMore: 'Mostrar más',
merchant: 'Comerciante',
category: 'Categoría',
+ billable: 'Facturable',
tag: 'Etiqueta',
receipt: 'Recibo',
replace: 'Sustituir',
@@ -1546,7 +1547,7 @@ export default {
completed: 'Completada',
messages: {
completed: 'tarea completada',
- canceled: 'tarea cancelada',
+ canceled: 'tarea eliminado',
reopened: 'tarea reabrir',
error: 'No tiene permiso para realizar la acción solicitada.',
},
@@ -2178,6 +2179,7 @@ export default {
parentReportAction: {
deletedMessage: '[Mensaje eliminado]',
deletedRequest: '[Pedido eliminado]',
+ deletedTask: '[Tarea eliminado]',
hiddenMessage: '[Mensaje oculto]',
},
threads: {
diff --git a/src/libs/LocalePhoneNumber.js b/src/libs/LocalePhoneNumber.ts
similarity index 82%
rename from src/libs/LocalePhoneNumber.js
rename to src/libs/LocalePhoneNumber.ts
index e5c7cbfa45ba..962040aee049 100644
--- a/src/libs/LocalePhoneNumber.js
+++ b/src/libs/LocalePhoneNumber.ts
@@ -3,20 +3,17 @@ import Str from 'expensify-common/lib/str';
import {parsePhoneNumber} from 'awesome-phonenumber';
import ONYXKEYS from '../ONYXKEYS';
-let countryCodeByIP;
+let countryCodeByIP: number;
Onyx.connect({
key: ONYXKEYS.COUNTRY_CODE,
- callback: (val) => (countryCodeByIP = val || 1),
+ callback: (val) => (countryCodeByIP = val ?? 1),
});
/**
* Returns a locally converted phone number for numbers from the same region
* and an internationally converted phone number with the country code for numbers from other regions
- *
- * @param {String} number
- * @returns {String}
*/
-function formatPhoneNumber(number) {
+function formatPhoneNumber(number: string): string {
if (!number) {
return '';
}
@@ -26,7 +23,7 @@ function formatPhoneNumber(number) {
// return the string untouched if it's not a phone number
if (!parsedPhoneNumber.valid) {
- if (parsedPhoneNumber.number && parsedPhoneNumber.number.international) {
+ if (parsedPhoneNumber.number?.international) {
return parsedPhoneNumber.number.international;
}
return numberWithoutSMSDomain;
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index 9396ea921b61..7916630d7b4e 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -187,23 +187,37 @@ function isTaskReport(report) {
}
/**
- * Checks if a report is an open task report.
+ * Checks if a task has been cancelled
+ * When a task is deleted, the parentReportAction is updated to have a isDeletedParentAction deleted flag
+ * This is because when you delete a task, we still allow you to chat on the report itself
+ * There's another situation where you don't have access to the parentReportAction (because it was created in a chat you don't have access to)
+ * In this case, we have added the key to the report itself
*
* @param {Object} report
+ * @param {Object} parentReportAction
* @returns {Boolean}
*/
-function isOpenTaskReport(report) {
- return isTaskReport(report) && report.stateNum === CONST.REPORT.STATE_NUM.OPEN && report.statusNum === CONST.REPORT.STATUS.OPEN;
+function isCanceledTaskReport(report = {}, parentReportAction = {}) {
+ if (!_.isEmpty(parentReportAction) && lodashGet(parentReportAction, ['message', 0, 'isDeletedParentAction'], false)) {
+ return true;
+ }
+
+ if (!_.isEmpty(report) && report.isDeletedParentAction) {
+ return true;
+ }
+
+ return false;
}
/**
- * Checks if the current user is assigned to the task report
+ * Checks if a report is an open task report.
*
* @param {Object} report
+ * @param {Object} parentReportAction - The parent report action of the report (Used to check if the task has been canceled)
* @returns {Boolean}
*/
-function isCanceledTaskReport(report) {
- return isTaskReport(report) && report.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report.statusNum === CONST.REPORT.STATUS.CLOSED;
+function isOpenTaskReport(report, parentReportAction = {}) {
+ return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report.stateNum === CONST.REPORT.STATE_NUM.OPEN && report.statusNum === CONST.REPORT.STATUS.OPEN;
}
/**
@@ -484,7 +498,7 @@ function shouldDisableDetailPage(report) {
*/
function isExpensifyOnlyParticipantInReport(report) {
const reportParticipants = _.without(lodashGet(report, 'participantAccountIDs', []), currentUserAccountID);
- return lodashGet(report, 'participantAccountIDs', []).length === 1 && _.some(reportParticipants, (accountID) => _.contains(CONST.EXPENSIFY_ACCOUNT_IDS, accountID));
+ return reportParticipants.length === 1 && _.some(reportParticipants, (accountID) => _.contains(CONST.EXPENSIFY_ACCOUNT_IDS, accountID));
}
/**
@@ -1610,8 +1624,8 @@ function getParentReport(report) {
*/
function getReportName(report, policy = undefined) {
let formattedName;
+ const parentReportAction = ReportActionsUtils.getParentReportAction(report);
if (isChatThread(report)) {
- const parentReportAction = ReportActionsUtils.getParentReportAction(report);
if (ReportActionsUtils.isTransactionThread(parentReportAction)) {
return getTransactionReportName(parentReportAction);
}
@@ -1629,6 +1643,11 @@ function getReportName(report, policy = undefined) {
}
return parentReportActionMessage || Localize.translateLocal('parentReportAction.deletedMessage');
}
+
+ if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) {
+ return Localize.translateLocal('parentReportAction.deletedTask');
+ }
+
if (isChatRoom(report) || isTaskReport(report)) {
formattedName = report.reportName;
}
diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js
index dccabd74772b..c7553a22cb56 100644
--- a/src/libs/TransactionUtils.js
+++ b/src/libs/TransactionUtils.js
@@ -34,6 +34,7 @@ Onyx.connect({
* @param {String} [filename]
* @param {String} [existingTransactionID] When creating a distance request, an empty transaction has already been created with a transactionID. In that case, the transaction here needs to have it's transactionID match what was already generated.
* @param {String} [category]
+ * @param {Boolean} [billable]
* @returns {Object}
*/
function buildOptimisticTransaction(
@@ -49,6 +50,7 @@ function buildOptimisticTransaction(
filename = '',
existingTransactionID = null,
category = '',
+ billable = false,
) {
// transactionIDs are random, positive, 64-bit numeric strings.
// Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID)
@@ -77,6 +79,7 @@ function buildOptimisticTransaction(
receipt,
filename,
category,
+ billable,
};
}
diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js
index 3cefcd00ed60..8bbcb58f71f4 100644
--- a/src/libs/actions/IOU.js
+++ b/src/libs/actions/IOU.js
@@ -98,6 +98,7 @@ function resetMoneyRequestInfo(id = '') {
receiptPath: '',
receiptSource: '',
transactionID: '',
+ billable: null,
});
}
@@ -341,6 +342,7 @@ function buildOnyxDataForMoneyRequest(
* @param {Object} [receipt]
* @param {String} [existingTransactionID]
* @param {String} [category]
+ * @param {Boolean} [billable]
* @returns {Object} data
* @returns {String} data.payerEmail
* @returns {Object} data.iouReport
@@ -368,6 +370,7 @@ function getMoneyRequestInformation(
receipt = undefined,
existingTransactionID = undefined,
category = undefined,
+ billable = undefined,
) {
const payerEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(participant.login);
const payerAccountID = Number(participant.accountID);
@@ -433,6 +436,7 @@ function getMoneyRequestInformation(
filename,
existingTransactionID,
category,
+ billable,
);
const uniquePolicyRecentlyUsedCategories = allRecentlyUsedCategories
@@ -600,8 +604,9 @@ function createDistanceRequest(report, participant, comment, created, transactio
* @param {String} comment
* @param {Object} [receipt]
* @param {String} [category]
+ * @param {Boolean} [billable]
*/
-function requestMoney(report, amount, currency, created, merchant, payeeEmail, payeeAccountID, participant, comment, receipt = undefined, category = undefined) {
+function requestMoney(report, amount, currency, created, merchant, payeeEmail, payeeAccountID, participant, comment, receipt = undefined, category = undefined, billable = undefined) {
// If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function
const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report);
const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report;
@@ -618,6 +623,7 @@ function requestMoney(report, amount, currency, created, merchant, payeeEmail, p
receipt,
undefined,
category,
+ billable,
);
API.write(
@@ -638,6 +644,7 @@ function requestMoney(report, amount, currency, created, merchant, payeeEmail, p
reportPreviewReportActionID: reportPreviewAction.reportActionID,
receipt,
category,
+ billable,
},
onyxData,
);
@@ -1948,6 +1955,13 @@ function resetMoneyRequestCategory() {
Onyx.merge(ONYXKEYS.IOU, {category: ''});
}
+/**
+ * @param {Boolean} billable
+ */
+function setMoneyRequestBillable(billable) {
+ Onyx.merge(ONYXKEYS.IOU, {billable});
+}
+
/**
* @param {Object[]} participants
*/
@@ -2032,6 +2046,7 @@ export {
setMoneyRequestMerchant,
setMoneyRequestCategory,
resetMoneyRequestCategory,
+ setMoneyRequestBillable,
setMoneyRequestParticipants,
setMoneyRequestReceipt,
createEmptyTransaction,
diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js
index 2d599053ea51..a5e8ac325217 100644
--- a/src/libs/actions/Task.js
+++ b/src/libs/actions/Task.js
@@ -755,26 +755,50 @@ function getShareDestination(reportID, reports, personalDetails) {
* @param {number} originalStatusNum
*/
function cancelTask(taskReportID, taskTitle, originalStateNum, originalStatusNum) {
- const message = `canceled task: ${taskTitle}`;
+ const message = `deleted task: ${taskTitle}`;
const optimisticCancelReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED, message);
const optimisticReportActionID = optimisticCancelReportAction.reportActionID;
+ const taskReport = ReportUtils.getReport(taskReportID);
+ const parentReportAction = ReportActionsUtils.getParentReportAction(taskReport);
+ const parentReport = ReportUtils.getParentReport(taskReport);
+
+ const optimisticReportActions = {
+ [parentReportAction.reportActionID]: {
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ previousMessage: parentReportAction.message,
+ message: [
+ {
+ translationKey: '',
+ type: 'COMMENT',
+ html: '',
+ text: '',
+ isEdited: true,
+ isDeletedParentAction: true,
+ },
+ ],
+ errors: null,
+ linkMetaData: [],
+ },
+ };
const optimisticData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`,
value: {
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
- statusNum: CONST.REPORT.STATUS.CLOSED,
+ lastVisibleActionCreated: optimisticCancelReportAction.created,
+ lastMessageText: message,
+ lastActorAccountID: optimisticCancelReportAction.actorAccountID,
+ updateReportInLHN: true,
+ isDeletedParentAction: true,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`,
value: {
- lastVisibleActionCreated: optimisticCancelReportAction.created,
- lastMessageText: message,
- lastActorAccountID: optimisticCancelReportAction.actorAccountID,
+ lastMessageText: ReportActionsUtils.getLastVisibleMessage(parentReport.reportID, {[parentReportAction.reportActionID]: null}).lastMessageText,
+ lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(parentReport.reportID, {[parentReportAction.reportActionID]: null}).created,
},
},
{
@@ -784,6 +808,11 @@ function cancelTask(taskReportID, taskTitle, originalStateNum, originalStatusNum
[optimisticReportActionID]: optimisticCancelReportAction,
},
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport.reportID}`,
+ value: optimisticReportActions,
+ },
];
const successData = [
diff --git a/src/pages/ErrorPage/GenericErrorPage.js b/src/pages/ErrorPage/GenericErrorPage.js
index 3ff3bc686419..02ab38a1ef20 100644
--- a/src/pages/ErrorPage/GenericErrorPage.js
+++ b/src/pages/ErrorPage/GenericErrorPage.js
@@ -1,6 +1,6 @@
import React from 'react';
-import PropTypes from 'prop-types';
import {View} from 'react-native';
+import {useErrorBoundary} from 'react-error-boundary';
import Icon from '../../components/Icon';
import defaultTheme from '../../styles/themes/default';
import * as Expensicons from '../../components/Icon/Expensicons';
@@ -19,12 +19,11 @@ import * as StyleUtils from '../../styles/StyleUtils';
const propTypes = {
...withLocalizePropTypes,
-
- /** Callback to call on refresh button click */
- onRefresh: PropTypes.func.isRequired,
};
-function GenericErrorPage(props) {
+function GenericErrorPage({translate}) {
+ const {resetBoundary} = useErrorBoundary();
+
return (
{({paddingBottom}) => (
@@ -40,12 +39,12 @@ function GenericErrorPage(props) {
/>
- {props.translate('genericErrorPage.title')}
+ {translate('genericErrorPage.title')}
- {`${props.translate('genericErrorPage.body.helpTextConcierge')} `}
+ {`${translate('genericErrorPage.body.helpTextConcierge')} `}
diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js
index 8d389a8c8581..477d063c1747 100644
--- a/src/pages/home/HeaderView.js
+++ b/src/pages/home/HeaderView.js
@@ -29,6 +29,7 @@ import reportActionPropTypes from './report/reportActionPropTypes';
import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback';
import PinButton from '../../components/PinButton';
import TaskHeaderActionButton from '../../components/TaskHeaderActionButton';
+import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
import ParentNavigationSubtitle from '../../components/ParentNavigationSubtitle';
const propTypes = {
@@ -93,12 +94,14 @@ function HeaderView(props) {
const isConcierge = ReportUtils.hasSingleParticipant(props.report) && _.contains(participants, CONST.ACCOUNT_ID.CONCIERGE);
const isAutomatedExpensifyAccount = ReportUtils.hasSingleParticipant(props.report) && ReportUtils.hasAutomatedExpensifyAccountIDs(participants);
const guideCalendarLink = lodashGet(props.account, 'guideCalendarLink');
+ const parentReportAction = ReportActionsUtils.getParentReportAction(props.report);
+ const isCanceledTaskReport = ReportUtils.isCanceledTaskReport(props.report, parentReportAction);
// We hide the button when we are chatting with an automated Expensify account since it's not possible to contact
// these users via alternative means. It is possible to request a call with Concierge so we leave the option for them.
const shouldShowCallButton = (isConcierge && guideCalendarLink) || (!isAutomatedExpensifyAccount && !isTaskReport);
const threeDotMenuItems = [];
- if (isTaskReport) {
+ if (isTaskReport && !isCanceledTaskReport) {
const canModifyTask = Task.canModifyTask(props.report, props.session.accountID);
if (ReportUtils.isOpenTaskReport(props.report) && canModifyTask) {
threeDotMenuItems.push({
diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js
index a4145843ab87..00ee5da147f9 100644
--- a/src/pages/home/ReportScreen.js
+++ b/src/pages/home/ReportScreen.js
@@ -335,7 +335,7 @@ function ReportScreen({
needsOffscreenAlphaCompositing
>
{headerView}
- {ReportUtils.isTaskReport(report) && isSmallScreenWidth && ReportUtils.isOpenTaskReport(report) && (
+ {ReportUtils.isTaskReport(report) && isSmallScreenWidth && ReportUtils.isOpenTaskReport(report, parentReportAction) && (
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index fae5c518bbfe..f696976eb0b4 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -67,6 +67,7 @@ import * as BankAccounts from '../../../libs/actions/BankAccounts';
import usePrevious from '../../../hooks/usePrevious';
import ReportScreenContext from '../ReportScreenContext';
import Permissions from '../../../libs/Permissions';
+import RenderHTML from '../../../components/RenderHTML';
import ReportAttachmentsContext from './ReportAttachmentsContext';
const propTypes = {
@@ -520,6 +521,22 @@ function ReportActionItem(props) {
);
}
if (ReportUtils.isTaskReport(props.report)) {
+ if (ReportUtils.isCanceledTaskReport(props.report, parentReportAction)) {
+ return (
+ <>
+
+ ${props.translate('parentReportAction.deletedTask')}`} />
+
+
+ >
+ );
+ }
+
return (
`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`,
+ },
+ }),
)(MoneyRequestConfirmPage);
diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.js
index 15e3f03964e6..a6db3d80c03e 100644
--- a/src/pages/tasks/TaskShareDestinationSelectorModal.js
+++ b/src/pages/tasks/TaskShareDestinationSelectorModal.js
@@ -49,7 +49,11 @@ function TaskShareDestinationSelectorModal(props) {
const filteredReports = useMemo(() => {
const reports = {};
_.keys(props.reports).forEach((reportKey) => {
- if (ReportUtils.shouldDisableWriteActions(props.reports[reportKey]) || ReportUtils.isExpensifyOnlyParticipantInReport(props.reports[reportKey])) {
+ if (
+ ReportUtils.shouldDisableWriteActions(props.reports[reportKey]) ||
+ ReportUtils.isExpensifyOnlyParticipantInReport(props.reports[reportKey]) ||
+ ReportUtils.isCanceledTaskReport(props.reports[reportKey])
+ ) {
return;
}
reports[reportKey] = props.reports[reportKey];
diff --git a/src/styles/utilities/overflow.ts b/src/styles/utilities/overflow.ts
index 3961758e40bf..48807283c9b4 100644
--- a/src/styles/utilities/overflow.ts
+++ b/src/styles/utilities/overflow.ts
@@ -1,5 +1,6 @@
import {CSSProperties} from 'react';
import overflowAuto from './overflowAuto';
+import overscrollBehaviorContain from './overscrollBehaviorContain';
/**
* Overflow utility styles with Bootstrap inspired naming.
@@ -23,5 +24,7 @@ export default {
overscrollBehaviorX: 'none',
},
+ overscrollBehaviorContain,
+
overflowAuto,
} satisfies Record;
diff --git a/src/styles/utilities/overscrollBehaviorContain/index.js b/src/styles/utilities/overscrollBehaviorContain/index.js
new file mode 100644
index 000000000000..1377e4b47d28
--- /dev/null
+++ b/src/styles/utilities/overscrollBehaviorContain/index.js
@@ -0,0 +1,3 @@
+export default {
+ overscrollBehavior: 'contain',
+};
diff --git a/src/styles/utilities/overscrollBehaviorContain/index.native.js b/src/styles/utilities/overscrollBehaviorContain/index.native.js
new file mode 100644
index 000000000000..ff8b4c56321a
--- /dev/null
+++ b/src/styles/utilities/overscrollBehaviorContain/index.native.js
@@ -0,0 +1 @@
+export default {};