From 94e2deee606832478f5d28c22e4bc4226f7b8620 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 30 Oct 2023 20:45:04 +0200 Subject: [PATCH 001/132] ReportAction text for transactions edited in OldDot --- src/libs/ReportUtils.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 0e9091fd6cb2..d14336256894 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1844,6 +1844,8 @@ function getModifiedExpenseMessage(reportAction) { return Localize.translateLocal('iou.changedTheRequest'); } + const messageFragments = []; + const hasModifiedAmount = _.has(reportActionOriginalMessage, 'oldAmount') && _.has(reportActionOriginalMessage, 'oldCurrency') && @@ -1864,12 +1866,12 @@ function getModifiedExpenseMessage(reportAction) { return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); } - return getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false)); } const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true)); } const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); @@ -1877,27 +1879,34 @@ function getModifiedExpenseMessage(reportAction) { // Take only the YYYY-MM-DD value as the original date includes timestamp let formattedOldCreated = new Date(reportActionOriginalMessage.oldCreated); formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false)); } if (hasModifiedMerchant) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true)); } const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true)); } const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); if (hasModifiedTag) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, Localize.translateLocal('common.tag'), true); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, Localize.translateLocal('common.tag'), true)); } const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true)); } + + return messageFragments.reduce((acc, value, index) => { + if (index == 0) { + return acc + value; + } + return acc + ". " + value.charAt(0).toUpperCase() + value.slice(1); + }, ""); } /** From 6e5d6b04de5cfe67f3179ad65cb4d0d2340e3201 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 30 Oct 2023 21:02:22 +0200 Subject: [PATCH 002/132] Make Lint happy. --- src/libs/ReportUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index d14336256894..3e8860543d5f 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1902,10 +1902,10 @@ function getModifiedExpenseMessage(reportAction) { } return messageFragments.reduce((acc, value, index) => { - if (index == 0) { + if (index === 0) { return acc + value; } - return acc + ". " + value.charAt(0).toUpperCase() + value.slice(1); + return `${acc}. ${value.charAt(0).toUpperCase()}${value.slice(1)}`; }, ""); } From a7f3f383524cf5d3b1aef2a31006916aacb56e10 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 30 Oct 2023 21:07:35 +0200 Subject: [PATCH 003/132] Run prettier. --- src/libs/ReportUtils.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 3e8860543d5f..49bf6a5f6310 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1871,7 +1871,9 @@ function getModifiedExpenseMessage(reportAction) { const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { - messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true)); + messageFragments.push( + getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true), + ); } const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); @@ -1883,12 +1885,16 @@ function getModifiedExpenseMessage(reportAction) { } if (hasModifiedMerchant) { - messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true)); + messageFragments.push( + getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true), + ); } const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true)); + messageFragments.push( + getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true), + ); } const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); @@ -1898,15 +1904,17 @@ function getModifiedExpenseMessage(reportAction) { const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true)); + messageFragments.push( + getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true), + ); } - + return messageFragments.reduce((acc, value, index) => { if (index === 0) { return acc + value; } return `${acc}. ${value.charAt(0).toUpperCase()}${value.slice(1)}`; - }, ""); + }, ''); } /** From a0a856cddc840617f1f5fd33416766b4b0d502f5 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 00:07:47 +0200 Subject: [PATCH 004/132] Improve message for MODIFIEDEXPENSE action --- src/languages/en.ts | 9 ++-- src/languages/es.ts | 9 ++-- src/libs/ReportUtils.js | 99 ++++++++++++++++++++++++++++++++--------- 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index d99b3c7d04d1..919548a3d918 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -573,11 +573,11 @@ export default { noReimbursableExpenses: 'This report has an invalid amount', pendingConversionMessage: "Total will update when you're back online", changedTheRequest: 'changed the request', - setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `set the ${valueName} to ${newValueToDisplay}`, + setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `the ${valueName} to ${newValueToDisplay}`, setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) => `set the distance to ${newDistanceToDisplay}, which set the amount to ${newAmountToDisplay}`, - removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `removed the ${valueName} (previously ${oldValueToDisplay})`, + removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `the ${valueName} (previously ${oldValueToDisplay})`, updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => - `changed the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, + `the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => `changed the distance to ${newDistanceToDisplay} (previously ${oldDistanceToDisplay}), which updated the amount to ${newAmountToDisplay} (previously ${oldAmountToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, @@ -597,6 +597,9 @@ export default { }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Started settling up, payment is held until ${submitterDisplayName} enables their Wallet`, enableWallet: 'Enable Wallet', + set: 'set', + changed: 'changed', + removed: 'removed', }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index dea7760a35ce..8d5f678c63e8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -565,13 +565,13 @@ export default { noReimbursableExpenses: 'El importe de este informe no es válido', pendingConversionMessage: 'El total se actualizará cuando estés online', changedTheRequest: 'cambió la solicitud', - setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `estableció ${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay}`, + setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay}`, setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) => `estableció la distancia a ${newDistanceToDisplay}, lo que estableció el importe a ${newAmountToDisplay}`, removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => - `eliminó ${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} (previamente ${oldValueToDisplay})`, + `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} (previamente ${oldValueToDisplay})`, updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => - `cambió ${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`, + `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`, updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => `cambió la distancia a ${newDistanceToDisplay} (previamente ${oldDistanceToDisplay}), lo que cambió el importe a ${newAmountToDisplay} (previamente ${oldAmountToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Solicitud de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, @@ -591,6 +591,9 @@ export default { }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`, enableWallet: 'Habilitar Billetera', + set: 'estableció', + changed: 'cambió', + removed: 'eliminó', }, notificationPreferencesPage: { header: 'Preferencias de avisos', diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 1297171d3149..9f4b50965f3e 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1917,7 +1917,9 @@ function getModifiedExpenseMessage(reportAction) { return Localize.translateLocal('iou.changedTheRequest'); } - const messageFragments = []; + const removalFragments = []; + const setFragments = []; + const changeFragments = []; const hasModifiedAmount = _.has(reportActionOriginalMessage, 'oldAmount') && @@ -1939,14 +1941,26 @@ function getModifiedExpenseMessage(reportAction) { return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); } - messageFragments.push(getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false)); + const fragment = getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); + if (!oldAmount) { + setFragments.push(fragment); + } else if (!amount) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { - messageFragments.push( - getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true), - ); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true); + if (!reportActionOriginalMessage.oldComment) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.newComment) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); @@ -1954,40 +1968,81 @@ function getModifiedExpenseMessage(reportAction) { // Take only the YYYY-MM-DD value as the original date includes timestamp let formattedOldCreated = new Date(reportActionOriginalMessage.oldCreated); formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false)); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); + if (!formattedOldCreated) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.created) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } if (hasModifiedMerchant) { - messageFragments.push( - getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true), - ); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true); + if (!reportActionOriginalMessage.oldMerchant) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.merchant) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - messageFragments.push( - getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true), - ); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true); + if (!reportActionOriginalMessage.oldCategory) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.category) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); if (hasModifiedTag) { - messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, Localize.translateLocal('common.tag'), true)); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, Localize.translateLocal('common.tag'), true); + if (!reportActionOriginalMessage.oldTag) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.tag) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - messageFragments.push( - getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true), - ); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true); + if (!reportActionOriginalMessage.oldBillable) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.billable) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } - return messageFragments.reduce((acc, value, index) => { - if (index === 0) { - return acc + value; - } - return `${acc}. ${value.charAt(0).toUpperCase()}${value.slice(1)}`; - }, ''); + let message = ''; + if (setFragments.length > 0) { + message = setFragments.reduce((acc, value) => { + return `${acc} ${value},`; + }, `${message} ${Localize.translateLocal('iou.set')}`); + } + if (changeFragments.length > 0) { + message = changeFragments.reduce((acc, value) => { + return `${acc} ${value},`; + }, `${message} ${Localize.translateLocal('iou.changed')}`); + } + if (removalFragments.length > 0) { + message = removalFragments.reduce((acc, value) => { + return `${acc} ${value},`; + }, `${message} ${Localize.translateLocal('iou.removed')}`); + } + message = `${message.substring(1, message.length - 1)}.`; + return message; } /** From 960d00a36d28f7f60966df3686d6d2be6f87db68 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 00:09:28 +0200 Subject: [PATCH 005/132] Add a safety check --- src/libs/ReportUtils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 9f4b50965f3e..97bc468de8eb 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2041,6 +2041,9 @@ function getModifiedExpenseMessage(reportAction) { return `${acc} ${value},`; }, `${message} ${Localize.translateLocal('iou.removed')}`); } + if (message === '') { + return message; + } message = `${message.substring(1, message.length - 1)}.`; return message; } From a4e82a8baaf2eabf78a40848dbaa28179ad96d89 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 00:21:09 +0200 Subject: [PATCH 006/132] Improve MODIFIEDEXPENSE action message --- src/libs/ReportUtils.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 97bc468de8eb..a2de3dc73161 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2026,20 +2026,20 @@ function getModifiedExpenseMessage(reportAction) { } let message = ''; - if (setFragments.length > 0) { - message = setFragments.reduce((acc, value) => { + if (changeFragments.length > 0) { + message = changeFragments.reduce((acc, value, index) => { return `${acc} ${value},`; - }, `${message} ${Localize.translateLocal('iou.set')}`); + }, `${message}\n${Localize.translateLocal('iou.changed')}`); } - if (changeFragments.length > 0) { - message = changeFragments.reduce((acc, value) => { + if (setFragments.length > 0) { + message = setFragments.reduce((acc, value) => { return `${acc} ${value},`; - }, `${message} ${Localize.translateLocal('iou.changed')}`); + }, `${message}\n${Localize.translateLocal('iou.set')}`); } if (removalFragments.length > 0) { message = removalFragments.reduce((acc, value) => { return `${acc} ${value},`; - }, `${message} ${Localize.translateLocal('iou.removed')}`); + }, `${message}\n${Localize.translateLocal('iou.removed')}`); } if (message === '') { return message; From b9b7ef462310e69498e261137bc8ab619af75c98 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 00:33:22 +0200 Subject: [PATCH 007/132] Improve MODIFIEDEXPENSE action message --- src/libs/ReportUtils.js | 43 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index a2de3dc73161..5680ea7b017a 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2028,17 +2028,50 @@ function getModifiedExpenseMessage(reportAction) { let message = ''; if (changeFragments.length > 0) { message = changeFragments.reduce((acc, value, index) => { - return `${acc} ${value},`; + if (index === changeFragments.length - 1) { + if (changeFragments.length === 1) { + return `${acc} ${value}.`; + } else if (changeFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + } else { + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + } else if (index === 0) { + return `${acc} ${value}`; + } + return `${acc}, ${value}`; }, `${message}\n${Localize.translateLocal('iou.changed')}`); } if (setFragments.length > 0) { - message = setFragments.reduce((acc, value) => { - return `${acc} ${value},`; + message = setFragments.reduce((acc, value, index) => { + if (index === setFragments.length - 1) { + if (setFragments.length === 1) { + return `${acc} ${value}.`; + } else if (setFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + } else { + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + } else if (index === 0) { + return `${acc} ${value}`; + } + return `${acc}, ${value}`; }, `${message}\n${Localize.translateLocal('iou.set')}`); } if (removalFragments.length > 0) { - message = removalFragments.reduce((acc, value) => { - return `${acc} ${value},`; + message = removalFragments.reduce((acc, value, index) => { + if (index === removalFragments.length - 1) { + if (removalFragments.length === 1) { + return `${acc} ${value}.`; + } else if (removalFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + } else { + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + } else if (index === 0) { + return `${acc} ${value}`; + } + return `${acc}, ${value}`; }, `${message}\n${Localize.translateLocal('iou.removed')}`); } if (message === '') { From 5190539d1b65224687b503204749623915271deb Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 00:39:25 +0200 Subject: [PATCH 008/132] Remove redundancy. --- src/libs/ReportUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 5680ea7b017a..5004e5d87dc0 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2077,7 +2077,7 @@ function getModifiedExpenseMessage(reportAction) { if (message === '') { return message; } - message = `${message.substring(1, message.length - 1)}.`; + message = `${message.substring(1, message.length)}`; return message; } From b5518be159aa4cea4aad09ee70ddb760e5b42245 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 15:59:14 +0200 Subject: [PATCH 009/132] Update function doc --- src/libs/ReportUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 5004e5d87dc0..d5f56bea8c5c 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1857,7 +1857,7 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip } /** - * Get the proper message schema for modified expense message. + * Get the proper message schema for a modified field on the expense. * * @param {String} newValue * @param {String} oldValue From be978f9b0138b1474094ae1a3d95ea35eefea982 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 16:17:08 +0200 Subject: [PATCH 010/132] Create reusable function --- src/libs/ReportUtils.js | 84 +++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 50 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index d5f56bea8c5c..13ffeb6432f5 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1857,7 +1857,7 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip } /** - * Get the proper message schema for a modified field on the expense. + * Get the proper message schema for a modified a field on the expense. * * @param {String} newValue * @param {String} oldValue @@ -1880,6 +1880,36 @@ function getProperSchemaForModifiedExpenseMessage(newValue, oldValue, valueName, return Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); } +/** + * Get the proper message line for a modified expense. + * + * @param {String} newValue + * @param {String} oldValue + * @param {String} valueName + * @param {Boolean} valueInQuotes + * @returns {String} + */ + +function getProperLineForModifiedExpenseMessage(prefix, messageFragments) { + if (messageFragments.length === 0) { + return ''; + } + return messageFragments.reduce((acc, value, index) => { + if (index === messageFragments.length - 1) { + if (messageFragments.length === 1) { + return `${acc} ${value}.`; + } else if (messageFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + } else { + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + } else if (index === 0) { + return `${acc} ${value}`; + } + return `${acc}, ${value}`; + }, prefix); +} + /** * Get the proper message schema for modified distance message. * @@ -2025,55 +2055,9 @@ function getModifiedExpenseMessage(reportAction) { } } - let message = ''; - if (changeFragments.length > 0) { - message = changeFragments.reduce((acc, value, index) => { - if (index === changeFragments.length - 1) { - if (changeFragments.length === 1) { - return `${acc} ${value}.`; - } else if (changeFragments.length === 2) { - return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; - } else { - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; - } - } else if (index === 0) { - return `${acc} ${value}`; - } - return `${acc}, ${value}`; - }, `${message}\n${Localize.translateLocal('iou.changed')}`); - } - if (setFragments.length > 0) { - message = setFragments.reduce((acc, value, index) => { - if (index === setFragments.length - 1) { - if (setFragments.length === 1) { - return `${acc} ${value}.`; - } else if (setFragments.length === 2) { - return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; - } else { - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; - } - } else if (index === 0) { - return `${acc} ${value}`; - } - return `${acc}, ${value}`; - }, `${message}\n${Localize.translateLocal('iou.set')}`); - } - if (removalFragments.length > 0) { - message = removalFragments.reduce((acc, value, index) => { - if (index === removalFragments.length - 1) { - if (removalFragments.length === 1) { - return `${acc} ${value}.`; - } else if (removalFragments.length === 2) { - return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; - } else { - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; - } - } else if (index === 0) { - return `${acc} ${value}`; - } - return `${acc}, ${value}`; - }, `${message}\n${Localize.translateLocal('iou.removed')}`); - } + let message = getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.set')}`, setFragments) + + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); if (message === '') { return message; } From b31ba92326163585034ac04cd466d39765b7876b Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 16:18:59 +0200 Subject: [PATCH 011/132] Run prettier --- src/languages/en.ts | 3 +-- src/languages/es.ts | 3 +-- src/libs/ReportUtils.js | 51 +++++++++++++++++++++++++++++------------ 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 919548a3d918..3be57af83cc5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -576,8 +576,7 @@ export default { setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `the ${valueName} to ${newValueToDisplay}`, setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) => `set the distance to ${newDistanceToDisplay}, which set the amount to ${newAmountToDisplay}`, removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `the ${valueName} (previously ${oldValueToDisplay})`, - updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => - `the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, + updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => `the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => `changed the distance to ${newDistanceToDisplay} (previously ${oldDistanceToDisplay}), which updated the amount to ${newAmountToDisplay} (previously ${oldAmountToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 8d5f678c63e8..e2eb899a343a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -568,8 +568,7 @@ export default { setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay}`, setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) => `estableció la distancia a ${newDistanceToDisplay}, lo que estableció el importe a ${newAmountToDisplay}`, - removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => - `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} (previamente ${oldValueToDisplay})`, + removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} (previamente ${oldValueToDisplay})`, updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`, updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 13ffeb6432f5..68555644405c 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1897,7 +1897,7 @@ function getProperLineForModifiedExpenseMessage(prefix, messageFragments) { return messageFragments.reduce((acc, value, index) => { if (index === messageFragments.length - 1) { if (messageFragments.length === 1) { - return `${acc} ${value}.`; + return `${acc} ${value}.`; } else if (messageFragments.length === 2) { return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; } else { @@ -1976,19 +1976,24 @@ function getModifiedExpenseMessage(reportAction) { setFragments.push(fragment); } else if (!amount) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { - const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true); + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.newComment, + reportActionOriginalMessage.oldComment, + Localize.translateLocal('common.description'), + true, + ); if (!reportActionOriginalMessage.oldComment) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.newComment) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } @@ -2003,30 +2008,40 @@ function getModifiedExpenseMessage(reportAction) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.created) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } if (hasModifiedMerchant) { - const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true); + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.merchant, + reportActionOriginalMessage.oldMerchant, + Localize.translateLocal('common.merchant'), + true, + ); if (!reportActionOriginalMessage.oldMerchant) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.merchant) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true); + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.category, + reportActionOriginalMessage.oldCategory, + Localize.translateLocal('common.category'), + true, + ); if (!reportActionOriginalMessage.oldCategory) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.category) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } @@ -2038,26 +2053,32 @@ function getModifiedExpenseMessage(reportAction) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.tag) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true); + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.billable, + reportActionOriginalMessage.oldBillable, + Localize.translateLocal('iou.request'), + true, + ); if (!reportActionOriginalMessage.oldBillable) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.billable) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } - let message = getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.set')}`, setFragments) + - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); + let message = + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.set')}`, setFragments) + + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); if (message === '') { return message; } From 376944c425bc61756f7421bd47ed26f5794b90c8 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 16:22:23 +0200 Subject: [PATCH 012/132] Fix lint. --- src/libs/ReportUtils.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 68555644405c..70487dc0862f 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1898,12 +1898,13 @@ function getProperLineForModifiedExpenseMessage(prefix, messageFragments) { if (index === messageFragments.length - 1) { if (messageFragments.length === 1) { return `${acc} ${value}.`; - } else if (messageFragments.length === 2) { + } + if (messageFragments.length === 2) { return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; - } else { - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; } - } else if (index === 0) { + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + if (index === 0) { return `${acc} ${value}`; } return `${acc}, ${value}`; From b9f42439e8ff3ee28402bb3b91f9530445c41cbf Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 18:11:51 +0200 Subject: [PATCH 013/132] Update src/libs/ReportUtils.js Co-authored-by: Tim Golen --- src/libs/ReportUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 70487dc0862f..07265d95edc7 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1857,7 +1857,7 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip } /** - * Get the proper message schema for a modified a field on the expense. + * Get the proper message schema for a modified field on the expense. * * @param {String} newValue * @param {String} oldValue From 271d86cb408b7f949d7bd6cfaea5c8aef5312e8f Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 9 Nov 2023 17:29:32 +0100 Subject: [PATCH 014/132] Props types for MenuItem --- src/components/{MenuItem.js => MenuItem.tsx} | 162 ++++++++++++++++++- 1 file changed, 157 insertions(+), 5 deletions(-) rename src/components/{MenuItem.js => MenuItem.tsx} (79%) diff --git a/src/components/MenuItem.js b/src/components/MenuItem.tsx similarity index 79% rename from src/components/MenuItem.js rename to src/components/MenuItem.tsx index 103d063f9024..3b6ad37a9f5e 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.tsx @@ -1,6 +1,6 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {useEffect, useMemo} from 'react'; -import {View} from 'react-native'; +import React, {ForwardedRef, ReactNode, useEffect, useMemo} from 'react'; +import {StyleProp, View, ViewStyle} from 'react-native'; import _ from 'underscore'; import useWindowDimensions from '@hooks/useWindowDimensions'; import ControlSelection from '@libs/ControlSelection'; @@ -13,6 +13,8 @@ import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; +import AvatarType from '@src/types/onyx/Avatar'; +import { AnimatedStyle } from 'react-native-reanimated'; import Avatar from './Avatar'; import Badge from './Badge'; import DisplayNames from './DisplayNames'; @@ -20,14 +22,164 @@ import Hoverable from './Hoverable'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import * as defaultWorkspaceAvatars from './Icon/WorkspaceDefaultAvatars'; -import menuItemPropTypes from './menuItemPropTypes'; import MultipleAvatars from './MultipleAvatars'; import PressableWithSecondaryInteraction from './PressableWithSecondaryInteraction'; import RenderHTML from './RenderHTML'; import SelectCircle from './SelectCircle'; import Text from './Text'; -const propTypes = menuItemPropTypes; +type MenuItemProps = { + /** Text to be shown as badge near the right end. */ + badgeText: string; + + /** Function to fire when component is pressed */ + onPress?: (event: Event) => void; + + /** Used to apply offline styles to child text components */ + style?: StyleProp; + + /** Any additional styles to apply */ + wrapperStyle?: StyleProp; + + /** Used to apply styles specifically to the title */ + titleStyle?: StyleProp; + + /** Icon to display on the left side of component */ + icon: ReactNode | string | AvatarType; + + /** Secondary icon to display on the left side of component, right of the icon */ + secondaryIcon: ReactNode; + + /** Icon Width */ + iconWidth: number; + + /** Icon Height */ + iconHeight: number; + + /** Text to display for the item */ + title: string; + + /** Text that appears above the title */ + label: string; + + /** Boolean whether to display the title right icon */ + shouldShowTitleIcon: boolean; + + /** Icon to display at right side of title */ + titleIcon: () => void; + + /** Boolean whether to display the right icon */ + shouldShowRightIcon: boolean; + + /** Should we make this selectable with a checkbox */ + shouldShowSelectedState: boolean; + + /** Should the title show with normal font weight (not bold) */ + shouldShowBasicTitle: boolean; + + /** Should the description be shown above the title (instead of the other way around) */ + shouldShowDescriptionOnTop: boolean; + + /** Whether this item is selected */ + isSelected: boolean; + + /** A boolean flag that gives the icon a green fill if true */ + success: boolean; + + /** Overrides the icon for shouldShowRightIcon */ + iconRight: ReactNode; + + /** A description text to show under the title */ + description: string; + + /** Any additional styles to pass to the icon container. */ + iconStyles: Array>; + + /** The fill color to pass into the icon. */ + iconFill: string; + + /** The fill color to pass into the secondary icon. */ + secondaryIconFill: string; + + /** Whether item is focused or active */ + focused: boolean; + + /** Should we disable this menu item? */ + disabled: boolean; + + /** A right-aligned subtitle for this menu option */ + subtitle: string | number; + + /** Flag to choose between avatar image or an icon */ + iconType: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_ICON | typeof CONST.ICON_TYPE_WORKSPACE; + + /** Whether the menu item should be interactive at all */ + interactive: boolean; + + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon: string | (() => void); + + /** Avatars to show on the right of the menu item */ + floatRightAvatars: AvatarType; + + /** The type of brick road indicator to show. */ + brickRoadIndicator: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | typeof CONST.BRICK_ROAD_INDICATOR_STATUS.INFO | ''; + + /** Prop to identify if we should load avatars vertically instead of diagonally */ + shouldStackHorizontally: boolean; + + /** Prop to represent the size of the float right avatar images to be shown */ + floatRightAvatarSize: typeof CONST.AVATAR_SIZE; + + /** Prop to represent the size of the avatar images to be shown */ + avatarSize: typeof CONST.AVATAR_SIZE; + + /** The function that should be called when this component is LongPressed or right-clicked. */ + onSecondaryInteraction: () => void; + + /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ + shouldBlockSelection: boolean; + + /** Any adjustments to style when menu item is hovered or pressed */ + hoverAndPressStyle: Array>>, + + /** Text to display under the main item */ + furtherDetails: string; + + /** An icon to display under the main item */ + furtherDetailsIcon: ReactNode | string; + + /** The action accept for anonymous user or not */ + isAnonymousAction: boolean; + + /** Whether we should use small avatar subscript sizing the for menu item */ + isSmallAvatarSubscriptMenu: boolean; + + /** Should we grey out the menu item when it is disabled? */ + shouldGreyOutWhenDisabled: boolean; + + /** Error to display below the title */ + error: string; + + /** Should render the content in HTML format */ + shouldRenderAsHTML: boolean; + + /** Component to be displayed on the right */ + rightComponent: ReactNode; + + /** Should render component on the right */ + shouldShowRightComponent: boolean; + + /** Array of objects that map display names to their corresponding tooltip */ + titleWithTooltips: ReactNode[]; + + /** Should check anonymous user in onPress function */ + shouldCheckActionAllowedOnPress: boolean; +}; + +// TODO: Destructure props +// TODO: Adjust default values +// TODO: Adjust () => void in AvatarProps - always just used () => void without checking the usage const defaultProps = { badgeText: undefined, @@ -84,7 +236,7 @@ const defaultProps = { shouldCheckActionAllowedOnPress: true, }; -const MenuItem = React.forwardRef((props, ref) => { +const MenuItem = React.forwardRef((props: MenuItemProps, ref: ForwardedRef) => { const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = React.useState(''); From 8dba84c7e4ca8d0f1e090fd2a7a269d9ece5d6ac Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 9 Nov 2023 17:42:07 +0100 Subject: [PATCH 015/132] Destructure props --- src/components/MenuItem.tsx | 222 ++++++++++++++++++------------------ 1 file changed, 112 insertions(+), 110 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 3b6ad37a9f5e..590bdf73cce8 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1,6 +1,6 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import React, {ForwardedRef, ReactNode, useEffect, useMemo} from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import _ from 'underscore'; import useWindowDimensions from '@hooks/useWindowDimensions'; import ControlSelection from '@libs/ControlSelection'; @@ -236,234 +236,236 @@ const defaultProps = { shouldCheckActionAllowedOnPress: true, }; -const MenuItem = React.forwardRef((props: MenuItemProps, ref: ForwardedRef) => { +const MenuItem = React.forwardRef(({badgeText,onPress,style,wrapperStyle,titleStyle,icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, + shouldCheckActionAllowedOnPress +}: MenuItemProps, ref: ForwardedRef) => { const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = React.useState(''); - const isDeleted = _.contains(props.style, styles.offlineFeedback.deleted); - const descriptionVerticalMargin = props.shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; + const isDeleted = _.contains(style, styles.offlineFeedback.deleted); + const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; const titleTextStyle = StyleUtils.combineStyles( [ styles.flexShrink1, styles.popoverMenuText, - props.icon && !_.isArray(props.icon) && (props.avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3), - props.shouldShowBasicTitle ? undefined : styles.textStrong, - props.shouldShowHeaderTitle ? styles.textHeadlineH1 : undefined, - props.numberOfLinesTitle !== 1 ? styles.preWrap : styles.pre, - props.interactive && props.disabled ? {...styles.userSelectNone} : undefined, + icon && !_.isArray(icon) && (avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3), + shouldShowBasicTitle ? undefined : styles.textStrong, + shouldShowHeaderTitle ? styles.textHeadlineH1 : undefined, + numberOfLinesTitle !== 1 ? styles.preWrap : styles.pre, + interactive && disabled ? {...styles.userSelectNone} : undefined, styles.ltr, isDeleted ? styles.offlineFeedback.deleted : undefined, ], - props.titleStyle, + titleStyle, ); const descriptionTextStyle = StyleUtils.combineStyles([ styles.textLabelSupporting, - props.icon && !_.isArray(props.icon) ? styles.ml3 : undefined, - props.title ? descriptionVerticalMargin : StyleUtils.getFontSizeStyle(variables.fontSizeNormal), - props.descriptionTextStyle, + icon && !_.isArray(icon) ? styles.ml3 : undefined, + title ? descriptionVerticalMargin : StyleUtils.getFontSizeStyle(variables.fontSizeNormal), + descriptionTextStyle, isDeleted ? styles.offlineFeedback.deleted : undefined, ]); - const fallbackAvatarSize = props.viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; + const fallbackAvatarSize = viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; const titleRef = React.useRef(''); useEffect(() => { - if (!props.title || (titleRef.current.length && titleRef.current === props.title) || !props.shouldParseTitle) { + if (!title || (titleRef.current.length && titleRef.current === title) || !shouldParseTitle) { return; } const parser = new ExpensiMark(); - setHtml(parser.replace(props.title)); - titleRef.current = props.title; - }, [props.title, props.shouldParseTitle]); + setHtml(parser.replace(title)); + titleRef.current = title; + }, [title, shouldParseTitle]); const getProcessedTitle = useMemo(() => { let title = ''; - if (props.shouldRenderAsHTML) { - title = convertToLTR(props.title); + if (shouldRenderAsHTML) { + title = convertToLTR(title); } - if (props.shouldParseTitle) { + if (shouldParseTitle) { title = html; } return title ? `${title}` : ''; - }, [props.title, props.shouldRenderAsHTML, props.shouldParseTitle, html]); + }, [title, shouldRenderAsHTML, shouldParseTitle, html]); - const hasPressableRightComponent = props.iconRight || (props.rightComponent && props.shouldShowRightComponent); + const hasPressableRightComponent = iconRight || (rightComponent && shouldShowRightComponent); const renderTitleContent = () => { - if (props.titleWithTooltips && _.isArray(props.titleWithTooltips) && props.titleWithTooltips.length > 0) { + if (titleWithTooltips && _.isArray(titleWithTooltips) && titleWithTooltips.length > 0) { return ( ); } - return convertToLTR(props.title); + return convertToLTR(title); }; - const onPressAction = (e) => { - if (props.disabled || !props.interactive) { + const onPressAction = (event: GestureResponderEvent | KeyboardEvent) => { + if (disabled || !interactive) { return; } - if (e && e.type === 'click') { - e.currentTarget.blur(); + if (event && event.type === 'click') { + event.currentTarget.blur(); } - props.onPress(e); + onPress(e); }; return ( {(isHovered) => ( props.shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPress={shouldCheckActionAllowedOnPress ? Session.checkIfActionIsAllowed(onPressAction, isAnonymousAction) : onPressAction} + onPressIn={() => shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={ControlSelection.unblock} - onSecondaryInteraction={props.onSecondaryInteraction} + onSecondaryInteraction={onSecondaryInteraction} style={({pressed}) => [ - props.style, - !props.interactive && styles.cursorDefault, - StyleUtils.getButtonBackgroundColorStyle(getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true), - (isHovered || pressed) && props.hoverAndPressStyle, - ...(_.isArray(props.wrapperStyle) ? props.wrapperStyle : [props.wrapperStyle]), - props.shouldGreyOutWhenDisabled && props.disabled && styles.buttonOpacityDisabled, + style, + !interactive && styles.cursorDefault, + StyleUtils.getButtonBackgroundColorStyle(getButtonState(focused || isHovered, pressed, success, disabled, interactive), true), + (isHovered || pressed) && hoverAndPressStyle, + ...(_.isArray(wrapperStyle) ? wrapperStyle : [wrapperStyle]), + shouldGreyOutWhenDisabled && disabled && styles.buttonOpacityDisabled, ]} - disabled={props.disabled} + disabled={disabled} ref={ref} role={CONST.ACCESSIBILITY_ROLE.MENUITEM} - accessibilityLabel={props.title ? props.title.toString() : ''} + accessibilityLabel={title ? title.toString() : ''} > {({pressed}) => ( <> - {Boolean(props.label) && ( - + {Boolean(label) && ( + - {props.label} + {label} )} - - {Boolean(props.icon) && _.isArray(props.icon) && ( + + {Boolean(icon) && _.isArray(icon) && ( )} - {Boolean(props.icon) && !_.isArray(props.icon) && ( - - {props.iconType === CONST.ICON_TYPE_ICON && ( + {Boolean(icon) && !_.isArray(icon) && ( + + {iconType === CONST.ICON_TYPE_ICON && ( )} - {props.iconType === CONST.ICON_TYPE_WORKSPACE && ( + {iconType === CONST.ICON_TYPE_WORKSPACE && ( )} - {props.iconType === CONST.ICON_TYPE_AVATAR && ( + {iconType === CONST.ICON_TYPE_AVATAR && ( )} )} - {Boolean(props.secondaryIcon) && ( - + {Boolean(secondaryIcon) && ( + )} - - {Boolean(props.description) && props.shouldShowDescriptionOnTop && ( + + {Boolean(description) && shouldShowDescriptionOnTop && ( - {props.description} + {description} )} - {Boolean(props.title) && (Boolean(props.shouldRenderAsHTML) || (Boolean(props.shouldParseTitle) && Boolean(html.length))) && ( + {Boolean(title) && (Boolean(shouldRenderAsHTML) || (Boolean(shouldParseTitle) && Boolean(html.length))) && ( )} - {!props.shouldRenderAsHTML && !props.shouldParseTitle && Boolean(props.title) && ( + {!shouldRenderAsHTML && !shouldParseTitle && Boolean(title) && ( {renderTitleContent()} )} - {Boolean(props.shouldShowTitleIcon) && ( + {Boolean(shouldShowTitleIcon) && ( )} - {Boolean(props.description) && !props.shouldShowDescriptionOnTop && ( + {Boolean(description) && !shouldShowDescriptionOnTop && ( - {props.description} + {description} )} - {Boolean(props.error) && ( + {Boolean(error) && ( - {props.error} + {error} )} - {Boolean(props.furtherDetails) && ( + {Boolean(furtherDetails) && ( style={[styles.furtherDetailsText, styles.ph2, styles.pt1]} numberOfLines={2} > - {props.furtherDetails} + {furtherDetails} )} @@ -480,52 +482,52 @@ const MenuItem = React.forwardRef((props: MenuItemProps, ref: ForwardedRef - {Boolean(props.badgeText) && ( + {Boolean(badgeText) && ( )} {/* Since subtitle can be of type number, we should allow 0 to be shown */} - {(props.subtitle || props.subtitle === 0) && ( + {(subtitle || subtitle === 0) && ( - {props.subtitle} + {subtitle} )} - {!_.isEmpty(props.floatRightAvatars) && ( - + {!_.isEmpty(floatRightAvatars) && ( + )} - {Boolean(props.brickRoadIndicator) && ( + {Boolean(brickRoadIndicator) && ( )} - {Boolean(props.shouldShowRightIcon) && ( - + {Boolean(shouldShowRightIcon) && ( + )} - {props.shouldShowRightComponent && props.rightComponent} - {props.shouldShowSelectedState && } + {shouldShowRightComponent && rightComponent} + {shouldShowSelectedState && } )} From 12ec1ed805e7c4afa23b4cb683d382989e6f30df Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 9 Nov 2023 17:44:30 +0100 Subject: [PATCH 016/132] Make the component a function declaration --- src/components/MenuItem.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 590bdf73cce8..3036269790e8 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1,5 +1,5 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {ForwardedRef, ReactNode, useEffect, useMemo} from 'react'; +import React, {ForwardedRef, ReactNode, forwardRef, useEffect, useMemo} from 'react'; import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import _ from 'underscore'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -236,9 +236,9 @@ const defaultProps = { shouldCheckActionAllowedOnPress: true, }; -const MenuItem = React.forwardRef(({badgeText,onPress,style,wrapperStyle,titleStyle,icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, +function MenuItem({badgeText,onPress,style,wrapperStyle,titleStyle,icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, shouldCheckActionAllowedOnPress -}: MenuItemProps, ref: ForwardedRef) => { +}: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = React.useState(''); @@ -535,10 +535,8 @@ const MenuItem = React.forwardRef(({badgeText,onPress,style,wrapperStyle,titleSt )} ); -}); +} -MenuItem.propTypes = propTypes; -MenuItem.defaultProps = defaultProps; MenuItem.displayName = 'MenuItem'; -export default MenuItem; +export default forwardRef(MenuItem); From 49d35b131c9e8ad9e7593da7cf01f4be04ef0d8c Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 10 Nov 2023 11:28:42 +0100 Subject: [PATCH 017/132] Type onPress --- src/components/MenuItem.tsx | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 3036269790e8..b6de792d4a0d 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -28,12 +28,23 @@ import RenderHTML from './RenderHTML'; import SelectCircle from './SelectCircle'; import Text from './Text'; -type MenuItemProps = { - /** Text to be shown as badge near the right end. */ - badgeText: string; - +type ResponsiveProps = { /** Function to fire when component is pressed */ - onPress?: (event: Event) => void; + onPress: (event: GestureResponderEvent | KeyboardEvent) => void; + + interactive?: true; +} + +type UnresponsiveProps = { + onPress?: undefined; + + /** Whether the menu item should be interactive at all */ + interactive: false; +} + +type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { + /** Text to be shown as badge near the right end. */ + badgeText?: string; /** Used to apply offline styles to child text components */ style?: StyleProp; @@ -113,9 +124,6 @@ type MenuItemProps = { /** Flag to choose between avatar image or an icon */ iconType: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_ICON | typeof CONST.ICON_TYPE_WORKSPACE; - /** Whether the menu item should be interactive at all */ - interactive: boolean; - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ fallbackIcon: string | (() => void); @@ -182,7 +190,6 @@ type MenuItemProps = { // TODO: Adjust () => void in AvatarProps - always just used () => void without checking the usage const defaultProps = { - badgeText: undefined, shouldShowRightIcon: false, shouldShowSelectedState: false, shouldShowBasicTitle: false, @@ -236,7 +243,9 @@ const defaultProps = { shouldCheckActionAllowedOnPress: true, }; -function MenuItem({badgeText,onPress,style,wrapperStyle,titleStyle,icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, +function MenuItem({badgeText, onPress, + // Props not validated below - Validate if required and default value + ,style,wrapperStyle,titleStyle,icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, shouldCheckActionAllowedOnPress }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -313,11 +322,11 @@ function MenuItem({badgeText,onPress,style,wrapperStyle,titleStyle,icon,secondar return; } - if (event && event.type === 'click') { - event.currentTarget.blur(); + if (event && event.type === 'click' && event.currentTarget instanceof EventTarget) { + (event.currentTarget as HTMLElement).blur(); } - onPress(e); + onPress(event); }; return ( From 0cedba9095e705a99c5f50fc89d81dc553f96d8d Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 10 Nov 2023 11:31:56 +0100 Subject: [PATCH 018/132] Validate style props --- src/components/MenuItem.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index b6de792d4a0d..84e19fcbd5ad 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -55,6 +55,8 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** Used to apply styles specifically to the title */ titleStyle?: StyleProp; + // ------------------------------- VALID PROPS ABOVE + /** Icon to display on the left side of component */ icon: ReactNode | string | AvatarType; @@ -196,9 +198,6 @@ const defaultProps = { shouldShowDescriptionOnTop: false, shouldShowHeaderTitle: false, shouldParseTitle: false, - wrapperStyle: [], - style: styles.popoverMenuItem, - titleStyle: {}, shouldShowTitleIcon: false, titleIcon: () => {}, descriptionTextStyle: styles.breakWord, @@ -217,7 +216,6 @@ const defaultProps = { isSelected: false, subtitle: undefined, iconType: CONST.ICON_TYPE_ICON, - onPress: () => {}, onSecondaryInteraction: undefined, interactive: true, fallbackIcon: Expensicons.FallbackAvatar, @@ -243,9 +241,9 @@ const defaultProps = { shouldCheckActionAllowedOnPress: true, }; -function MenuItem({badgeText, onPress, +function MenuItem({badgeText, onPress, style = styles.popoverMenuItem, wrapperStyle, titleStyle, // Props not validated below - Validate if required and default value - ,style,wrapperStyle,titleStyle,icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, + icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, shouldCheckActionAllowedOnPress }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); From b6e092535411ae4f746cd3c394ad26ebdbf81f26 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 10 Nov 2023 16:17:08 +0100 Subject: [PATCH 019/132] Prop types -> Typescript props --- src/components/MenuItem.tsx | 185 ++++++++++++++++++------------------ 1 file changed, 90 insertions(+), 95 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 84e19fcbd5ad..8c9ab27ce3b5 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1,5 +1,5 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {ForwardedRef, ReactNode, forwardRef, useEffect, useMemo} from 'react'; +import React, {FC, ForwardedRef, ReactNode, forwardRef, useEffect, useMemo} from 'react'; import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import _ from 'underscore'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -15,6 +15,8 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import AvatarType from '@src/types/onyx/Avatar'; import { AnimatedStyle } from 'react-native-reanimated'; +import IconType from '@types/Icon'; +import { SvgProps } from 'react-native-svg'; import Avatar from './Avatar'; import Badge from './Badge'; import DisplayNames from './DisplayNames'; @@ -42,7 +44,35 @@ type UnresponsiveProps = { interactive: false; } -type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { +type TitleIconProps = { + /** Boolean whether to display the title right icon */ + shouldShowTitleIcon: true; + + /** Icon to display at right side of title */ + titleIcon: IconType; +} + +type NoTitleIconProps = { + shouldShowTitleIcon?: false; + + titleIcon?: undefined; +} + +type RightIconProps = { + /** Boolean whether to display the right icon */ + shouldShowRightIcon: true; + + /** Overrides the icon for shouldShowRightIcon */ + iconRight: IconType; +} + +type NoRightIconProps = { + shouldShowRightIcon?: false; + + iconRight?: IconType; +} + +type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | NoTitleIconProps) & (RightIconProps | NoRightIconProps) &{ /** Text to be shown as badge near the right end. */ badgeText?: string; @@ -55,79 +85,79 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** Used to apply styles specifically to the title */ titleStyle?: StyleProp; - // ------------------------------- VALID PROPS ABOVE + /** Any adjustments to style when menu item is hovered or pressed */ + hoverAndPressStyle: StyleProp>; /** Icon to display on the left side of component */ - icon: ReactNode | string | AvatarType; + icon?: ReactNode | string | AvatarType; + + /** The fill color to pass into the icon. */ + iconFill?: string; /** Secondary icon to display on the left side of component, right of the icon */ - secondaryIcon: ReactNode; + secondaryIcon?: ReactNode; + + /** The fill color to pass into the secondary icon. */ + secondaryIconFill?: string; + + /** Flag to choose between avatar image or an icon */ + iconType?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_ICON | typeof CONST.ICON_TYPE_WORKSPACE; /** Icon Width */ - iconWidth: number; + iconWidth?: number; /** Icon Height */ - iconHeight: number; + iconHeight?: number; - /** Text to display for the item */ - title: string; + /** Any additional styles to pass to the icon container. */ + iconStyles?: StyleProp; - /** Text that appears above the title */ - label: string; + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon?: FC; - /** Boolean whether to display the title right icon */ - shouldShowTitleIcon: boolean; + /** An icon to display under the main item */ + furtherDetailsIcon?: IconType; - /** Icon to display at right side of title */ - titleIcon: () => void; + /** A description text to show under the title */ + description?: string; - /** Boolean whether to display the right icon */ - shouldShowRightIcon: boolean; - - /** Should we make this selectable with a checkbox */ - shouldShowSelectedState: boolean; - - /** Should the title show with normal font weight (not bold) */ - shouldShowBasicTitle: boolean; - /** Should the description be shown above the title (instead of the other way around) */ - shouldShowDescriptionOnTop: boolean; - - /** Whether this item is selected */ - isSelected: boolean; - + shouldShowDescriptionOnTop?: boolean; + + /** Error to display below the title */ + error?: string; + /** A boolean flag that gives the icon a green fill if true */ - success: boolean; + success?: boolean; - /** Overrides the icon for shouldShowRightIcon */ - iconRight: ReactNode; + /** Whether item is focused or active */ + focused?: boolean; - /** A description text to show under the title */ - description: string; + /** Should we disable this menu item? */ + disabled?: boolean; - /** Any additional styles to pass to the icon container. */ - iconStyles: Array>; + /** Text that appears above the title */ + label?: string; - /** The fill color to pass into the icon. */ - iconFill: string; + /** Text to display for the item */ + title?: string; - /** The fill color to pass into the secondary icon. */ - secondaryIconFill: string; + /** A right-aligned subtitle for this menu option */ + subtitle?: string | number; - /** Whether item is focused or active */ - focused: boolean; + /** Should we make this selectable with a checkbox */ + shouldShowSelectedState?: boolean; - /** Should we disable this menu item? */ - disabled: boolean; + /** Whether this item is selected */ + isSelected?: boolean; - /** A right-aligned subtitle for this menu option */ - subtitle: string | number; + /** Prop to identify if we should load avatars vertically instead of diagonally */ + shouldStackHorizontally: boolean; - /** Flag to choose between avatar image or an icon */ - iconType: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_ICON | typeof CONST.ICON_TYPE_WORKSPACE; + // ------------------------------- VALID PROPS ABOVE - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon: string | (() => void); + /** Should the title show with normal font weight (not bold) */ + shouldShowBasicTitle: boolean; /** Avatars to show on the right of the menu item */ floatRightAvatars: AvatarType; @@ -135,9 +165,6 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** The type of brick road indicator to show. */ brickRoadIndicator: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | typeof CONST.BRICK_ROAD_INDICATOR_STATUS.INFO | ''; - /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally: boolean; - /** Prop to represent the size of the float right avatar images to be shown */ floatRightAvatarSize: typeof CONST.AVATAR_SIZE; @@ -150,15 +177,9 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ shouldBlockSelection: boolean; - /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle: Array>>, - /** Text to display under the main item */ furtherDetails: string; - /** An icon to display under the main item */ - furtherDetailsIcon: ReactNode | string; - /** The action accept for anonymous user or not */ isAnonymousAction: boolean; @@ -168,9 +189,6 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** Should we grey out the menu item when it is disabled? */ shouldGreyOutWhenDisabled: boolean; - /** Error to display below the title */ - error: string; - /** Should render the content in HTML format */ shouldRenderAsHTML: boolean; @@ -192,58 +210,35 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { // TODO: Adjust () => void in AvatarProps - always just used () => void without checking the usage const defaultProps = { - shouldShowRightIcon: false, - shouldShowSelectedState: false, shouldShowBasicTitle: false, - shouldShowDescriptionOnTop: false, shouldShowHeaderTitle: false, shouldParseTitle: false, - shouldShowTitleIcon: false, - titleIcon: () => {}, descriptionTextStyle: styles.breakWord, - success: false, - icon: undefined, - secondaryIcon: undefined, - iconWidth: undefined, - iconHeight: undefined, - description: undefined, - iconRight: Expensicons.ArrowRight, - iconStyles: [], - iconFill: undefined, - secondaryIconFill: undefined, - focused: false, - disabled: false, - isSelected: false, - subtitle: undefined, - iconType: CONST.ICON_TYPE_ICON, - onSecondaryInteraction: undefined, interactive: true, - fallbackIcon: Expensicons.FallbackAvatar, brickRoadIndicator: '', floatRightAvatars: [], - shouldStackHorizontally: false, avatarSize: CONST.AVATAR_SIZE.DEFAULT, - floatRightAvatarSize: undefined, shouldBlockSelection: false, - hoverAndPressStyle: [], furtherDetails: '', - furtherDetailsIcon: undefined, isAnonymousAction: false, isSmallAvatarSubscriptMenu: false, - title: '', numberOfLinesTitle: 1, shouldGreyOutWhenDisabled: true, - error: '', shouldRenderAsHTML: false, - rightComponent: undefined, shouldShowRightComponent: false, titleWithTooltips: [], shouldCheckActionAllowedOnPress: true, }; -function MenuItem({badgeText, onPress, style = styles.popoverMenuItem, wrapperStyle, titleStyle, +function MenuItem({ + badgeText, onPress, style = styles.popoverMenuItem, wrapperStyle, titleStyle, hoverAndPressStyle, + icon, iconFill, secondaryIcon, secondaryIconFill, iconType = CONST.ICON_TYPE_ICON, iconWidth, iconHeight, iconStyles, fallbackIcon = Expensicons.FallbackAvatar, shouldShowTitleIcon = false, titleIcon, + shouldShowRightIcon = false, iconRight = Expensicons.ArrowRight, furtherDetailsIcon, + description, error, success = false, focused = false, disabled = false, + title, subtitle, label, shouldShowSelectedState = false, isSelected = false, shouldStackHorizontally = false, + shouldShowDescriptionOnTop = false, // Props not validated below - Validate if required and default value - icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, + shouldShowBasicTitle,interactive,floatRightAvatars,brickRoadIndicator = '',floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,furtherDetails,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, shouldCheckActionAllowedOnPress }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -373,7 +368,7 @@ function MenuItem({badgeText, onPress, style = styles.popoverMenuItem, wrapperSt /> )} {Boolean(icon) && !_.isArray(icon) && ( - + {iconType === CONST.ICON_TYPE_ICON && ( )} {Boolean(secondaryIcon) && ( - + Date: Fri, 10 Nov 2023 16:31:20 +0100 Subject: [PATCH 020/132] Prop types -> Typescript props --- src/components/MenuItem.tsx | 63 ++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 8c9ab27ce3b5..8c7e426a4cfe 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -72,7 +72,22 @@ type NoRightIconProps = { iconRight?: IconType; } -type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | NoTitleIconProps) & (RightIconProps | NoRightIconProps) &{ +type RightComponent = { + /** Should render component on the right */ + shouldShowRightComponent: true; + + /** Component to be displayed on the right */ + rightComponent: ReactNode; +} + +type NoRightComponent = { + shouldShowRightComponent?: false; + + rightComponent?: undefined; +} + +type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | NoTitleIconProps) & +(RightIconProps | NoRightIconProps) & (RightComponent | NoRightComponent) & { /** Text to be shown as badge near the right end. */ badgeText?: string; @@ -145,6 +160,9 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N /** A right-aligned subtitle for this menu option */ subtitle?: string | number; + /** Should the title show with normal font weight (not bold) */ + shouldShowBasicTitle?: boolean; + /** Should we make this selectable with a checkbox */ shouldShowSelectedState?: boolean; @@ -154,23 +172,23 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N /** Prop to identify if we should load avatars vertically instead of diagonally */ shouldStackHorizontally: boolean; - // ------------------------------- VALID PROPS ABOVE + /** Prop to represent the size of the avatar images to be shown */ + avatarSize?: typeof CONST.AVATAR_SIZE; - /** Should the title show with normal font weight (not bold) */ - shouldShowBasicTitle: boolean; - /** Avatars to show on the right of the menu item */ - floatRightAvatars: AvatarType; - - /** The type of brick road indicator to show. */ - brickRoadIndicator: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | typeof CONST.BRICK_ROAD_INDICATOR_STATUS.INFO | ''; + floatRightAvatars?: AvatarType[]; /** Prop to represent the size of the float right avatar images to be shown */ - floatRightAvatarSize: typeof CONST.AVATAR_SIZE; + floatRightAvatarSize?: typeof CONST.AVATAR_SIZE; - /** Prop to represent the size of the avatar images to be shown */ - avatarSize: typeof CONST.AVATAR_SIZE; +/** Whether we should use small avatar subscript sizing the for menu item */ + isSmallAvatarSubscriptMenu?: boolean; + // ------------------------------- VALID PROPS ABOVE + + /** The type of brick road indicator to show. */ + brickRoadIndicator: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | typeof CONST.BRICK_ROAD_INDICATOR_STATUS.INFO | ''; + /** The function that should be called when this component is LongPressed or right-clicked. */ onSecondaryInteraction: () => void; @@ -183,21 +201,12 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N /** The action accept for anonymous user or not */ isAnonymousAction: boolean; - /** Whether we should use small avatar subscript sizing the for menu item */ - isSmallAvatarSubscriptMenu: boolean; - /** Should we grey out the menu item when it is disabled? */ shouldGreyOutWhenDisabled: boolean; /** Should render the content in HTML format */ shouldRenderAsHTML: boolean; - /** Component to be displayed on the right */ - rightComponent: ReactNode; - - /** Should render component on the right */ - shouldShowRightComponent: boolean; - /** Array of objects that map display names to their corresponding tooltip */ titleWithTooltips: ReactNode[]; @@ -210,8 +219,6 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N // TODO: Adjust () => void in AvatarProps - always just used () => void without checking the usage const defaultProps = { - shouldShowBasicTitle: false, - shouldShowHeaderTitle: false, shouldParseTitle: false, descriptionTextStyle: styles.breakWord, interactive: true, @@ -221,11 +228,9 @@ const defaultProps = { shouldBlockSelection: false, furtherDetails: '', isAnonymousAction: false, - isSmallAvatarSubscriptMenu: false, numberOfLinesTitle: 1, shouldGreyOutWhenDisabled: true, shouldRenderAsHTML: false, - shouldShowRightComponent: false, titleWithTooltips: [], shouldCheckActionAllowedOnPress: true, }; @@ -235,10 +240,11 @@ function MenuItem({ icon, iconFill, secondaryIcon, secondaryIconFill, iconType = CONST.ICON_TYPE_ICON, iconWidth, iconHeight, iconStyles, fallbackIcon = Expensicons.FallbackAvatar, shouldShowTitleIcon = false, titleIcon, shouldShowRightIcon = false, iconRight = Expensicons.ArrowRight, furtherDetailsIcon, description, error, success = false, focused = false, disabled = false, - title, subtitle, label, shouldShowSelectedState = false, isSelected = false, shouldStackHorizontally = false, - shouldShowDescriptionOnTop = false, + title, subtitle, shouldShowBasicTitle, label, shouldShowSelectedState = false, isSelected = false, shouldStackHorizontally = false, + shouldShowDescriptionOnTop = false, shouldShowRightComponent = false, rightComponent, + floatRightAvatars = [], floatRightAvatarSize, avatarSize = CONST.AVATAR_SIZE.DEFAULT, isSmallAvatarSubscriptMenu = false, // Props not validated below - Validate if required and default value - shouldShowBasicTitle,interactive,floatRightAvatars,brickRoadIndicator = '',floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,furtherDetails,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, + interactive,brickRoadIndicator = '',avatarSize,onSecondaryInteraction,shouldBlockSelection,furtherDetails,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,shouldRenderAsHTML,titleWithTooltips, shouldCheckActionAllowedOnPress }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -252,7 +258,6 @@ function MenuItem({ styles.popoverMenuText, icon && !_.isArray(icon) && (avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3), shouldShowBasicTitle ? undefined : styles.textStrong, - shouldShowHeaderTitle ? styles.textHeadlineH1 : undefined, numberOfLinesTitle !== 1 ? styles.preWrap : styles.pre, interactive && disabled ? {...styles.userSelectNone} : undefined, styles.ltr, From 87db8e24ca9fd7f868b177e112f1ce12721b47db Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 10 Nov 2023 16:56:52 +0100 Subject: [PATCH 021/132] Prop types -> Typescript props --- src/components/MenuItem.tsx | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 8c7e426a4cfe..c53ab3c3bdb1 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -173,7 +173,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N shouldStackHorizontally: boolean; /** Prop to represent the size of the avatar images to be shown */ - avatarSize?: typeof CONST.AVATAR_SIZE; + avatarSize?: typeof CONST.AVATAR_SIZE[keyof typeof CONST.AVATAR_SIZE]; /** Avatars to show on the right of the menu item */ floatRightAvatars?: AvatarType[]; @@ -181,13 +181,13 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N /** Prop to represent the size of the float right avatar images to be shown */ floatRightAvatarSize?: typeof CONST.AVATAR_SIZE; -/** Whether we should use small avatar subscript sizing the for menu item */ + /** Whether we should use small avatar subscript sizing the for menu item */ isSmallAvatarSubscriptMenu?: boolean; - // ------------------------------- VALID PROPS ABOVE - /** The type of brick road indicator to show. */ - brickRoadIndicator: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | typeof CONST.BRICK_ROAD_INDICATOR_STATUS.INFO | ''; + brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS[keyof typeof CONST.BRICK_ROAD_INDICATOR_STATUS]; + + // ------------------------------- VALID PROPS ABOVE /** The function that should be called when this component is LongPressed or right-clicked. */ onSecondaryInteraction: () => void; @@ -221,10 +221,6 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N const defaultProps = { shouldParseTitle: false, descriptionTextStyle: styles.breakWord, - interactive: true, - brickRoadIndicator: '', - floatRightAvatars: [], - avatarSize: CONST.AVATAR_SIZE.DEFAULT, shouldBlockSelection: false, furtherDetails: '', isAnonymousAction: false, @@ -236,15 +232,16 @@ const defaultProps = { }; function MenuItem({ - badgeText, onPress, style = styles.popoverMenuItem, wrapperStyle, titleStyle, hoverAndPressStyle, + interactive = true, onPress, badgeText, style = styles.popoverMenuItem, wrapperStyle, titleStyle, hoverAndPressStyle, icon, iconFill, secondaryIcon, secondaryIconFill, iconType = CONST.ICON_TYPE_ICON, iconWidth, iconHeight, iconStyles, fallbackIcon = Expensicons.FallbackAvatar, shouldShowTitleIcon = false, titleIcon, shouldShowRightIcon = false, iconRight = Expensicons.ArrowRight, furtherDetailsIcon, description, error, success = false, focused = false, disabled = false, title, subtitle, shouldShowBasicTitle, label, shouldShowSelectedState = false, isSelected = false, shouldStackHorizontally = false, shouldShowDescriptionOnTop = false, shouldShowRightComponent = false, rightComponent, floatRightAvatars = [], floatRightAvatarSize, avatarSize = CONST.AVATAR_SIZE.DEFAULT, isSmallAvatarSubscriptMenu = false, + brickRoadIndicator, // Props not validated below - Validate if required and default value - interactive,brickRoadIndicator = '',avatarSize,onSecondaryInteraction,shouldBlockSelection,furtherDetails,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,shouldRenderAsHTML,titleWithTooltips, + onSecondaryInteraction,shouldBlockSelection,furtherDetails,isAnonymousAction,shouldGreyOutWhenDisabled,shouldRenderAsHTML,titleWithTooltips, shouldCheckActionAllowedOnPress }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -301,7 +298,7 @@ function MenuItem({ const hasPressableRightComponent = iconRight || (rightComponent && shouldShowRightComponent); const renderTitleContent = () => { - if (titleWithTooltips && _.isArray(titleWithTooltips) && titleWithTooltips.length > 0) { + if (titleWithTooltips && Array.isArray(titleWithTooltips) && titleWithTooltips.length > 0) { return ( { From 3683fcb5a99470da05ce4ed96f21a3af7e31ae7d Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 10 Nov 2023 17:02:44 +0100 Subject: [PATCH 022/132] Prop types -> Typescript props --- src/components/MenuItem.tsx | 60 +++++++++++++------------------------ 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index c53ab3c3bdb1..b5686926258a 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -187,62 +187,42 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N /** The type of brick road indicator to show. */ brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS[keyof typeof CONST.BRICK_ROAD_INDICATOR_STATUS]; - // ------------------------------- VALID PROPS ABOVE - - /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction: () => void; - - /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ - shouldBlockSelection: boolean; + /** Should render the content in HTML format */ + shouldRenderAsHTML?: boolean; - /** Text to display under the main item */ - furtherDetails: string; + /** Should we grey out the menu item when it is disabled? */ + shouldGreyOutWhenDisabled?: boolean; /** The action accept for anonymous user or not */ - isAnonymousAction: boolean; + isAnonymousAction?: boolean; - /** Should we grey out the menu item when it is disabled? */ - shouldGreyOutWhenDisabled: boolean; + /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ + shouldBlockSelection?: boolean; - /** Should render the content in HTML format */ - shouldRenderAsHTML: boolean; - - /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: ReactNode[]; + shouldParseTitle?: false; /** Should check anonymous user in onPress function */ - shouldCheckActionAllowedOnPress: boolean; -}; + shouldCheckActionAllowedOnPress?: boolean; -// TODO: Destructure props -// TODO: Adjust default values -// TODO: Adjust () => void in AvatarProps - always just used () => void without checking the usage - -const defaultProps = { - shouldParseTitle: false, - descriptionTextStyle: styles.breakWord, - shouldBlockSelection: false, - furtherDetails: '', - isAnonymousAction: false, - numberOfLinesTitle: 1, - shouldGreyOutWhenDisabled: true, - shouldRenderAsHTML: false, - titleWithTooltips: [], - shouldCheckActionAllowedOnPress: true, -}; + /** Text to display under the main item */ + furtherDetails?: string; + + /** The function that should be called when this component is LongPressed or right-clicked. */ + onSecondaryInteraction: () => void; + /** Array of objects that map display names to their corresponding tooltip */ + titleWithTooltips: ReactNode[]; +}; function MenuItem({ interactive = true, onPress, badgeText, style = styles.popoverMenuItem, wrapperStyle, titleStyle, hoverAndPressStyle, icon, iconFill, secondaryIcon, secondaryIconFill, iconType = CONST.ICON_TYPE_ICON, iconWidth, iconHeight, iconStyles, fallbackIcon = Expensicons.FallbackAvatar, shouldShowTitleIcon = false, titleIcon, - shouldShowRightIcon = false, iconRight = Expensicons.ArrowRight, furtherDetailsIcon, + shouldShowRightIcon = false, iconRight = Expensicons.ArrowRight, furtherDetailsIcon, furtherDetails, description, error, success = false, focused = false, disabled = false, title, subtitle, shouldShowBasicTitle, label, shouldShowSelectedState = false, isSelected = false, shouldStackHorizontally = false, shouldShowDescriptionOnTop = false, shouldShowRightComponent = false, rightComponent, floatRightAvatars = [], floatRightAvatarSize, avatarSize = CONST.AVATAR_SIZE.DEFAULT, isSmallAvatarSubscriptMenu = false, - brickRoadIndicator, - // Props not validated below - Validate if required and default value - onSecondaryInteraction,shouldBlockSelection,furtherDetails,isAnonymousAction,shouldGreyOutWhenDisabled,shouldRenderAsHTML,titleWithTooltips, - shouldCheckActionAllowedOnPress + brickRoadIndicator, shouldRenderAsHTML = false, shouldGreyOutWhenDisabled = true, isAnonymousAction = false, + shouldBlockSelection = false, shouldParseTitle = false, shouldCheckActionAllowedOnPress = true, onSecondaryInteraction, titleWithTooltips }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = React.useState(''); From bab1de41b8e579d609142d2364659c70c831fcd0 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 20 Nov 2023 18:37:47 +0200 Subject: [PATCH 023/132] Move logic in a separate unit --- src/libs/ModifiedExpenseMessage.ts | 276 ++++++++++++++++++ .../LocalNotification/BrowserNotifications.js | 3 +- src/libs/OptionsListUtils.js | 3 +- src/libs/ReportUtils.js | 264 ----------------- .../report/ContextMenu/ContextMenuActions.js | 3 +- src/pages/home/report/ReportActionItem.js | 3 +- 6 files changed, 284 insertions(+), 268 deletions(-) create mode 100644 src/libs/ModifiedExpenseMessage.ts diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts new file mode 100644 index 000000000000..d323edcd38f8 --- /dev/null +++ b/src/libs/ModifiedExpenseMessage.ts @@ -0,0 +1,276 @@ +import {format} from 'date-fns'; +import lodashGet from 'lodash/get'; +import * as Localize from './Localize'; +import * as PolicyUtils from './PolicyUtils'; +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as ReportUtils from './ReportUtils'; +import * as CurrencyUtils from './CurrencyUtils'; +import _ from 'underscore'; + +let allPolicyTags = {}; + +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTags = {}; + return; + } + + allPolicyTags = value; + }, +}); + +function getPolicyTags(policyID: string) { + return lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); +} + + +/** + * Get the proper message schema for a modified field on the expense. + * + * @param {String} newValue + * @param {String} oldValue + * @param {String} valueName + * @param {Boolean} valueInQuotes + * @param {Boolean} shouldConvertToLowercase + * @returns {String} + */ + +function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true) { + const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; + const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; + const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; + + if (!oldValue) { + return Localize.translateLocal('iou.setTheRequest', {valueName: displayValueName, newValueToDisplay}); + } + if (!newValue) { + return Localize.translateLocal('iou.removedTheRequest', {valueName: displayValueName, oldValueToDisplay}); + } + return Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); +} + +/** + * Get the proper message line for a modified expense. + * + * @param {String} newValue + * @param {String} oldValue + * @param {String} valueName + * @param {Boolean} valueInQuotes + * @returns {String} + */ + +function getProperLineForModifiedExpenseMessage(prefix: string, messageFragments: Array) { + if (messageFragments.length === 0) { + return ''; + } + return messageFragments.reduce((acc, value, index) => { + if (index === messageFragments.length - 1) { + if (messageFragments.length === 1) { + return `${acc} ${value}.`; + } + if (messageFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + } + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + if (index === 0) { + return `${acc} ${value}`; + } + return `${acc}, ${value}`; + }, prefix); +} + +/** + * Get the proper message schema for modified distance message. + * + * @param {String} newDistance + * @param {String} oldDistance + * @param {String} newAmount + * @param {String} oldAmount + * @returns {String} + */ + +function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { + if (!oldDistance) { + return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); + } + return Localize.translateLocal('iou.updatedTheDistance', { + newDistanceToDisplay: newDistance, + oldDistanceToDisplay: oldDistance, + newAmountToDisplay: newAmount, + oldAmountToDisplay: oldAmount, + }); +} + +/** + * Get the report action message when expense has been modified. + * + * ModifiedExpense::getNewDotComment in Web-Expensify should match this. + * If we change this function be sure to update the backend as well. + * + * @param {Object} reportAction + * @returns {String} + */ +function getModifiedExpenseMessage(reportAction: Object): string { + const reportActionOriginalMessage: any = lodashGet(reportAction, 'originalMessage', {}); + if (_.isEmpty(reportActionOriginalMessage)) { + return Localize.translateLocal('iou.changedTheRequest'); + } + const reportID = lodashGet(reportAction, 'reportID', ''); + const policyID = lodashGet(ReportUtils.getReport(reportID), 'policyID', ''); + const policyTags = getPolicyTags(policyID); + const policyTag = PolicyUtils.getTag(policyTags); + const policyTagListName = lodashGet(policyTag, 'name', Localize.translateLocal('common.tag')); + + const removalFragments = []; + const setFragments = []; + const changeFragments = []; + + const hasModifiedAmount = + _.has(reportActionOriginalMessage, 'oldAmount') && + _.has(reportActionOriginalMessage, 'oldCurrency') && + _.has(reportActionOriginalMessage, 'amount') && + _.has(reportActionOriginalMessage, 'currency'); + + const hasModifiedMerchant = _.has(reportActionOriginalMessage, 'oldMerchant') && _.has(reportActionOriginalMessage, 'merchant'); + if (hasModifiedAmount) { + const oldCurrency = reportActionOriginalMessage.oldCurrency; + const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.oldAmount, oldCurrency); + + const currency = reportActionOriginalMessage.currency; + const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.amount, currency); + + // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. + // We check the merchant is in distance format (includes @) as a sanity check + if (hasModifiedMerchant && reportActionOriginalMessage.merchant.includes('@')) { + return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); + } + + const fragment = getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); + if (!oldAmount) { + setFragments.push(fragment); + } else if (!amount) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); + if (hasModifiedComment) { + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.newComment, + reportActionOriginalMessage.oldComment, + Localize.translateLocal('common.description'), + true, + ); + if (!reportActionOriginalMessage.oldComment) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.newComment) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); + if (hasModifiedCreated) { + // Take only the YYYY-MM-DD value as the original date includes timestamp + let formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); + if (!formattedOldCreated) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.created) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + if (hasModifiedMerchant) { + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.merchant, + reportActionOriginalMessage.oldMerchant, + Localize.translateLocal('common.merchant'), + true, + ); + if (!reportActionOriginalMessage.oldMerchant) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.merchant) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); + if (hasModifiedCategory) { + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.category, + reportActionOriginalMessage.oldCategory, + Localize.translateLocal('common.category'), + true, + ); + if (!reportActionOriginalMessage.oldCategory) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.category) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); + if (hasModifiedTag) { + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.tag, + reportActionOriginalMessage.oldTag, + policyTagListName, + true, + policyTagListName === Localize.translateLocal('common.tag'), + ); + if (!reportActionOriginalMessage.oldTag) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.tag) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); + if (hasModifiedBillable) { + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.billable, + reportActionOriginalMessage.oldBillable, + Localize.translateLocal('iou.request'), + true, + ); + if (!reportActionOriginalMessage.oldBillable) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.billable) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + let message = + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.set')}`, setFragments) + + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); + if (message === '') { + return message; + } + message = `${message.substring(1, message.length)}`; + return message; +} + +export default { + getModifiedExpenseMessage, +}; \ No newline at end of file diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.js b/src/libs/Notification/LocalNotification/BrowserNotifications.js index 20d9be48d915..497b686e7d5e 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.js +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.js @@ -4,6 +4,7 @@ import EXPENSIFY_ICON_URL from '@assets/images/expensify-logo-round-clearspace.p import * as ReportUtils from '@libs/ReportUtils'; import * as AppUpdate from '@userActions/AppUpdate'; import focusApp from './focusApp'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; const DEFAULT_DELAY = 4000; @@ -131,7 +132,7 @@ export default { pushModifiedExpenseNotification({reportAction, onClick}, usesIcon = false) { push({ title: _.map(reportAction.person, (f) => f.text).join(', '), - body: ReportUtils.getModifiedExpenseMessage(reportAction), + body: ModifiedExpenseMessage.getModifiedExpenseMessage(reportAction), delay: 0, onClick, icon: usesIcon ? EXPENSIFY_ICON_URL : '', diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 7e0aaa8ffb2f..af0407a18112 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -20,6 +20,7 @@ import * as ReportActionUtils from './ReportActionsUtils'; import * as ReportUtils from './ReportUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; +import ModifiedExpenseMessage from './ModifiedExpenseMessage'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -402,7 +403,7 @@ function getLastMessageTextForReport(report) { } else if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) { lastMessageTextFromReport = `[${Localize.translateLocal(report.lastMessageTranslationKey || 'common.attachment')}]`; } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) { - const properSchemaForModifiedExpenseMessage = ReportUtils.getModifiedExpenseMessage(lastReportAction); + const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getModifiedExpenseMessage(lastReportAction); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true); } else if ( lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 4968fc33b04b..2a3db8437518 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1,5 +1,4 @@ /* eslint-disable rulesdir/prefer-underscore-method */ -import {format} from 'date-fns'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; @@ -81,25 +80,6 @@ Onyx.connect({ callback: (val) => (loginList = val), }); -let allPolicyTags = {}; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - allPolicyTags = {}; - return; - } - - allPolicyTags = value; - }, -}); - -function getPolicyTags(policyID) { - return lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); -} - function getChatType(report) { return report ? report.chatType : ''; } @@ -1884,249 +1864,6 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip return Localize.translateLocal(containsNonReimbursable ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {payer: payerName, amount: formattedAmount}); } -/** - * Get the proper message schema for a modified field on the expense. - * - * @param {String} newValue - * @param {String} oldValue - * @param {String} valueName - * @param {Boolean} valueInQuotes - * @param {Boolean} shouldConvertToLowercase - * @returns {String} - */ - -function getProperSchemaForModifiedExpenseMessage(newValue, oldValue, valueName, valueInQuotes, shouldConvertToLowercase = true) { - const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; - const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; - const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; - - if (!oldValue) { - return Localize.translateLocal('iou.setTheRequest', {valueName: displayValueName, newValueToDisplay}); - } - if (!newValue) { - return Localize.translateLocal('iou.removedTheRequest', {valueName: displayValueName, oldValueToDisplay}); - } - return Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); -} - -/** - * Get the proper message line for a modified expense. - * - * @param {String} newValue - * @param {String} oldValue - * @param {String} valueName - * @param {Boolean} valueInQuotes - * @returns {String} - */ - -function getProperLineForModifiedExpenseMessage(prefix, messageFragments) { - if (messageFragments.length === 0) { - return ''; - } - return messageFragments.reduce((acc, value, index) => { - if (index === messageFragments.length - 1) { - if (messageFragments.length === 1) { - return `${acc} ${value}.`; - } - if (messageFragments.length === 2) { - return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; - } - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; - } - if (index === 0) { - return `${acc} ${value}`; - } - return `${acc}, ${value}`; - }, prefix); -} - -/** - * Get the proper message schema for modified distance message. - * - * @param {String} newDistance - * @param {String} oldDistance - * @param {String} newAmount - * @param {String} oldAmount - * @returns {String} - */ - -function getProperSchemaForModifiedDistanceMessage(newDistance, oldDistance, newAmount, oldAmount) { - if (!oldDistance) { - return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); - } - return Localize.translateLocal('iou.updatedTheDistance', { - newDistanceToDisplay: newDistance, - oldDistanceToDisplay: oldDistance, - newAmountToDisplay: newAmount, - oldAmountToDisplay: oldAmount, - }); -} - -/** - * Get the report action message when expense has been modified. - * - * ModifiedExpense::getNewDotComment in Web-Expensify should match this. - * If we change this function be sure to update the backend as well. - * - * @param {Object} reportAction - * @returns {String} - */ -function getModifiedExpenseMessage(reportAction) { - const reportActionOriginalMessage = lodashGet(reportAction, 'originalMessage', {}); - if (_.isEmpty(reportActionOriginalMessage)) { - return Localize.translateLocal('iou.changedTheRequest'); - } - const reportID = lodashGet(reportAction, 'reportID', ''); - const policyID = lodashGet(getReport(reportID), 'policyID', ''); - const policyTags = getPolicyTags(policyID); - const policyTag = PolicyUtils.getTag(policyTags); - const policyTagListName = lodashGet(policyTag, 'name', Localize.translateLocal('common.tag')); - - const removalFragments = []; - const setFragments = []; - const changeFragments = []; - - const hasModifiedAmount = - _.has(reportActionOriginalMessage, 'oldAmount') && - _.has(reportActionOriginalMessage, 'oldCurrency') && - _.has(reportActionOriginalMessage, 'amount') && - _.has(reportActionOriginalMessage, 'currency'); - - const hasModifiedMerchant = _.has(reportActionOriginalMessage, 'oldMerchant') && _.has(reportActionOriginalMessage, 'merchant'); - if (hasModifiedAmount) { - const oldCurrency = reportActionOriginalMessage.oldCurrency; - const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.oldAmount, oldCurrency); - - const currency = reportActionOriginalMessage.currency; - const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.amount, currency); - - // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. - // We check the merchant is in distance format (includes @) as a sanity check - if (hasModifiedMerchant && reportActionOriginalMessage.merchant.includes('@')) { - return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); - } - - const fragment = getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); - if (!oldAmount) { - setFragments.push(fragment); - } else if (!amount) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); - if (hasModifiedComment) { - const fragment = getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.newComment, - reportActionOriginalMessage.oldComment, - Localize.translateLocal('common.description'), - true, - ); - if (!reportActionOriginalMessage.oldComment) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.newComment) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); - if (hasModifiedCreated) { - // Take only the YYYY-MM-DD value as the original date includes timestamp - let formattedOldCreated = new Date(reportActionOriginalMessage.oldCreated); - formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); - if (!formattedOldCreated) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.created) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - if (hasModifiedMerchant) { - const fragment = getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.merchant, - reportActionOriginalMessage.oldMerchant, - Localize.translateLocal('common.merchant'), - true, - ); - if (!reportActionOriginalMessage.oldMerchant) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.merchant) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); - if (hasModifiedCategory) { - const fragment = getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.category, - reportActionOriginalMessage.oldCategory, - Localize.translateLocal('common.category'), - true, - ); - if (!reportActionOriginalMessage.oldCategory) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.category) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); - if (hasModifiedTag) { - const fragment = getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.tag, - reportActionOriginalMessage.oldTag, - policyTagListName, - true, - policyTagListName === Localize.translateLocal('common.tag'), - ); - if (!reportActionOriginalMessage.oldTag) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.tag) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); - if (hasModifiedBillable) { - const fragment = getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.billable, - reportActionOriginalMessage.oldBillable, - Localize.translateLocal('iou.request'), - true, - ); - if (!reportActionOriginalMessage.oldBillable) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.billable) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - let message = - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.set')}`, setFragments) + - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); - if (message === '') { - return message; - } - message = `${message.substring(1, message.length)}`; - return message; -} - /** * Given the updates user made to the request, compose the originalMessage * object of the modified expense action. @@ -4514,7 +4251,6 @@ export { getParentReport, getRootParentReport, getReportPreviewMessage, - getModifiedExpenseMessage, canUserPerformWriteAction, getOriginalReportID, canAccessReport, diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 4f35926c5957..0d788dbefda4 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -22,6 +22,7 @@ import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import {clearActiveReportAction, hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; /** * Gets the HTML version of the message in an action. @@ -276,7 +277,7 @@ export default [ const displayMessage = ReportUtils.getReportPreviewMessage(iouReport, reportAction); Clipboard.setString(displayMessage); } else if (ReportActionsUtils.isModifiedExpenseAction(reportAction)) { - const modifyExpenseMessage = ReportUtils.getModifiedExpenseMessage(reportAction); + const modifyExpenseMessage = ModifiedExpenseMessage.getModifiedExpenseMessage(reportAction); Clipboard.setString(modifyExpenseMessage); } else if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { const displayMessage = ReportUtils.getIOUReportActionDisplayMessage(reportAction); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 9f803f72cbbb..ed89e4c2f413 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -74,6 +74,7 @@ import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; import reportActionPropTypes from './reportActionPropTypes'; import ReportAttachmentsContext from './ReportAttachmentsContext'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; const propTypes = { ...windowDimensionsPropTypes, @@ -414,7 +415,7 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { - children = ; + children = ; } else { const hasBeenFlagged = !_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], moderationDecision); children = ( From 4ea44ee4d1c1be7f8d79ee0d8388278262d5b681 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 20 Nov 2023 18:43:45 +0200 Subject: [PATCH 024/132] Move policy tags logic to PolicyUtils --- src/libs/ModifiedExpenseMessage.ts | 34 ++++++------------------------ src/libs/PolicyUtils.ts | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index d323edcd38f8..18767c68e010 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -2,42 +2,20 @@ import {format} from 'date-fns'; import lodashGet from 'lodash/get'; import * as Localize from './Localize'; import * as PolicyUtils from './PolicyUtils'; -import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import * as ReportUtils from './ReportUtils'; import * as CurrencyUtils from './CurrencyUtils'; import _ from 'underscore'; -let allPolicyTags = {}; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - allPolicyTags = {}; - return; - } - - allPolicyTags = value; - }, -}); - -function getPolicyTags(policyID: string) { - return lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); -} - /** * Get the proper message schema for a modified field on the expense. * - * @param {String} newValue - * @param {String} oldValue - * @param {String} valueName - * @param {Boolean} valueInQuotes - * @param {Boolean} shouldConvertToLowercase - * @returns {String} + * @param newValue + * @param oldValue + * @param valueName + * @param valueInQuotes + * @param shouldConvertToLowercase */ function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true) { @@ -123,7 +101,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { } const reportID = lodashGet(reportAction, 'reportID', ''); const policyID = lodashGet(ReportUtils.getReport(reportID), 'policyID', ''); - const policyTags = getPolicyTags(policyID); + const policyTags = PolicyUtils.getPolicyTags(policyID); const policyTag = PolicyUtils.getTag(policyTags); const policyTagListName = lodashGet(policyTag, 'name', Localize.translateLocal('common.tag')); diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 62640a11311a..aa33a74dedf9 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -3,11 +3,32 @@ import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {PersonalDetails, Policy, PolicyMembers, PolicyTags} from '@src/types/onyx'; +import Onyx from 'react-native-onyx'; +import lodashGet from 'lodash/get'; type MemberEmailsToAccountIDs = Record; type PersonalDetailsList = Record; type UnitRate = {rate: number}; +let allPolicyTags = {}; + +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTags = {}; + return; + } + + allPolicyTags = value; + }, +}); + +function getPolicyTags(policyID: string) { + return lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); +} + /** * Filter out the active policies, which will exclude policies with pending deletion * These are policies that we can use to create reports with in NewDot. @@ -199,6 +220,7 @@ function isPendingDeletePolicy(policy: OnyxEntry): boolean { export { getActivePolicies, + getPolicyTags, hasPolicyMemberError, hasPolicyError, hasPolicyErrorFields, From b9229a774e6919d00cc553c2866a11d02ed1d68d Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 20 Nov 2023 22:35:25 +0200 Subject: [PATCH 025/132] Better naming --- src/libs/ModifiedExpenseMessage.ts | 59 +++++++++---------- .../LocalNotification/BrowserNotifications.js | 2 +- src/libs/OptionsListUtils.js | 2 +- .../report/ContextMenu/ContextMenuActions.js | 2 +- src/pages/home/report/ReportActionItem.js | 2 +- 5 files changed, 32 insertions(+), 35 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 18767c68e010..f95c2ddfef99 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -9,7 +9,7 @@ import _ from 'underscore'; /** - * Get the proper message schema for a modified field on the expense. + * Get the partial message for a modified field on the expense. * * @param newValue * @param oldValue @@ -18,7 +18,7 @@ import _ from 'underscore'; * @param shouldConvertToLowercase */ -function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true) { +function getPartialMessageForValue(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true) { const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; @@ -33,16 +33,15 @@ function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: st } /** - * Get the proper message line for a modified expense. + * Get the message line for a modified expense. * - * @param {String} newValue - * @param {String} oldValue - * @param {String} valueName - * @param {Boolean} valueInQuotes - * @returns {String} + * @param newValue + * @param oldValue + * @param valueName + * @param valueInQuotes */ -function getProperLineForModifiedExpenseMessage(prefix: string, messageFragments: Array) { +function getMessageLine(prefix: string, messageFragments: Array) { if (messageFragments.length === 0) { return ''; } @@ -64,16 +63,15 @@ function getProperLineForModifiedExpenseMessage(prefix: string, messageFragments } /** - * Get the proper message schema for modified distance message. + * Get the message for a modified distance request. * - * @param {String} newDistance - * @param {String} oldDistance - * @param {String} newAmount - * @param {String} oldAmount - * @returns {String} + * @param newDistance + * @param oldDistance + * @param newAmount + * @param oldAmount */ -function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { +function getForDistanceRequest(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { if (!oldDistance) { return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); } @@ -91,10 +89,9 @@ function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDista * ModifiedExpense::getNewDotComment in Web-Expensify should match this. * If we change this function be sure to update the backend as well. * - * @param {Object} reportAction - * @returns {String} + * @param reportAction */ -function getModifiedExpenseMessage(reportAction: Object): string { +function getForReportAction(reportAction: Object): string { const reportActionOriginalMessage: any = lodashGet(reportAction, 'originalMessage', {}); if (_.isEmpty(reportActionOriginalMessage)) { return Localize.translateLocal('iou.changedTheRequest'); @@ -126,10 +123,10 @@ function getModifiedExpenseMessage(reportAction: Object): string { // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. // We check the merchant is in distance format (includes @) as a sanity check if (hasModifiedMerchant && reportActionOriginalMessage.merchant.includes('@')) { - return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); + return getForDistanceRequest(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); } - const fragment = getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); + const fragment = getPartialMessageForValue(amount, oldAmount, Localize.translateLocal('iou.amount'), false); if (!oldAmount) { setFragments.push(fragment); } else if (!amount) { @@ -141,7 +138,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { - const fragment = getProperSchemaForModifiedExpenseMessage( + const fragment = getPartialMessageForValue( reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), @@ -160,7 +157,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp let formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); - const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); + const fragment = getPartialMessageForValue(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); if (!formattedOldCreated) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.created) { @@ -171,7 +168,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { } if (hasModifiedMerchant) { - const fragment = getProperSchemaForModifiedExpenseMessage( + const fragment = getPartialMessageForValue( reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), @@ -188,7 +185,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - const fragment = getProperSchemaForModifiedExpenseMessage( + const fragment = getPartialMessageForValue( reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), @@ -205,7 +202,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); if (hasModifiedTag) { - const fragment = getProperSchemaForModifiedExpenseMessage( + const fragment = getPartialMessageForValue( reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, policyTagListName, @@ -223,7 +220,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - const fragment = getProperSchemaForModifiedExpenseMessage( + const fragment = getPartialMessageForValue( reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), @@ -239,9 +236,9 @@ function getModifiedExpenseMessage(reportAction: Object): string { } let message = - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.set')}`, setFragments) + - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); + getMessageLine(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + + getMessageLine(`\n${Localize.translateLocal('iou.set')}`, setFragments) + + getMessageLine(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); if (message === '') { return message; } @@ -250,5 +247,5 @@ function getModifiedExpenseMessage(reportAction: Object): string { } export default { - getModifiedExpenseMessage, + getForReportAction, }; \ No newline at end of file diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.js b/src/libs/Notification/LocalNotification/BrowserNotifications.js index 497b686e7d5e..853106adafe8 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.js +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.js @@ -132,7 +132,7 @@ export default { pushModifiedExpenseNotification({reportAction, onClick}, usesIcon = false) { push({ title: _.map(reportAction.person, (f) => f.text).join(', '), - body: ModifiedExpenseMessage.getModifiedExpenseMessage(reportAction), + body: ModifiedExpenseMessage.getForReportAction(reportAction), delay: 0, onClick, icon: usesIcon ? EXPENSIFY_ICON_URL : '', diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index af0407a18112..fd6031f5c901 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -403,7 +403,7 @@ function getLastMessageTextForReport(report) { } else if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) { lastMessageTextFromReport = `[${Localize.translateLocal(report.lastMessageTranslationKey || 'common.attachment')}]`; } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) { - const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getModifiedExpenseMessage(lastReportAction); + const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getForReportAction(lastReportAction); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true); } else if ( lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 0d788dbefda4..bebb2e2688c5 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -277,7 +277,7 @@ export default [ const displayMessage = ReportUtils.getReportPreviewMessage(iouReport, reportAction); Clipboard.setString(displayMessage); } else if (ReportActionsUtils.isModifiedExpenseAction(reportAction)) { - const modifyExpenseMessage = ModifiedExpenseMessage.getModifiedExpenseMessage(reportAction); + const modifyExpenseMessage = ModifiedExpenseMessage.getForReportAction(reportAction); Clipboard.setString(modifyExpenseMessage); } else if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { const displayMessage = ReportUtils.getIOUReportActionDisplayMessage(reportAction); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index ed89e4c2f413..ce4c151bf2d6 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -415,7 +415,7 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { - children = ; + children = ; } else { const hasBeenFlagged = !_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], moderationDecision); children = ( From 72714adfc3730fd32f9e2d551b0ad9ed8016de8b Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 20 Nov 2023 22:44:31 +0200 Subject: [PATCH 026/132] Dry up code. --- src/libs/ModifiedExpenseMessage.ts | 103 ++++++++++------------------- 1 file changed, 34 insertions(+), 69 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index f95c2ddfef99..d3f0b07cbcb8 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -9,7 +9,7 @@ import _ from 'underscore'; /** - * Get the partial message for a modified field on the expense. + * Builds the partial message fragment for a modified field on the expense. * * @param newValue * @param oldValue @@ -18,18 +18,30 @@ import _ from 'underscore'; * @param shouldConvertToLowercase */ -function getPartialMessageForValue(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true) { +function buildMessageFragmentForValue( + newValue: string, + oldValue: string, + valueName: string, + valueInQuotes: boolean, + setFragments: Array, + removalFragments: Array, + changeFragments: Array, + shouldConvertToLowercase = true +) { const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; if (!oldValue) { - return Localize.translateLocal('iou.setTheRequest', {valueName: displayValueName, newValueToDisplay}); + const fragment = Localize.translateLocal('iou.setTheRequest', {valueName: displayValueName, newValueToDisplay}); + setFragments.push(fragment); + } else if (!newValue) { + const fragment = Localize.translateLocal('iou.removedTheRequest', {valueName: displayValueName, oldValueToDisplay}); + removalFragments.push(fragment); + } else { + const fragment = Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); + changeFragments.push(fragment); } - if (!newValue) { - return Localize.translateLocal('iou.removedTheRequest', {valueName: displayValueName, oldValueToDisplay}); - } - return Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); } /** @@ -102,9 +114,9 @@ function getForReportAction(reportAction: Object): string { const policyTag = PolicyUtils.getTag(policyTags); const policyTagListName = lodashGet(policyTag, 'name', Localize.translateLocal('common.tag')); - const removalFragments = []; - const setFragments = []; - const changeFragments = []; + const removalFragments: Array = []; + const setFragments: Array = []; + const changeFragments: Array = []; const hasModifiedAmount = _.has(reportActionOriginalMessage, 'oldAmount') && @@ -126,113 +138,66 @@ function getForReportAction(reportAction: Object): string { return getForDistanceRequest(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); } - const fragment = getPartialMessageForValue(amount, oldAmount, Localize.translateLocal('iou.amount'), false); - if (!oldAmount) { - setFragments.push(fragment); - } else if (!amount) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } + buildMessageFragmentForValue(amount, oldAmount, Localize.translateLocal('iou.amount'), false, setFragments, removalFragments, changeFragments); } const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { - const fragment = getPartialMessageForValue( + buildMessageFragmentForValue( reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), - true, + true, setFragments, removalFragments, changeFragments ); - if (!reportActionOriginalMessage.oldComment) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.newComment) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } } const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp let formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); - const fragment = getPartialMessageForValue(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); - if (!formattedOldCreated) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.created) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } + buildMessageFragmentForValue(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false, setFragments, removalFragments, changeFragments); } if (hasModifiedMerchant) { - const fragment = getPartialMessageForValue( + buildMessageFragmentForValue( reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true, + setFragments, removalFragments, changeFragments ); - if (!reportActionOriginalMessage.oldMerchant) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.merchant) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } } const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - const fragment = getPartialMessageForValue( + buildMessageFragmentForValue( reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), - true, + true, setFragments, removalFragments, changeFragments ); - if (!reportActionOriginalMessage.oldCategory) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.category) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } } const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); if (hasModifiedTag) { - const fragment = getPartialMessageForValue( + buildMessageFragmentForValue( reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, policyTagListName, true, + setFragments, removalFragments, changeFragments, policyTagListName === Localize.translateLocal('common.tag'), ); - if (!reportActionOriginalMessage.oldTag) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.tag) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } } const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - const fragment = getPartialMessageForValue( + buildMessageFragmentForValue( reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), - true, + true, setFragments, removalFragments, changeFragments ); - if (!reportActionOriginalMessage.oldBillable) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.billable) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } } let message = From 18d1bf0fb300c9eac3c69ec8ff4e72d8097a0f77 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 20 Nov 2023 22:50:10 +0200 Subject: [PATCH 027/132] Run prettier --- src/libs/ModifiedExpenseMessage.ts | 66 ++++++++++++------- .../LocalNotification/BrowserNotifications.js | 2 +- src/libs/OptionsListUtils.js | 2 +- src/libs/PolicyUtils.ts | 4 +- .../report/ContextMenu/ContextMenuActions.js | 2 +- src/pages/home/report/ReportActionItem.js | 2 +- 6 files changed, 49 insertions(+), 29 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index d3f0b07cbcb8..d0ebce1275b5 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,12 +1,11 @@ import {format} from 'date-fns'; import lodashGet from 'lodash/get'; +import _ from 'underscore'; +import CONST from '@src/CONST'; +import * as CurrencyUtils from './CurrencyUtils'; import * as Localize from './Localize'; import * as PolicyUtils from './PolicyUtils'; -import CONST from '@src/CONST'; import * as ReportUtils from './ReportUtils'; -import * as CurrencyUtils from './CurrencyUtils'; -import _ from 'underscore'; - /** * Builds the partial message fragment for a modified field on the expense. @@ -19,14 +18,14 @@ import _ from 'underscore'; */ function buildMessageFragmentForValue( - newValue: string, - oldValue: string, - valueName: string, - valueInQuotes: boolean, - setFragments: Array, - removalFragments: Array, - changeFragments: Array, - shouldConvertToLowercase = true + newValue: string, + oldValue: string, + valueName: string, + valueInQuotes: boolean, + setFragments: string[], + removalFragments: string[], + changeFragments: string[], + shouldConvertToLowercase = true, ) { const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; @@ -53,7 +52,7 @@ function buildMessageFragmentForValue( * @param valueInQuotes */ -function getMessageLine(prefix: string, messageFragments: Array) { +function getMessageLine(prefix: string, messageFragments: string[]) { if (messageFragments.length === 0) { return ''; } @@ -114,9 +113,9 @@ function getForReportAction(reportAction: Object): string { const policyTag = PolicyUtils.getTag(policyTags); const policyTagListName = lodashGet(policyTag, 'name', Localize.translateLocal('common.tag')); - const removalFragments: Array = []; - const setFragments: Array = []; - const changeFragments: Array = []; + const removalFragments: string[] = []; + const setFragments: string[] = []; + const changeFragments: string[] = []; const hasModifiedAmount = _.has(reportActionOriginalMessage, 'oldAmount') && @@ -147,7 +146,10 @@ function getForReportAction(reportAction: Object): string { reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), - true, setFragments, removalFragments, changeFragments + true, + setFragments, + removalFragments, + changeFragments, ); } @@ -155,7 +157,15 @@ function getForReportAction(reportAction: Object): string { if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp let formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); - buildMessageFragmentForValue(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false, setFragments, removalFragments, changeFragments); + buildMessageFragmentForValue( + reportActionOriginalMessage.created, + formattedOldCreated, + Localize.translateLocal('common.date'), + false, + setFragments, + removalFragments, + changeFragments, + ); } if (hasModifiedMerchant) { @@ -164,7 +174,9 @@ function getForReportAction(reportAction: Object): string { reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true, - setFragments, removalFragments, changeFragments + setFragments, + removalFragments, + changeFragments, ); } @@ -174,7 +186,10 @@ function getForReportAction(reportAction: Object): string { reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), - true, setFragments, removalFragments, changeFragments + true, + setFragments, + removalFragments, + changeFragments, ); } @@ -185,7 +200,9 @@ function getForReportAction(reportAction: Object): string { reportActionOriginalMessage.oldTag, policyTagListName, true, - setFragments, removalFragments, changeFragments, + setFragments, + removalFragments, + changeFragments, policyTagListName === Localize.translateLocal('common.tag'), ); } @@ -196,7 +213,10 @@ function getForReportAction(reportAction: Object): string { reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), - true, setFragments, removalFragments, changeFragments + true, + setFragments, + removalFragments, + changeFragments, ); } @@ -213,4 +233,4 @@ function getForReportAction(reportAction: Object): string { export default { getForReportAction, -}; \ No newline at end of file +}; diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.js b/src/libs/Notification/LocalNotification/BrowserNotifications.js index 853106adafe8..4c990a87878c 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.js +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.js @@ -1,10 +1,10 @@ // Web and desktop implementation only. Do not import for direct use. Use LocalNotification. import _ from 'underscore'; import EXPENSIFY_ICON_URL from '@assets/images/expensify-logo-round-clearspace.png'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import * as ReportUtils from '@libs/ReportUtils'; import * as AppUpdate from '@userActions/AppUpdate'; import focusApp from './focusApp'; -import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; const DEFAULT_DELAY = 4000; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index fd6031f5c901..9f0780c6a909 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -13,6 +13,7 @@ import * as ErrorUtils from './ErrorUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; import * as LoginUtils from './LoginUtils'; +import ModifiedExpenseMessage from './ModifiedExpenseMessage'; import Navigation from './Navigation/Navigation'; import Permissions from './Permissions'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; @@ -20,7 +21,6 @@ import * as ReportActionUtils from './ReportActionsUtils'; import * as ReportUtils from './ReportUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -import ModifiedExpenseMessage from './ModifiedExpenseMessage'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index aa33a74dedf9..ead8d20f838e 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,10 +1,10 @@ import Str from 'expensify-common/lib/str'; +import lodashGet from 'lodash/get'; import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {PersonalDetails, Policy, PolicyMembers, PolicyTags} from '@src/types/onyx'; -import Onyx from 'react-native-onyx'; -import lodashGet from 'lodash/get'; type MemberEmailsToAccountIDs = Record; type PersonalDetailsList = Record; diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index bebb2e2688c5..8be004cbe6a5 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -10,6 +10,7 @@ import Clipboard from '@libs/Clipboard'; import * as Environment from '@libs/Environment/Environment'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -22,7 +23,6 @@ import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import {clearActiveReportAction, hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; -import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; /** * Gets the HTML version of the message in an action. diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index ce4c151bf2d6..6ae2241093e4 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -36,6 +36,7 @@ import compose from '@libs/compose'; import ControlSelection from '@libs/ControlSelection'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import focusTextInputAfterAnimation from '@libs/focusTextInputAfterAnimation'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -74,7 +75,6 @@ import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; import reportActionPropTypes from './reportActionPropTypes'; import ReportAttachmentsContext from './ReportAttachmentsContext'; -import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; const propTypes = { ...windowDimensionsPropTypes, From 5444abd6030d0c45df9a3ff0a4c88b432f234ecf Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 21 Nov 2023 01:33:08 +0200 Subject: [PATCH 028/132] Fix lint errors --- ...seMessage.ts => ModifiedExpenseMessage.js} | 64 +++++++++---------- src/libs/PolicyUtils.ts | 13 ++-- 2 files changed, 34 insertions(+), 43 deletions(-) rename src/libs/{ModifiedExpenseMessage.ts => ModifiedExpenseMessage.js} (82%) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.js similarity index 82% rename from src/libs/ModifiedExpenseMessage.ts rename to src/libs/ModifiedExpenseMessage.js index d0ebce1275b5..5428413efb96 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.js @@ -17,16 +17,7 @@ import * as ReportUtils from './ReportUtils'; * @param shouldConvertToLowercase */ -function buildMessageFragmentForValue( - newValue: string, - oldValue: string, - valueName: string, - valueInQuotes: boolean, - setFragments: string[], - removalFragments: string[], - changeFragments: string[], - shouldConvertToLowercase = true, -) { +function buildMessageFragmentForValue(newValue, oldValue, valueName, valueInQuotes, setFragments, removalFragments, changeFragments, shouldConvertToLowercase = true) { const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; @@ -52,25 +43,29 @@ function buildMessageFragmentForValue( * @param valueInQuotes */ -function getMessageLine(prefix: string, messageFragments: string[]) { +function getMessageLine(prefix, messageFragments) { if (messageFragments.length === 0) { return ''; } - return messageFragments.reduce((acc, value, index) => { - if (index === messageFragments.length - 1) { - if (messageFragments.length === 1) { - return `${acc} ${value}.`; + return _.reduce( + messageFragments, + (acc, value, index) => { + if (index === messageFragments.length - 1) { + if (messageFragments.length === 1) { + return `${acc} ${value}.`; + } + if (messageFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + } + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; } - if (messageFragments.length === 2) { - return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + if (index === 0) { + return `${acc} ${value}`; } - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; - } - if (index === 0) { - return `${acc} ${value}`; - } - return `${acc}, ${value}`; - }, prefix); + return `${acc}, ${value}`; + }, + prefix, + ); } /** @@ -80,9 +75,10 @@ function getMessageLine(prefix: string, messageFragments: string[]) { * @param oldDistance * @param newAmount * @param oldAmount + * @returns {String} */ -function getForDistanceRequest(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { +function getForDistanceRequest(newDistance, oldDistance, newAmount, oldAmount) { if (!oldDistance) { return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); } @@ -100,22 +96,22 @@ function getForDistanceRequest(newDistance: string, oldDistance: string, newAmou * ModifiedExpense::getNewDotComment in Web-Expensify should match this. * If we change this function be sure to update the backend as well. * - * @param reportAction + * @param {Object} reportAction + * @returns {String} */ -function getForReportAction(reportAction: Object): string { - const reportActionOriginalMessage: any = lodashGet(reportAction, 'originalMessage', {}); +function getForReportAction(reportAction) { + const reportActionOriginalMessage = lodashGet(reportAction, 'originalMessage', {}); if (_.isEmpty(reportActionOriginalMessage)) { return Localize.translateLocal('iou.changedTheRequest'); } const reportID = lodashGet(reportAction, 'reportID', ''); const policyID = lodashGet(ReportUtils.getReport(reportID), 'policyID', ''); const policyTags = PolicyUtils.getPolicyTags(policyID); - const policyTag = PolicyUtils.getTag(policyTags); - const policyTagListName = lodashGet(policyTag, 'name', Localize.translateLocal('common.tag')); + const policyTagListName = PolicyUtils.getTagListName(policyTags) || Localize.translateLocal('common.tag'); - const removalFragments: string[] = []; - const setFragments: string[] = []; - const changeFragments: string[] = []; + const removalFragments = []; + const setFragments = []; + const changeFragments = []; const hasModifiedAmount = _.has(reportActionOriginalMessage, 'oldAmount') && @@ -156,7 +152,7 @@ function getForReportAction(reportAction: Object): string { const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp - let formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); + const formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); buildMessageFragmentForValue( reportActionOriginalMessage.created, formattedOldCreated, diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index ead8d20f838e..ee1243cc573d 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,7 +1,5 @@ import Str from 'expensify-common/lib/str'; -import lodashGet from 'lodash/get'; -import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {PersonalDetails, Policy, PolicyMembers, PolicyTags} from '@src/types/onyx'; @@ -10,23 +8,20 @@ type MemberEmailsToAccountIDs = Record; type PersonalDetailsList = Record; type UnitRate = {rate: number}; -let allPolicyTags = {}; - +let allPolicyTags: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, waitForCollectionCallback: true, callback: (value) => { if (!value) { - allPolicyTags = {}; return; } - - allPolicyTags = value; + allPolicyTags = Object.fromEntries(Object.entries(value).filter(([, policyTags]) => !!policyTags)); }, }); function getPolicyTags(policyID: string) { - return lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); + return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; } /** From 371f138602ffb8f5ce4fb72d3d060bd46738e6ec Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 21 Nov 2023 02:04:39 +0200 Subject: [PATCH 029/132] Make it a ts file. --- src/libs/{ModifiedExpenseMessage.js => ModifiedExpenseMessage.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/libs/{ModifiedExpenseMessage.js => ModifiedExpenseMessage.ts} (100%) diff --git a/src/libs/ModifiedExpenseMessage.js b/src/libs/ModifiedExpenseMessage.ts similarity index 100% rename from src/libs/ModifiedExpenseMessage.js rename to src/libs/ModifiedExpenseMessage.ts From ceaa9156cf5b12284654b0c790509e9ee0423933 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 21 Nov 2023 02:49:30 +0200 Subject: [PATCH 030/132] Migrate to TS --- src/libs/ModifiedExpenseMessage.ts | 120 ++++++++++++++--------------- src/types/onyx/OriginalMessage.ts | 21 ++++- 2 files changed, 79 insertions(+), 62 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 5428413efb96..6ac50cc2984a 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,7 +1,7 @@ import {format} from 'date-fns'; import lodashGet from 'lodash/get'; -import _ from 'underscore'; import CONST from '@src/CONST'; +import {ReportAction} from '@src/types/onyx'; import * as CurrencyUtils from './CurrencyUtils'; import * as Localize from './Localize'; import * as PolicyUtils from './PolicyUtils'; @@ -17,7 +17,16 @@ import * as ReportUtils from './ReportUtils'; * @param shouldConvertToLowercase */ -function buildMessageFragmentForValue(newValue, oldValue, valueName, valueInQuotes, setFragments, removalFragments, changeFragments, shouldConvertToLowercase = true) { +function buildMessageFragmentForValue( + newValue: string, + oldValue: string, + valueName: string, + valueInQuotes: boolean, + setFragments: string[], + removalFragments: string[], + changeFragments: string[], + shouldConvertToLowercase = true, +) { const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; @@ -43,29 +52,25 @@ function buildMessageFragmentForValue(newValue, oldValue, valueName, valueInQuot * @param valueInQuotes */ -function getMessageLine(prefix, messageFragments) { +function getMessageLine(prefix: string, messageFragments: string[]) { if (messageFragments.length === 0) { return ''; } - return _.reduce( - messageFragments, - (acc, value, index) => { - if (index === messageFragments.length - 1) { - if (messageFragments.length === 1) { - return `${acc} ${value}.`; - } - if (messageFragments.length === 2) { - return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; - } - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + return messageFragments.reduce((acc, value, index) => { + if (index === messageFragments.length - 1) { + if (messageFragments.length === 1) { + return `${acc} ${value}.`; } - if (index === 0) { - return `${acc} ${value}`; + if (messageFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; } - return `${acc}, ${value}`; - }, - prefix, - ); + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + if (index === 0) { + return `${acc} ${value}`; + } + return `${acc}, ${value}`; + }, prefix); } /** @@ -78,7 +83,7 @@ function getMessageLine(prefix, messageFragments) { * @returns {String} */ -function getForDistanceRequest(newDistance, oldDistance, newAmount, oldAmount) { +function getForDistanceRequest(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { if (!oldDistance) { return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); } @@ -96,51 +101,46 @@ function getForDistanceRequest(newDistance, oldDistance, newAmount, oldAmount) { * ModifiedExpense::getNewDotComment in Web-Expensify should match this. * If we change this function be sure to update the backend as well. * - * @param {Object} reportAction - * @returns {String} + * @param reportAction */ -function getForReportAction(reportAction) { - const reportActionOriginalMessage = lodashGet(reportAction, 'originalMessage', {}); - if (_.isEmpty(reportActionOriginalMessage)) { - return Localize.translateLocal('iou.changedTheRequest'); +function getForReportAction(reportAction: ReportAction) { + if (reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { + return ''; } + const reportActionOriginalMessage = reportAction.originalMessage; const reportID = lodashGet(reportAction, 'reportID', ''); const policyID = lodashGet(ReportUtils.getReport(reportID), 'policyID', ''); const policyTags = PolicyUtils.getPolicyTags(policyID); const policyTagListName = PolicyUtils.getTagListName(policyTags) || Localize.translateLocal('common.tag'); - const removalFragments = []; - const setFragments = []; - const changeFragments = []; + const removalFragments: string[] = []; + const setFragments: string[] = []; + const changeFragments: string[] = []; - const hasModifiedAmount = - _.has(reportActionOriginalMessage, 'oldAmount') && - _.has(reportActionOriginalMessage, 'oldCurrency') && - _.has(reportActionOriginalMessage, 'amount') && - _.has(reportActionOriginalMessage, 'currency'); + const hasModifiedAmount = reportActionOriginalMessage.oldAmount && reportActionOriginalMessage.oldCurrency && reportActionOriginalMessage.amount && reportActionOriginalMessage.currency; - const hasModifiedMerchant = _.has(reportActionOriginalMessage, 'oldMerchant') && _.has(reportActionOriginalMessage, 'merchant'); + const hasModifiedMerchant = reportActionOriginalMessage.oldMerchant && reportActionOriginalMessage.merchant; if (hasModifiedAmount) { - const oldCurrency = reportActionOriginalMessage.oldCurrency; - const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.oldAmount, oldCurrency); + const oldCurrency = reportActionOriginalMessage.oldCurrency ?? ''; + const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.oldAmount ?? 0, oldCurrency); - const currency = reportActionOriginalMessage.currency; - const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.amount, currency); + const currency = reportActionOriginalMessage.currency ?? ''; + const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.amount ?? 0, currency); // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. // We check the merchant is in distance format (includes @) as a sanity check - if (hasModifiedMerchant && reportActionOriginalMessage.merchant.includes('@')) { - return getForDistanceRequest(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); + if (hasModifiedMerchant && (reportActionOriginalMessage.merchant ?? '').includes('@')) { + return getForDistanceRequest(reportActionOriginalMessage.merchant ?? '', reportActionOriginalMessage.oldMerchant ?? '', amount, oldAmount); } buildMessageFragmentForValue(amount, oldAmount, Localize.translateLocal('iou.amount'), false, setFragments, removalFragments, changeFragments); } - const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); + const hasModifiedComment = reportActionOriginalMessage.oldComment && reportActionOriginalMessage.newComment; if (hasModifiedComment) { buildMessageFragmentForValue( - reportActionOriginalMessage.newComment, - reportActionOriginalMessage.oldComment, + reportActionOriginalMessage.newComment ?? '', + reportActionOriginalMessage.oldComment ?? '', Localize.translateLocal('common.description'), true, setFragments, @@ -149,12 +149,12 @@ function getForReportAction(reportAction) { ); } - const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); + const hasModifiedCreated = reportActionOriginalMessage.oldCreated && reportActionOriginalMessage.created; if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp - const formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); + const formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated ?? ''), CONST.DATE.FNS_FORMAT_STRING); buildMessageFragmentForValue( - reportActionOriginalMessage.created, + reportActionOriginalMessage.created ?? '', formattedOldCreated, Localize.translateLocal('common.date'), false, @@ -166,8 +166,8 @@ function getForReportAction(reportAction) { if (hasModifiedMerchant) { buildMessageFragmentForValue( - reportActionOriginalMessage.merchant, - reportActionOriginalMessage.oldMerchant, + reportActionOriginalMessage.merchant ?? '', + reportActionOriginalMessage.oldMerchant ?? '', Localize.translateLocal('common.merchant'), true, setFragments, @@ -176,11 +176,11 @@ function getForReportAction(reportAction) { ); } - const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); + const hasModifiedCategory = reportActionOriginalMessage.oldCategory && reportActionOriginalMessage.category; if (hasModifiedCategory) { buildMessageFragmentForValue( - reportActionOriginalMessage.category, - reportActionOriginalMessage.oldCategory, + reportActionOriginalMessage.category ?? '', + reportActionOriginalMessage.oldCategory ?? '', Localize.translateLocal('common.category'), true, setFragments, @@ -189,11 +189,11 @@ function getForReportAction(reportAction) { ); } - const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); + const hasModifiedTag = reportActionOriginalMessage.oldTag && reportActionOriginalMessage.tag; if (hasModifiedTag) { buildMessageFragmentForValue( - reportActionOriginalMessage.tag, - reportActionOriginalMessage.oldTag, + reportActionOriginalMessage.tag ?? '', + reportActionOriginalMessage.oldTag ?? '', policyTagListName, true, setFragments, @@ -203,11 +203,11 @@ function getForReportAction(reportAction) { ); } - const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); + const hasModifiedBillable = reportActionOriginalMessage.oldBillable && reportActionOriginalMessage.billable; if (hasModifiedBillable) { buildMessageFragmentForValue( - reportActionOriginalMessage.billable, - reportActionOriginalMessage.oldBillable, + reportActionOriginalMessage.billable ?? '', + reportActionOriginalMessage.oldBillable ?? '', Localize.translateLocal('iou.request'), true, setFragments, @@ -221,7 +221,7 @@ function getForReportAction(reportAction) { getMessageLine(`\n${Localize.translateLocal('iou.set')}`, setFragments) + getMessageLine(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); if (message === '') { - return message; + return Localize.translateLocal('iou.changedTheRequest'); } message = `${message.substring(1, message.length)}`; return message; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index b5e4b25a6508..c8ee5d5771de 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -171,7 +171,24 @@ type OriginalMessagePolicyTask = { type OriginalMessageModifiedExpense = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE; - originalMessage: unknown; + originalMessage: { + oldMerchant?: string; + merchant?: string; + oldCurrency?: string; + currency?: string; + oldAmount?: number; + amount?: number; + oldComment?: string; + newComment?: string; + oldCreated?: string; + created?: string; + oldCategory?: string; + category?: string; + oldTag?: string; + tag?: string; + oldBillable?: string; + billable?: string; + }; }; type OriginalMessageReimbursementQueued = { @@ -196,4 +213,4 @@ type OriginalMessage = | OriginalMessageReimbursementQueued; export default OriginalMessage; -export type {ChronosOOOEvent, Decision, Reaction, ActionName}; +export type {ChronosOOOEvent, Decision, Reaction, ActionName, OriginalMessageModifiedExpense}; From 3bec4fd1f562b596cbb6f327f77aee3a210e8ab6 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 21 Nov 2023 03:42:23 +0200 Subject: [PATCH 031/132] Make Lint happy. --- src/libs/ModifiedExpenseMessage.ts | 5 +---- src/libs/ReportUtils.js | 11 +++++++++++ src/types/onyx/OriginalMessage.ts | 2 +- src/types/onyx/ReportAction.ts | 1 + 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 6ac50cc2984a..44259180f4eb 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,5 +1,4 @@ import {format} from 'date-fns'; -import lodashGet from 'lodash/get'; import CONST from '@src/CONST'; import {ReportAction} from '@src/types/onyx'; import * as CurrencyUtils from './CurrencyUtils'; @@ -80,7 +79,6 @@ function getMessageLine(prefix: string, messageFragments: string[]) { * @param oldDistance * @param newAmount * @param oldAmount - * @returns {String} */ function getForDistanceRequest(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { @@ -108,8 +106,7 @@ function getForReportAction(reportAction: ReportAction) { return ''; } const reportActionOriginalMessage = reportAction.originalMessage; - const reportID = lodashGet(reportAction, 'reportID', ''); - const policyID = lodashGet(ReportUtils.getReport(reportID), 'policyID', ''); + const policyID = ReportUtils.getReportPolicyID(reportAction.reportID ?? ''); const policyTags = PolicyUtils.getPolicyTags(policyID); const policyTagListName = PolicyUtils.getTagListName(policyTags) || Localize.translateLocal('common.tag'); diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 2a3db8437518..248df849ca52 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -823,6 +823,16 @@ function getReport(reportID) { return lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}) || {}; } +/** + * Get the report policyID given a reportID + * + * @param {String} reportID + * @returns {String} + */ +function getReportPolicyID(reportID) { + return getReport(reportID).policyID || ''; +} + /** * Get the notification preference given a report * @@ -4173,6 +4183,7 @@ export { getDisplayNamesWithTooltips, getDisplayNamesStringFromTooltips, getReportName, + getReportPolicyID, getReport, getReportNotificationPreference, getReportIDFromLink, diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index c8ee5d5771de..5e1bdfb7cf81 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -213,4 +213,4 @@ type OriginalMessage = | OriginalMessageReimbursementQueued; export default OriginalMessage; -export type {ChronosOOOEvent, Decision, Reaction, ActionName, OriginalMessageModifiedExpense}; +export type {ChronosOOOEvent, Decision, Reaction, ActionName}; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 2195c4e3ff0b..e67c075e54f1 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -90,6 +90,7 @@ type ReportActionBase = { avatar?: string; automatic?: boolean; shouldShow?: boolean; + reportID?: string; childReportID?: string; childReportName?: string; childType?: string; From 8802892701b70a68208bb0c54e67f42f9e6ef1c8 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Tue, 21 Nov 2023 10:25:11 +0100 Subject: [PATCH 032/132] Get rid of too many union types --- src/components/MenuItem.tsx | 67 ++++++++++++------------------------- 1 file changed, 21 insertions(+), 46 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index d9b8489ca7d3..12f9aa36c913 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -44,50 +44,7 @@ type UnresponsiveProps = { interactive: false; } -type TitleIconProps = { - /** Boolean whether to display the title right icon */ - shouldShowTitleIcon: true; - - /** Icon to display at right side of title */ - titleIcon: IconType; -} - -type NoTitleIconProps = { - shouldShowTitleIcon?: false; - - titleIcon?: undefined; -} - -type RightIconProps = { - /** Boolean whether to display the right icon */ - shouldShowRightIcon: true; - - /** Overrides the icon for shouldShowRightIcon */ - iconRight: IconType; -} - -type NoRightIconProps = { - shouldShowRightIcon?: false; - - iconRight?: IconType; -} - -type RightComponent = { - /** Should render component on the right */ - shouldShowRightComponent: true; - - /** Component to be displayed on the right */ - rightComponent: ReactNode; -} - -type NoRightComponent = { - shouldShowRightComponent?: false; - - rightComponent?: undefined; -} - -type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | NoTitleIconProps) & -(RightIconProps | NoRightIconProps) & (RightComponent | NoRightComponent) & { +type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** Text to be shown as badge near the right end. */ badgeText?: string; @@ -133,6 +90,24 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N /** An icon to display under the main item */ furtherDetailsIcon?: IconType; + /** Boolean whether to display the title right icon */ + shouldShowTitleIcon?: boolean; + + /** Icon to display at right side of title */ + titleIcon?: IconType; + + /** Boolean whether to display the right icon */ + shouldShowRightIcon?: boolean; + + /** Overrides the icon for shouldShowRightIcon */ + iconRight?: IconType; + + /** Should render component on the right */ + shouldShowRightComponent?: boolean; + + /** Component to be displayed on the right */ + rightComponent?: ReactNode; + /** A description text to show under the title */ description?: string; @@ -278,7 +253,7 @@ function MenuItem({ return title ? `${title}` : ''; }, [title, shouldRenderAsHTML, shouldParseTitle, html]); - const hasPressableRightComponent = iconRight || (rightComponent && shouldShowRightComponent); + const hasPressableRightComponent = iconRight || (shouldShowRightComponent && rightComponent); const renderTitleContent = () => { if (titleWithTooltips && Array.isArray(titleWithTooltips) && titleWithTooltips.length > 0) { @@ -427,7 +402,7 @@ function MenuItem({ {renderTitleContent()} )} - {Boolean(shouldShowTitleIcon) && ( + {Boolean(shouldShowTitleIcon) && Boolean(titleIcon) && ( Date: Tue, 21 Nov 2023 13:00:13 +0100 Subject: [PATCH 033/132] Typing --- src/components/Badge.tsx | 2 +- src/components/DisplayNames/types.ts | 6 +- src/components/Icon/index.tsx | 4 ++ src/components/MenuItem.tsx | 98 +++++++++++++++------------- 4 files changed, 63 insertions(+), 47 deletions(-) diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 22c056dfdfc4..575646f7dd9c 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -29,7 +29,7 @@ type BadgeProps = { textStyles?: StyleProp; /** Callback to be called on onPress */ - onPress: (event?: GestureResponderEvent | KeyboardEvent) => void; + onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void; }; function Badge({success = false, error = false, pressable = false, text, environment = CONST.ENVIRONMENT.DEV, badgeStyles, textStyles, onPress = () => {}}: BadgeProps) { diff --git a/src/components/DisplayNames/types.ts b/src/components/DisplayNames/types.ts index 94e4fc7c39c6..1dcaf3e1f9c1 100644 --- a/src/components/DisplayNames/types.ts +++ b/src/components/DisplayNames/types.ts @@ -29,7 +29,7 @@ type DisplayNamesProps = { tooltipEnabled?: boolean; /** Arbitrary styles of the displayName text */ - textStyles: StyleProp; + textStyles?: StyleProp; /** * Overrides the text that's read by the screen reader when the user interacts with the element. By default, the @@ -42,3 +42,7 @@ type DisplayNamesProps = { }; export default DisplayNamesProps; + +export type { + DisplayNameWithTooltip +} diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 022c740907ea..4b8f27ad1358 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -100,3 +100,7 @@ class Icon extends PureComponent { } export default Icon; + +export type { + SrcProps +} diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 12f9aa36c913..6516056c3906 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1,7 +1,6 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {FC, ForwardedRef, ReactNode, forwardRef, useEffect, useMemo} from 'react'; -import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; -import _ from 'underscore'; +import React, {FC, ForwardedRef, ReactNode, forwardRef, useEffect, useMemo, useRef, useState} from 'react'; +import {GestureResponderEvent, PressableStateCallbackType, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import useWindowDimensions from '@hooks/useWindowDimensions'; import ControlSelection from '@libs/ControlSelection'; import convertToLTR from '@libs/convertToLTR'; @@ -21,7 +20,7 @@ import Avatar from './Avatar'; import Badge from './Badge'; import DisplayNames from './DisplayNames'; import Hoverable from './Hoverable'; -import Icon from './Icon'; +import Icon, { SrcProps } from './Icon'; import * as Expensicons from './Icon/Expensicons'; import * as defaultWorkspaceAvatars from './Icon/WorkspaceDefaultAvatars'; import MultipleAvatars from './MultipleAvatars'; @@ -29,6 +28,7 @@ import PressableWithSecondaryInteraction from './PressableWithSecondaryInteracti import RenderHTML from './RenderHTML'; import SelectCircle from './SelectCircle'; import Text from './Text'; +import { DisplayNameWithTooltip } from './DisplayNames/types'; type ResponsiveProps = { /** Function to fire when component is pressed */ @@ -60,14 +60,16 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** Any adjustments to style when menu item is hovered or pressed */ hoverAndPressStyle: StyleProp>; + descriptionTextStyle?: StyleProp; + /** Icon to display on the left side of component */ icon?: ReactNode | string | AvatarType; - + /** The fill color to pass into the icon. */ iconFill?: string; /** Secondary icon to display on the left side of component, right of the icon */ - secondaryIcon?: ReactNode; + secondaryIcon?: (props: SrcProps) => React.ReactNode; /** The fill color to pass into the secondary icon. */ secondaryIconFill?: string; @@ -157,7 +159,11 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { floatRightAvatars?: AvatarType[]; /** Prop to represent the size of the float right avatar images to be shown */ - floatRightAvatarSize?: typeof CONST.AVATAR_SIZE; + floatRightAvatarSize?: typeof CONST.AVATAR_SIZE[keyof typeof CONST.AVATAR_SIZE]; + + viewMode?: typeof CONST.OPTION_MODE[keyof typeof CONST.OPTION_MODE]; + + numberOfLinesTitle?: number; /** Whether we should use small avatar subscript sizing the for menu item */ isSmallAvatarSubscriptMenu?: boolean; @@ -189,10 +195,12 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { onSecondaryInteraction: () => void; /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: ReactNode[]; + titleWithTooltips: DisplayNameWithTooltip[]; }; + function MenuItem({ interactive = true, onPress, badgeText, style = styles.popoverMenuItem, wrapperStyle, titleStyle, hoverAndPressStyle, + descriptionTextStyle = styles.breakWord, viewMode = CONST.OPTION_MODE.DEFAULT, numberOfLinesTitle = 1, icon, iconFill, secondaryIcon, secondaryIconFill, iconType = CONST.ICON_TYPE_ICON, iconWidth, iconHeight, iconStyles, fallbackIcon = Expensicons.FallbackAvatar, shouldShowTitleIcon = false, titleIcon, shouldShowRightIcon = false, iconRight = Expensicons.ArrowRight, furtherDetailsIcon, furtherDetails, description, error, success = false, focused = false, disabled = false, @@ -203,34 +211,32 @@ function MenuItem({ shouldBlockSelection = false, shouldParseTitle = false, shouldCheckActionAllowedOnPress = true, onSecondaryInteraction, titleWithTooltips }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); - const [html, setHtml] = React.useState(''); + const [html, setHtml] = useState(''); + const titleRef = useRef(''); - const isDeleted = _.contains(style, styles.offlineFeedback.deleted); const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; + const fallbackAvatarSize = viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; const titleTextStyle = StyleUtils.combineStyles( [ styles.flexShrink1, styles.popoverMenuText, - icon && !_.isArray(icon) && (avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3), + icon && !Array.isArray(icon) && (avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3), shouldShowBasicTitle ? undefined : styles.textStrong, numberOfLinesTitle !== 1 ? styles.preWrap : styles.pre, interactive && disabled ? {...styles.userSelectNone} : undefined, styles.ltr, - isDeleted ? styles.offlineFeedback.deleted : undefined, + styles.offlineFeedback.deleted ], titleStyle, ); - const descriptionTextStyle = StyleUtils.combineStyles([ + const descriptionTextStyles = StyleUtils.combineStyles([ styles.textLabelSupporting, - icon && !_.isArray(icon) ? styles.ml3 : undefined, + icon && !Array.isArray(icon) && styles.ml3, title ? descriptionVerticalMargin : StyleUtils.getFontSizeStyle(variables.fontSizeNormal), descriptionTextStyle, - isDeleted ? styles.offlineFeedback.deleted : undefined, + styles.offlineFeedback.deleted ]); - const fallbackAvatarSize = viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; - - const titleRef = React.useRef(''); useEffect(() => { if (!title || (titleRef.current.length && titleRef.current === title) || !shouldParseTitle) { return; @@ -241,22 +247,22 @@ function MenuItem({ }, [title, shouldParseTitle]); const getProcessedTitle = useMemo(() => { - let title = ''; + let processedTitle = ''; if (shouldRenderAsHTML) { - title = convertToLTR(title); + processedTitle = title ? convertToLTR(title) : ''; } if (shouldParseTitle) { - title = html; + processedTitle = html; } - return title ? `${title}` : ''; + return processedTitle ? `${processedTitle}` : ''; }, [title, shouldRenderAsHTML, shouldParseTitle, html]); const hasPressableRightComponent = iconRight || (shouldShowRightComponent && rightComponent); const renderTitleContent = () => { - if (titleWithTooltips && Array.isArray(titleWithTooltips) && titleWithTooltips.length > 0) { + if (title && titleWithTooltips && Array.isArray(titleWithTooltips) && titleWithTooltips.length > 0) { return ( shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={ControlSelection.unblock} onSecondaryInteraction={onSecondaryInteraction} - style={({pressed}) => [ + style={({pressed}: { pressed: PressableStateCallbackType }) => [ style, !interactive && styles.cursorDefault, - StyleUtils.getButtonBackgroundColorStyle(getButtonState(focused || isHovered, pressed, success, disabled, interactive), true), + StyleUtils.getButtonBackgroundColorStyle(getButtonState(focused || isHovered, Boolean(pressed), success, disabled, interactive), true), (isHovered || pressed) && hoverAndPressStyle, - ...(_.isArray(wrapperStyle) ? wrapperStyle : [wrapperStyle]), + ...(Array.isArray(wrapperStyle) ? wrapperStyle : [wrapperStyle]), shouldGreyOutWhenDisabled && disabled && styles.buttonOpacityDisabled, ]} disabled={disabled} @@ -308,16 +316,16 @@ function MenuItem({ {Boolean(label) && ( - + {label} )} - {Boolean(icon) && _.isArray(icon) && ( + {Boolean(icon) && Array.isArray(icon) && ( )} - {Boolean(icon) && !_.isArray(icon) && ( + {Boolean(icon) && !Array.isArray(icon) && ( {iconType === CONST.ICON_TYPE_ICON && ( )} - {Boolean(secondaryIcon) && ( + {secondaryIcon && ( @@ -381,7 +389,7 @@ function MenuItem({ {Boolean(description) && shouldShowDescriptionOnTop && ( {description} @@ -413,7 +421,7 @@ function MenuItem({ {Boolean(description) && !shouldShowDescriptionOnTop && ( {description} @@ -444,7 +452,7 @@ function MenuItem({ - {Boolean(badgeText) && ( + {badgeText && ( )} {/* Since subtitle can be of type number, we should allow 0 to be shown */} - {(subtitle || subtitle === 0) && ( + {(subtitle ?? subtitle === 0) && ( - {subtitle} + {subtitle} )} {!_.isEmpty(floatRightAvatars) && ( @@ -489,7 +497,7 @@ function MenuItem({ )} From 800d2b1ab29eb344244d3ce325eca31f7c59305a Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 22 Nov 2023 23:02:57 +0000 Subject: [PATCH 034/132] fix(expensify card page): not found state showing up and disappearing immediately --- .../settings/Wallet/ExpensifyCardPage.js | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index e92fca171817..beee9d63d984 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -87,7 +87,7 @@ const propTypes = { }; const defaultProps = { - cardList: {}, + cardList: null, draftValues: { addressLine1: '', addressLine2: '', @@ -127,17 +127,21 @@ function ExpensifyCardPage({ const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const domainCards = CardUtils.getDomainCards(cardList)[domain]; - const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; - const physicalCard = _.find(domainCards, (card) => !card.isVirtual) || {}; + const domainCards = useMemo(() => cardList && CardUtils.getDomainCards(cardList)[domain], [cardList, domain]); + const virtualCard = useMemo(() => (domainCards && _.find(domainCards, (card) => card.isVirtual)) || {}, [domainCards]); + const physicalCard = useMemo(() => (domainCards && _.find(domainCards, (card) => !card.isVirtual)) || {}, [domainCards]); const [isLoading, setIsLoading] = useState(false); + const [isNotFound, setIsNotFound] = useState(false); const [details, setDetails] = useState({}); const [cardDetailsError, setCardDetailsError] = useState(''); - if (_.isEmpty(virtualCard) && _.isEmpty(physicalCard)) { - return Navigation.goBack(ROUTES.SETTINGS_WALLET)} />; - } + useEffect(() => { + if (!cardList) { + return; + } + setIsNotFound(_.isEmpty(virtualCard) && _.isEmpty(physicalCard)); + }, [cardList, physicalCard, virtualCard]); const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend || virtualCard.availableSpend || 0); @@ -166,6 +170,10 @@ function ExpensifyCardPage({ const hasDetectedIndividualFraud = _.some(domainCards, (card) => card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL); const cardDetailsErrorObject = cardDetailsError ? {error: cardDetailsError} : {}; + if (isNotFound) { + return Navigation.goBack(ROUTES.SETTINGS_WALLET)} />; + } + return ( Date: Thu, 23 Nov 2023 12:19:13 +0000 Subject: [PATCH 035/132] fix(get physical card): weird back button behaviour --- src/libs/GetPhysicalCardUtils.ts | 2 +- src/libs/Navigation/linkTo.js | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts index 57a9d773cc9d..80391108e377 100644 --- a/src/libs/GetPhysicalCardUtils.ts +++ b/src/libs/GetPhysicalCardUtils.ts @@ -82,7 +82,7 @@ function setCurrentRoute(currentRoute: string, domain: string, privatePersonalDe } // Redirect the user if he's not allowed to be on the current step - Navigation.navigate(expectedRoute, CONST.NAVIGATION.ACTION_TYPE.REPLACE); + Navigation.goBack(expectedRoute); } /** diff --git a/src/libs/Navigation/linkTo.js b/src/libs/Navigation/linkTo.js index ca87a0d7b79c..db8b0493272c 100644 --- a/src/libs/Navigation/linkTo.js +++ b/src/libs/Navigation/linkTo.js @@ -89,11 +89,6 @@ export default function linkTo(navigation, path, type, isActiveRoute) { if (!isActiveRoute && type === CONST.NAVIGATION.ACTION_TYPE.PUSH) { minimalAction.type = CONST.NAVIGATION.ACTION_TYPE.PUSH; } - // There are situations when the user is trying to access a route which he has no access to - // So we want to redirect him to the right one and replace the one he tried to access - if (type === CONST.NAVIGATION.ACTION_TYPE.REPLACE) { - minimalAction.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE; - } root.dispatch(minimalAction); return; } From b350c6b9fcfde9eb5aa68cea5ba0d881bce6ec7c Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 00:36:24 +0200 Subject: [PATCH 036/132] Update src/libs/ModifiedExpenseMessage.ts Co-authored-by: Tim Golen --- src/libs/ModifiedExpenseMessage.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 44259180f4eb..544ced26cc28 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -72,15 +72,6 @@ function getMessageLine(prefix: string, messageFragments: string[]) { }, prefix); } -/** - * Get the message for a modified distance request. - * - * @param newDistance - * @param oldDistance - * @param newAmount - * @param oldAmount - */ - function getForDistanceRequest(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { if (!oldDistance) { return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); From 03931a24e14d0746eada51c63a4a3f630fff5c75 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 00:36:33 +0200 Subject: [PATCH 037/132] Update src/libs/ModifiedExpenseMessage.ts Co-authored-by: Tim Golen --- src/libs/ModifiedExpenseMessage.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 544ced26cc28..e82c037cf632 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -89,8 +89,6 @@ function getForDistanceRequest(newDistance: string, oldDistance: string, newAmou * * ModifiedExpense::getNewDotComment in Web-Expensify should match this. * If we change this function be sure to update the backend as well. - * - * @param reportAction */ function getForReportAction(reportAction: ReportAction) { if (reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { From 4e9a118da27b1233123d9c5552925d5f71475b5f Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 00:36:41 +0200 Subject: [PATCH 038/132] Update src/libs/ModifiedExpenseMessage.ts Co-authored-by: Tim Golen --- src/libs/ModifiedExpenseMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index e82c037cf632..8e7b27b55ce4 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -90,7 +90,7 @@ function getForDistanceRequest(newDistance: string, oldDistance: string, newAmou * ModifiedExpense::getNewDotComment in Web-Expensify should match this. * If we change this function be sure to update the backend as well. */ -function getForReportAction(reportAction: ReportAction) { +function getForReportAction(reportAction: ReportAction): string { if (reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { return ''; } From 41f8a94198a6eb74f4662e1edd4f17b6fe46c88f Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 17:26:37 +0200 Subject: [PATCH 039/132] Resolve conflicts after merge. --- src/libs/ReportUtils.ts | 143 ++------------------------------- src/types/onyx/ReportAction.ts | 1 - 2 files changed, 8 insertions(+), 136 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d93661778b83..da00bc1c5c18 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1953,140 +1953,6 @@ function getReportPreviewMessage( return Localize.translateLocal(containsNonReimbursable ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount}); } -/** - * Get the proper message schema for modified expense message. - */ - -function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true): string { - const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; - const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; - const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; - - if (!oldValue) { - return Localize.translateLocal('iou.setTheRequest', {valueName: displayValueName, newValueToDisplay}); - } - if (!newValue) { - return Localize.translateLocal('iou.removedTheRequest', {valueName: displayValueName, oldValueToDisplay}); - } - return Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); -} - -/** - * Get the proper message schema for modified distance message. - */ -function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string): string { - if (!oldDistance) { - return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); - } - return Localize.translateLocal('iou.updatedTheDistance', { - newDistanceToDisplay: newDistance, - oldDistanceToDisplay: oldDistance, - newAmountToDisplay: newAmount, - oldAmountToDisplay: oldAmount, - }); -} - -/** - * Get the report action message when expense has been modified. - * - * ModifiedExpense::getNewDotComment in Web-Expensify should match this. - * If we change this function be sure to update the backend as well. - */ -function getModifiedExpenseMessage(reportAction: OnyxEntry): string | undefined { - const reportActionOriginalMessage = reportAction?.originalMessage as ExpenseOriginalMessage | undefined; - if (isEmptyObject(reportActionOriginalMessage)) { - return Localize.translateLocal('iou.changedTheRequest'); - } - const reportID = reportAction?.reportID ?? ''; - const policyID = getReport(reportID)?.policyID ?? ''; - const policyTags = getPolicyTags(policyID); - const policyTag = PolicyUtils.getTag(policyTags); - const policyTagListName = policyTag?.name ?? Localize.translateLocal('common.tag'); - - const hasModifiedAmount = - reportActionOriginalMessage && - 'oldAmount' in reportActionOriginalMessage && - 'oldCurrency' in reportActionOriginalMessage && - 'amount' in reportActionOriginalMessage && - 'currency' in reportActionOriginalMessage; - - const hasModifiedMerchant = reportActionOriginalMessage && 'oldMerchant' in reportActionOriginalMessage && 'merchant' in reportActionOriginalMessage; - if (hasModifiedAmount) { - const oldCurrency = reportActionOriginalMessage?.oldCurrency; - const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency ?? ''); - - const currency = reportActionOriginalMessage?.currency; - const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.amount ?? 0, currency); - - // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. - // We check the merchant is in distance format (includes @) as a sanity check - if (hasModifiedMerchant && reportActionOriginalMessage?.merchant?.includes('@')) { - return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage?.merchant, reportActionOriginalMessage?.oldMerchant ?? '', amount, oldAmount); - } - - return getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); - } - - const hasModifiedComment = reportActionOriginalMessage && 'oldComment' in reportActionOriginalMessage && 'newComment' in reportActionOriginalMessage; - if (hasModifiedComment) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage?.newComment ?? '', - reportActionOriginalMessage?.oldComment ?? '', - Localize.translateLocal('common.description'), - true, - ); - } - - const hasModifiedCreated = reportActionOriginalMessage && 'oldCreated' in reportActionOriginalMessage && 'created' in reportActionOriginalMessage; - if (hasModifiedCreated) { - // Take only the YYYY-MM-DD value as the original date includes timestamp - let formattedOldCreated: Date | string = new Date(reportActionOriginalMessage?.oldCreated ?? 0); - formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.created ?? '', formattedOldCreated?.toString?.(), Localize.translateLocal('common.date'), false); - } - - if (hasModifiedMerchant) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage?.merchant ?? '', - reportActionOriginalMessage?.oldMerchant ?? '', - Localize.translateLocal('common.merchant'), - true, - ); - } - - const hasModifiedCategory = reportActionOriginalMessage && 'oldCategory' in reportActionOriginalMessage && 'category' in reportActionOriginalMessage; - if (hasModifiedCategory) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage?.category ?? '', - reportActionOriginalMessage?.oldCategory ?? '', - Localize.translateLocal('common.category'), - true, - ); - } - - const hasModifiedTag = reportActionOriginalMessage && 'oldTag' in reportActionOriginalMessage && 'tag' in reportActionOriginalMessage; - if (hasModifiedTag) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.tag ?? '', - reportActionOriginalMessage.oldTag ?? '', - policyTagListName, - true, - policyTagListName === Localize.translateLocal('common.tag'), - ); - } - - const hasModifiedBillable = reportActionOriginalMessage && 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage; - if (hasModifiedBillable) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage?.billable ?? '', - reportActionOriginalMessage?.oldBillable ?? '', - Localize.translateLocal('iou.request'), - true, - ); - } -} - /** * Given the updates user made to the request, compose the originalMessage * object of the modified expense action. @@ -3650,6 +3516,13 @@ function getReportIDFromLink(url: string | null): string { return reportID; } +/** + * Get the report policyID given a reportID + */ +function getReportPolicyID(reportID: string | undefined): string|undefined { + return getReport(reportID)?.policyID; +} + /** * Check if the chat report is linked to an iou that is waiting for the current user to add a credit bank account. */ @@ -4267,6 +4140,7 @@ export { getReport, getReportNotificationPreference, getReportIDFromLink, + getReportPolicyID, getRouteFromLink, getDeletedParentActionMessageForChatReport, getLastVisibleMessage, @@ -4342,7 +4216,6 @@ export { getParentReport, getRootParentReport, getReportPreviewMessage, - getModifiedExpenseMessage, canUserPerformWriteAction, getOriginalReportID, canAccessReport, diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 95f5574e5432..891a0ffcb7b8 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -90,7 +90,6 @@ type ReportActionBase = { automatic?: boolean; shouldShow?: boolean; - reportID?: string; /** The ID of childReport */ childReportID?: string; From ceff5c197b0301bd3dd95e0e5cd45389b76d9cb1 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 17:36:56 +0200 Subject: [PATCH 040/132] Move policyTags to ModifiedExpenseMessage --- src/libs/ModifiedExpenseMessage.ts | 21 ++++++++++++++++++--- src/libs/PolicyUtils.ts | 19 +------------------ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 8e7b27b55ce4..decc4c5a9863 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,11 +1,26 @@ import {format} from 'date-fns'; import CONST from '@src/CONST'; -import {ReportAction} from '@src/types/onyx'; +import {PolicyTags, ReportAction} from '@src/types/onyx'; +import Onyx, {OnyxCollection} from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; import * as CurrencyUtils from './CurrencyUtils'; import * as Localize from './Localize'; import * as PolicyUtils from './PolicyUtils'; import * as ReportUtils from './ReportUtils'; + +let allPolicyTags: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + return; + } + allPolicyTags = Object.fromEntries(Object.entries(value).filter(([, policyTags]) => !!policyTags)); + }, +}); + /** * Builds the partial message fragment for a modified field on the expense. * @@ -95,8 +110,8 @@ function getForReportAction(reportAction: ReportAction): string { return ''; } const reportActionOriginalMessage = reportAction.originalMessage; - const policyID = ReportUtils.getReportPolicyID(reportAction.reportID ?? ''); - const policyTags = PolicyUtils.getPolicyTags(policyID); + const policyID = ReportUtils.getReportPolicyID(reportAction.reportID) ?? ''; + const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; const policyTagListName = PolicyUtils.getTagListName(policyTags) || Localize.translateLocal('common.tag'); const removalFragments: string[] = []; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index e7328972cfa1..19129959d016 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,5 +1,5 @@ import Str from 'expensify-common/lib/str'; -import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {PersonalDetails, Policy, PolicyMembers, PolicyTag, PolicyTags} from '@src/types/onyx'; @@ -9,22 +9,6 @@ type MemberEmailsToAccountIDs = Record; type PersonalDetailsList = Record; type UnitRate = {rate: number}; -let allPolicyTags: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - return; - } - allPolicyTags = Object.fromEntries(Object.entries(value).filter(([, policyTags]) => !!policyTags)); - }, -}); - -function getPolicyTags(policyID: string) { - return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; -} - /** * Filter out the active policies, which will exclude policies with pending deletion * These are policies that we can use to create reports with in NewDot. @@ -216,7 +200,6 @@ function isPendingDeletePolicy(policy: OnyxEntry): boolean { export { getActivePolicies, - getPolicyTags, hasPolicyMemberError, hasPolicyError, hasPolicyErrorFields, From 57f083782f1e8ebbfc36ead4f7d0f49854262e99 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 17:55:30 +0200 Subject: [PATCH 041/132] Remove unused function. --- src/libs/ModifiedExpenseMessage.ts | 7 ++++--- src/libs/ReportUtils.ts | 19 ------------------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index decc4c5a9863..293411972858 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,7 +1,7 @@ import {format} from 'date-fns'; import CONST from '@src/CONST'; import {PolicyTags, ReportAction} from '@src/types/onyx'; -import Onyx, {OnyxCollection} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import * as CurrencyUtils from './CurrencyUtils'; import * as Localize from './Localize'; @@ -9,15 +9,16 @@ import * as PolicyUtils from './PolicyUtils'; import * as ReportUtils from './ReportUtils'; -let allPolicyTags: OnyxCollection = {}; +let allPolicyTags: Record = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, waitForCollectionCallback: true, callback: (value) => { if (!value) { + allPolicyTags = {}; return; } - allPolicyTags = Object.fromEntries(Object.entries(value).filter(([, policyTags]) => !!policyTags)); + allPolicyTags = value; }, }); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index da00bc1c5c18..2280f3283b80 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -400,25 +400,6 @@ Onyx.connect({ callback: (value) => (loginList = value), }); -let allPolicyTags: Record = {}; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - allPolicyTags = {}; - return; - } - - allPolicyTags = value; - }, -}); - -function getPolicyTags(policyID: string) { - return allPolicyTags[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; -} - function getChatType(report: OnyxEntry): ValueOf | undefined { return report?.chatType; } From 7dd7afb9265cc2d10eaab94313b72520749738b0 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 18:25:48 +0200 Subject: [PATCH 042/132] Make lint happy. --- src/libs/ModifiedExpenseMessage.ts | 5 ++--- src/libs/ReportUtils.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 293411972858..558409fe3a51 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,14 +1,13 @@ import {format} from 'date-fns'; -import CONST from '@src/CONST'; -import {PolicyTags, ReportAction} from '@src/types/onyx'; import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {PolicyTags, ReportAction} from '@src/types/onyx'; import * as CurrencyUtils from './CurrencyUtils'; import * as Localize from './Localize'; import * as PolicyUtils from './PolicyUtils'; import * as ReportUtils from './ReportUtils'; - let allPolicyTags: Record = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2280f3283b80..dabfa271f7c7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1,4 +1,3 @@ -import {format} from 'date-fns'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import lodashEscape from 'lodash/escape'; @@ -14,7 +13,7 @@ import CONST from '@src/CONST'; import {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {Beta, Login, PersonalDetails, Policy, PolicyTags, Report, ReportAction, Transaction} from '@src/types/onyx'; +import {Beta, Login, PersonalDetails, Policy, Report, ReportAction, Transaction} from '@src/types/onyx'; import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import {ChangeLog, IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage'; import {Message, ReportActions} from '@src/types/onyx/ReportAction'; @@ -3500,7 +3499,7 @@ function getReportIDFromLink(url: string | null): string { /** * Get the report policyID given a reportID */ -function getReportPolicyID(reportID: string | undefined): string|undefined { +function getReportPolicyID(reportID: string | undefined): string | undefined { return getReport(reportID)?.policyID; } From 1cb7f4751f3b99d9e462a871e2a33d821c50c761 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 18:33:51 +0200 Subject: [PATCH 043/132] Better checks. --- src/libs/ModifiedExpenseMessage.ts | 57 ++++++++++++++++-------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 558409fe3a51..aaa6cd10f656 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -118,30 +118,35 @@ function getForReportAction(reportAction: ReportAction): string { const setFragments: string[] = []; const changeFragments: string[] = []; - const hasModifiedAmount = reportActionOriginalMessage.oldAmount && reportActionOriginalMessage.oldCurrency && reportActionOriginalMessage.amount && reportActionOriginalMessage.currency; - - const hasModifiedMerchant = reportActionOriginalMessage.oldMerchant && reportActionOriginalMessage.merchant; + const hasModifiedAmount = + reportActionOriginalMessage && + 'oldAmount' in reportActionOriginalMessage && + 'oldCurrency' in reportActionOriginalMessage && + 'amount' in reportActionOriginalMessage && + 'currency' in reportActionOriginalMessage; + + const hasModifiedMerchant = reportActionOriginalMessage && 'oldMerchant' in reportActionOriginalMessage && 'merchant' in reportActionOriginalMessage; if (hasModifiedAmount) { - const oldCurrency = reportActionOriginalMessage.oldCurrency ?? ''; - const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.oldAmount ?? 0, oldCurrency); + const oldCurrency = reportActionOriginalMessage?.oldCurrency ?? ''; + const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency); - const currency = reportActionOriginalMessage.currency ?? ''; - const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.amount ?? 0, currency); + const currency = reportActionOriginalMessage?.currency ?? ''; + const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.amount ?? 0, currency); // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. // We check the merchant is in distance format (includes @) as a sanity check - if (hasModifiedMerchant && (reportActionOriginalMessage.merchant ?? '').includes('@')) { - return getForDistanceRequest(reportActionOriginalMessage.merchant ?? '', reportActionOriginalMessage.oldMerchant ?? '', amount, oldAmount); + if (hasModifiedMerchant && (reportActionOriginalMessage?.merchant ?? '').includes('@')) { + return getForDistanceRequest(reportActionOriginalMessage?.merchant ?? '', reportActionOriginalMessage?.oldMerchant ?? '', amount, oldAmount); } buildMessageFragmentForValue(amount, oldAmount, Localize.translateLocal('iou.amount'), false, setFragments, removalFragments, changeFragments); } - const hasModifiedComment = reportActionOriginalMessage.oldComment && reportActionOriginalMessage.newComment; + const hasModifiedComment = reportActionOriginalMessage && 'oldComment' in reportActionOriginalMessage && 'newComment' in reportActionOriginalMessage; if (hasModifiedComment) { buildMessageFragmentForValue( - reportActionOriginalMessage.newComment ?? '', - reportActionOriginalMessage.oldComment ?? '', + reportActionOriginalMessage?.newComment ?? '', + reportActionOriginalMessage?.oldComment ?? '', Localize.translateLocal('common.description'), true, setFragments, @@ -150,12 +155,12 @@ function getForReportAction(reportAction: ReportAction): string { ); } - const hasModifiedCreated = reportActionOriginalMessage.oldCreated && reportActionOriginalMessage.created; + const hasModifiedCreated = reportActionOriginalMessage && 'oldCreated' in reportActionOriginalMessage && 'created' in reportActionOriginalMessage; if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp - const formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated ?? ''), CONST.DATE.FNS_FORMAT_STRING); + const formattedOldCreated = format(new Date(reportActionOriginalMessage?.oldCreated ?? ''), CONST.DATE.FNS_FORMAT_STRING); buildMessageFragmentForValue( - reportActionOriginalMessage.created ?? '', + reportActionOriginalMessage?.created ?? '', formattedOldCreated, Localize.translateLocal('common.date'), false, @@ -167,8 +172,8 @@ function getForReportAction(reportAction: ReportAction): string { if (hasModifiedMerchant) { buildMessageFragmentForValue( - reportActionOriginalMessage.merchant ?? '', - reportActionOriginalMessage.oldMerchant ?? '', + reportActionOriginalMessage?.merchant ?? '', + reportActionOriginalMessage?.oldMerchant ?? '', Localize.translateLocal('common.merchant'), true, setFragments, @@ -177,11 +182,11 @@ function getForReportAction(reportAction: ReportAction): string { ); } - const hasModifiedCategory = reportActionOriginalMessage.oldCategory && reportActionOriginalMessage.category; + const hasModifiedCategory = reportActionOriginalMessage && 'oldCategory' in reportActionOriginalMessage && 'category' in reportActionOriginalMessage; if (hasModifiedCategory) { buildMessageFragmentForValue( - reportActionOriginalMessage.category ?? '', - reportActionOriginalMessage.oldCategory ?? '', + reportActionOriginalMessage?.category ?? '', + reportActionOriginalMessage?.oldCategory ?? '', Localize.translateLocal('common.category'), true, setFragments, @@ -190,11 +195,11 @@ function getForReportAction(reportAction: ReportAction): string { ); } - const hasModifiedTag = reportActionOriginalMessage.oldTag && reportActionOriginalMessage.tag; + const hasModifiedTag = reportActionOriginalMessage && 'oldTag' in reportActionOriginalMessage && 'tag' in reportActionOriginalMessage; if (hasModifiedTag) { buildMessageFragmentForValue( - reportActionOriginalMessage.tag ?? '', - reportActionOriginalMessage.oldTag ?? '', + reportActionOriginalMessage?.tag ?? '', + reportActionOriginalMessage?.oldTag ?? '', policyTagListName, true, setFragments, @@ -204,11 +209,11 @@ function getForReportAction(reportAction: ReportAction): string { ); } - const hasModifiedBillable = reportActionOriginalMessage.oldBillable && reportActionOriginalMessage.billable; + const hasModifiedBillable = reportActionOriginalMessage && 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage; if (hasModifiedBillable) { buildMessageFragmentForValue( - reportActionOriginalMessage.billable ?? '', - reportActionOriginalMessage.oldBillable ?? '', + reportActionOriginalMessage?.billable ?? '', + reportActionOriginalMessage?.oldBillable ?? '', Localize.translateLocal('iou.request'), true, setFragments, From 4bd79291e06168e6907590e10f4f00d425e33a0a Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 29 Nov 2023 16:52:04 +0000 Subject: [PATCH 044/132] refactor(typescript): migrate IFrame --- src/components/{IFrame.js => IFrame.tsx} | 35 ++++++++++-------------- 1 file changed, 15 insertions(+), 20 deletions(-) rename src/components/{IFrame.js => IFrame.tsx} (82%) diff --git a/src/components/IFrame.js b/src/components/IFrame.tsx similarity index 82% rename from src/components/IFrame.js rename to src/components/IFrame.tsx index aa85ad03ffbf..eca9524b1cb1 100644 --- a/src/components/IFrame.js +++ b/src/components/IFrame.tsx @@ -1,10 +1,9 @@ -/* eslint-disable es/no-nullish-coalescing-operators */ -import PropTypes from 'prop-types'; import React, {useEffect, useState} from 'react'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +import {Session} from '@src/types/onyx'; -function getNewDotURL(url) { +function getNewDotURL(url: string | URL) { const urlObj = new URL(url); const paramString = urlObj.searchParams.get('param') ?? ''; const pathname = urlObj.pathname.slice(1); @@ -48,7 +47,7 @@ function getNewDotURL(url) { return pathname; } -function getOldDotURL(url) { +function getOldDotURL(url: string | URL) { const urlObj = new URL(url); const pathname = urlObj.pathname; const paths = pathname.slice(1).split('/'); @@ -86,18 +85,13 @@ function getOldDotURL(url) { return pathname; } -const propTypes = { - // The session of the logged in person - session: PropTypes.shape({ - // The email of the logged in person - email: PropTypes.string, - - // The authToken of the logged in person - authToken: PropTypes.string, - }).isRequired, +type OldDotIFrameOnyxProps = { + session: OnyxEntry; }; -function OldDotIFrame({session}) { +type OldDotIFrameProps = OldDotIFrameOnyxProps; + +function OldDotIFrame({session}: OldDotIFrameProps) { const [oldDotURL, setOldDotURL] = useState('https://staging.expensify.com'); useEffect(() => { @@ -106,15 +100,18 @@ function OldDotIFrame({session}) { window.addEventListener('message', (event) => { const url = event.data; // TODO: use this value to navigate to a new path - // eslint-disable-next-line no-unused-vars + // eslint-disable-next-line @typescript-eslint/no-unused-vars const newDotURL = getNewDotURL(url); }); }, []); useEffect(() => { + if (!session) { + return; + } document.cookie = `authToken=${session.authToken}; domain=expensify.com.dev; path=/;`; document.cookie = `email=${session.email}; domain=expensify.com.dev; path=/;`; - }, [session.authToken, session.email]); + }, [session]); return (