, 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) ?? '';
@@ -4538,32 +4628,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
*/
@@ -4790,7 +4854,6 @@ export {
canEditWriteCapability,
hasSmartscanError,
shouldAutoFocusOnKeyPress,
- getReportFieldTitle,
shouldDisplayThreadReplies,
shouldDisableThread,
doesReportBelongToWorkspace,
@@ -4798,6 +4861,8 @@ export {
isReportParticipant,
isValidReport,
isReportFieldOfTypeTitle,
+ isReportFieldDisabled,
+ getAvailableReportFields,
};
export type {
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 9119907e9393..066f6fa65bd1 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 a9e1b09ed984..d5095a19b1ed 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 1f6905cfb8e0..221c7f8d4f88 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -63,7 +63,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';
@@ -175,6 +175,12 @@ Linking.getInitialURL().then((url) => {
reportIDDeeplinkedFromOldDot = reportID;
});
+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}`;
@@ -1462,6 +1468,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) {
@@ -2721,5 +2858,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 47ade872d25a..df71bf47b0ac 100755
--- a/src/pages/DetailsPage.js
+++ b/src/pages/DetailsPage.js
@@ -137,6 +137,7 @@ function DetailsPage(props) {
headerTitle={displayName}
source={UserUtils.getFullSizeAvatar(details.avatar, details.accountID)}
originalFileName={details.originalFileName}
+ maybeIcon
>
{({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}
>
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/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/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx
index 89d863c47baa..4d9bd168f2f8 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 React, {useEffect, useMemo, useRef} from 'react';
import type {ReactNode} from 'react';
import {View} from 'react-native';
@@ -21,6 +20,7 @@ import ROUTES from '@src/ROUTES';
import type {Route} 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';
@@ -41,10 +41,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;
@@ -93,7 +93,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 policyName = policy?.name;
const hasVBA = achState === BankAccount.STATE.OPEN;
const content = children(hasVBA, policyID, isUsingECard);
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 3f7f59180c8b..5931e8b14843 100644
--- a/src/pages/workspace/bills/WorkspaceBillsFirstSection.js
+++ b/src/pages/workspace/bills/WorkspaceBillsFirstSection.tsx
@@ -1,56 +1,47 @@
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,
@@ -59,40 +50,35 @@ function WorkspaceBillsFirstSection(props) {
link: () => Link.buildOldDotURL(manageYourBillsUrl),
},
]}
- containerStyles={[styles.cardSection]}
+ containerStyles={styles.cardSection}
>
-
+
- {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 50%
rename from src/pages/workspace/bills/WorkspaceBillsNoVBAView.js
rename to src/pages/workspace/bills/WorkspaceBillsNoVBAView.tsx
index 8211f70163d5..72f4d0ad2b8e 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.js
deleted file mode 100644
index c607071a4365..000000000000
--- a/src/pages/workspace/bills/WorkspaceBillsPage.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
-import CONST from '@src/CONST';
-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,
-
- ...withLocalizePropTypes,
-};
-
-function WorkspaceBillsPage(props) {
- return (
-
- {(hasVBA, policyID) => (
- <>
- {!hasVBA && }
- {hasVBA && }
- >
- )}
-
- );
-}
-
-WorkspaceBillsPage.propTypes = propTypes;
-WorkspaceBillsPage.displayName = 'WorkspaceBillsPage';
-export default withLocalize(WorkspaceBillsPage);
diff --git a/src/pages/workspace/bills/WorkspaceBillsPage.tsx b/src/pages/workspace/bills/WorkspaceBillsPage.tsx
new file mode 100644
index 000000000000..da86839f39c1
--- /dev/null
+++ b/src/pages/workspace/bills/WorkspaceBillsPage.tsx
@@ -0,0 +1,35 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React from 'react';
+import useLocalize from '@hooks/useLocalize';
+import type {SettingsNavigatorParamList} 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';
+
+type WorkspaceBillsPageProps = StackScreenProps;
+
+function WorkspaceBillsPage({route}: WorkspaceBillsPageProps) {
+ const {translate} = useLocalize();
+
+ return (
+
+ {(hasVBA: boolean, policyID: string) => (
+ <>
+ {!hasVBA && }
+ {hasVBA && }
+ >
+ )}
+
+ );
+}
+
+WorkspaceBillsPage.displayName = 'WorkspaceBillsPage';
+
+export default WorkspaceBillsPage;
diff --git a/src/pages/workspace/bills/WorkspaceBillsVBAView.js b/src/pages/workspace/bills/WorkspaceBillsVBAView.tsx
similarity index 59%
rename from src/pages/workspace/bills/WorkspaceBillsVBAView.js
rename to src/pages/workspace/bills/WorkspaceBillsVBAView.tsx
index dd9c1c7fbaf6..ae06fac378dd 100644
--- a/src/pages/workspace/bills/WorkspaceBillsVBAView.js
+++ b/src/pages/workspace/bills/WorkspaceBillsVBAView.tsx
@@ -1,36 +1,35 @@
-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,
@@ -40,15 +39,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 79ff76204c69..ffd9a700ae7e 100644
--- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
+++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
@@ -1,15 +1,14 @@
-import type {RouteProp} from '@react-navigation/native';
+import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
import useLocalize from '@hooks/useLocalize';
+import type {SettingsNavigatorParamList} 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..aee03f1f74e9 100644
--- a/src/pages/workspace/withPolicy.tsx
+++ b/src/pages/workspace/withPolicy.tsx
@@ -5,13 +5,16 @@ 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 {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 PolicyRoute = RouteProp>;
function getPolicyIDFromRoute(route: PolicyRoute): string {
return route?.params?.policyID ?? '';
@@ -131,4 +134,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 c015627bfbc2..9c6d52a1020d 100644
--- a/src/types/onyx/Form.ts
+++ b/src/types/onyx/Form.ts
@@ -57,6 +57,8 @@ type PrivateNotesForm = Form<{
type PersonalBankAccountForm = Form;
+type ReportFieldEditForm = Form>;
+
export default Form;
export type {
@@ -70,4 +72,5 @@ export type {
IKnowATeacherForm,
IntroSchoolPrincipalForm,
PersonalBankAccountForm,
+ 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 2a3477ba7c2c..fe50bbb497d2 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -88,10 +88,15 @@ type 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 5b04cae58671..64eec736b5bf 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -9,7 +9,7 @@ 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} from './Form';
+import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm, PrivateNotesForm, ReportFieldEditForm} from './Form';
import type Form from './Form';
import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji';
import type {FundList} from './Fund';
@@ -151,4 +151,5 @@ export type {
IKnowATeacherForm,
IntroSchoolPrincipalForm,
PrivateNotesForm,
+ ReportFieldEditForm,
};
diff --git a/tests/perf-test/ReportScreen.perf-test.js b/tests/perf-test/ReportScreen.perf-test.js
index 5a144e715f5b..dbc7775d066e 100644
--- a/tests/perf-test/ReportScreen.perf-test.js
+++ b/tests/perf-test/ReportScreen.perf-test.js
@@ -15,7 +15,10 @@ import * as Localize from '../../src/libs/Localize';
import ONYXKEYS from '../../src/ONYXKEYS';
import {ReportAttachmentsProvider} from '../../src/pages/home/report/ReportAttachmentsContext';
import ReportScreen from '../../src/pages/home/ReportScreen';
-import * as LHNTestUtils from '../utils/LHNTestUtils';
+import createCollection from '../utils/collections/createCollection';
+import createPersonalDetails from '../utils/collections/personalDetails';
+import createRandomPolicy from '../utils/collections/policies';
+import createRandomReport from '../utils/collections/reports';
import PusherHelper from '../utils/PusherHelper';
import * as ReportTestUtils from '../utils/ReportTestUtils';
import * as TestHelper from '../utils/TestHelper';
@@ -56,6 +59,7 @@ jest.mock('../../src/hooks/useEnvironment', () =>
jest.mock('../../src/libs/Permissions', () => ({
canUseLinkPreviews: jest.fn(() => true),
+ canUseDefaultRooms: jest.fn(() => true),
}));
jest.mock('../../src/hooks/usePermissions.ts');
@@ -103,6 +107,18 @@ afterEach(() => {
PusherHelper.teardown();
});
+const policies = createCollection(
+ (item) => `${ONYXKEYS.COLLECTION.POLICY}${item.id}`,
+ (index) => createRandomPolicy(index),
+ 10,
+);
+
+const personalDetails = createCollection(
+ (item) => item.accountID,
+ (index) => createPersonalDetails(index),
+ 20,
+);
+
/**
* This is a helper function to create a mock for the addListener function of the react-navigation library.
* The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate
@@ -152,7 +168,11 @@ function ReportScreenWrapper(args) {
);
}
-test.skip('[ReportScreen] should render ReportScreen with composer interactions', () => {
+const report = {...createRandomReport(1), policyID: '1'};
+const reportActions = ReportTestUtils.getMockedReportActionsMap(500);
+const mockRoute = {params: {reportID: '1'}};
+
+test('[ReportScreen] should render ReportScreen with composer interactions', () => {
const {triggerTransitionEnd, addListener} = createAddListenerMock();
const scenario = async () => {
/**
@@ -166,9 +186,6 @@ test.skip('[ReportScreen] should render ReportScreen with composer interactions'
await act(triggerTransitionEnd);
- // Query for the report list
- await screen.findByTestId('report-actions-list');
-
// Query for the composer
const composer = await screen.findByTestId('composer');
@@ -189,15 +206,6 @@ test.skip('[ReportScreen] should render ReportScreen with composer interactions'
await screen.findByLabelText(hintHeaderText);
};
- const policy = {
- policyID: 1,
- name: 'Testing Policy',
- };
-
- const report = LHNTestUtils.getFakeReport();
- const reportActions = ReportTestUtils.getMockedReportActionsMap(1000);
- const mockRoute = {params: {reportID: '1'}};
-
const navigation = {addListener};
return waitForBatchedUpdates()
@@ -206,9 +214,9 @@ test.skip('[ReportScreen] should render ReportScreen with composer interactions'
[ONYXKEYS.IS_SIDEBAR_LOADED]: true,
[`${ONYXKEYS.COLLECTION.REPORT}${mockRoute.params.reportID}`]: report,
[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${mockRoute.params.reportID}`]: reportActions,
- [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails,
+ [ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails,
[ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS],
- [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy,
+ [`${ONYXKEYS.COLLECTION.POLICY}`]: policies,
[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${mockRoute.params.reportID}`]: {
isLoadingReportActions: false,
},
@@ -225,7 +233,7 @@ test.skip('[ReportScreen] should render ReportScreen with composer interactions'
);
});
-test.skip('[ReportScreen] should press of the report item', () => {
+test('[ReportScreen] should press of the report item', () => {
const {triggerTransitionEnd, addListener} = createAddListenerMock();
const scenario = async () => {
/**
@@ -242,9 +250,6 @@ test.skip('[ReportScreen] should press of the report item', () => {
// Query for the report list
await screen.findByTestId('report-actions-list');
- // Query for the composer
- await screen.findByTestId('composer');
-
const hintReportPreviewText = Localize.translateLocal('iou.viewDetails');
// Query for report preview buttons
@@ -254,15 +259,6 @@ test.skip('[ReportScreen] should press of the report item', () => {
fireEvent.press(reportPreviewButtons[0]);
};
- const policy = {
- policyID: 123,
- name: 'Testing Policy',
- };
-
- const report = LHNTestUtils.getFakeReport();
- const reportActions = ReportTestUtils.getMockedReportActionsMap(1000);
- const mockRoute = {params: {reportID: '2'}};
-
const navigation = {addListener};
return waitForBatchedUpdates()
@@ -271,9 +267,9 @@ test.skip('[ReportScreen] should press of the report item', () => {
[ONYXKEYS.IS_SIDEBAR_LOADED]: true,
[`${ONYXKEYS.COLLECTION.REPORT}${mockRoute.params.reportID}`]: report,
[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${mockRoute.params.reportID}`]: reportActions,
- [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails,
+ [ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails,
[ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS],
- [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy,
+ [`${ONYXKEYS.COLLECTION.POLICY}`]: policies,
[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${mockRoute.params.reportID}`]: {
isLoadingReportActions: false,
},
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/ReportTestUtils.js b/tests/utils/ReportTestUtils.js
index 910f2200876b..86899e4045f6 100644
--- a/tests/utils/ReportTestUtils.js
+++ b/tests/utils/ReportTestUtils.js
@@ -1,4 +1,5 @@
import _ from 'underscore';
+import createRandomReportAction from './collections/reportActions';
const actionNames = ['ADDCOMMENT', 'IOU', 'REPORTPREVIEW', 'CLOSED'];
@@ -51,7 +52,13 @@ const getMockedReportActionsMap = (length = 100) => {
const mockReports = Array.from({length}, (__, i) => {
const reportID = i + 1;
const actionName = i === 0 ? 'CREATED' : actionNames[i % actionNames.length];
- const reportAction = getFakeReportAction(reportID, actionName);
+ const reportAction = {
+ ...createRandomReportAction(reportID),
+ actionName,
+ originalMessage: {
+ linkedReportID: reportID.toString(),
+ },
+ };
return {[reportID]: reportAction};
});
diff --git a/tests/utils/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/tests/utils/collections/reportActions.ts b/tests/utils/collections/reportActions.ts
index cc258e89c041..bb14a2c7a41b 100644
--- a/tests/utils/collections/reportActions.ts
+++ b/tests/utils/collections/reportActions.ts
@@ -1,4 +1,5 @@
-import {rand, randAggregation, randBoolean, randPastDate, randWord} from '@ngneat/falso';
+import {rand, randAggregation, randBoolean, randWord} from '@ngneat/falso';
+import {format} from 'date-fns';
import CONST from '@src/CONST';
import type {ReportAction} from '@src/types/onyx';
@@ -17,6 +18,15 @@ const flattenActionNamesValues = (actionNames: any) => {
return result;
};
+const getRandomDate = (): string => {
+ const randomTimestamp = Math.random() * new Date().getTime();
+ const randomDate = new Date(randomTimestamp);
+
+ const formattedDate = format(randomDate, CONST.DATE.FNS_DB_FORMAT_STRING);
+
+ return formattedDate;
+};
+
export default function createRandomReportAction(index: number): ReportAction {
return {
// we need to add any here because of the way we are generating random values
@@ -32,7 +42,7 @@ export default function createRandomReportAction(index: number): ReportAction {
text: randWord(),
},
],
- created: randPastDate().toISOString(),
+ created: getRandomDate(),
message: [
{
type: randWord(),
@@ -57,13 +67,13 @@ export default function createRandomReportAction(index: number): ReportAction {
],
originalMessage: {
html: randWord(),
- type: rand(Object.values(CONST.IOU.REPORT_ACTION_TYPE)),
+ lastModified: getRandomDate(),
},
whisperedToAccountIDs: randAggregation(),
avatar: randWord(),
automatic: randBoolean(),
shouldShow: randBoolean(),
- lastModified: randPastDate().toISOString(),
+ lastModified: getRandomDate(),
pendingAction: rand(Object.values(CONST.RED_BRICK_ROAD_PENDING_ACTION)),
delegateAccountID: index,
errors: {},
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;
}