, policy: OnyxEntry): boolean {
+ return reportField?.type === 'formula' && reportField?.fieldID === CONST.REPORT_FIELD_TITLE_FIELD_ID;
+}
+
+/**
+ * Given a report field, check if the field can be edited or not.
+ * For title fields, its considered disabled if `deletable` prop is `true` (https://github.com/Expensify/App/issues/35043#issuecomment-1911275433)
+ * For non title fields, its considered disabled if:
+ * 1. The user is not admin of the report
+ * 2. Report is settled or it is closed
+ */
+function isReportFieldDisabled(report: OnyxEntry, reportField: OnyxEntry, policy: OnyxEntry): boolean {
+ const isReportSettled = isSettled(report?.reportID);
+ const isReportClosed = report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED;
+ const isTitleField = isReportFieldOfTypeTitle(reportField);
+ const isAdmin = isPolicyAdmin(report?.policyID ?? '', {[`${ONYXKEYS.COLLECTION.POLICY}${policy?.id ?? ''}`]: policy});
+ return isTitleField ? !reportField?.deletable : !isAdmin && (isReportSettled || isReportClosed);
+}
+
+/**
+ * Given a set of report fields, return the field of type formula
+ */
+function getFormulaTypeReportField(reportFields: PolicyReportFields) {
+ return Object.values(reportFields).find((field) => field.type === 'formula');
+}
+
+/**
+ * Get the report fields attached to the policy given policyID
+ */
+function getReportFieldsByPolicyID(policyID: string) {
+ return Object.entries(allPolicyReportFields ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS, '') === policyID)?.[1];
+}
+
+/**
+ * Get the report fields that we should display a MoneyReportView gets opened
+ */
+
+function getAvailableReportFields(report: Report, policyReportFields: PolicyReportField[]): PolicyReportField[] {
+ // Get the report fields that are attached to a report. These will persist even if a field is deleted from the policy.
+ const reportFields = Object.values(report.reportFields ?? {});
+ const reportIsSettled = isSettled(report.reportID);
+
+ // If the report is settled, we don't want to show any new field that gets added to the policy.
+ if (reportIsSettled) {
+ return reportFields;
+ }
+
+ // If the report is unsettled, we want to merge the new fields that get added to the policy with the fields that
+ // are attached to the report.
+ const mergedFieldIds = Array.from(new Set([...policyReportFields.map(({fieldID}) => fieldID), ...reportFields.map(({fieldID}) => fieldID)]));
+ return mergedFieldIds.map((id) => report?.reportFields?.[id] ?? policyReportFields.find(({fieldID}) => fieldID === id)) as PolicyReportField[];
+}
+
/**
* Get the title for an IOU or expense chat which will be showing the payer and the amount
*/
function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string {
+ const isReportSettled = isSettled(report?.reportID ?? '');
+ const reportFields = isReportSettled ? report?.reportFields : getReportFieldsByPolicyID(report?.policyID ?? '');
+ const titleReportField = getFormulaTypeReportField(reportFields ?? {});
+
+ if (titleReportField && report?.reportName && Permissions.canUseReportFields(allBetas ?? [])) {
+ return report.reportName;
+ }
+
const moneyRequestTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend;
const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID));
const payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? '';
@@ -4540,32 +4630,6 @@ function navigateToPrivateNotes(report: Report, session: Session) {
Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID));
}
-/**
- * Given a report field and a report, get the title of the field.
- * This is specially useful when we have a report field of type formula.
- */
-function getReportFieldTitle(report: OnyxEntry, reportField: PolicyReportField): string {
- const value = report?.reportFields?.[reportField.fieldID] ?? reportField.defaultValue;
-
- if (reportField.type !== 'formula') {
- return value;
- }
-
- return value.replaceAll(CONST.REGEX.REPORT_FIELD_TITLE, (match, property) => {
- if (report && property in report) {
- return report[property as keyof Report]?.toString() ?? match;
- }
- return match;
- });
-}
-
-/**
- * Given a report field, check if the field is for the report title.
- */
-function isReportFieldOfTypeTitle(reportField: PolicyReportField): boolean {
- return reportField.type === 'formula' && reportField.fieldID === CONST.REPORT_FIELD_TITLE_FIELD_ID;
-}
-
/**
* Checks if thread replies should be displayed
*/
@@ -4792,7 +4856,6 @@ export {
canEditWriteCapability,
hasSmartscanError,
shouldAutoFocusOnKeyPress,
- getReportFieldTitle,
shouldDisplayThreadReplies,
shouldDisableThread,
doesReportBelongToWorkspace,
@@ -4800,6 +4863,8 @@ export {
isReportParticipant,
isValidReport,
isReportFieldOfTypeTitle,
+ isReportFieldDisabled,
+ getAvailableReportFields,
};
export type {
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 01d157a4cf3c..02c32e089016 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -373,7 +373,7 @@ function getOptionData({
? Localize.translate(preferredLocale, 'workspace.invite.invited')
: Localize.translate(preferredLocale, 'workspace.invite.removed');
const users = Localize.translate(preferredLocale, targetAccountIDs.length > 1 ? 'workspace.invite.users' : 'workspace.invite.user');
- result.alternateText = `${verb} ${targetAccountIDs.length} ${users}`;
+ result.alternateText = `${lastActorDisplayName} ${verb} ${targetAccountIDs.length} ${users}`.trim();
const roomName = lastAction?.originalMessage?.roomName ?? '';
if (roomName) {
diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js
index 5e7396e5cd8f..f2bdb097497e 100644
--- a/src/libs/actions/IOU.js
+++ b/src/libs/actions/IOU.js
@@ -349,7 +349,7 @@ function getOutstandingChildRequest(policy, needsToBeManuallySubmitted) {
* @param {Array} optimisticPolicyRecentlyUsedCategories
* @param {Array} optimisticPolicyRecentlyUsedTags
* @param {boolean} isNewChatReport
- * @param {boolean} isNewIOUReport
+ * @param {boolean} shouldCreateNewMoneyRequestReport
* @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts)
* @param {Array} policyTags
* @param {Array} policyCategories
@@ -368,7 +368,7 @@ function buildOnyxDataForMoneyRequest(
optimisticPolicyRecentlyUsedCategories,
optimisticPolicyRecentlyUsedTags,
isNewChatReport,
- isNewIOUReport,
+ shouldCreateNewMoneyRequestReport,
policy,
policyTags,
policyCategories,
@@ -391,14 +391,14 @@ function buildOnyxDataForMoneyRequest(
},
},
{
- onyxMethod: isNewIOUReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE,
+ onyxMethod: shouldCreateNewMoneyRequestReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
value: {
...iouReport,
lastMessageText: iouAction.message[0].text,
lastMessageHtml: iouAction.message[0].html,
pendingFields: {
- ...(isNewIOUReport ? {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : {preview: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
+ ...(shouldCreateNewMoneyRequestReport ? {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : {preview: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
},
},
},
@@ -416,10 +416,10 @@ function buildOnyxDataForMoneyRequest(
},
},
{
- onyxMethod: isNewIOUReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE,
+ onyxMethod: shouldCreateNewMoneyRequestReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
value: {
- ...(isNewIOUReport ? {[iouCreatedAction.reportActionID]: iouCreatedAction} : {}),
+ ...(shouldCreateNewMoneyRequestReport ? {[iouCreatedAction.reportActionID]: iouCreatedAction} : {}),
[iouAction.reportActionID]: iouAction,
},
},
@@ -507,7 +507,7 @@ function buildOnyxDataForMoneyRequest(
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
value: {
- ...(isNewIOUReport
+ ...(shouldCreateNewMoneyRequestReport
? {
[iouCreatedAction.reportActionID]: {
pendingAction: null,
@@ -547,7 +547,7 @@ function buildOnyxDataForMoneyRequest(
value: {
pendingFields: null,
errorFields: {
- ...(isNewIOUReport ? {createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage')} : {}),
+ ...(shouldCreateNewMoneyRequestReport ? {createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage')} : {}),
},
},
},
@@ -593,7 +593,7 @@ function buildOnyxDataForMoneyRequest(
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
value: {
- ...(isNewIOUReport
+ ...(shouldCreateNewMoneyRequestReport
? {
[iouCreatedAction.reportActionID]: {
errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt.filename, isScanRequest),
@@ -634,7 +634,7 @@ function buildOnyxDataForMoneyRequest(
* Gathers all the data needed to make a money request. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then
* it creates optimistic versions of them and uses those instead
*
- * @param {Object} report
+ * @param {Object} parentChatReport
* @param {Object} participant
* @param {String} comment
* @param {Number} amount
@@ -651,6 +651,7 @@ function buildOnyxDataForMoneyRequest(
* @param {Object} [policy]
* @param {Object} [policyTags]
* @param {Object} [policyCategories]
+ * @param {Number} [moneyRequestReportID] - If user requests money via the report composer on some money request report, we always add a request to that specific report.
* @returns {Object} data
* @returns {String} data.payerEmail
* @returns {Object} data.iouReport
@@ -666,7 +667,7 @@ function buildOnyxDataForMoneyRequest(
* @returns {Object} data.onyxData.failureData
*/
function getMoneyRequestInformation(
- report,
+ parentChatReport,
participant,
comment,
amount,
@@ -683,6 +684,7 @@ function getMoneyRequestInformation(
policy = undefined,
policyTags = undefined,
policyCategories = undefined,
+ moneyRequestReportID = 0,
) {
const payerEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(participant.login);
const payerAccountID = Number(participant.accountID);
@@ -690,7 +692,7 @@ function getMoneyRequestInformation(
// STEP 1: Get existing chat report OR build a new optimistic one
let isNewChatReport = false;
- let chatReport = lodashGet(report, 'reportID', null) ? report : null;
+ let chatReport = lodashGet(parentChatReport, 'reportID', null) ? parentChatReport : null;
// If this is a policyExpenseChat, the chatReport must exist and we can get it from Onyx.
// report is null if the flow is initiated from the global create menu. However, participant always stores the reportID if it exists, which is the case for policyExpenseChats
@@ -708,9 +710,15 @@ function getMoneyRequestInformation(
chatReport = ReportUtils.buildOptimisticChatReport([payerAccountID]);
}
- // STEP 2: Get existing IOU report and update its total OR build a new optimistic one
- const isNewIOUReport = !chatReport.iouReportID || ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(chatReport);
- let iouReport = isNewIOUReport ? null : allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`];
+ // STEP 2: Get the money request report. If the moneyRequestReportID has been provided, we want to add the transaction to this specific report.
+ // If no such reportID has been provided, let's use the chatReport.iouReportID property. In case that is not present, build a new optimistic money request report.
+ let iouReport = null;
+ const shouldCreateNewMoneyRequestReport = !moneyRequestReportID && (!chatReport.iouReportID || ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(chatReport));
+ if (moneyRequestReportID > 0) {
+ iouReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReportID}`];
+ } else if (!shouldCreateNewMoneyRequestReport) {
+ iouReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`];
+ }
// Check if the Scheduled Submit is enabled in case of expense report
let needsToBeManuallySubmitted = true;
@@ -719,7 +727,7 @@ function getMoneyRequestInformation(
isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy);
// If the scheduled submit is turned off on the policy, user needs to manually submit the report which is indicated by GBR in LHN
- needsToBeManuallySubmitted = isFromPaidPolicy && !(policy.isHarvestingEnabled || false);
+ needsToBeManuallySubmitted = isFromPaidPolicy && !(lodashGet(policy, 'harvesting.enabled', policy.isHarvestingEnabled) || false);
// If the linked expense report on paid policy is not draft, we need to create a new draft expense report
if (iouReport && isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport)) {
@@ -807,7 +815,7 @@ function getMoneyRequestInformation(
currentTime,
);
- let reportPreviewAction = isNewIOUReport ? null : ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID);
+ let reportPreviewAction = shouldCreateNewMoneyRequestReport ? null : ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID);
if (reportPreviewAction) {
reportPreviewAction = ReportUtils.updateReportPreview(iouReport, reportPreviewAction, false, comment, optimisticTransaction);
} else {
@@ -845,7 +853,7 @@ function getMoneyRequestInformation(
optimisticPolicyRecentlyUsedCategories,
optimisticPolicyRecentlyUsedTags,
isNewChatReport,
- isNewIOUReport,
+ shouldCreateNewMoneyRequestReport,
policy,
policyTags,
policyCategories,
@@ -860,7 +868,7 @@ function getMoneyRequestInformation(
transaction: optimisticTransaction,
iouAction,
createdChatReportActionID: isNewChatReport ? optimisticCreatedActionForChat.reportActionID : 0,
- createdIOUReportActionID: isNewIOUReport ? optimisticCreatedActionForIOU.reportActionID : 0,
+ createdIOUReportActionID: shouldCreateNewMoneyRequestReport ? optimisticCreatedActionForIOU.reportActionID : 0,
reportPreviewAction,
onyxData: {
optimisticData,
@@ -892,6 +900,7 @@ function createDistanceRequest(report, participant, comment, created, category,
// If the report is an 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;
+ const moneyRequestReportID = isMoneyRequestReport ? report.reportID : 0;
const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created);
const optimisticReceipt = {
@@ -916,6 +925,7 @@ function createDistanceRequest(report, participant, comment, created, category,
policy,
policyTags,
policyCategories,
+ moneyRequestReportID,
);
API.write(
'CreateDistanceRequest',
@@ -1313,6 +1323,7 @@ function requestMoney(
// 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;
+ const moneyRequestReportID = isMoneyRequestReport ? report.reportID : 0;
const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created);
const {payerAccountID, payerEmail, iouReport, chatReport, transaction, iouAction, createdChatReportActionID, createdIOUReportActionID, reportPreviewAction, onyxData} =
getMoneyRequestInformation(
@@ -1333,6 +1344,7 @@ function requestMoney(
policy,
policyTags,
policyCategories,
+ moneyRequestReportID,
);
const activeReportID = isMoneyRequestReport ? report.reportID : chatReport.reportID;
@@ -1389,10 +1401,11 @@ function requestMoney(
* @param {String} category
* @param {String} tag
* @param {String} existingSplitChatReportID - the report ID where the split bill happens, could be a group chat or a workspace chat
+ * @param {Boolean} billable
*
* @return {Object}
*/
-function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, merchant, category, tag, existingSplitChatReportID = '') {
+function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, merchant, category, tag, existingSplitChatReportID = '', billable = false) {
const currentUserEmailForIOUSplit = OptionsListUtils.addSMSDomainIfPhoneNumber(currentUserLogin);
const participantAccountIDs = _.map(participants, (participant) => Number(participant.accountID));
const existingSplitChatReport =
@@ -1416,6 +1429,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco
undefined,
category,
tag,
+ billable,
);
// Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat
@@ -1617,6 +1631,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco
undefined,
category,
tag,
+ billable,
);
// STEP 4: Build optimistic reportActions. We need:
@@ -1734,8 +1749,9 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco
* @param {String} category
* @param {String} tag
* @param {String} existingSplitChatReportID - Either a group DM or a workspace chat
+ * @param {Boolean} billable
*/
-function splitBill(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, merchant, category, tag, existingSplitChatReportID = '') {
+function splitBill(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, merchant, category, tag, existingSplitChatReportID = '', billable = false) {
const {splitData, splits, onyxData} = createSplitsAndOnyxData(
participants,
currentUserLogin,
@@ -1747,6 +1763,7 @@ function splitBill(participants, currentUserLogin, currentUserAccountID, amount,
category,
tag,
existingSplitChatReportID,
+ billable,
);
API.write(
'SplitBill',
@@ -1759,6 +1776,7 @@ function splitBill(participants, currentUserLogin, currentUserAccountID, amount,
category,
merchant,
tag,
+ billable,
transactionID: splitData.transactionID,
reportActionID: splitData.reportActionID,
createdReportActionID: splitData.createdReportActionID,
@@ -1782,9 +1800,10 @@ function splitBill(participants, currentUserLogin, currentUserAccountID, amount,
* @param {String} merchant
* @param {String} category
* @param {String} tag
+ * @param {Boolean} billable
*/
-function splitBillAndOpenReport(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, merchant, category, tag) {
- const {splitData, splits, onyxData} = createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, merchant, category, tag);
+function splitBillAndOpenReport(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, merchant, category, tag, billable) {
+ const {splitData, splits, onyxData} = createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, merchant, category, tag, billable);
API.write(
'SplitBillAndOpenReport',
@@ -1797,6 +1816,7 @@ function splitBillAndOpenReport(participants, currentUserLogin, currentUserAccou
comment,
category,
tag,
+ billable,
transactionID: splitData.transactionID,
reportActionID: splitData.reportActionID,
createdReportActionID: splitData.createdReportActionID,
@@ -1821,8 +1841,9 @@ function splitBillAndOpenReport(participants, currentUserLogin, currentUserAccou
* @param {String} tag
* @param {Object} receipt
* @param {String} existingSplitChatReportID - Either a group DM or a workspace chat
+ * @param {Boolean} billable
*/
-function startSplitBill(participants, currentUserLogin, currentUserAccountID, comment, category, tag, receipt, existingSplitChatReportID = '') {
+function startSplitBill(participants, currentUserLogin, currentUserAccountID, comment, category, tag, receipt, existingSplitChatReportID = '', billable = false) {
const currentUserEmailForIOUSplit = OptionsListUtils.addSMSDomainIfPhoneNumber(currentUserLogin);
const participantAccountIDs = _.map(participants, (participant) => Number(participant.accountID));
const existingSplitChatReport =
@@ -1850,6 +1871,7 @@ function startSplitBill(participants, currentUserLogin, currentUserAccountID, co
undefined,
category,
tag,
+ billable,
);
// Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat
@@ -2063,6 +2085,7 @@ function startSplitBill(participants, currentUserLogin, currentUserAccountID, co
category,
tag,
isFromGroupDM: !existingSplitChatReport,
+ billable,
...(existingSplitChatReport ? {} : {createdReportActionID: splitChatCreatedReportAction.reportActionID}),
},
{optimisticData, successData, failureData},
@@ -3752,6 +3775,15 @@ function navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath,
FileUtils.readFileAsync(receiptPath, receiptFilename, onSuccess, onFailure);
}
+/**
+ * Save the preferred payment method for a policy
+ * @param {String} policyID
+ * @param {String} paymentMethod
+ */
+function savePreferredPaymentMethod(policyID, paymentMethod) {
+ Onyx.merge(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {[policyID]: paymentMethod});
+}
+
export {
setMoneyRequestParticipants,
createDistanceRequest,
@@ -3812,4 +3844,5 @@ export {
getIOUReportID,
editMoneyRequest,
navigateToStartStepIfScanFileCannotBeRead,
+ savePreferredPaymentMethod,
};
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 41362c90135d..b4a7a12ccfca 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -66,7 +66,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
-import type {PersonalDetails, PersonalDetailsList, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx';
+import type {PersonalDetails, PersonalDetailsList, PolicyReportField, RecentlyUsedReportFields, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx';
import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage';
import type {NotificationPreference, WriteCapability} from '@src/types/onyx/Report';
import type Report from '@src/types/onyx/Report';
@@ -189,6 +189,12 @@ Onyx.connect({
},
});
+let allRecentlyUsedReportFields: OnyxEntry = {};
+Onyx.connect({
+ key: ONYXKEYS.RECENTLY_USED_REPORT_FIELDS,
+ callback: (val) => (allRecentlyUsedReportFields = val),
+});
+
/** Get the private pusher channel name for a Report. */
function getReportChannelName(reportID: string): string {
return `${CONST.PUSHER.PRIVATE_REPORT_CHANNEL_PREFIX}${reportID}${CONFIG.PUSHER.SUFFIX}`;
@@ -1477,6 +1483,137 @@ function toggleSubscribeToChildReport(childReportID = '0', parentReportAction: P
}
}
+function updateReportName(reportID: string, value: string, previousValue: string) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {
+ reportName: value,
+ pendingFields: {
+ reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ },
+ ];
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {
+ reportName: previousValue,
+ pendingFields: {
+ reportName: null,
+ },
+ errorFields: {
+ reportName: ErrorUtils.getMicroSecondOnyxError('report.genericUpdateReporNameEditFailureMessage'),
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {
+ pendingFields: {
+ reportName: null,
+ },
+ errorFields: {
+ reportName: null,
+ },
+ },
+ },
+ ];
+
+ const parameters = {
+ reportID,
+ reportName: value,
+ };
+
+ API.write(WRITE_COMMANDS.SET_REPORT_NAME, parameters, {optimisticData, failureData, successData});
+}
+
+function updateReportField(reportID: string, reportField: PolicyReportField, previousReportField: PolicyReportField) {
+ const recentlyUsedValues = allRecentlyUsedReportFields?.[reportField.fieldID] ?? [];
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {
+ reportFields: {
+ [reportField.fieldID]: reportField,
+ },
+ pendingFields: {
+ [reportField.fieldID]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ },
+ ];
+
+ if (reportField.type === 'dropdown') {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.RECENTLY_USED_REPORT_FIELDS,
+ value: {
+ [reportField.fieldID]: [...new Set([...recentlyUsedValues, reportField.value])],
+ },
+ });
+ }
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {
+ reportFields: {
+ [reportField.fieldID]: previousReportField,
+ },
+ pendingFields: {
+ [reportField.fieldID]: null,
+ },
+ errorFields: {
+ [reportField.fieldID]: ErrorUtils.getMicroSecondOnyxError('report.genericUpdateReportFieldFailureMessage'),
+ },
+ },
+ },
+ ];
+
+ if (reportField.type === 'dropdown') {
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.RECENTLY_USED_REPORT_FIELDS,
+ value: {
+ [reportField.fieldID]: recentlyUsedValues,
+ },
+ });
+ }
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {
+ pendingFields: {
+ [reportField.fieldID]: null,
+ },
+ errorFields: {
+ [reportField.fieldID]: null,
+ },
+ },
+ },
+ ];
+
+ const parameters = {
+ reportID,
+ reportFields: JSON.stringify({[reportField.fieldID]: reportField}),
+ };
+
+ API.write(WRITE_COMMANDS.SET_REPORT_FIELD, parameters, {optimisticData, failureData, successData});
+}
+
function updateWelcomeMessage(reportID: string, previousValue: string, newValue: string) {
// No change needed, navigate back
if (previousValue === newValue) {
@@ -2745,5 +2882,7 @@ export {
getDraftPrivateNote,
updateLastVisitTime,
clearNewRoomFormError,
+ updateReportField,
+ updateReportName,
resolveActionableMentionWhisper,
};
diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts
index a7aab98f02c6..60c05d0cb677 100644
--- a/src/libs/actions/Task.ts
+++ b/src/libs/actions/Task.ts
@@ -729,7 +729,6 @@ function deleteTask(taskReportID: string, taskTitle: string, originalStateNum: n
],
errors: undefined,
linkMetadata: [],
- reportActionID: '',
};
const optimisticReportActions = {
[parentReportAction.reportActionID]: optimisticReportAction,
@@ -751,8 +750,7 @@ function deleteTask(taskReportID: string, taskTitle: string, originalStateNum: n
key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport?.reportID}`,
value: {
lastMessageText: ReportActionsUtils.getLastVisibleMessage(parentReport?.reportID ?? '', optimisticReportActions as OnyxTypes.ReportActions).lastMessageText ?? '',
- lastVisibleActionCreated:
- ReportActionsUtils.getLastVisibleAction(parentReport?.reportID ?? '', optimisticReportActions as OnyxTypes.ReportActions)?.childLastVisibleActionCreated ?? 'created',
+ lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(parentReport?.reportID ?? '', optimisticReportActions as OnyxTypes.ReportActions)?.created,
},
},
{
diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js
index 99db8b420d33..cdf5dc5a0502 100755
--- a/src/pages/DetailsPage.js
+++ b/src/pages/DetailsPage.js
@@ -136,8 +136,8 @@ function DetailsPage(props) {
{({show}) => (
void;
+ onSubmit: (form: OnyxFormValuesFields) => void;
};
-function EditReportFieldDatePage({fieldName, onSubmit, fieldValue, fieldID}: EditReportFieldDatePageProps) {
+function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, fieldID}: EditReportFieldDatePageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const inputRef = useRef(null);
const validate = useCallback(
- (values: OnyxFormValuesFields) => {
+ (value: OnyxFormValuesFields) => {
const errors: Errors = {};
- const value = values[fieldID];
- if (typeof value === 'string' && value.trim() === '') {
+ if (isRequired && value[fieldID].trim() === '') {
errors[fieldID] = 'common.error.fieldRequired';
}
return errors;
},
- [fieldID],
+ [fieldID, isRequired],
);
return (
inputRef.current?.focus()}
+ onEntryTransitionEnd={() => {
+ inputRef.current?.focus();
+ }}
testID={EditReportFieldDatePage.displayName}
>
void;
+ onSubmit: (form: Record) => void;
+};
+
+type EditReportFieldDropdownPageOnyxProps = {
+ recentlyUsedReportFields: OnyxEntry;
};
-function EditReportFieldDropdownPage({fieldName, onSubmit, fieldValue, fieldOptions}: EditReportFieldDropdownPageProps) {
+type EditReportFieldDropdownPageProps = EditReportFieldDropdownPageComponentProps & EditReportFieldDropdownPageOnyxProps;
+
+function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, fieldOptions, recentlyUsedReportFields}: EditReportFieldDropdownPageProps) {
const [searchValue, setSearchValue] = useState('');
const styles = useThemeStyles();
const {getSafeAreaMargins} = useStyleUtils();
const {translate} = useLocalize();
+ const recentlyUsedOptions = useMemo(() => recentlyUsedReportFields?.[fieldID] ?? [], [recentlyUsedReportFields, fieldID]);
const sections = useMemo(() => {
- const filteredOptions = fieldOptions.filter((option) => option.toLowerCase().includes(searchValue.toLowerCase()));
+ const filteredRecentOptions = recentlyUsedOptions.filter((option) => option.toLowerCase().includes(searchValue.toLowerCase()));
+ const filteredRestOfOptions = fieldOptions.filter((option) => !filteredRecentOptions.includes(option) && option.toLowerCase().includes(searchValue.toLowerCase()));
+
return [
{
title: translate('common.recents'),
shouldShow: true,
- data: [],
+ data: filteredRecentOptions.map((option) => ({
+ text: option,
+ keyForList: option,
+ searchText: option,
+ tooltipText: option,
+ })),
},
{
title: translate('common.all'),
shouldShow: true,
- data: filteredOptions.map((option) => ({
+ data: filteredRestOfOptions.map((option) => ({
text: option,
keyForList: option,
searchText: option,
@@ -45,7 +70,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldValue, fieldOpti
})),
},
];
- }, [fieldOptions, searchValue, translate]);
+ }, [fieldOptions, recentlyUsedOptions, searchValue, translate]);
return (
) => onSubmit({[fieldID]: option.text})}
onChangeText={setSearchValue}
highlightSelectedOptions
isRowMultilineSupported
@@ -79,4 +104,8 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldValue, fieldOpti
EditReportFieldDropdownPage.displayName = 'EditReportFieldDropdownPage';
-export default EditReportFieldDropdownPage;
+export default withOnyx({
+ recentlyUsedReportFields: {
+ key: () => ONYXKEYS.RECENTLY_USED_REPORT_FIELDS,
+ },
+})(EditReportFieldDropdownPage);
diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx
index d74582708995..5bb53ef9122e 100644
--- a/src/pages/EditReportFieldPage.tsx
+++ b/src/pages/EditReportFieldPage.tsx
@@ -1,10 +1,15 @@
-import React, {useEffect} from 'react';
+import Str from 'expensify-common/lib/str';
+import React from 'react';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import type {OnyxFormValuesFields} from '@components/Form/types';
import ScreenWrapper from '@components/ScreenWrapper';
+import Navigation from '@libs/Navigation/Navigation';
+import * as ReportUtils from '@libs/ReportUtils';
+import * as ReportActions from '@src/libs/actions/Report';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {PolicyReportFields, Report} from '@src/types/onyx';
+import type {Policy, PolicyReportFields, Report} from '@src/types/onyx';
import EditReportFieldDatePage from './EditReportFieldDatePage';
import EditReportFieldDropdownPage from './EditReportFieldDropdownPage';
import EditReportFieldTextPage from './EditReportFieldTextPage';
@@ -15,6 +20,9 @@ type EditReportFieldPageOnyxProps = {
/** Policy report fields */
policyReportFields: OnyxEntry;
+
+ /** Policy to which the report belongs to */
+ policy: OnyxEntry;
};
type EditReportFieldPageProps = EditReportFieldPageOnyxProps & {
@@ -34,61 +42,77 @@ type EditReportFieldPageProps = EditReportFieldPageOnyxProps & {
};
};
-function EditReportFieldPage({route, report, policyReportFields}: EditReportFieldPageProps) {
- const policyReportField = policyReportFields?.[route.params.fieldID];
- const reportFieldValue = report?.reportFields?.[policyReportField?.fieldID ?? ''];
-
- // Decides whether to allow or disallow editing a money request
- useEffect(() => {}, []);
-
- if (policyReportField) {
- if (policyReportField.type === 'text' || policyReportField.type === 'formula') {
- return (
- {}}
- />
- );
- }
+function EditReportFieldPage({route, policy, report, policyReportFields}: EditReportFieldPageProps) {
+ const reportField = report?.reportFields?.[route.params.fieldID] ?? policyReportFields?.[route.params.fieldID];
+ const isDisabled = ReportUtils.isReportFieldDisabled(report, reportField ?? null, policy);
- if (policyReportField.type === 'date') {
- return (
- {}}
+ if (!reportField || !report || isDisabled) {
+ return (
+
+ {}}
+ onLinkPress={() => {}}
/>
- );
- }
+
+ );
+ }
- if (policyReportField.type === 'dropdown') {
- return (
- {}}
- />
- );
+ const isReportFieldTitle = ReportUtils.isReportFieldOfTypeTitle(reportField);
+
+ const handleReportFieldChange = (form: OnyxFormValuesFields) => {
+ const value = form[reportField.fieldID] || '';
+ if (isReportFieldTitle) {
+ ReportActions.updateReportName(report.reportID, value, report.reportName ?? '');
+ } else {
+ ReportActions.updateReportField(report.reportID, {...reportField, value}, reportField);
}
+
+ Navigation.dismissModal(report?.reportID);
+ };
+
+ const fieldValue = isReportFieldTitle ? report.reportName ?? '' : reportField.value ?? reportField.defaultValue;
+
+ if (reportField.type === 'text' || isReportFieldTitle) {
+ return (
+
+ );
+ }
+
+ if (reportField.type === 'date') {
+ return (
+
+ );
}
- return (
-
- {}}
- onLinkPress={() => {}}
+ if (reportField.type === 'dropdown') {
+ return (
+
-
- );
+ );
+ }
}
EditReportFieldPage.displayName = 'EditReportFieldPage';
@@ -100,4 +124,7 @@ export default withOnyx(
policyReportFields: {
key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${route.params.policyID}`,
},
+ policy: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`,
+ },
})(EditReportFieldPage);
diff --git a/src/pages/EditReportFieldTextPage.tsx b/src/pages/EditReportFieldTextPage.tsx
index 733bfd6e5fee..ea9d2d3bed6d 100644
--- a/src/pages/EditReportFieldTextPage.tsx
+++ b/src/pages/EditReportFieldTextPage.tsx
@@ -23,38 +23,42 @@ type EditReportFieldTextPageProps = {
/** ID of the policy report field */
fieldID: string;
+ /** Flag to indicate if the field can be left blank */
+ isRequired: boolean;
+
/** Callback to fire when the Save button is pressed */
- onSubmit: () => void;
+ onSubmit: (form: OnyxFormValuesFields) => void;
};
-function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, fieldID}: EditReportFieldTextPageProps) {
+function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldID}: EditReportFieldTextPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const inputRef = useRef(null);
const validate = useCallback(
- (values: OnyxFormValuesFields) => {
+ (values: OnyxFormValuesFields) => {
const errors: Errors = {};
- const value = values[fieldID];
- if (typeof value === 'string' && value.trim() === '') {
+ if (isRequired && values[fieldID].trim() === '') {
errors[fieldID] = 'common.error.fieldRequired';
}
return errors;
},
- [fieldID],
+ [fieldID, isRequired],
);
return (
inputRef.current?.focus()}
+ onEntryTransitionEnd={() => {
+ inputRef.current?.focus();
+ }}
testID={EditReportFieldTextPage.displayName}
>
{
- Report.searchInServer(text);
- setSearchTerm(text);
- }, []);
-
const {inputCallbackRef} = useAutoFocusInput();
return (
diff --git a/src/pages/ReportAvatar.tsx b/src/pages/ReportAvatar.tsx
index c61a0a748f7d..29142294084c 100644
--- a/src/pages/ReportAvatar.tsx
+++ b/src/pages/ReportAvatar.tsx
@@ -35,6 +35,7 @@ function ReportAvatar({report = {} as Report, policies, isLoadingApp = true}: Re
Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? ''));
}}
isWorkspaceAvatar
+ maybeIcon
originalFileName={policy?.originalFileName ?? policyName}
shouldShowNotFoundPage={!report?.reportID && !isLoadingApp}
isLoading={(!report?.reportID || !policy?.id) && !!isLoadingApp}
diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js
index 3e682d592370..513ccbbe307c 100644
--- a/src/pages/ReportDetailsPage.js
+++ b/src/pages/ReportDetailsPage.js
@@ -165,7 +165,7 @@ function ReportDetailsPage(props) {
return ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participants, props.personalDetails), hasMultipleParticipants);
}, [participants, props.personalDetails]);
- const icons = useMemo(() => ReportUtils.getIcons(props.report, props.personalDetails, props.policies), [props.report, props.personalDetails, props.policies]);
+ const icons = useMemo(() => ReportUtils.getIcons(props.report, props.personalDetails, null, '', -1, policy), [props.report, props.personalDetails, policy]);
const chatRoomSubtitleText = chatRoomSubtitle ? (
_.pick(policy, ['role']),
},
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
})(HeaderView),
);
diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js
index 4d043f12351e..f28e418865ff 100644
--- a/src/pages/home/ReportScreen.js
+++ b/src/pages/home/ReportScreen.js
@@ -1,7 +1,7 @@
import {useIsFocused} from '@react-navigation/native';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
@@ -11,7 +11,6 @@ import DragAndDropProvider from '@components/DragAndDrop/Provider';
import MoneyReportHeader from '@components/MoneyReportHeader';
import MoneyRequestHeader from '@components/MoneyRequestHeader';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
-import {usePersonalDetails} from '@components/OnyxProvider';
import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView';
import ScreenWrapper from '@components/ScreenWrapper';
import TaskHeaderActionButton from '@components/TaskHeaderActionButton';
@@ -28,14 +27,13 @@ import Navigation from '@libs/Navigation/Navigation';
import clearReportNotifications from '@libs/Notification/clearReportNotifications';
import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector';
import Performance from '@libs/Performance';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
-import personalDetailsPropType from '@pages/personalDetailsPropType';
import reportMetadataPropTypes from '@pages/reportMetadataPropTypes';
import reportPropTypes from '@pages/reportPropTypes';
import * as ComposerActions from '@userActions/Composer';
import * as Report from '@userActions/Report';
-import * as Task from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -93,9 +91,6 @@ const propTypes = {
/** The account manager report ID */
accountManagerReportID: PropTypes.string,
- /** All of the personal details for everyone */
- personalDetails: PropTypes.objectOf(personalDetailsPropType),
-
/** Onyx function that marks the component ready for hydration */
markReadyForHydration: PropTypes.func,
@@ -121,7 +116,6 @@ const defaultProps = {
policies: {},
accountManagerReportID: null,
userLeavingStatus: false,
- personalDetails: {},
markReadyForHydration: null,
...withCurrentReportIDDefaultProps,
};
@@ -146,12 +140,11 @@ function getReportID(route) {
function ReportScreen({
betas,
route,
- report,
+ report: reportProp,
reportMetadata,
reportActions,
parentReportAction,
accountManagerReportID,
- personalDetails,
markReadyForHydration,
policies,
isSidebarLoaded,
@@ -169,6 +162,77 @@ function ReportScreen({
const firstRenderRef = useRef(true);
const flatListRef = useRef();
const reactionListRef = useRef();
+ /**
+ * Create a lightweight Report so as to keep the re-rendering as light as possible by
+ * passing in only the required props.
+ *
+ * Also, this plays nicely in contrast with Onyx,
+ * which creates a new object every time collection changes. Because of this we can't
+ * put this into onyx selector as it will be the same.
+ */
+ const report = useMemo(
+ () => ({
+ lastReadTime: reportProp.lastReadTime,
+ reportID: reportProp.reportID,
+ policyID: reportProp.policyID,
+ lastVisibleActionCreated: reportProp.lastVisibleActionCreated,
+ statusNum: reportProp.statusNum,
+ stateNum: reportProp.stateNum,
+ writeCapability: reportProp.writeCapability,
+ type: reportProp.type,
+ errorFields: reportProp.errorFields,
+ isPolicyExpenseChat: reportProp.isPolicyExpenseChat,
+ parentReportID: reportProp.parentReportID,
+ parentReportActionID: reportProp.parentReportActionID,
+ chatType: reportProp.chatType,
+ pendingFields: reportProp.pendingFields,
+ isDeletedParentAction: reportProp.isDeletedParentAction,
+ reportName: reportProp.reportName,
+ description: reportProp.description,
+ managerID: reportProp.managerID,
+ total: reportProp.total,
+ nonReimbursableTotal: reportProp.nonReimbursableTotal,
+ reportFields: reportProp.reportFields,
+ ownerAccountID: reportProp.ownerAccountID,
+ currency: reportProp.currency,
+ participantAccountIDs: reportProp.participantAccountIDs,
+ isWaitingOnBankAccount: reportProp.isWaitingOnBankAccount,
+ iouReportID: reportProp.iouReportID,
+ isOwnPolicyExpenseChat: reportProp.isOwnPolicyExpenseChat,
+ notificationPreference: reportProp.notificationPreference,
+ }),
+ [
+ reportProp.lastReadTime,
+ reportProp.reportID,
+ reportProp.policyID,
+ reportProp.lastVisibleActionCreated,
+ reportProp.statusNum,
+ reportProp.stateNum,
+ reportProp.writeCapability,
+ reportProp.type,
+ reportProp.errorFields,
+ reportProp.isPolicyExpenseChat,
+ reportProp.parentReportID,
+ reportProp.parentReportActionID,
+ reportProp.chatType,
+ reportProp.pendingFields,
+ reportProp.isDeletedParentAction,
+ reportProp.reportName,
+ reportProp.description,
+ reportProp.managerID,
+ reportProp.total,
+ reportProp.nonReimbursableTotal,
+ reportProp.reportFields,
+ reportProp.ownerAccountID,
+ reportProp.currency,
+ reportProp.participantAccountIDs,
+ reportProp.isWaitingOnBankAccount,
+ reportProp.iouReportID,
+ reportProp.isOwnPolicyExpenseChat,
+ reportProp.notificationPreference,
+ ],
+ );
+
const prevReport = usePrevious(report);
const prevUserLeavingStatus = usePrevious(userLeavingStatus);
const [isBannerVisible, setIsBannerVisible] = useState(true);
@@ -184,12 +248,20 @@ function ReportScreen({
const reportID = getReportID(route);
const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report);
const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}];
-
+ const isEmptyChat = useMemo(() => _.isEmpty(reportActions), [reportActions]);
// There are no reportActions at all to display and we are still in the process of loading the next set of actions.
const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions;
const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS_NUM.CLOSED;
const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas);
- const isLoading = !reportID || !isSidebarLoaded || _.isEmpty(personalDetails);
+
+ const isLoading = !reportID || !isSidebarLoaded || PersonalDetailsUtils.isPersonalDetailsEmpty();
+ const lastReportAction = useMemo(
+ () =>
+ reportActions.length
+ ? _.find([...reportActions, parentReportAction], (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action))
+ : {},
+ [reportActions, parentReportAction],
+ );
const isSingleTransactionView = ReportUtils.isMoneyRequest(report);
const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] || {};
const isTopMostReportId = currentReportID === getReportID(route);
@@ -210,7 +282,6 @@ function ReportScreen({
);
@@ -220,7 +291,6 @@ function ReportScreen({
@@ -232,7 +302,6 @@ function ReportScreen({
);
@@ -286,54 +355,6 @@ function ReportScreen({
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(accountManagerReportID));
}, [accountManagerReportID]);
- const allPersonalDetails = usePersonalDetails();
-
- /**
- * @param {String} text
- */
- const handleCreateTask = useCallback(
- (text) => {
- /**
- * Matching task rule by group
- * Group 1: Start task rule with []
- * Group 2: Optional email group between \s+....\s* start rule with @+valid email
- * Group 3: Title is remaining characters
- */
- const taskRegex = /^\[\]\s+(?:@([^\s@]+@[\w.-]+\.[a-zA-Z]{2,}))?\s*([\s\S]*)/;
-
- const match = text.match(taskRegex);
- if (!match) {
- return false;
- }
- const title = match[2] ? match[2].trim().replace(/\n/g, ' ') : undefined;
- if (!title) {
- return false;
- }
- const email = match[1] ? match[1].trim() : undefined;
- let assignee = {};
- if (email) {
- assignee = _.find(_.values(allPersonalDetails), (p) => p.login === email) || {};
- }
- Task.createTaskAndNavigate(getReportID(route), title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, report.policyID);
- return true;
- },
- [allPersonalDetails, report.policyID, route],
- );
-
- /**
- * @param {String} text
- */
- const onSubmitComment = useCallback(
- (text) => {
- const isTaskCreated = handleCreateTask(text);
- if (isTaskCreated) {
- return;
- }
- Report.addComment(getReportID(route), text);
- },
- [route, handleCreateTask],
- );
-
// Clear notifications for the current report when it's opened and re-focused
const clearNotifications = useCallback(() => {
// Check if this is the top-most ReportScreen since the Navigator preserves multiple at a time
@@ -538,14 +559,12 @@ function ReportScreen({
{isReportReadyForDisplay ? (
) : (
@@ -604,9 +623,6 @@ export default compose(
key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID,
initialValue: null,
},
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
userLeavingStatus: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`,
initialValue: false,
@@ -625,4 +641,23 @@ export default compose(
},
true,
),
-)(ReportScreen);
+)(
+ memo(
+ ReportScreen,
+ (prevProps, nextProps) =>
+ prevProps.isSidebarLoaded === nextProps.isSidebarLoaded &&
+ _.isEqual(prevProps.reportActions, nextProps.reportActions) &&
+ _.isEqual(prevProps.reportMetadata, nextProps.reportMetadata) &&
+ prevProps.isComposerFullSize === nextProps.isComposerFullSize &&
+ _.isEqual(prevProps.betas, nextProps.betas) &&
+ _.isEqual(prevProps.policies, nextProps.policies) &&
+ prevProps.accountManagerReportID === nextProps.accountManagerReportID &&
+ prevProps.userLeavingStatus === nextProps.userLeavingStatus &&
+ prevProps.report.reportID === nextProps.report.reportID &&
+ prevProps.report.policyID === nextProps.report.policyID &&
+ prevProps.report.isOptimisticReport === nextProps.report.isOptimisticReport &&
+ prevProps.report.statusNum === nextProps.report.statusNum &&
+ _.isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) &&
+ prevProps.currentReportID === nextProps.currentReportID,
+ ),
+);
diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js
index 444dd939142b..df5645ae61ad 100644
--- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js
+++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js
@@ -59,7 +59,7 @@ const propTypes = {
isBlockedFromConcierge: PropTypes.bool.isRequired,
/** Whether or not the attachment picker is disabled */
- disabled: PropTypes.bool.isRequired,
+ disabled: PropTypes.bool,
/** Sets the menu visibility */
setMenuVisibility: PropTypes.func.isRequired,
@@ -100,6 +100,7 @@ const propTypes = {
const defaultProps = {
reportParticipantIDs: [],
+ disabled: false,
policy: {},
};
diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js
index 90c2ba0b42cf..4b5a54a6c428 100644
--- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js
+++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js
@@ -1,6 +1,6 @@
import {useIsFocused, useNavigation} from '@react-navigation/native';
import lodashGet from 'lodash/get';
-import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
+import React, {memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
@@ -74,8 +74,10 @@ function ComposerWithSuggestions({
isKeyboardShown,
// Props: Report
reportID,
- report,
- reportActions,
+ includeChronos,
+ isEmptyChat,
+ lastReportAction,
+ parentReportActionID,
// Focus
onFocus,
onBlur,
@@ -114,12 +116,13 @@ function ComposerWithSuggestions({
const isFocused = useIsFocused();
const navigation = useNavigation();
const emojisPresentBefore = useRef([]);
+
+ const draftComment = getDraftComment(reportID) || '';
const [value, setValue] = useState(() => {
- const draft = getDraftComment(reportID) || '';
- if (draft) {
- emojisPresentBefore.current = EmojiUtils.extractEmojis(draft);
+ if (draftComment) {
+ emojisPresentBefore.current = EmojiUtils.extractEmojis(draftComment);
}
- return draft;
+ return draftComment;
});
const commentRef = useRef(value);
const lastTextRef = useRef(value);
@@ -127,8 +130,7 @@ function ComposerWithSuggestions({
const {isSmallScreenWidth} = useWindowDimensions();
const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES;
- const isEmptyChat = useMemo(() => _.size(reportActions) === 1, [reportActions]);
- const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]);
+ const parentReportAction = lodashGet(parentReportActions, [parentReportActionID]);
const shouldAutoFocus = !modal.isVisible && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction))) && shouldShowComposeInput;
const valueRef = useRef(value);
@@ -384,18 +386,14 @@ function ComposerWithSuggestions({
// Trigger the edit box for last sent message if ArrowUp is pressed and the comment is empty and Chronos is not in the participants
const valueLength = valueRef.current.length;
- if (e.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && textInputRef.current.selectionStart === 0 && valueLength === 0 && !ReportUtils.chatIncludesChronos(report)) {
+ if (e.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && textInputRef.current.selectionStart === 0 && valueLength === 0 && !includeChronos) {
e.preventDefault();
- const lastReportAction = _.find(
- [...reportActions, parentReportAction],
- (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action),
- );
if (lastReportAction) {
Report.saveReportActionDraft(reportID, lastReportAction, _.last(lastReportAction.message).html);
}
}
},
- [isKeyboardShown, isSmallScreenWidth, parentReportAction, report, reportActions, reportID, handleSendMessage, suggestionsRef, valueRef],
+ [isSmallScreenWidth, isKeyboardShown, suggestionsRef, includeChronos, handleSendMessage, lastReportAction, reportID],
);
const onChangeText = useCallback(
@@ -574,6 +572,22 @@ function ComposerWithSuggestions({
onValueChange(value);
}, [onValueChange, value]);
+ const onLayout = useCallback(
+ (e) => {
+ const composerLayoutHeight = e.nativeEvent.layout.height;
+ if (composerHeight === composerLayoutHeight) {
+ return;
+ }
+ setComposerHeight(composerLayoutHeight);
+ },
+ [composerHeight],
+ );
+
+ const onClear = useCallback(() => {
+ setTextInputShouldClear(false);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
return (
<>
@@ -594,7 +608,7 @@ function ComposerWithSuggestions({
onClick={setShouldBlockSuggestionCalcToFalse}
onPasteFile={displayFileInModal}
shouldClear={textInputShouldClear}
- onClear={() => setTextInputShouldClear(false)}
+ onClear={onClear}
isDisabled={isBlockedFromConcierge || disabled}
isReportActionCompose
selection={selection}
@@ -607,13 +621,7 @@ function ComposerWithSuggestions({
numberOfLines={numberOfLines}
onNumberOfLinesChange={updateNumberOfLines}
shouldCalculateCaretPosition
- onLayout={(e) => {
- const composerLayoutHeight = e.nativeEvent.layout.height;
- if (composerHeight === composerLayoutHeight) {
- return;
- }
- setComposerHeight(composerLayoutHeight);
- }}
+ onLayout={onLayout}
onScroll={hideSuggestionMenu}
shouldContainScroll={Browser.isMobileSafari()}
/>
@@ -637,7 +645,6 @@ function ComposerWithSuggestions({
`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`,
+ key: ({parentReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`,
canEvict: false,
initWithStoredValues: false,
},
}),
-)(ComposerWithSuggestionsWithRef);
+)(memo(ComposerWithSuggestionsWithRef));
diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/composerWithSuggestionsProps.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/composerWithSuggestionsProps.js
index e2aa1d86af03..9d05db572949 100644
--- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/composerWithSuggestionsProps.js
+++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/composerWithSuggestionsProps.js
@@ -1,5 +1,4 @@
import PropTypes from 'prop-types';
-import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
import CONST from '@src/CONST';
const propTypes = {
@@ -18,20 +17,9 @@ const propTypes = {
/** Whether the keyboard is open or not */
isKeyboardShown: PropTypes.bool.isRequired,
- /** The actions from the parent report */
- parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
-
- /** Array of report actions for this report */
- reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)),
-
/** The ID of the report */
reportID: PropTypes.string.isRequired,
- /** The report currently being looked at */
- report: PropTypes.shape({
- parentReportID: PropTypes.string,
- }).isRequired,
-
/** Callback when the input is focused */
onFocus: PropTypes.func.isRequired,
@@ -60,7 +48,7 @@ const propTypes = {
isBlockedFromConcierge: PropTypes.bool.isRequired,
/** Whether the input is disabled or not */
- disabled: PropTypes.bool.isRequired,
+ disabled: PropTypes.bool,
/** Whether the full composer is available or not */
isFullComposerAvailable: PropTypes.bool.isRequired,
@@ -121,6 +109,7 @@ const defaultProps = {
reportActions: [],
forwardedRef: null,
measureParentContainer: () => {},
+ disabled: false,
};
export {propTypes, defaultProps};
diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js
index c52b8ec6760a..cc07716209a2 100644
--- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js
@@ -1,7 +1,7 @@
import {PortalHost} from '@gorhom/portal';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import {runOnJS, setNativeProps, useAnimatedRef} from 'react-native-reanimated';
@@ -26,7 +26,6 @@ import getModalState from '@libs/getModalState';
import * as ReportUtils from '@libs/ReportUtils';
import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
import ParticipantLocalTime from '@pages/home/report/ParticipantLocalTime';
-import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
import ReportDropUI from '@pages/home/report/ReportDropUI';
import ReportTypingIndicator from '@pages/home/report/ReportTypingIndicator';
import reportPropTypes from '@pages/reportPropTypes';
@@ -46,9 +45,6 @@ const propTypes = {
/** The ID of the report actions will be created for */
reportID: PropTypes.string.isRequired,
- /** Array of report actions for this report */
- reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)),
-
/** The report currently being looked at */
report: reportPropTypes,
@@ -106,7 +102,8 @@ function ReportActionCompose({
pendingAction,
report,
reportID,
- reportActions,
+ isEmptyChat,
+ lastReportAction,
listHeight,
shouldShowComposeInput,
isReportReadyForDisplay,
@@ -117,6 +114,7 @@ function ReportActionCompose({
const animatedRef = useAnimatedRef();
const actionButtonRef = useRef(null);
const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
+
/**
* Updates the Highlight state of the composer
*/
@@ -180,7 +178,9 @@ function ReportActionCompose({
[personalDetails, report, currentUserPersonalDetails.accountID, isComposerFullSize],
);
- const isBlockedFromConcierge = useMemo(() => ReportUtils.chatIncludesConcierge(report) && User.isBlockedFromConcierge(blockedFromConcierge), [report, blockedFromConcierge]);
+ const includesConcierge = useMemo(() => ReportUtils.chatIncludesConcierge({participantAccountIDs: report.participantAccountIDs}), [report.participantAccountIDs]);
+ const userBlockedFromConcierge = useMemo(() => User.isBlockedFromConcierge(blockedFromConcierge), [blockedFromConcierge]);
+ const isBlockedFromConcierge = useMemo(() => includesConcierge && userBlockedFromConcierge, [includesConcierge, userBlockedFromConcierge]);
// If we are on a small width device then don't show last 3 items from conciergePlaceholderOptions
const conciergePlaceholderRandomIndex = useMemo(
@@ -191,8 +191,8 @@ function ReportActionCompose({
// Placeholder to display in the chat input.
const inputPlaceholder = useMemo(() => {
- if (ReportUtils.chatIncludesConcierge(report)) {
- if (User.isBlockedFromConcierge(blockedFromConcierge)) {
+ if (includesConcierge) {
+ if (userBlockedFromConcierge) {
return translate('reportActionCompose.blockedFromConcierge');
}
@@ -200,7 +200,7 @@ function ReportActionCompose({
}
return translate('reportActionCompose.writeSomething');
- }, [report, blockedFromConcierge, translate, conciergePlaceholderRandomIndex]);
+ }, [includesConcierge, translate, userBlockedFromConcierge, conciergePlaceholderRandomIndex]);
const focus = () => {
if (composerRef === null || composerRef.current === null) {
@@ -421,8 +421,11 @@ function ReportActionCompose({
isScrollLikelyLayoutTriggered={isScrollLikelyLayoutTriggered}
raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLikelyLayoutTriggered}
reportID={reportID}
- report={report}
- reportActions={reportActions}
+ parentReportID={report.parentReportID}
+ parentReportActionID={report.parentReportActionID}
+ includesChronos={ReportUtils.chatIncludesChronos(report)}
+ isEmptyChat={isEmptyChat}
+ lastReportAction={lastReportAction}
isMenuVisible={isMenuVisible}
inputPlaceholder={inputPlaceholder}
isComposerFullSize={isComposerFullSize}
@@ -501,4 +504,4 @@ export default compose(
key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT,
},
}),
-)(ReportActionCompose);
+)(memo(ReportActionCompose));
diff --git a/src/pages/home/report/ReportActionCompose/SendButton.js b/src/pages/home/report/ReportActionCompose/SendButton.js
index d0b0453ace2f..e9e3ef244f9c 100644
--- a/src/pages/home/report/ReportActionCompose/SendButton.js
+++ b/src/pages/home/report/ReportActionCompose/SendButton.js
@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
-import React from 'react';
+import React, {memo} from 'react';
import {View} from 'react-native';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import Icon from '@components/Icon';
@@ -63,4 +63,4 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}) {
SendButton.propTypes = propTypes;
SendButton.displayName = 'SendButton';
-export default SendButton;
+export default memo(SendButton);
diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.js b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.js
index 9aa997a892f4..23d69ec7defc 100644
--- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.js
+++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.js
@@ -9,12 +9,6 @@ const propTypes = {
/** The comment of the report */
comment: PropTypes.string,
- /** The report associated with the comment */
- report: PropTypes.shape({
- /** The ID of the report */
- reportID: PropTypes.string,
- }).isRequired,
-
/** The value of the comment */
value: PropTypes.string.isRequired,
@@ -26,6 +20,8 @@ const propTypes = {
/** Updates the comment */
updateComment: PropTypes.func.isRequired,
+
+ reportID: PropTypes.string.isRequired,
};
const defaultProps = {
@@ -38,9 +34,9 @@ const defaultProps = {
* re-rendering a UI component for that. That's why the side effect was moved down to a separate component.
* @returns {null}
*/
-function SilentCommentUpdater({comment, commentRef, report, value, updateComment}) {
+function SilentCommentUpdater({comment, commentRef, reportID, value, updateComment}) {
const prevCommentProp = usePrevious(comment);
- const prevReportId = usePrevious(report.reportID);
+ const prevReportId = usePrevious(reportID);
const {preferredLocale} = useLocalize();
const prevPreferredLocale = usePrevious(preferredLocale);
@@ -56,12 +52,12 @@ function SilentCommentUpdater({comment, commentRef, report, value, updateComment
// As the report IDs change, make sure to update the composer comment as we need to make sure
// we do not show incorrect data in there (ie. draft of message from other report).
- if (preferredLocale === prevPreferredLocale && report.reportID === prevReportId && !shouldSyncComment) {
+ if (preferredLocale === prevPreferredLocale && reportID === prevReportId && !shouldSyncComment) {
return;
}
updateComment(comment);
- }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, report.reportID, updateComment, value, commentRef]);
+ }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, reportID, updateComment, value, commentRef]);
return null;
}
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index 141b2b8a5a6d..c290c9378e65 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -107,7 +107,6 @@ const propTypes = {
/** Stores user's preferred skin tone */
preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- ...windowDimensionsPropTypes,
emojiReactions: EmojiReactionsPropTypes,
/** IOU report for this action, if any */
@@ -150,7 +149,7 @@ function ReportActionItem(props) {
const prevDraftMessage = usePrevious(props.draftMessage);
const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action);
const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID);
- const isReportActionLinked = props.linkedReportActionID === props.action.reportActionID;
+ const isReportActionLinked = props.linkedReportActionID && props.action.reportActionID && props.linkedReportActionID === props.action.reportActionID;
const highlightedBackgroundColorIfNeeded = useMemo(
() => (isReportActionLinked ? StyleUtils.getBackgroundColorStyle(theme.hoverComponentBG) : {}),
@@ -509,7 +508,6 @@ function ReportActionItem(props) {
reportID={props.report.reportID}
index={props.index}
ref={textInputRef}
- report={props.report}
// Avoid defining within component due to an existing Onyx bug
preferredSkinTone={props.preferredSkinTone}
shouldDisableEmojiPicker={
@@ -652,6 +650,7 @@ function ReportActionItem(props) {
@@ -807,6 +806,10 @@ export default compose(
key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined),
initialValue: [],
},
+ policy: {
+ key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}` : undefined),
+ initialValue: {},
+ },
emojiReactions: {
key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`,
initialValue: {},
@@ -850,6 +853,7 @@ export default compose(
lodashGet(prevProps.report, 'nonReimbursableTotal', 0) === lodashGet(nextProps.report, 'nonReimbursableTotal', 0) &&
prevProps.linkedReportActionID === nextProps.linkedReportActionID &&
_.isEqual(prevProps.policyReportFields, nextProps.policyReportFields) &&
- _.isEqual(prevProps.report.reportFields, nextProps.report.reportFields),
+ _.isEqual(prevProps.report.reportFields, nextProps.report.reportFields) &&
+ _.isEqual(prevProps.policy, nextProps.policy),
),
);
diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx
index 82c6bebd9ba1..f8345ca7d2d0 100644
--- a/src/pages/home/report/ReportActionItemCreated.tsx
+++ b/src/pages/home/report/ReportActionItemCreated.tsx
@@ -115,6 +115,7 @@ export default withOnyx ({
+ reportActionID: reportAction.reportActionID,
+ message: reportAction.message,
+ pendingAction: reportAction.pendingAction,
+ actionName: reportAction.actionName,
+ errors: reportAction.errors,
+ originalMessage: reportAction.originalMessage,
+ childCommenterCount: reportAction.childCommenterCount,
+ linkMetadata: reportAction.linkMetadata,
+ childReportID: reportAction.childReportID,
+ childLastVisibleActionCreated: reportAction.childLastVisibleActionCreated,
+ whisperedToAccountIDs: reportAction.whisperedToAccountIDs,
+ error: reportAction.error,
+ created: reportAction.created,
+ actorAccountID: reportAction.actorAccountID,
+ childVisibleActionCount: reportAction.childVisibleActionCount,
+ childOldestFourAccountIDs: reportAction.childOldestFourAccountIDs,
+ childType: reportAction.childType,
+ person: reportAction.person,
+ isOptimisticAction: reportAction.isOptimisticAction,
+ delegateAccountID: reportAction.delegateAccountID,
+ previousMessage: reportAction.previousMessage,
+ attachmentInfo: reportAction.attachmentInfo,
+ childStateNum: reportAction.childStateNum,
+ childStatusNum: reportAction.childStatusNum,
+ childReportName: reportAction.childReportName,
+ childManagerAccountID: reportAction.childManagerAccountID,
+ childMoneyRequestCount: reportAction.childMoneyRequestCount,
+ }),
+ [
+ reportAction.actionName,
+ reportAction.childCommenterCount,
+ reportAction.childLastVisibleActionCreated,
+ reportAction.childReportID,
+ reportAction.created,
+ reportAction.error,
+ reportAction.errors,
+ reportAction.linkMetadata,
+ reportAction.message,
+ reportAction.originalMessage,
+ reportAction.pendingAction,
+ reportAction.reportActionID,
+ reportAction.whisperedToAccountIDs,
+ reportAction.actorAccountID,
+ reportAction.childVisibleActionCount,
+ reportAction.childOldestFourAccountIDs,
+ reportAction.person,
+ reportAction.isOptimisticAction,
+ reportAction.childType,
+ reportAction.delegateAccountID,
+ reportAction.previousMessage,
+ reportAction.attachmentInfo,
+ reportAction.childStateNum,
+ reportAction.childStatusNum,
+ reportAction.childReportName,
+ reportAction.childManagerAccountID,
+ reportAction.childMoneyRequestCount,
+ ],
+ );
+
return shouldDisplayParentAction ? (
{
const wasLoginChangedDetected = prevAuthTokenType === 'anonymousAccount' && !props.session.authTokenType;
@@ -159,7 +159,7 @@ function ReportActionsView(props) {
// update ref with current state
prevIsSmallScreenWidthRef.current = props.isSmallScreenWidth;
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [props.isSmallScreenWidth, props.report, props.reportActions, isReportFullyVisible]);
+ }, [props.isSmallScreenWidth, props.reportActions, isReportFullyVisible]);
useEffect(() => {
// Ensures subscription event succeeds when the report/workspace room is created optimistically.
@@ -171,7 +171,7 @@ function ReportActionsView(props) {
Report.subscribeToReportTypingEvents(reportID);
didSubscribeToReportTypingEvents.current = true;
}
- }, [props.report, didSubscribeToReportTypingEvents, reportID]);
+ }, [props.report.pendingFields, didSubscribeToReportTypingEvents, reportID]);
/**
* Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently
@@ -278,14 +278,6 @@ function arePropsEqual(oldProps, newProps) {
return false;
}
- if (!_.isEqual(oldProps.report.pendingFields, newProps.report.pendingFields)) {
- return false;
- }
-
- if (!_.isEqual(oldProps.report.errorFields, newProps.report.errorFields)) {
- return false;
- }
-
if (lodashGet(oldProps.network, 'isOffline') !== lodashGet(newProps.network, 'isOffline')) {
return false;
}
@@ -306,10 +298,6 @@ function arePropsEqual(oldProps, newProps) {
return false;
}
- if (oldProps.report.lastReadTime !== newProps.report.lastReadTime) {
- return false;
- }
-
if (newProps.isSmallScreenWidth !== oldProps.isSmallScreenWidth) {
return false;
}
@@ -318,10 +306,6 @@ function arePropsEqual(oldProps, newProps) {
return false;
}
- if (lodashGet(newProps.report, 'statusNum') !== lodashGet(oldProps.report, 'statusNum') || lodashGet(newProps.report, 'stateNum') !== lodashGet(oldProps.report, 'stateNum')) {
- return false;
- }
-
if (lodashGet(newProps, 'policy.avatar') !== lodashGet(oldProps, 'policy.avatar')) {
return false;
}
@@ -330,35 +314,14 @@ function arePropsEqual(oldProps, newProps) {
return false;
}
- if (lodashGet(newProps, 'report.reportName') !== lodashGet(oldProps, 'report.reportName')) {
- return false;
- }
-
- if (lodashGet(newProps, 'report.description') !== lodashGet(oldProps, 'report.description')) {
- return false;
- }
-
- if (lodashGet(newProps, 'report.managerID') !== lodashGet(oldProps, 'report.managerID')) {
- return false;
- }
-
- if (lodashGet(newProps, 'report.total') !== lodashGet(oldProps, 'report.total')) {
- return false;
- }
-
- if (lodashGet(newProps, 'report.nonReimbursableTotal') !== lodashGet(oldProps, 'report.nonReimbursableTotal')) {
- return false;
- }
-
- if (lodashGet(newProps, 'report.writeCapability') !== lodashGet(oldProps, 'report.writeCapability')) {
- return false;
- }
-
- if (lodashGet(newProps, 'report.participantAccountIDs', 0) !== lodashGet(oldProps, 'report.participantAccountIDs', 0)) {
- return false;
- }
-
- return _.isEqual(lodashGet(newProps.report, 'icons', []), lodashGet(oldProps.report, 'icons', []));
+ return (
+ oldProps.report.lastReadTime === newProps.report.lastReadTime &&
+ oldProps.report.reportID === newProps.report.reportID &&
+ oldProps.report.policyID === newProps.report.policyID &&
+ oldProps.report.lastVisibleActionCreated === newProps.report.lastVisibleActionCreated &&
+ oldProps.report.isOptimisticReport === newProps.report.isOptimisticReport &&
+ _.isEqual(oldProps.report.pendingFields, newProps.report.pendingFields)
+ );
}
const MemoizedReportActionsView = React.memo(ReportActionsView, arePropsEqual);
diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js
index 48bfd5d18bcc..1761e135481a 100644
--- a/src/pages/home/report/ReportFooter.js
+++ b/src/pages/home/report/ReportFooter.js
@@ -1,11 +1,12 @@
import PropTypes from 'prop-types';
-import React from 'react';
+import React, {memo, useCallback} from 'react';
import {Keyboard, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
+import _, {isEqual} from 'underscore';
import AnonymousReportFooter from '@components/AnonymousReportFooter';
import ArchivedReportFooter from '@components/ArchivedReportFooter';
import OfflineIndicator from '@components/OfflineIndicator';
-import participantPropTypes from '@components/participantPropTypes';
+import {usePersonalDetails} from '@components/OnyxProvider';
import SwipeableView from '@components/SwipeableView';
import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
import useNetwork from '@hooks/useNetwork';
@@ -14,7 +15,9 @@ import compose from '@libs/compose';
import * as ReportUtils from '@libs/ReportUtils';
import reportPropTypes from '@pages/reportPropTypes';
import variables from '@styles/variables';
+import * as Report from '@userActions/Report';
import * as Session from '@userActions/Session';
+import * as Task from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ReportActionCompose from './ReportActionCompose/ReportActionCompose';
@@ -24,43 +27,33 @@ const propTypes = {
/** Report object for the current report */
report: reportPropTypes,
- /** Report actions for the current report */
- reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)),
+ lastReportAction: PropTypes.shape(reportActionPropTypes),
- /** Callback fired when the comment is submitted */
- onSubmitComment: PropTypes.func,
+ isEmptyChat: PropTypes.bool,
/** The pending action when we are adding a chat */
pendingAction: PropTypes.string,
- /** Personal details of all the users */
- personalDetails: PropTypes.objectOf(participantPropTypes),
-
- /** Whether the composer input should be shown */
- shouldShowComposeInput: PropTypes.bool,
-
- /** Whether user interactions should be disabled */
- shouldDisableCompose: PropTypes.bool,
-
/** Height of the list which the composer is part of */
listHeight: PropTypes.number,
/** Whetjer the report is ready for display */
isReportReadyForDisplay: PropTypes.bool,
+ /** Whether to show the compose input */
+ shouldShowComposeInput: PropTypes.bool,
+
...windowDimensionsPropTypes,
};
const defaultProps = {
report: {reportID: '0'},
- reportActions: [],
- onSubmitComment: () => {},
pendingAction: null,
- personalDetails: {},
- shouldShowComposeInput: true,
- shouldDisableCompose: false,
listHeight: 0,
isReportReadyForDisplay: true,
+ lastReportAction: null,
+ isEmptyChat: true,
+ shouldShowComposeInput: false,
};
function ReportFooter(props) {
@@ -73,6 +66,52 @@ function ReportFooter(props) {
const isSmallSizeLayout = props.windowWidth - (props.isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint;
const hideComposer = !ReportUtils.canUserPerformWriteAction(props.report);
+ const allPersonalDetails = usePersonalDetails();
+
+ /**
+ * @param {String} text
+ */
+ const handleCreateTask = useCallback(
+ (text) => {
+ /**
+ * Matching task rule by group
+ * Group 1: Start task rule with []
+ * Group 2: Optional email group between \s+....\s* start rule with @+valid email
+ * Group 3: Title is remaining characters
+ */
+ const taskRegex = /^\[\]\s+(?:@([^\s@]+@[\w.-]+\.[a-zA-Z]{2,}))?\s*([\s\S]*)/;
+
+ const match = text.match(taskRegex);
+ if (!match) {
+ return false;
+ }
+ const title = match[2] ? match[2].trim().replace(/\n/g, ' ') : undefined;
+ if (!title) {
+ return false;
+ }
+ const email = match[1] ? match[1].trim() : undefined;
+ let assignee = {};
+ if (email) {
+ assignee = _.find(_.values(allPersonalDetails), (p) => p.login === email) || {};
+ }
+ Task.createTaskAndNavigate(props.report.reportID, title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, props.report.policyID);
+ return true;
+ },
+ [allPersonalDetails, props.report.policyID, props.report.reportID],
+ );
+
+ const onSubmitComment = useCallback(
+ (text) => {
+ const isTaskCreated = handleCreateTask(text);
+ if (isTaskCreated) {
+ return;
+ }
+ Report.addComment(props.report.reportID, text);
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [props.report.reportID, handleCreateTask],
+ );
+
return (
<>
{hideComposer && (
@@ -81,7 +120,6 @@ function ReportFooter(props) {
)}
{isArchivedRoom && }
@@ -94,13 +132,13 @@ function ReportFooter(props) {
@@ -122,4 +160,19 @@ export default compose(
initialValue: false,
},
}),
-)(ReportFooter);
+)(
+ memo(
+ ReportFooter,
+ (prevProps, nextProps) =>
+ isEqual(prevProps.report, nextProps.report) &&
+ prevProps.pendingAction === nextProps.pendingAction &&
+ prevProps.listHeight === nextProps.listHeight &&
+ prevProps.isComposerFullSize === nextProps.isComposerFullSize &&
+ prevProps.isEmptyChat === nextProps.isEmptyChat &&
+ prevProps.lastReportAction === nextProps.lastReportAction &&
+ prevProps.shouldShowComposeInput === nextProps.shouldShowComposeInput &&
+ prevProps.windowWidth === nextProps.windowWidth &&
+ prevProps.isSmallScreenWidth === nextProps.isSmallScreenWidth &&
+ prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay,
+ ),
+);
diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js
index 785f1e3f6a1e..41471eaa50de 100755
--- a/src/pages/home/report/ReportTypingIndicator.js
+++ b/src/pages/home/report/ReportTypingIndicator.js
@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
-import React, {useMemo} from 'react';
+import React, {memo, useMemo} from 'react';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import Text from '@components/Text';
@@ -68,4 +68,4 @@ export default withOnyx({
key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`,
initialValue: {},
},
-})(ReportTypingIndicator);
+})(memo(ReportTypingIndicator));
diff --git a/src/pages/iou/SplitBillDetailsPage.js b/src/pages/iou/SplitBillDetailsPage.js
index 94c2f1c31242..be3afb822723 100644
--- a/src/pages/iou/SplitBillDetailsPage.js
+++ b/src/pages/iou/SplitBillDetailsPage.js
@@ -102,6 +102,7 @@ function SplitBillDetailsPage(props) {
created: splitCreated,
category: splitCategory,
tag: splitTag,
+ billable: splitBillable,
} = isEditingSplitBill && props.draftTransaction ? ReportUtils.getTransactionDetails(props.draftTransaction) : ReportUtils.getTransactionDetails(props.transaction);
const onConfirm = useCallback(
@@ -133,6 +134,7 @@ function SplitBillDetailsPage(props) {
iouMerchant={splitMerchant}
iouCategory={splitCategory}
iouTag={splitTag}
+ iouIsBillable={splitBillable}
iouType={CONST.IOU.TYPE.SPLIT}
isReadOnly={!isEditingSplitBill}
shouldShowSmartScanFields
@@ -146,7 +148,7 @@ function SplitBillDetailsPage(props) {
transaction={isEditingSplitBill ? props.draftTransaction || props.transaction : props.transaction}
onConfirm={onConfirm}
isPolicyExpenseChat={ReportUtils.isPolicyExpenseChat(props.report)}
- policyID={ReportUtils.isPolicyExpenseChat(props.report) && props.report.policyID}
+ policyID={ReportUtils.isPolicyExpenseChat(props.report) ? props.report.policyID : null}
/>
)}
diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
index 15f98205839e..e47b0b8198ea 100644
--- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
+++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
@@ -13,8 +13,8 @@ import SelectCircle from '@components/SelectCircle';
import SelectionList from '@components/SelectionList';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
+import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as Report from '@libs/actions/Report';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReportUtils from '@libs/ReportUtils';
@@ -88,6 +88,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : '';
const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
+ const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached);
/**
* Returns the sections needed for the OptionsSelector
@@ -133,9 +134,10 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
participants,
chatOptions.recentReports,
chatOptions.personalDetails,
+ maxParticipantsReached,
+ indexOffset,
personalDetails,
true,
- indexOffset,
);
newSections.push(formatResults.section);
indexOffset = formatResults.newIndexOffset;
@@ -243,14 +245,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
[maxParticipantsReached, newChatOptions, participants, searchTerm],
);
- // When search term updates we will fetch any reports
- const setSearchTermAndSearchInServer = useCallback((text = '') => {
- if (text.length) {
- Report.searchInServer(text);
- }
- setSearchTerm(text);
- }, []);
-
// Right now you can't split a request with a workspace and other additional participants
// This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent
// the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js
index 6028a735d132..0f1c2b27ad2e 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.js
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js
@@ -226,6 +226,7 @@ function IOURequestStepConfirmation({
transaction.tag,
receiptFile,
report.reportID,
+ transaction.billable,
);
return;
}
@@ -244,6 +245,7 @@ function IOURequestStepConfirmation({
transaction.category,
transaction.tag,
report.reportID,
+ transaction.billable,
);
return;
}
@@ -260,6 +262,7 @@ function IOURequestStepConfirmation({
transaction.merchant,
transaction.category,
transaction.tag,
+ transaction.billable,
);
return;
}
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
index 59081599736c..daaa63aae147 100755
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
@@ -13,8 +13,8 @@ import SelectCircle from '@components/SelectCircle';
import SelectionList from '@components/SelectionList';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
+import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as Report from '@libs/actions/Report';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import reportPropTypes from '@pages/reportPropTypes';
@@ -89,6 +89,9 @@ function MoneyRequestParticipantsSelector({
const {isOffline} = useNetwork();
const personalDetails = usePersonalDetails();
+ const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
+ const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached);
+
const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : '';
const newChatOptions = useMemo(() => {
@@ -124,8 +127,6 @@ function MoneyRequestParticipantsSelector({
};
}, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest]);
- const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
-
/**
* Returns the sections needed for the OptionsSelector
*
@@ -140,9 +141,10 @@ function MoneyRequestParticipantsSelector({
participants,
newChatOptions.recentReports,
newChatOptions.personalDetails,
+ maxParticipantsReached,
+ indexOffset,
personalDetails,
true,
- indexOffset,
);
newSections.push(formatResults.section);
indexOffset = formatResults.newIndexOffset;
@@ -259,12 +261,6 @@ function MoneyRequestParticipantsSelector({
[maxParticipantsReached, newChatOptions.personalDetails.length, newChatOptions.recentReports.length, newChatOptions.userToInvite, participants, searchTerm],
);
- // When search term updates we will fetch any reports
- const setSearchTermAndSearchInServer = useCallback((text = '') => {
- Report.searchInServer(text);
- setSearchTerm(text);
- }, []);
-
// Right now you can't split a request with a workspace and other additional participants
// This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent
// the app from crashing on native when you try to do this, we'll going to show error message if you have a workspace and other participants
diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx
index 54b4cca84e9e..70198f38f18c 100644
--- a/src/pages/workspace/WorkspacePageWithSections.tsx
+++ b/src/pages/workspace/WorkspacePageWithSections.tsx
@@ -1,4 +1,3 @@
-import type {RouteProp} from '@react-navigation/native';
import type {ReactNode} from 'react';
import React, {useEffect, useMemo, useRef} from 'react';
import {View} from 'react-native';
@@ -22,6 +21,7 @@ import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import type {Policy, ReimbursementAccount, User} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import type {PolicyRoute} from './withPolicy';
import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading';
import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
@@ -42,10 +42,10 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps &
headerText: string;
/** The route object passed to this page from the navigator */
- route: RouteProp<{params: {policyID: string}}>;
+ route: PolicyRoute;
/** Main content of the page */
- children: (hasVBA?: boolean, policyID?: string, isUsingECard?: boolean) => ReactNode;
+ children: (hasVBA: boolean, policyID: string, isUsingECard: boolean) => ReactNode;
/** Content to be added as fixed footer */
footer?: ReactNode;
@@ -107,7 +107,7 @@ function WorkspacePageWithSections({
const isLoading = reimbursementAccount?.isLoading ?? true;
const achState = reimbursementAccount?.achData?.state ?? '';
const isUsingECard = user?.isUsingExpensifyCard ?? false;
- const policyID = route.params.policyID;
+ const policyID = route.params?.policyID ?? '';
const hasVBA = achState === BankAccount.STATE.OPEN;
const content = children(hasVBA, policyID, isUsingECard);
const {isSmallScreenWidth} = useWindowDimensions();
diff --git a/src/pages/workspace/bills/WorkspaceBillsFirstSection.js b/src/pages/workspace/bills/WorkspaceBillsFirstSection.tsx
similarity index 54%
rename from src/pages/workspace/bills/WorkspaceBillsFirstSection.js
rename to src/pages/workspace/bills/WorkspaceBillsFirstSection.tsx
index 0007c301482b..638ab9d58c31 100644
--- a/src/pages/workspace/bills/WorkspaceBillsFirstSection.js
+++ b/src/pages/workspace/bills/WorkspaceBillsFirstSection.tsx
@@ -1,57 +1,48 @@
import Str from 'expensify-common/lib/str';
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
import CopyTextToClipboard from '@components/CopyTextToClipboard';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import Section from '@components/Section';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
-import userPropTypes from '@pages/settings/userPropTypes';
import * as Link from '@userActions/Link';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {Session, User} from '@src/types/onyx';
-const propTypes = {
- /** The policy ID currently being configured */
- policyID: PropTypes.string.isRequired,
-
- ...withLocalizePropTypes,
-
- /* From Onyx */
+type WorkspaceBillsFirstSectionOnyxProps = {
/** Session of currently logged in user */
- session: PropTypes.shape({
- /** Email address */
- email: PropTypes.string.isRequired,
- }),
+ session: OnyxEntry;
/** Information about the logged in user's account */
- user: userPropTypes,
+ user: OnyxEntry;
};
-const defaultProps = {
- session: {
- email: null,
- },
- user: {},
+type WorkspaceBillsFirstSectionProps = WorkspaceBillsFirstSectionOnyxProps & {
+ /** The policy ID currently being configured */
+ policyID: string;
};
-function WorkspaceBillsFirstSection(props) {
+function WorkspaceBillsFirstSection({session, policyID, user}: WorkspaceBillsFirstSectionProps) {
const styles = useThemeStyles();
- const emailDomain = Str.extractEmailDomain(props.session.email);
- const manageYourBillsUrl = `reports?policyID=${props.policyID}&from=all&type=bill&showStates=Open,Processing,Approved,Reimbursed,Archived&isAdvancedFilterMode=true`;
+ const {translate} = useLocalize();
+
+ const emailDomain = Str.extractEmailDomain(session?.email ?? '');
+ const manageYourBillsUrl = `reports?policyID=${policyID}&from=all&type=bill&showStates=Open,Processing,Approved,Reimbursed,Archived&isAdvancedFilterMode=true`;
+
return (
Link.openOldDotLink(manageYourBillsUrl),
icon: Expensicons.Bill,
shouldShowRightIcon: true,
@@ -60,40 +51,35 @@ function WorkspaceBillsFirstSection(props) {
link: () => Link.buildOldDotURL(manageYourBillsUrl),
},
]}
- containerStyles={[styles.cardSectionContainer]}
+ containerStyles={styles.cardSectionContainer}
>
-
+
- {props.translate('workspace.bills.askYourVendorsBeforeEmail')}
- {props.user.isFromPublicDomain ? (
+ {translate('workspace.bills.askYourVendorsBeforeEmail')}
+ {user?.isFromPublicDomain ? (
Link.openExternalLink('https://community.expensify.com/discussion/7500/how-to-pay-your-company-bills-in-expensify/')}>
example.com@expensify.cash
) : (
)}
- {props.translate('workspace.bills.askYourVendorsAfterEmail')}
+ {translate('workspace.bills.askYourVendorsAfterEmail')}
);
}
-WorkspaceBillsFirstSection.propTypes = propTypes;
-WorkspaceBillsFirstSection.defaultProps = defaultProps;
WorkspaceBillsFirstSection.displayName = 'WorkspaceBillsFirstSection';
-export default compose(
- withLocalize,
- withOnyx({
- session: {
- key: ONYXKEYS.SESSION,
- },
- user: {
- key: ONYXKEYS.USER,
- },
- }),
-)(WorkspaceBillsFirstSection);
+export default withOnyx({
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ user: {
+ key: ONYXKEYS.USER,
+ },
+})(WorkspaceBillsFirstSection);
diff --git a/src/pages/workspace/bills/WorkspaceBillsNoVBAView.js b/src/pages/workspace/bills/WorkspaceBillsNoVBAView.tsx
similarity index 53%
rename from src/pages/workspace/bills/WorkspaceBillsNoVBAView.js
rename to src/pages/workspace/bills/WorkspaceBillsNoVBAView.tsx
index 01a054df94ce..3503d8565a39 100644
--- a/src/pages/workspace/bills/WorkspaceBillsNoVBAView.js
+++ b/src/pages/workspace/bills/WorkspaceBillsNoVBAView.tsx
@@ -1,45 +1,43 @@
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import ConnectBankAccountButton from '@components/ConnectBankAccountButton';
import * as Illustrations from '@components/Icon/Illustrations';
import Section from '@components/Section';
import Text from '@components/Text';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import WorkspaceBillsFirstSection from './WorkspaceBillsFirstSection';
-const propTypes = {
+type WorkspaceBillsNoVBAViewProps = {
/** The policy ID currently being configured */
- policyID: PropTypes.string.isRequired,
-
- ...withLocalizePropTypes,
+ policyID: string;
};
-function WorkspaceBillsNoVBAView(props) {
+function WorkspaceBillsNoVBAView({policyID}: WorkspaceBillsNoVBAViewProps) {
const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
return (
<>
-
+
-
- {props.translate('workspace.bills.unlockNoVBACopy')}
+
+ {translate('workspace.bills.unlockNoVBACopy')}
>
);
}
-WorkspaceBillsNoVBAView.propTypes = propTypes;
WorkspaceBillsNoVBAView.displayName = 'WorkspaceBillsNoVBAView';
-export default withLocalize(WorkspaceBillsNoVBAView);
+export default WorkspaceBillsNoVBAView;
diff --git a/src/pages/workspace/bills/WorkspaceBillsPage.js b/src/pages/workspace/bills/WorkspaceBillsPage.tsx
similarity index 60%
rename from src/pages/workspace/bills/WorkspaceBillsPage.js
rename to src/pages/workspace/bills/WorkspaceBillsPage.tsx
index 3a23c3f17f47..85cceb29b661 100644
--- a/src/pages/workspace/bills/WorkspaceBillsPage.js
+++ b/src/pages/workspace/bills/WorkspaceBillsPage.tsx
@@ -1,40 +1,32 @@
-import PropTypes from 'prop-types';
+import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
import {View} from 'react-native';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import type {CentralPaneNavigatorParamList} from '@navigation/types';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
import CONST from '@src/CONST';
+import type SCREENS from '@src/SCREENS';
import WorkspaceBillsNoVBAView from './WorkspaceBillsNoVBAView';
import WorkspaceBillsVBAView from './WorkspaceBillsVBAView';
-const propTypes = {
- /** 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,
+type WorkspaceBillsPageProps = StackScreenProps;
- ...withLocalizePropTypes,
-};
-
-function WorkspaceBillsPage(props) {
+function WorkspaceBillsPage({route}: WorkspaceBillsPageProps) {
+ const {translate} = useLocalize();
const styles = useThemeStyles();
const {isSmallScreenWidth} = useWindowDimensions();
return (
- {(hasVBA, policyID) => (
+ {(hasVBA: boolean, policyID: string) => (
{!hasVBA && }
{hasVBA && }
@@ -44,6 +36,6 @@ function WorkspaceBillsPage(props) {
);
}
-WorkspaceBillsPage.propTypes = propTypes;
WorkspaceBillsPage.displayName = 'WorkspaceBillsPage';
-export default withLocalize(WorkspaceBillsPage);
+
+export default WorkspaceBillsPage;
diff --git a/src/pages/workspace/bills/WorkspaceBillsVBAView.js b/src/pages/workspace/bills/WorkspaceBillsVBAView.tsx
similarity index 60%
rename from src/pages/workspace/bills/WorkspaceBillsVBAView.js
rename to src/pages/workspace/bills/WorkspaceBillsVBAView.tsx
index 467f3583b7ba..4709061b8567 100644
--- a/src/pages/workspace/bills/WorkspaceBillsVBAView.js
+++ b/src/pages/workspace/bills/WorkspaceBillsVBAView.tsx
@@ -1,37 +1,36 @@
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import Section from '@components/Section';
import Text from '@components/Text';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Link from '@userActions/Link';
import WorkspaceBillsFirstSection from './WorkspaceBillsFirstSection';
-const propTypes = {
+type WorkspaceBillsVBAViewProps = {
/** The policy ID currently being configured */
- policyID: PropTypes.string.isRequired,
-
- ...withLocalizePropTypes,
+ policyID: string;
};
-function WorkspaceBillsVBAView(props) {
+function WorkspaceBillsVBAView({policyID}: WorkspaceBillsVBAViewProps) {
const styles = useThemeStyles();
- const reportsUrl = `reports?policyID=${props.policyID}&from=all&type=bill&showStates=Processing,Approved&isAdvancedFilterMode=true`;
+ const {translate} = useLocalize();
+
+ const reportsUrl = `reports?policyID=${policyID}&from=all&type=bill&showStates=Processing,Approved&isAdvancedFilterMode=true`;
return (
<>
-
+
Link.openOldDotLink(reportsUrl),
icon: Expensicons.Bill,
shouldShowRightIcon: true,
@@ -41,15 +40,14 @@ function WorkspaceBillsVBAView(props) {
},
]}
>
-
- {props.translate('workspace.bills.VBACopy')}
+
+ {translate('workspace.bills.VBACopy')}
>
);
}
-WorkspaceBillsVBAView.propTypes = propTypes;
WorkspaceBillsVBAView.displayName = 'WorkspaceBillsVBAView';
-export default withLocalize(WorkspaceBillsVBAView);
+export default WorkspaceBillsVBAView;
diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
index 8885d9b1e3af..96aa350496b5 100644
--- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
+++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
@@ -1,18 +1,17 @@
-import type {RouteProp} from '@react-navigation/native';
+import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import type {CentralPaneNavigatorParamList} from '@navigation/types';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
import CONST from '@src/CONST';
+import type SCREENS from '@src/SCREENS';
import WorkspaceInvoicesNoVBAView from './WorkspaceInvoicesNoVBAView';
import WorkspaceInvoicesVBAView from './WorkspaceInvoicesVBAView';
-/** Defined route object that contains the policyID param, WorkspacePageWithSections is a common component for Workspaces and expect the route prop that includes the policyID */
-type WorkspaceInvoicesPageProps = {
- route: RouteProp<{params: {policyID: string}}>;
-};
+type WorkspaceInvoicesPageProps = StackScreenProps;
function WorkspaceInvoicesPage({route}: WorkspaceInvoicesPageProps) {
const {translate} = useLocalize();
diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx
index ec38b61fb0dc..8764412c87ad 100644
--- a/src/pages/workspace/withPolicy.tsx
+++ b/src/pages/workspace/withPolicy.tsx
@@ -5,13 +5,17 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react';
import React, {forwardRef} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import type {BottomTabNavigatorParamList, CentralPaneNavigatorParamList, SettingsNavigatorParamList} from '@navigation/types';
import policyMemberPropType from '@pages/policyMemberPropType';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
-type PolicyRoute = RouteProp<{params: {policyID: string}}>;
+type WorkspaceParamList = BottomTabNavigatorParamList & CentralPaneNavigatorParamList & SettingsNavigatorParamList;
+type PolicyRoute = RouteProp>;
function getPolicyIDFromRoute(route: PolicyRoute): string {
return route?.params?.policyID ?? '';
@@ -131,4 +135,4 @@ export default function (WrappedComponent:
}
export {policyPropTypes, policyDefaultProps};
-export type {WithPolicyOnyxProps, WithPolicyProps};
+export type {WithPolicyOnyxProps, WithPolicyProps, PolicyRoute};
diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts
index 4312b7adb453..3235f340e723 100644
--- a/src/types/onyx/Form.ts
+++ b/src/types/onyx/Form.ts
@@ -61,6 +61,8 @@ type WorkspaceSettingsForm = Form<{
name: string;
}>;
+type ReportFieldEditForm = Form>;
+
export default Form;
export type {
@@ -75,4 +77,5 @@ export type {
IntroSchoolPrincipalForm,
PersonalBankAccountForm,
WorkspaceSettingsForm,
+ ReportFieldEditForm,
};
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index 070b91e2d920..98732df57e38 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -284,6 +284,7 @@ export type {
OriginalMessageIOU,
OriginalMessageCreated,
OriginalMessageAddComment,
+ OriginalMessageChronosOOOList,
OriginalMessageSource,
OriginalMessageReimbursementDequeued,
PaymentMethodType,
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index 96ed47f210c9..f55b3b797bf0 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -85,13 +85,18 @@ type Policy = {
/** Whether the auto reporting is enabled */
autoReporting?: boolean;
- /** The scheduled submit frequency set up on the this policy */
+ /** The scheduled submit frequency set up on this policy */
autoReportingFrequency?: ValueOf;
- /** Whether the scheduled submit is enabled */
+ /** @deprecated Whether the scheduled submit is enabled */
isHarvestingEnabled?: boolean;
/** Whether the scheduled submit is enabled */
+ harvesting?: {
+ enabled: boolean;
+ };
+
+ /** Whether the self approval or submitting is enabled */
isPreventSelfApprovalEnabled?: boolean;
/** When the monthly scheduled submit should happen */
diff --git a/src/types/onyx/PolicyReportField.ts b/src/types/onyx/PolicyReportField.ts
index a1724a9ff52f..de385070aa25 100644
--- a/src/types/onyx/PolicyReportField.ts
+++ b/src/types/onyx/PolicyReportField.ts
@@ -19,6 +19,9 @@ type PolicyReportField = {
/** Tells if the field is required or not */
deletable: boolean;
+ /** Value of the field */
+ value: string;
+
/** Options to select from if field is of type dropdown */
values: string[];
};
diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts
index f3b20c68038e..d26bdd4f282e 100644
--- a/src/types/onyx/Report.ts
+++ b/src/types/onyx/Report.ts
@@ -2,6 +2,7 @@ import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
import type * as OnyxCommon from './OnyxCommon';
import type PersonalDetails from './PersonalDetails';
+import type {PolicyReportField} from './PolicyReportField';
type NotificationPreference = ValueOf;
@@ -170,7 +171,7 @@ type Report = {
selected?: boolean;
/** If the report contains reportFields, save the field id and its value */
- reportFields?: Record;
+ reportFields?: Record;
};
export default Report;
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index d742814061d5..939793b3b4a8 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -9,7 +9,17 @@ import type Credentials from './Credentials';
import type Currency from './Currency';
import type CustomStatusDraft from './CustomStatusDraft';
import type Download from './Download';
-import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm, PrivateNotesForm, WorkspaceSettingsForm} from './Form';
+import type {
+ AddDebitCardForm,
+ DateOfBirthForm,
+ DisplayNameForm,
+ IKnowATeacherForm,
+ IntroSchoolPrincipalForm,
+ NewRoomForm,
+ PrivateNotesForm,
+ ReportFieldEditForm,
+ WorkspaceSettingsForm,
+} from './Form';
import type Form from './Form';
import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji';
import type {FundList} from './Fund';
@@ -152,4 +162,5 @@ export type {
IKnowATeacherForm,
IntroSchoolPrincipalForm,
PrivateNotesForm,
+ ReportFieldEditForm,
};
diff --git a/tests/perf-test/SignInPage.perf-test.tsx b/tests/perf-test/SignInPage.perf-test.tsx
index 80964c3c49cd..dde8596fb2ae 100644
--- a/tests/perf-test/SignInPage.perf-test.tsx
+++ b/tests/perf-test/SignInPage.perf-test.tsx
@@ -18,17 +18,6 @@ import * as TestHelper from '../utils/TestHelper';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates';
-jest.mock('../../src/libs/Navigation/Navigation', () => {
- const actualNav = jest.requireActual('../../src/libs/Navigation/Navigation');
- return {
- ...actualNav,
- navigationRef: {
- addListener: () => jest.fn(),
- removeListener: () => jest.fn(),
- },
- } as typeof Navigation;
-});
-
const mockedNavigate = jest.fn();
jest.mock('@react-navigation/native', () => {
const actualNav = jest.requireActual('@react-navigation/native');
@@ -43,7 +32,10 @@ jest.mock('@react-navigation/native', () => {
navigate: jest.fn(),
addListener: () => jest.fn(),
}),
- createNavigationContainerRef: jest.fn(),
+ createNavigationContainerRef: () => ({
+ addListener: () => jest.fn(),
+ removeListener: () => jest.fn(),
+ }),
} as typeof NativeNavigation;
});
diff --git a/tests/unit/ReportActionItemSingleTest.js b/tests/unit/ReportActionItemSingleTest.js
index 55cae01c19f1..08fac8e77551 100644
--- a/tests/unit/ReportActionItemSingleTest.js
+++ b/tests/unit/ReportActionItemSingleTest.js
@@ -1,4 +1,4 @@
-import {cleanup, screen} from '@testing-library/react-native';
+import {cleanup, screen, waitFor} from '@testing-library/react-native';
import Onyx from 'react-native-onyx';
import * as LHNTestUtils from '../utils/LHNTestUtils';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
@@ -70,7 +70,9 @@ describe('ReportActionItemSingle', () => {
const expectedSecondaryIconTestId = 'SvgDefaultAvatar_w Icon';
return setup().then(() => {
- expect(screen.getByTestId(expectedSecondaryIconTestId)).toBeDefined();
+ waitFor(() => {
+ expect(screen.getByTestId(expectedSecondaryIconTestId)).toBeDefined();
+ });
});
});
diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js
index 6c72558e5df3..04246c1c438a 100644
--- a/tests/utils/LHNTestUtils.js
+++ b/tests/utils/LHNTestUtils.js
@@ -256,7 +256,9 @@ function getFakePolicy(id = 1, name = 'Workspace-Test-001') {
lastModified: 1697323926777105,
autoReporting: true,
autoReportingFrequency: 'immediate',
- isHarvestingEnabled: true,
+ harvesting: {
+ enabled: true,
+ },
autoReportingOffset: 1,
isPreventSelfApprovalEnabled: true,
submitsTo: 123456,
diff --git a/tests/utils/collections/policies.ts b/tests/utils/collections/policies.ts
index 8547c171c7a7..4223c7e41941 100644
--- a/tests/utils/collections/policies.ts
+++ b/tests/utils/collections/policies.ts
@@ -11,7 +11,9 @@ export default function createRandomPolicy(index: number): Policy {
autoReporting: randBoolean(),
isPolicyExpenseChatEnabled: randBoolean(),
autoReportingFrequency: rand(Object.values(CONST.POLICY.AUTO_REPORTING_FREQUENCIES)),
- isHarvestingEnabled: randBoolean(),
+ harvesting: {
+ enabled: randBoolean(),
+ },
autoReportingOffset: 1,
isPreventSelfApprovalEnabled: randBoolean(),
submitsTo: index,
diff --git a/web/index.html b/web/index.html
index aff3feb87dbb..49ffd0d0a62f 100644
--- a/web/index.html
+++ b/web/index.html
@@ -111,10 +111,6 @@
transition-property: opacity;
}
- .splash-logo > svg {
- color: #03d47c;
- }
-
.animation {
display: flex;
}