From 4187fbb6e6bc3abc041a45aa7e9102e9af85e31c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 3 Oct 2023 13:44:48 +0200 Subject: [PATCH 01/63] ref: started migrating ReportUtils to TS --- src/libs/{ReportUtils.js => ReportUtils.ts} | 270 +++++++------------- src/types/onyx/Report.ts | 6 + 2 files changed, 98 insertions(+), 178 deletions(-) rename src/libs/{ReportUtils.js => ReportUtils.ts} (95%) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.ts similarity index 95% rename from src/libs/ReportUtils.js rename to src/libs/ReportUtils.ts index c03858cb15f3..bf3d422d66ad 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.ts @@ -4,7 +4,7 @@ import {format, parseISO} from 'date-fns'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import lodashIntersection from 'lodash/intersection'; -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; @@ -23,171 +23,139 @@ import isReportMessageAttachment from './isReportMessageAttachment'; import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAvatars'; import * as CurrencyUtils from './CurrencyUtils'; import * as UserUtils from './UserUtils'; +import {Login, PersonalDetails, Policy, Report, ReportAction} from '../types/onyx'; +import {ValueOf} from 'type-fest'; -let currentUserEmail; -let currentUserAccountID; -let isAnonymousUser; +let currentUserEmail: string | undefined; +let currentUserAccountID: number | undefined; +let isAnonymousUser = false; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { + callback: (value) => { // When signed out, val is undefined - if (!val) { + if (!value) { return; } - currentUserEmail = val.email; - currentUserAccountID = val.accountID; - isAnonymousUser = val.authTokenType === 'anonymousAccount'; + currentUserEmail = value.email; + currentUserAccountID = value.accountID; + // TODO: There is no such a field so it will always be false should we remove it? + isAnonymousUser = value.authTokenType === 'anonymousAccount'; }, }); -let allPersonalDetails; -let currentUserPersonalDetails; +let allPersonalDetails: OnyxCollection; +let currentUserPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => { - currentUserPersonalDetails = lodashGet(val, currentUserAccountID, {}); - allPersonalDetails = val || {}; + callback: (value) => { + currentUserPersonalDetails = value?.[currentUserAccountID ?? '']; + allPersonalDetails = value ?? {}; }, }); -let allReports; +let allReports: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, waitForCollectionCallback: true, callback: (val) => (allReports = val), }); -let doesDomainHaveApprovedAccountant; +let doesDomainHaveApprovedAccountant = false; Onyx.connect({ key: ONYXKEYS.ACCOUNT, waitForCollectionCallback: true, - callback: (val) => (doesDomainHaveApprovedAccountant = lodashGet(val, 'doesDomainHaveApprovedAccountant', false)), + callback: (value) => (doesDomainHaveApprovedAccountant = value?.doesDomainHaveApprovedAccountant ?? false), }); -let allPolicies; +let allPolicies: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, waitForCollectionCallback: true, callback: (val) => (allPolicies = val), }); -let loginList; +let loginList: OnyxEntry; Onyx.connect({ key: ONYXKEYS.LOGIN_LIST, callback: (val) => (loginList = val), }); -function getChatType(report) { - return report ? report.chatType : ''; +function getChatType(report: OnyxEntry): ValueOf | undefined { + return report?.chatType; } -/** - * @param {String} policyID - * @returns {Object} - */ -function getPolicy(policyID) { +function getPolicy(policyID: string): Policy | null | {} { if (!allPolicies || !policyID) { return {}; } - return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] || {}; + return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}; } /** * Get the policy type from a given report - * @param {Object} report - * @param {String} report.policyID - * @param {Object} policies must have Onyxkey prefix (i.e 'policy_') for keys - * @returns {String} + * @param report + * @param policies must have Onyxkey prefix (i.e 'policy_') for keys */ -function getPolicyType(report, policies) { - return lodashGet(policies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'type'], ''); +function getPolicyType(report: OnyxEntry, policies: OnyxCollection): string { + return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.type ?? ''; } /** * Get the policy name from a given report - * @param {Object} report - * @param {String} report.policyID - * @param {String} report.oldPolicyName - * @param {String} report.policyName - * @param {Boolean} [returnEmptyIfNotFound] - * @param {Object} [policy] - * @returns {String} */ -function getPolicyName(report, returnEmptyIfNotFound = false, policy = undefined) { +function getPolicyName(report: OnyxEntry, returnEmptyIfNotFound = false, policy: OnyxEntry = undefined): string { const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); - if (_.isEmpty(report)) { + if (Object.keys(report ?? {}).length === 0) { return noPolicyFound; } - if ((!allPolicies || _.size(allPolicies) === 0) && !report.policyName) { + if ((!allPolicies || Object.keys(allPolicies).length === 0) && !report?.policyName) { return Localize.translateLocal('workspace.common.unavailable'); } - const finalPolicy = policy || _.get(allPolicies, `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`); + const finalPolicy = policy || allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; // Public rooms send back the policy name with the reportSummary, // since they can also be accessed by people who aren't in the workspace - const policyName = lodashGet(finalPolicy, 'name') || report.policyName || report.oldPolicyName || noPolicyFound; + const policyName = finalPolicy?.name ?? report?.policyName ?? report?.oldPolicyName ?? noPolicyFound; return policyName; } /** * Returns the concatenated title for the PrimaryLogins of a report - * - * @param {Array} accountIDs - * @returns {string} */ -function getReportParticipantsTitle(accountIDs) { - return ( - _.chain(accountIDs) - - // Somehow it's possible for the logins coming from report.participantAccountIDs to contain undefined values so we use compact to remove them. - .compact() - .value() - .join(', ') - ); +function getReportParticipantsTitle(accountIDs: number[]): string { + return accountIDs.filter(Boolean).join(', '); } /** * Checks if a report is a chat report. - * - * @param {Object} report - * @returns {Boolean} */ -function isChatReport(report) { - return report && report.type === CONST.REPORT.TYPE.CHAT; +function isChatReport(report: OnyxEntry): boolean { + return report?.type === CONST.REPORT.TYPE.CHAT; } /** * Checks if a report is an Expense report. - * - * @param {Object} report - * @returns {Boolean} */ -function isExpenseReport(report) { - return report && report.type === CONST.REPORT.TYPE.EXPENSE; +function isExpenseReport(report: OnyxEntry): boolean { + return report?.type === CONST.REPORT.TYPE.EXPENSE; } /** * Checks if a report is an IOU report. - * - * @param {Object} report - * @returns {Boolean} */ -function isIOUReport(report) { - return report && report.type === CONST.REPORT.TYPE.IOU; +function isIOUReport(report: OnyxEntry): boolean { + return report?.type === CONST.REPORT.TYPE.IOU; } /** * Checks if a report is a task report. - * - * @param {Object} report - * @returns {Boolean} */ -function isTaskReport(report) { - return report && report.type === CONST.REPORT.TYPE.TASK; +function isTaskReport(report: OnyxEntry): boolean { + return report?.type === CONST.REPORT.TYPE.TASK; } /** @@ -201,12 +169,12 @@ function isTaskReport(report) { * @param {Object} parentReportAction * @returns {Boolean} */ -function isCanceledTaskReport(report = {}, parentReportAction = {}) { - if (!_.isEmpty(parentReportAction) && lodashGet(parentReportAction, ['message', 0, 'isDeletedParentAction'], false)) { +function isCanceledTaskReport(report: OnyxEntry = {}, parentReportAction: OnyxEntry = {}): boolean { + if (Object.keys(parentReportAction ?? {}).length > 0 && (parentReportAction?.message?.[0].isDeletedParentAction ?? false)) { return true; } - if (!_.isEmpty(report) && report.isDeletedParentAction) { + if (Object.keys(report ?? {}).length > 0 && report?.isDeletedParentAction) { return true; } @@ -216,70 +184,56 @@ function isCanceledTaskReport(report = {}, parentReportAction = {}) { /** * Checks if a report is an open task report. * - * @param {Object} report - * @param {Object} parentReportAction - The parent report action of the report (Used to check if the task has been canceled) - * @returns {Boolean} + * @param report + * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ -function isOpenTaskReport(report, parentReportAction = {}) { - return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report.stateNum === CONST.REPORT.STATE_NUM.OPEN && report.statusNum === CONST.REPORT.STATUS.OPEN; +function isOpenTaskReport(report: OnyxEntry, parentReportAction: OnyxEntry = {}): boolean { + return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; } /** * Checks if a report is a completed task report. - * - * @param {Object} report - * @returns {Boolean} */ -function isCompletedTaskReport(report) { - return isTaskReport(report) && report.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report.statusNum === CONST.REPORT.STATUS.APPROVED; +function isCompletedTaskReport(report: OnyxEntry) { + return isTaskReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS.APPROVED; } /** * Checks if the current user is the manager of the supplied report - * - * @param {Object} report - * @returns {Boolean} */ -function isReportManager(report) { - return report && report.managerID === currentUserAccountID; +function isReportManager(report: OnyxEntry): boolean { + return report?.managerID === currentUserAccountID; } /** * Checks if the supplied report has been approved - * - * @param {Object} report - * @returns {Boolean} */ -function isReportApproved(report) { - return report && report.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report.statusNum === CONST.REPORT.STATUS.APPROVED; +function isReportApproved(report: OnyxEntry): boolean { + return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report.statusNum === CONST.REPORT.STATUS.APPROVED; } /** * Given a collection of reports returns them sorted by last read - * - * @param {Object} reports - * @returns {Array} */ -function sortReportsByLastRead(reports) { - return _.chain(reports) - .toArray() - .filter((report) => report && report.reportID && report.lastReadTime) - .sortBy('lastReadTime') - .value(); +function sortReportsByLastRead(reports: OnyxCollection): OnyxEntry[] { + return Object.values(reports ?? {}) + .filter((report) => report?.reportID && report?.lastReadTime) + .sort((a, b) => { + const aTime = a?.lastReadTime ? parseISO(a.lastReadTime) : 0; + const bTime = b?.lastReadTime ? parseISO(b.lastReadTime) : 0; + return Number(aTime) - Number(bTime); + }); } /** * Whether the Money Request report is settled - * - * @param {String} reportID - * @returns {Boolean} */ -function isSettled(reportID) { +function isSettled(reportID: string): boolean { if (!allReports) { return false; } - const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] || {}; - if ((typeof report === 'object' && Object.keys(report).length === 0) || report.isWaitingOnBankAccount) { + const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; + if ((typeof report === 'object' && Object.keys(report).length === 0) || report?.isWaitingOnBankAccount) { return false; } @@ -288,151 +242,111 @@ function isSettled(reportID) { /** * Whether the current user is the submitter of the report - * - * @param {String} reportID - * @returns {Boolean} */ -function isCurrentUserSubmitter(reportID) { +function isCurrentUserSubmitter(reportID: string): boolean { if (!allReports) { return false; } - const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] || {}; - return report && report.ownerEmail === currentUserEmail; + const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; + return report?.ownerEmail === currentUserEmail; } /** * Whether the provided report is an Admin room - * @param {Object} report - * @param {String} report.chatType - * @returns {Boolean} */ -function isAdminRoom(report) { +function isAdminRoom(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS; } /** * Whether the provided report is an Admin-only posting room - * @param {Object} report - * @param {String} report.writeCapability - * @returns {Boolean} */ -function isAdminsOnlyPostingRoom(report) { - return lodashGet(report, 'writeCapability', CONST.REPORT.WRITE_CAPABILITIES.ALL) === CONST.REPORT.WRITE_CAPABILITIES.ADMINS; +function isAdminsOnlyPostingRoom(report: OnyxEntry): boolean { + return (report?.writeCapability ?? CONST.REPORT.WRITE_CAPABILITIES.ALL) === CONST.REPORT.WRITE_CAPABILITIES.ADMINS; } /** * Whether the provided report is a Announce room - * @param {Object} report - * @param {String} report.chatType - * @returns {Boolean} */ -function isAnnounceRoom(report) { +function isAnnounceRoom(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE; } /** * Whether the provided report is a default room - * @param {Object} report - * @param {String} report.chatType - * @returns {Boolean} */ -function isDefaultRoom(report) { +function isDefaultRoom(report: OnyxEntry): boolean { return [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL].indexOf(getChatType(report)) > -1; } /** * Whether the provided report is a Domain room - * @param {Object} report - * @param {String} report.chatType - * @returns {Boolean} */ -function isDomainRoom(report) { +function isDomainRoom(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.DOMAIN_ALL; } /** * Whether the provided report is a user created policy room - * @param {Object} report - * @param {String} report.chatType - * @returns {Boolean} */ -function isUserCreatedPolicyRoom(report) { +function isUserCreatedPolicyRoom(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_ROOM; } /** * Whether the provided report is a Policy Expense chat. - * @param {Object} report - * @param {String} report.chatType - * @returns {Boolean} */ -function isPolicyExpenseChat(report) { +function isPolicyExpenseChat(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT; } /** Wether the provided report belongs to a Control policy and is an epxense chat - * @param {Object} report - * @returns {Boolean} */ -function isControlPolicyExpenseChat(report) { +function isControlPolicyExpenseChat(report: OnyxEntry): boolean { return isPolicyExpenseChat(report) && getPolicyType(report, allPolicies) === CONST.POLICY.TYPE.CORPORATE; } /** Wether the provided report belongs to a Control policy and is an epxense report - * @param {Object} report - * @returns {Boolean} */ -function isControlPolicyExpenseReport(report) { +function isControlPolicyExpenseReport(report: OnyxEntry): boolean { return isExpenseReport(report) && getPolicyType(report, allPolicies) === CONST.POLICY.TYPE.CORPORATE; } /** * Whether the provided report is a chat room - * @param {Object} report - * @param {String} report.chatType - * @returns {Boolean} */ -function isChatRoom(report) { +function isChatRoom(report: OnyxEntry): boolean { return isUserCreatedPolicyRoom(report) || isDefaultRoom(report); } /** * Whether the provided report is a public room - * @param {Object} report - * @param {String} report.visibility - * @returns {Boolean} */ -function isPublicRoom(report) { - return report && (report.visibility === CONST.REPORT.VISIBILITY.PUBLIC || report.visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE); +function isPublicRoom(report: OnyxEntry): boolean { + return report?.visibility === CONST.REPORT.VISIBILITY.PUBLIC || report?.visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE; } /** * Whether the provided report is a public announce room - * @param {Object} report - * @param {String} report.visibility - * @returns {Boolean} */ -function isPublicAnnounceRoom(report) { - return report && report.visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE; +function isPublicAnnounceRoom(report: OnyxEntry): boolean { + return report?.visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE; } /** * If the report is a policy expense, the route should be for adding bank account for that policy * else since the report is a personal IOU, the route should be for personal bank account. - * @param {Object} report - * @returns {String} */ -function getBankAccountRoute(report) { - return isPolicyExpenseChat(report) ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', report.policyID) : ROUTES.SETTINGS_ADD_BANK_ACCOUNT; +function getBankAccountRoute(report: OnyxEntry): string { + return isPolicyExpenseChat(report) ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', report?.policyID) : ROUTES.SETTINGS_ADD_BANK_ACCOUNT; } /** * Check if personal detail of accountID is empty or optimistic data - * @param {String} accountID user accountID - * @returns {Boolean} */ -function isOptimisticPersonalDetail(accountID) { - return _.isEmpty(allPersonalDetails[accountID]) || !!allPersonalDetails[accountID].isOptimisticPersonalDetail; +function isOptimisticPersonalDetail(accountID: number): boolean { + console.log(allPersonalDetails?.[accountID]); + return _.isEmpty(allPersonalDetails?.[accountID]) || !!allPersonalDetails?.[accountID]?.isOptimisticPersonalDetail; } /** diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 46e51fe41238..964b23223a4a 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -45,6 +45,9 @@ type Report = { /** Linked policy's ID */ policyID?: string; + /** Linked policy's name */ + policyName?: string | null; + /** Name of the report */ reportName?: string; @@ -77,6 +80,9 @@ type Report = { participantAccountIDs?: number[]; total?: number; currency?: string; + isDeletedParentAction?: boolean; + isWaitingOnBankAccount?: boolean; + visibility?: ValueOf; }; export default Report; From acaa8eaca781ea1287032e8012a5194793a7686a Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 3 Oct 2023 16:29:09 +0200 Subject: [PATCH 02/63] ref: continue to migrate ReportUtils to Ts --- src/libs/ReportUtils.ts | 185 ++++++++++-------------------- src/types/onyx/PersonalDetails.ts | 3 + 2 files changed, 63 insertions(+), 125 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 3ec147072c37..ce3b069336fd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -82,7 +82,7 @@ Onyx.connect({ callback: (val) => (loginList = val), }); -function getChatType(report: OnyxEntry): ValueOf | undefined { +function getChatType(report?: OnyxEntry): ValueOf | undefined { return report?.chatType; } @@ -140,7 +140,7 @@ function isChatReport(report: OnyxEntry): boolean { /** * Checks if a report is an Expense report. */ -function isExpenseReport(report: OnyxEntry): boolean { +function isExpenseReport(report?: OnyxEntry): boolean { return report?.type === CONST.REPORT.TYPE.EXPENSE; } @@ -164,12 +164,8 @@ function isTaskReport(report: OnyxEntry): boolean { * This is because when you delete a task, we still allow you to chat on the report itself * There's another situation where you don't have access to the parentReportAction (because it was created in a chat you don't have access to) * In this case, we have added the key to the report itself - * - * @param {Object} report - * @param {Object} parentReportAction - * @returns {Boolean} */ -function isCanceledTaskReport(report: OnyxEntry = {}, parentReportAction: OnyxEntry = {}): boolean { +function isCanceledTaskReport(report: OnyxEntry, parentReportAction: OnyxEntry): boolean { if (Object.keys(parentReportAction ?? {}).length > 0 && (parentReportAction?.message?.[0].isDeletedParentAction ?? false)) { return true; } @@ -187,7 +183,7 @@ function isCanceledTaskReport(report: OnyxEntry = {}, parentReportAction * @param report * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ -function isOpenTaskReport(report: OnyxEntry, parentReportAction: OnyxEntry = {}): boolean { +function isOpenTaskReport(report: OnyxEntry, parentReportAction: OnyxEntry): boolean { return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; } @@ -232,12 +228,12 @@ function isSettled(reportID: string): boolean { if (!allReports) { return false; } - const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; - if ((typeof report === 'object' && Object.keys(report).length === 0) || report?.isWaitingOnBankAccount) { + const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + if ((typeof report === 'object' && Object.keys(report ?? {}).length === 0) || report?.isWaitingOnBankAccount) { return false; } - return report.statusNum === CONST.REPORT.STATUS.REIMBURSED; + return report?.statusNum === CONST.REPORT.STATUS.REIMBURSED; } /** @@ -247,7 +243,7 @@ function isCurrentUserSubmitter(reportID: string): boolean { if (!allReports) { return false; } - const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; + const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; return report?.ownerEmail === currentUserEmail; } @@ -296,7 +292,7 @@ function isUserCreatedPolicyRoom(report: OnyxEntry): boolean { /** * Whether the provided report is a Policy Expense chat. */ -function isPolicyExpenseChat(report: OnyxEntry): boolean { +function isPolicyExpenseChat(report?: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT; } @@ -345,63 +341,47 @@ function getBankAccountRoute(report: OnyxEntry): string { * Check if personal detail of accountID is empty or optimistic data */ function isOptimisticPersonalDetail(accountID: number): boolean { - console.log(allPersonalDetails?.[accountID]); - return _.isEmpty(allPersonalDetails?.[accountID]) || !!allPersonalDetails?.[accountID]?.isOptimisticPersonalDetail; + return Object.keys(allPersonalDetails?.[accountID] ?? {}).length === 0 || !!allPersonalDetails?.[accountID]?.isOptimisticPersonalDetail; } /** * Checks if a report is a task report from a policy expense chat. - * - * @param {Object} report - * @returns {Boolean} */ -function isWorkspaceTaskReport(report) { +function isWorkspaceTaskReport(report: OnyxEntry): boolean { if (!isTaskReport(report)) { return false; } - const parentReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; return isPolicyExpenseChat(parentReport); } /** * Returns true if report has a parent - * - * @param {Object} report - * @returns {Boolean} */ -function isThread(report) { - return Boolean(report && report.parentReportID && report.parentReportActionID); +function isThread(report: OnyxEntry): boolean { + return Boolean(report?.parentReportID && report?.parentReportActionID); } /** * Returns true if report is of type chat and has a parent and is therefore a Thread. - * - * @param {Object} report - * @returns {Boolean} */ -function isChatThread(report) { - return isThread(report) && report.type === CONST.REPORT.TYPE.CHAT; +function isChatThread(report: OnyxEntry): boolean { + return isThread(report) && report?.type === CONST.REPORT.TYPE.CHAT; } /** * Only returns true if this is our main 1:1 DM report with Concierge - * - * @param {Object} report - * @returns {Boolean} */ -function isConciergeChatReport(report) { - return lodashGet(report, 'participantAccountIDs', []).length === 1 && Number(report.participantAccountIDs[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); +function isConciergeChatReport(report: OnyxEntry): boolean { + return report?.participantAccountIDs?.length === 1 && Number(report?.participantAccountIDs?.[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); } /** * Check if the report is a single chat report that isn't a thread * and personal detail of participant is optimistic data - * @param {Object} report - * @param {Array} report.participantAccountIDs - * @returns {Boolean} */ -function shouldDisableDetailPage(report) { - const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []); +function shouldDisableDetailPage(report: OnyxEntry): boolean { + const participantAccountIDs = report?.participantAccountIDs ?? []; if (isChatRoom(report) || isPolicyExpenseChat(report) || isChatThread(report) || isTaskReport(report)) { return false; @@ -414,12 +394,10 @@ function shouldDisableDetailPage(report) { /** * Returns true if this report has only one participant and it's an Expensify account. - * @param {Object} report - * @returns {Boolean} */ -function isExpensifyOnlyParticipantInReport(report) { - const reportParticipants = _.without(lodashGet(report, 'participantAccountIDs', []), currentUserAccountID); - return reportParticipants.length === 1 && _.some(reportParticipants, (accountID) => _.contains(CONST.EXPENSIFY_ACCOUNT_IDS, accountID)); +function isExpensifyOnlyParticipantInReport(report: OnyxEntry): boolean { + const reportParticipants = report?.participantAccountIDs?.filter((accountID) => accountID !== currentUserAccountID) ?? []; + return reportParticipants.length === 1 && reportParticipants.some((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)); } /** @@ -429,30 +407,26 @@ function isExpensifyOnlyParticipantInReport(report) { * @param {Array} accountIDs * @return {Boolean} */ -function hasExpensifyEmails(accountIDs) { - return _.some(accountIDs, (accountID) => Str.extractEmailDomain(lodashGet(allPersonalDetails, [accountID, 'login'], '')) === CONST.EXPENSIFY_PARTNER_NAME); +function hasExpensifyEmails(accountIDs: number[]): boolean { + return accountIDs.some((accountID) => Str.extractCompanyNameFromEmailDomain(allPersonalDetails?.[accountID]?.login ?? '') === CONST.EXPENSIFY_PARTNER_NAME); } /** * Returns true if there are any guides accounts (team.expensify.com) in a list of accountIDs * by cross-referencing the accountIDs with personalDetails since guides that are participants * of the user's chats should have their personal details in Onyx. - * @param {Array} accountIDs - * @returns {Boolean} */ -function hasExpensifyGuidesEmails(accountIDs) { - return _.some(accountIDs, (accountID) => Str.extractEmailDomain(lodashGet(allPersonalDetails, [accountID, 'login'], '')) === CONST.EMAIL.GUIDES_DOMAIN); +function hasExpensifyGuidesEmails(accountIDs: number[]): boolean { + return accountIDs.some((accountID) => Str.extractCompanyNameFromEmailDomain(allPersonalDetails?.[accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN); } -/** - * @param {Record|Array<{lastReadTime, reportID}>} reports - * @param {Boolean} [ignoreDomainRooms] - * @param {Object} policies - * @param {Boolean} isFirstTimeNewExpensifyUser - * @param {Boolean} openOnAdminRoom - * @returns {Object} - */ -function findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom = false) { +function findLastAccessedReport( + reports: OnyxCollection, + ignoreDomainRooms: boolean, + policies: OnyxCollection, + isFirstTimeNewExpensifyUser: boolean, + openOnAdminRoom = false, +): OnyxEntry | undefined { // If it's the user's first time using New Expensify, then they could either have: // - just a Concierge report, if so we'll return that // - their Concierge report, and a separate report that must have deeplinked them to the app before they created their account. @@ -462,7 +436,7 @@ function findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTim let adminReport; if (openOnAdminRoom) { - adminReport = _.find(sortedReports, (report) => { + adminReport = sortedReports.find((report) => { const chatType = getChatType(report); return chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS; }); @@ -473,42 +447,34 @@ function findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTim return sortedReports[0]; } - return adminReport || _.find(sortedReports, (report) => !isConciergeChatReport(report)); + return adminReport || sortedReports.find((report) => !isConciergeChatReport(report)); } if (ignoreDomainRooms) { // We allow public announce rooms, admins, and announce rooms through since we bypass the default rooms beta for them. // Check where ReportUtils.findLastAccessedReport is called in MainDrawerNavigator.js for more context. // Domain rooms are now the only type of default room that are on the defaultRooms beta. - sortedReports = _.filter( - sortedReports, + sortedReports = sortedReports.filter( (report) => !isDomainRoom(report) || getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE || hasExpensifyGuidesEmails(lodashGet(report, ['participantAccountIDs'], [])), ); } - return adminReport || _.last(sortedReports); + return adminReport || sortedReports[sortedReports.length - 1]; } /** * Whether the provided report is an archived room - * @param {Object} report - * @param {Number} report.stateNum - * @param {Number} report.statusNum - * @returns {Boolean} */ -function isArchivedRoom(report) { - return report && report.statusNum === CONST.REPORT.STATUS.CLOSED && report.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED; +function isArchivedRoom(report: OnyxEntry): boolean { + return report?.statusNum === CONST.REPORT.STATUS.CLOSED && report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED; } /** * Checks if the current user is allowed to comment on the given report. - * @param {Object} report - * @param {String} [report.writeCapability] - * @returns {Boolean} */ -function isAllowedToComment(report) { +function isAllowedToComment(report: OnyxEntry): boolean { // Default to allowing all users to post - const capability = lodashGet(report, 'writeCapability', CONST.REPORT.WRITE_CAPABILITIES.ALL) || CONST.REPORT.WRITE_CAPABILITIES.ALL; + const capability = (report?.writeCapability ?? CONST.REPORT.WRITE_CAPABILITIES.ALL) || CONST.REPORT.WRITE_CAPABILITIES.ALL; if (capability === CONST.REPORT.WRITE_CAPABILITIES.ALL) { return true; @@ -521,111 +487,83 @@ function isAllowedToComment(report) { // If we've made it here, commenting on this report is restricted. // If the user is an admin, allow them to post. - const policy = allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; - return lodashGet(policy, 'role', '') === CONST.POLICY.ROLE.ADMIN; + const policy = allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; + return (policy?.role ?? '') === CONST.POLICY.ROLE.ADMIN; } /** * Checks if the current user is the admin of the policy given the policy expense chat. - * @param {Object} report - * @param {String} report.policyID - * @param {Object} policies must have OnyxKey prefix (i.e 'policy_') for keys - * @returns {Boolean} */ -function isPolicyExpenseChatAdmin(report, policies) { +function isPolicyExpenseChatAdmin(report: OnyxEntry, policies: OnyxCollection): boolean { if (!isPolicyExpenseChat(report)) { return false; } - const policyRole = lodashGet(policies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'role']); + const policyRole = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.role ?? ''; return policyRole === CONST.POLICY.ROLE.ADMIN; } /** * Checks if the current user is the admin of the policy. - * @param {String} policyID - * @param {Object} policies must have OnyxKey prefix (i.e 'policy_') for keys - * @returns {Boolean} */ -function isPolicyAdmin(policyID, policies) { - const policyRole = lodashGet(policies, [`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, 'role']); +function isPolicyAdmin(policyID: string, policies: OnyxCollection): boolean { + const policyRole = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.role ?? ''; return policyRole === CONST.POLICY.ROLE.ADMIN; } /** * Returns true if report is a DM/Group DM chat. - * - * @param {Object} report - * @returns {Boolean} */ -function isDM(report) { +function isDM(report: OnyxEntry): boolean { return !getChatType(report); } /** * Returns true if report has a single participant. - * - * @param {Object} report - * @returns {Boolean} */ -function hasSingleParticipant(report) { - return report && report.participantAccountIDs && report.participantAccountIDs.length === 1; +function hasSingleParticipant(report: OnyxEntry): boolean { + return report?.participantAccountIDs?.length === 1; } /** * If the report is a thread and has a chat type set, it is a workspace chat. - * - * @param {Object} report - * @returns {Boolean} */ -function isWorkspaceThread(report) { +function isWorkspaceThread(report: OnyxEntry): boolean { return Boolean(isThread(report) && !isDM(report)); } /** * Returns true if reportAction has a child. - * - * @param {Object} reportAction - * @returns {Boolean} */ -function isThreadParent(reportAction) { - return reportAction && reportAction.childReportID && reportAction.childReportID !== 0; +// TODO: It's not used anywhere should I remove it? +function isThreadParent(reportAction: OnyxEntry): boolean { + return reportAction?.childReportID !== 0; } /** * Returns true if reportAction is the first chat preview of a Thread - * - * @param {Object} reportAction - * @param {String} reportID - * @returns {Boolean} */ -function isThreadFirstChat(reportAction, reportID) { - return !_.isUndefined(reportAction.childReportID) && reportAction.childReportID.toString() === reportID; +function isThreadFirstChat(reportAction: OnyxEntry, reportID: string): boolean { + return reportAction?.childReportID?.toString() === reportID; } /** * Checks if a report is a child report. - * - * @param {Object} report - * @returns {Boolean} */ -function isChildReport(report) { +function isChildReport(report: OnyxEntry): boolean { return isThread(report) || isTaskReport(report); } /** * An Expense Request is a thread where the parent report is an Expense Report and * the parentReportAction is a transaction. - * - * @param {Object} report - * @returns {Boolean} */ -function isExpenseRequest(report) { +function isExpenseRequest(report: OnyxEntry): boolean { if (isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const parentReport = lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]); + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; return isExpenseReport(parentReport) && ReportActionsUtils.isTransactionThread(parentReportAction); } return false; @@ -634,9 +572,6 @@ function isExpenseRequest(report) { /** * An IOU Request is a thread where the parent report is an IOU Report and * the parentReportAction is a transaction. - * - * @param {Object} report - * @returns {Boolean} */ function isIOURequest(report) { if (isThread(report)) { diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 64911dbfecb1..21fdde50ab92 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -40,6 +40,9 @@ type PersonalDetails = { /** Whether timezone is automatically set */ automatic?: boolean; }; + + /** If trying to get PersonalDetails from the server and user is offling */ + isOptimisticPersonalDetail?: boolean; }; export default PersonalDetails; From 73d6d6ef8013f630f0cc9eba2a8d0332315e4fe8 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 5 Oct 2023 13:02:50 +0200 Subject: [PATCH 03/63] ref: ReportUtils continue of migration --- src/libs/ReportUtils.ts | 594 +++++++++++++++-------------------- src/libs/TransactionUtils.ts | 4 +- src/types/onyx/Report.ts | 1 + 3 files changed, 259 insertions(+), 340 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 657f1bb6edee..cb796be9716d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1,9 +1,11 @@ /* eslint-disable rulesdir/prefer-underscore-method */ import _ from 'underscore'; import {format, parseISO} from 'date-fns'; +import {SvgProps} from 'react-native-svg'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import lodashIntersection from 'lodash/intersection'; +import {EmptyObject, ValueOf} from 'type-fest'; import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import ONYXKEYS from '../ONYXKEYS'; @@ -23,9 +25,36 @@ import isReportMessageAttachment from './isReportMessageAttachment'; import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAvatars'; import * as CurrencyUtils from './CurrencyUtils'; import * as UserUtils from './UserUtils'; -import {Login, PersonalDetails, Policy, Report, ReportAction} from '../types/onyx'; -import {ValueOf} from 'type-fest'; - +import {Login, PersonalDetails, Policy, Report, ReportAction, Transaction} from '../types/onyx'; +import OriginalMessage from '../types/onyx/OriginalMessage'; +import {Comment} from '../types/onyx/Transaction'; + +type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string}; +type Avatar = { + id: number; + source: React.FC | string; + type: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; + name: string; + fallBackIcon?: React.FC | string; +}; +type ExpanseOriginalMessage = { + oldComment?: string; + newComment?: Comment; + merchant?: string; + oldCreated?: string; + created?: string; + oldMerchant?: string; + oldAmount?: number; + amount?: number; + oldCurrency?: string; + currency?: string; + category?: string; + oldCategory?: string; + tag?: string; + oldTag?: string; + billable?: string; + oldBillable?: string; +}; let currentUserEmail: string | undefined; let currentUserAccountID: number | undefined; let isAnonymousUser = false; @@ -86,11 +115,11 @@ function getChatType(report?: OnyxEntry): ValueOf { if (!allPolicies || !policyID) { - return {}; + return null; } - return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}; + return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; } /** @@ -114,7 +143,7 @@ function getPolicyName(report: OnyxEntry, returnEmptyIfNotFound = false, if ((!allPolicies || Object.keys(allPolicies).length === 0) && !report?.policyName) { return Localize.translateLocal('workspace.common.unavailable'); } - const finalPolicy = policy || allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; + const finalPolicy = policy ?? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; // Public rooms send back the policy name with the reportSummary, // since they can also be accessed by people who aren't in the workspace @@ -147,7 +176,7 @@ function isExpenseReport(report?: OnyxEntry): boolean { /** * Checks if a report is an IOU report. */ -function isIOUReport(report: OnyxEntry): boolean { +function isIOUReport(report?: OnyxEntry): boolean { return report?.type === CONST.REPORT.TYPE.IOU; } @@ -516,15 +545,15 @@ function isPolicyAdmin(policyID: string, policies: OnyxCollection): bool /** * Returns true if report is a DM/Group DM chat. */ -function isDM(report: OnyxEntry): boolean { +function isDM(report?: OnyxEntry): boolean { return !getChatType(report); } /** * Returns true if report has a single participant. */ -function hasSingleParticipant(report: OnyxEntry): boolean { - return report?.participantAccountIDs?.length === 1; +function hasSingleParticipant(report?: OnyxEntry): boolean { + return Boolean(report?.participantAccountIDs?.length === 1); } /** @@ -560,8 +589,8 @@ function isChildReport(report: OnyxEntry): boolean { * An Expense Request is a thread where the parent report is an Expense Report and * the parentReportAction is a transaction. */ -function isExpenseRequest(report: OnyxEntry): boolean { - if (isThread(report)) { +function isExpenseRequest(report?: OnyxEntry): boolean { + if (report && isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; return isExpenseReport(parentReport) && ReportActionsUtils.isTransactionThread(parentReportAction); @@ -573,10 +602,10 @@ function isExpenseRequest(report: OnyxEntry): boolean { * An IOU Request is a thread where the parent report is an IOU Report and * the parentReportAction is a transaction. */ -function isIOURequest(report) { - if (isThread(report)) { +function isIOURequest(report?: OnyxEntry): boolean { + if (report && isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const parentReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; return isIOUReport(parentReport) && ReportActionsUtils.isTransactionThread(parentReportAction); } return false; @@ -584,76 +613,62 @@ function isIOURequest(report) { /** * Checks if a report is an IOU or expense request. - * - * @param {Object|String} reportOrID - * @returns {Boolean} */ -function isMoneyRequest(reportOrID) { - const report = _.isObject(reportOrID) ? reportOrID : allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`]; +function isMoneyRequest(reportOrID: OnyxEntry | string): boolean { + const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`]; return isIOURequest(report) || isExpenseRequest(report); } /** * Checks if a report is an IOU or expense report. - * - * @param {Object|String} reportOrID - * @returns {Boolean} */ -function isMoneyRequestReport(reportOrID) { - const report = typeof reportOrID === 'object' ? reportOrID : allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`]; +function isMoneyRequestReport(reportOrID: OnyxEntry | string): boolean { + const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`]; return isIOUReport(report) || isExpenseReport(report); } /** * Can only delete if the author is this user and the action is an ADDCOMMENT action or an IOU action in an unsettled report, or if the user is a * policy admin - * - * @param {Object} reportAction - * @param {String} reportID - * @returns {Boolean} */ -function canDeleteReportAction(reportAction, reportID) { +function canDeleteReportAction(reportAction: OnyxEntry, reportID: string): boolean { // For now, users cannot delete split actions - if (ReportActionsUtils.isMoneyRequestAction(reportAction) && lodashGet(reportAction, 'originalMessage.type') === CONST.IOU.REPORT_ACTION_TYPE.SPLIT) { + if (ReportActionsUtils.isMoneyRequestAction(reportAction) && reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT) { return false; } - const isActionOwner = reportAction.actorAccountID === currentUserAccountID; - if (isActionOwner && ReportActionsUtils.isMoneyRequestAction(reportAction) && !isSettled(reportAction.originalMessage.IOUReportID)) { + const isActionOwner = reportAction?.actorAccountID === currentUserAccountID; + if (isActionOwner && ReportActionsUtils.isMoneyRequestAction(reportAction) && !isSettled(reportAction?.originalMessage?.IOUReportID)) { return true; } if ( - reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || - reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || + reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || + reportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || ReportActionsUtils.isCreatedTaskReportAction(reportAction) || (ReportActionsUtils.isMoneyRequestAction(reportAction) && isSettled(reportAction.originalMessage.IOUReportID)) || - reportAction.actorAccountID === CONST.ACCOUNT_ID.CONCIERGE + reportAction?.actorAccountID === CONST.ACCOUNT_ID.CONCIERGE ) { return false; } if (isActionOwner) { return true; } - const report = lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}); - const policy = lodashGet(allPolicies, `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`) || {}; - return policy.role === CONST.POLICY.ROLE.ADMIN && !isDM(report); + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; + return policy?.role === CONST.POLICY.ROLE.ADMIN && !isDM(report); } /** * Get welcome message based on room type - * @param {Object} report - * @param {Boolean} isUserPolicyAdmin - * @returns {Object} */ - -function getRoomWelcomeMessage(report, isUserPolicyAdmin) { - const welcomeMessage = {showReportName: true}; +function getRoomWelcomeMessage(report: OnyxEntry, isUserPolicyAdmin: boolean) { + const welcomeMessage: WelcomeMessage = {showReportName: true}; const workspaceName = getPolicyName(report); if (isArchivedRoom(report)) { welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartOne'); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartTwo'); } else if (isDomainRoom(report)) { - welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartOne', {domainRoom: report.reportName}); + welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartOne', {domainRoom: report?.reportName}); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartTwo'); } else if (isAdminRoom(report)) { welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartOne', {workspaceName}); @@ -675,52 +690,44 @@ function getRoomWelcomeMessage(report, isUserPolicyAdmin) { /** * Returns true if Concierge is one of the chat participants (1:1 as well as group chats) - * @param {Object} report - * @returns {Boolean} */ -function chatIncludesConcierge(report) { - return !_.isEmpty(report.participantAccountIDs) && _.contains(report.participantAccountIDs, CONST.ACCOUNT_ID.CONCIERGE); +function chatIncludesConcierge(report: OnyxEntry): boolean { + return Boolean((report?.participantAccountIDs?.length ?? 0) > 0 && report?.participantAccountIDs?.includes(CONST.ACCOUNT_ID.CONCIERGE)); } /** * Returns true if there is any automated expensify account `in accountIDs - * @param {Array} accountIDs - * @returns {Boolean} */ -function hasAutomatedExpensifyAccountIDs(accountIDs) { - return _.intersection(accountIDs, CONST.EXPENSIFY_ACCOUNT_IDS).length > 0; +function hasAutomatedExpensifyAccountIDs(accountIDs: number[]): boolean { + return accountIDs.filter((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)).length > 0; } -/** - * @param {Object} report - * @param {Number} currentLoginAccountID - * @returns {Array} - */ -function getReportRecipientAccountIDs(report, currentLoginAccountID) { - let finalReport = report; +function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAccountID: number): Array { + let finalReport: OnyxEntry | undefined = report; // In 1:1 chat threads, the participants will be the same as parent report. If a report is specifically a 1:1 chat thread then we will // get parent report and use its participants array. if (isThread(report) && !(isTaskReport(report) || isMoneyRequestReport(report))) { - const parentReport = lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]); + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; if (hasSingleParticipant(parentReport)) { finalReport = parentReport; } } - let finalParticipantAccountIDs = []; + let finalParticipantAccountIDs: Array | undefined = []; if (isMoneyRequestReport(report)) { // For money requests i.e the IOU (1:1 person) and Expense (1:* person) reports, use the full `initialParticipantAccountIDs` array // and add the `ownerAccountId`. Money request reports don't add `ownerAccountId` in `participantAccountIDs` array - finalParticipantAccountIDs = _.union(lodashGet(finalReport, 'participantAccountIDs'), [report.ownerAccountID]); - } else if (isTaskReport(report)) { + const defaultParticipantAccountIDs = finalReport?.participantAccountIDs ?? []; + const setOfParticipantAccountIDs = new Set([...defaultParticipantAccountIDs, ...[report?.ownerAccountID]]); + finalParticipantAccountIDs = [...setOfParticipantAccountIDs]; // Task reports `managerID` will change when assignee is changed, in that case the old `managerID` is still present in `participantAccountIDs` // array along with the new one. We only need the `managerID` as a participant here. - finalParticipantAccountIDs = [report.managerID]; + finalParticipantAccountIDs = [report?.managerID]; } else { - finalParticipantAccountIDs = lodashGet(finalReport, 'participantAccountIDs'); + finalParticipantAccountIDs = finalReport?.participantAccountIDs; } - const reportParticipants = _.without(finalParticipantAccountIDs, currentLoginAccountID); + const reportParticipants = finalParticipantAccountIDs?.filter((accountID) => accountID !== currentLoginAccountID) ?? []; const participantsWithoutExpensifyAccountIDs = _.difference(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS); return participantsWithoutExpensifyAccountIDs; } @@ -732,30 +739,19 @@ function getReportRecipientAccountIDs(report, currentLoginAccountID) { * @param {Number} accountID * @return {Boolean} */ -function canShowReportRecipientLocalTime(personalDetails, report, accountID) { +function canShowReportRecipientLocalTime(personalDetails: OnyxCollection, report: OnyxEntry, accountID: number): boolean { const reportRecipientAccountIDs = getReportRecipientAccountIDs(report, accountID); const hasMultipleParticipants = reportRecipientAccountIDs.length > 1; - const reportRecipient = personalDetails[reportRecipientAccountIDs[0]]; - const reportRecipientTimezone = lodashGet(reportRecipient, 'timezone', CONST.DEFAULT_TIME_ZONE); + const reportRecipient = personalDetails?.[reportRecipientAccountIDs[0]]; + const reportRecipientTimezone = reportRecipient?.timezone ?? CONST.DEFAULT_TIME_ZONE; const isReportParticipantValidated = lodashGet(reportRecipient, 'validated', false); - return Boolean( - !hasMultipleParticipants && - !isChatRoom(report) && - !isPolicyExpenseChat(report) && - reportRecipient && - reportRecipientTimezone && - reportRecipientTimezone.selected && - isReportParticipantValidated, - ); + return Boolean(!hasMultipleParticipants && !isChatRoom(report) && !isPolicyExpenseChat(report) && reportRecipient && reportRecipientTimezone?.selected && isReportParticipantValidated); } /** * Shorten last message text to fixed length and trim spaces. - * @param {String} lastMessageText - * @param {Boolean} isModifiedExpenseMessage - * @returns {String} */ -function formatReportLastMessageText(lastMessageText, isModifiedExpenseMessage = false) { +function formatReportLastMessageText(lastMessageText: string, isModifiedExpenseMessage = false): string { if (isModifiedExpenseMessage) { return String(lastMessageText).trim().replace(CONST.REGEX.LINE_BREAK, '').trim(); } @@ -764,10 +760,8 @@ function formatReportLastMessageText(lastMessageText, isModifiedExpenseMessage = /** * Helper method to return the default avatar associated with the given login - * @param {String} [workspaceName] - * @returns {String} */ -function getDefaultWorkspaceAvatar(workspaceName) { +function getDefaultWorkspaceAvatar(workspaceName?: string): string { if (!workspaceName) { return defaultWorkspaceAvatars.WorkspaceBuilding; } @@ -778,57 +772,53 @@ function getDefaultWorkspaceAvatar(workspaceName) { .replace(/[^0-9a-z]/gi, '') .toUpperCase(); - return !alphaNumeric ? defaultWorkspaceAvatars.WorkspaceBuilding : defaultWorkspaceAvatars[`Workspace${alphaNumeric[0]}`]; + const defaultWorkspaceAvatar = defaultWorkspaceAvatars[`Workspace${alphaNumeric[0]}`] as React.FC; + + return !alphaNumeric ? defaultWorkspaceAvatars.WorkspaceBuilding : defaultWorkspaceAvatar; } -function getWorkspaceAvatar(report) { - const workspaceName = getPolicyName(report, allPolicies); - return lodashGet(allPolicies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'avatar']) || getDefaultWorkspaceAvatar(workspaceName); +function getWorkspaceAvatar(report: OnyxEntry) { + const workspaceName = getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]); + return allPolicies?.[`policy${report?.policyID}`]?.avatar ?? getDefaultWorkspaceAvatar(workspaceName); } /** * Returns the appropriate icons for the given chat report using the stored personalDetails. * The Avatar sources can be URLs or Icon components according to the chat type. - * - * @param {Array} participants - * @param {Object} personalDetails - * @returns {Array<*>} */ -function getIconsForParticipants(participants, personalDetails) { - const participantDetails = []; +function getIconsForParticipants(participants: number[], personalDetails: OnyxCollection) { + const participantDetails: Array<[number, string, string | React.FC, React.FC]> = []; const participantsList = participants || []; - for (let i = 0; i < participantsList.length; i++) { - const accountID = participantsList[i]; + for (const accountID of participantsList) { const avatarSource = UserUtils.getAvatar(lodashGet(personalDetails, [accountID, 'avatar'], ''), accountID); - const displayNameLogin = lodashGet(personalDetails, [accountID, 'displayName']) || lodashGet(personalDetails, [accountID, 'login'], ''); - participantDetails.push([accountID, displayNameLogin, avatarSource, lodashGet(personalDetails, [accountID, 'fallBackIcon'])]); + const displayNameLogin = personalDetails?.[accountID]?.displayName ?? personalDetails?.[accountID]?.login ?? ''; + participantDetails.push([accountID, displayNameLogin, avatarSource, personalDetails?.[accountID]?.fallBackIcon]); } - const sortedParticipantDetails = _.chain(participantDetails) - .sort((first, second) => { - // First sort by displayName/login - const displayNameLoginOrder = first[1].localeCompare(second[1]); - if (displayNameLoginOrder !== 0) { - return displayNameLoginOrder; - } + const sortedParticipantDetails = participantDetails.sort((first, second) => { + // First sort by displayName/login + const displayNameLoginOrder = first[1].localeCompare(second[1]); + if (displayNameLoginOrder !== 0) { + return displayNameLoginOrder; + } - // Then fallback on accountID as the final sorting criteria. - // This will ensure that the order of avatars with same login/displayName - // stay consistent across all users and devices - return first[0] > second[0]; - }) - .value(); + // Then fallback on accountID as the final sorting criteria. + // This will ensure that the order of avatars with same login/displayName + // stay consistent across all users and devices + return first[0] > second[0]; + }); // Now that things are sorted, gather only the avatars (second element in the array) and return those - const avatars = []; - for (let i = 0; i < sortedParticipantDetails.length; i++) { + const avatars: Avatar[] = []; + + for (const sortedParticipantDetail of sortedParticipantDetails) { const userIcon = { - id: sortedParticipantDetails[i][0], - source: sortedParticipantDetails[i][2], + id: sortedParticipantDetail[0], + source: sortedParticipantDetail[2], type: CONST.ICON_TYPE_AVATAR, - name: sortedParticipantDetails[i][1], - fallBackIcon: sortedParticipantDetails[i][3], + name: sortedParticipantDetail[1], + fallBackIcon: sortedParticipantDetail[3], }; avatars.push(userIcon); } @@ -838,14 +828,12 @@ function getIconsForParticipants(participants, personalDetails) { /** * Given a report, return the associated workspace icon. - * - * @param {Object} report - * @param {Object} [policy] - * @returns {Object} */ -function getWorkspaceIcon(report, policy = undefined) { +function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): Avatar { const workspaceName = getPolicyName(report, false, policy); - const policyExpenseChatAvatarSource = lodashGet(allPolicies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'avatar']) || getDefaultWorkspaceAvatar(workspaceName); + // TODO: Check why ?? is not working here + const policyExpenseChatAvatarSource = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar || getDefaultWorkspaceAvatar(workspaceName); + const workspaceIcon = { source: policyExpenseChatAvatarSource, type: CONST.ICON_TYPE_WORKSPACE, @@ -858,19 +846,18 @@ function getWorkspaceIcon(report, policy = undefined) { /** * Returns the appropriate icons for the given chat report using the stored personalDetails. * The Avatar sources can be URLs or Icon components according to the chat type. - * - * @param {Object} report - * @param {Object} personalDetails - * @param {*} [defaultIcon] - * @param {String} [defaultName] - * @param {Number} [defaultAccountID] - * @param {Object} [policy] - * @returns {Array<*>} */ -function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', defaultAccountID = -1, policy = undefined) { - if (_.isEmpty(report)) { - const fallbackIcon = { - source: defaultIcon || Expensicons.FallbackAvatar, +function getIcons( + report: OnyxEntry, + personalDetails: OnyxCollection, + defaultIcon: string | React.FC | null = null, + defaultName = '', + defaultAccountID = -1, + policy: OnyxEntry | undefined = undefined, +) { + if (Object.keys(report ?? {}).length === 0) { + const fallbackIcon: Avatar = { + source: defaultIcon ?? Expensicons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: defaultName, id: defaultAccountID, @@ -881,11 +868,11 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', const parentReportAction = ReportActionsUtils.getParentReportAction(report); const workspaceIcon = getWorkspaceIcon(report, policy); const memberIcon = { - source: UserUtils.getAvatar(lodashGet(personalDetails, [parentReportAction.actorAccountID, 'avatar']), parentReportAction.actorAccountID), + source: UserUtils.getAvatar(personalDetails?.[parentReportAction.actorAccountID]?.avatar ?? '', parentReportAction.actorAccountID), id: parentReportAction.actorAccountID, type: CONST.ICON_TYPE_AVATAR, - name: lodashGet(personalDetails, [parentReportAction.actorAccountID, 'displayName'], ''), - fallbackIcon: lodashGet(personalDetails, [parentReportAction.actorAccountID, 'fallbackIcon']), + name: personalDetails?.[parentReportAction.actorAccountID]?.displayName ?? '', + fallbackIcon: personalDetails?.[parentReportAction.actorAccountID]?.fallbackIcon, }; return [memberIcon, workspaceIcon]; @@ -893,14 +880,14 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', if (isChatThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const actorAccountID = lodashGet(parentReportAction, 'actorAccountID', -1); - const actorDisplayName = lodashGet(allPersonalDetails, [actorAccountID, 'displayName'], ''); + const actorAccountID = parentReportAction[actorAccountID] ?? -1; + const actorDisplayName = allPersonalDetails?.[actorAccountID]?.displayName ?? ''; const actorIcon = { id: actorAccountID, - source: UserUtils.getAvatar(lodashGet(personalDetails, [actorAccountID, 'avatar']), actorAccountID), + source: UserUtils.getAvatar(personalDetails?.[actorAccountID]?.avatar ?? '', actorAccountID), name: actorDisplayName, type: CONST.ICON_TYPE_AVATAR, - fallbackIcon: lodashGet(personalDetails, [parentReportAction.actorAccountID, 'fallbackIcon']), + fallbackIcon: personalDetails?.[parentReportAction.actorAccountID]?.fallbackIcon, }; if (isWorkspaceThread(report)) { @@ -911,11 +898,11 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', } if (isTaskReport(report)) { const ownerIcon = { - id: report.ownerAccountID, - source: UserUtils.getAvatar(lodashGet(personalDetails, [report.ownerAccountID, 'avatar']), report.ownerAccountID), + id: report?.ownerAccountID, + source: UserUtils.getAvatar(personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? '', report?.ownerAccountID ?? -1), type: CONST.ICON_TYPE_AVATAR, - name: lodashGet(personalDetails, [report.ownerAccountID, 'displayName'], ''), - fallbackIcon: lodashGet(personalDetails, [report.ownerAccountID, 'fallbackIcon']), + name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '', + fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon, }; if (isWorkspaceTaskReport(report)) { @@ -927,7 +914,7 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', } if (isDomainRoom(report)) { // Get domain name after the #. Domain Rooms use our default workspace avatar pattern. - const domainName = report.reportName.substring(1); + const domainName = report?.reportName?.substring(1); const policyExpenseChatAvatarSource = getDefaultWorkspaceAvatar(domainName); const domainIcon = { source: policyExpenseChatAvatarSource, @@ -944,43 +931,42 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', if (isPolicyExpenseChat(report) || isExpenseReport(report)) { const workspaceIcon = getWorkspaceIcon(report, policy); const memberIcon = { - source: UserUtils.getAvatar(lodashGet(personalDetails, [report.ownerAccountID, 'avatar']), report.ownerAccountID), - id: report.ownerAccountID, + source: UserUtils.getAvatar(personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? '', report?.ownerAccountID ?? -1), + id: report?.ownerAccountID, type: CONST.ICON_TYPE_AVATAR, - name: lodashGet(personalDetails, [report.ownerAccountID, 'displayName'], ''), - fallbackIcon: lodashGet(personalDetails, [report.ownerAccountID, 'fallbackIcon']), + name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '', + fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon, }; return isExpenseReport(report) ? [memberIcon, workspaceIcon] : [workspaceIcon, memberIcon]; } if (isIOUReport(report)) { const managerIcon = { - source: UserUtils.getAvatar(lodashGet(personalDetails, [report.managerID, 'avatar']), report.managerID), - id: report.managerID, + source: UserUtils.getAvatar(personalDetails?.[report?.managerID ?? -1]?.avatar ?? '', report?.managerID ?? -1), + id: report?.managerID, type: CONST.ICON_TYPE_AVATAR, - name: lodashGet(personalDetails, [report.managerID, 'displayName'], ''), - fallbackIcon: lodashGet(personalDetails, [report.managerID, 'fallbackIcon']), + name: personalDetails?.[report?.managerID ?? -1]?.displayName ?? '', + fallbackIcon: personalDetails?.[report?.managerID ?? -1]?.fallbackIcon, }; const ownerIcon = { - id: report.ownerAccountID, - source: UserUtils.getAvatar(lodashGet(personalDetails, [report.ownerAccountID, 'avatar']), report.ownerAccountID), + id: report?.ownerAccountID, + source: UserUtils.getAvatar(personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? '', report?.ownerAccountID ?? -1), type: CONST.ICON_TYPE_AVATAR, - name: lodashGet(personalDetails, [report.ownerAccountID, 'displayName'], ''), - fallbackIcon: lodashGet(personalDetails, [report.ownerAccountID, 'fallbackIcon']), + name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '', + fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon, }; - const isPayer = currentUserAccountID === report.managerID; + const isPayer = currentUserAccountID === report?.managerID; return isPayer ? [managerIcon, ownerIcon] : [ownerIcon, managerIcon]; } - return getIconsForParticipants(report.participantAccountIDs, personalDetails); + + return getIconsForParticipants(report?.participantAccountIDs ?? [], personalDetails); } /** * Gets the personal details for a login by looking in the ONYXKEYS.PERSONAL_DETAILS_LIST Onyx key (stored in the local variable, allPersonalDetails). If it doesn't exist in Onyx, * then a default object is constructed. - * @param {Number} accountID - * @returns {Object} */ -function getPersonalDetailsForAccountID(accountID) { +function getPersonalDetailsForAccountID(accountID: number): PersonalDetails { if (!accountID) { return {}; } @@ -993,7 +979,7 @@ function getPersonalDetailsForAccountID(accountID) { }; } return ( - (allPersonalDetails && allPersonalDetails[accountID]) || { + allPersonalDetails?.[accountID] ?? { avatar: UserUtils.getDefaultAvatar(accountID), } ); @@ -1001,34 +987,26 @@ function getPersonalDetailsForAccountID(accountID) { /** * Get the displayName for a single report participant. - * - * @param {Number} accountID - * @param {Boolean} [shouldUseShortForm] - * @returns {String} */ -function getDisplayNameForParticipant(accountID, shouldUseShortForm = false) { +function getDisplayNameForParticipant(accountID: number, shouldUseShortForm = false) { if (!accountID) { return ''; } const personalDetails = getPersonalDetailsForAccountID(accountID); const longName = personalDetails.displayName; - const shortName = personalDetails.firstName || longName; + // TODO: Check why ?? is not working + const shortName = personalDetails?.firstName || longName; return shouldUseShortForm ? shortName : longName; } -/** - * @param {Object} personalDetailsList - * @param {Boolean} isMultipleParticipantReport - * @returns {Array} - */ -function getDisplayNamesWithTooltips(personalDetailsList, isMultipleParticipantReport) { - return _.chain(personalDetailsList) - .map((user) => { - const accountID = Number(user.accountID); - const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport) || user.login || ''; +function getDisplayNamesWithTooltips(personalDetailsList: OnyxCollection, isMultipleParticipantReport: boolean) { + return Object.values(personalDetailsList ?? {}) + ?.map((user) => { + const accountID = Number(user?.accountID); + const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport) ?? user?.login ?? ''; const avatar = UserUtils.getDefaultAvatar(accountID); - let pronouns = user.pronouns; + let pronouns = user?.pronouns; if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) { const pronounTranslationKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); pronouns = Localize.translateLocal(`pronouns.${pronounTranslationKey}`); @@ -1037,7 +1015,7 @@ function getDisplayNamesWithTooltips(personalDetailsList, isMultipleParticipantR return { displayName, avatar, - login: user.login || '', + login: user?.login ?? '', accountID, pronouns, }; @@ -1051,51 +1029,41 @@ function getDisplayNamesWithTooltips(personalDetailsList, isMultipleParticipantR // Then fallback on accountID as the final sorting criteria. return first.accountID > second.accountID; - }) - .value(); + }); } /** * Gets a joined string of display names from the list of display name with tooltip objects. - * - * @param {Object} displayNamesWithTooltips - * @returns {String} */ -function getDisplayNamesStringFromTooltips(displayNamesWithTooltips) { - return _.filter( - _.map(displayNamesWithTooltips, ({displayName}) => displayName), - (displayName) => !_.isEmpty(displayName), - ).join(', '); +function getDisplayNamesStringFromTooltips(displayNamesWithTooltips: PersonalDetails[]) { + return displayNamesWithTooltips + .map(({displayName}) => displayName) + .filter((displayName) => !_.isEmpty(displayName)) + .join(', '); } /** * Get the report given a reportID - * - * @param {String} reportID - * @returns {Object} */ -function getReport(reportID) { +function getReport(reportID: string): OnyxEntry { // Deleted reports are set to null and lodashGet will still return null in that case, so we need to add an extra check - return lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}) || {}; + return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? ({} as OnyxEntry); } /** * Determines if a report has an IOU that is waiting for an action from the current user (either Pay or Add a credit bank account) - * - * @param {Object} report (chatReport or iouReport) - * @returns {boolean} */ -function isWaitingForIOUActionFromCurrentUser(report) { +function isWaitingForIOUActionFromCurrentUser(report: OnyxEntry): boolean { if (!report) { return false; } - if (isArchivedRoom(getReport(report.parentReportID))) { + if (isArchivedRoom(getReport(report.parentReportID ?? ''))) { return false; } - const policy = getPolicy(report.policyID); - if (policy.type === CONST.POLICY.TYPE.CORPORATE) { + const policy = getPolicy(report?.policyID ?? ''); + if (policy?.type === CONST.POLICY.TYPE.CORPORATE) { // If the report is already settled, there's no action required from any user. if (isSettled(report.reportID)) { return false; @@ -1126,30 +1094,24 @@ function isWaitingForIOUActionFromCurrentUser(report) { /** * Checks if a report is an open task report assigned to current user. * - * @param {Object} report - * @param {Object} parentReportAction - The parent report action of the report (Used to check if the task has been canceled) - * @returns {Boolean} + * @param report + * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ -function isWaitingForTaskCompleteFromAssignee(report, parentReportAction = {}) { +function isWaitingForTaskCompleteFromAssignee(report: OnyxEntry, parentReportAction: OnyxEntry): boolean { return isTaskReport(report) && isReportManager(report) && isOpenTaskReport(report, parentReportAction); } -/** - * @param {Object} report - * @param {Object} allReportsDict - * @returns {Number} - */ -function getMoneyRequestTotal(report, allReportsDict = null) { - const allAvailableReports = allReportsDict || allReports; +function getMoneyRequestTotal(report: OnyxEntry, allReportsDict: OnyxCollection = null): number { + const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport; if (isMoneyRequestReport(report)) { moneyRequestReport = report; } - if (allAvailableReports && report.hasOutstandingIOU && report.iouReportID) { + if (allAvailableReports && report?.hasOutstandingIOU && report?.iouReportID) { moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; } if (moneyRequestReport) { - const total = lodashGet(moneyRequestReport, 'total', 0); + const total = moneyRequestReport.total ?? 0; if (total !== 0) { // There is a possibility that if the Expense report has a negative total. @@ -1163,26 +1125,22 @@ function getMoneyRequestTotal(report, allReportsDict = null) { /** * Get the title for a policy expense chat which depends on the role of the policy member seeing this report - * - * @param {Object} report - * @param {Object} [policy] - * @returns {String} */ -function getPolicyExpenseChatName(report, policy = undefined) { - const reportOwnerDisplayName = getDisplayNameForParticipant(report.ownerAccountID) || lodashGet(allPersonalDetails, [report.ownerAccountID, 'login']) || report.reportName; +function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string { + const reportOwnerDisplayName = getDisplayNameForParticipant(report?.ownerAccountID ?? -1) ?? allPersonalDetails?.[report?.ownerAccountID ?? -1]?.login ?? report?.reportName; // If the policy expense chat is owned by this user, use the name of the policy as the report name. - if (report.isOwnPolicyExpenseChat) { + if (report?.isOwnPolicyExpenseChat) { return getPolicyName(report, false, policy); } - const policyExpenseChatRole = lodashGet(allPolicies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'role']) || 'user'; + const policyExpenseChatRole = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.role ?? 'user'; // If this user is not admin and this policy expense chat has been archived because of account merging, this must be an old workspace chat // of the account which was merged into the current user's account. Use the name of the policy as the name of the report. if (isArchivedRoom(report)) { - const lastAction = ReportActionsUtils.getLastVisibleAction(report.reportID); - const archiveReason = (lastAction && lastAction.originalMessage && lastAction.originalMessage.reason) || CONST.REPORT.ARCHIVE_REASON.DEFAULT; + const lastAction = ReportActionsUtils.getLastVisibleAction(report?.reportID ?? ''); + const archiveReason = lastAction?.originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED && policyExpenseChatRole !== CONST.POLICY.ROLE.ADMIN) { return getPolicyName(report, false, policy); } @@ -1194,24 +1152,20 @@ function getPolicyExpenseChatName(report, policy = undefined) { /** * Get the title for a IOU or expense chat which will be showing the payer and the amount - * - * @param {Object} report - * @param {Object} [policy] - * @returns {String} */ -function getMoneyRequestReportName(report, policy = undefined) { - const formattedAmount = CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(report), report.currency); - const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report.managerID); +function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined) { + const formattedAmount = CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(report), report?.currency); + const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID ?? -1); const payerPaidAmountMesssage = Localize.translateLocal('iou.payerPaidAmount', { payer: payerName, amount: formattedAmount, }); - if (report.isWaitingOnBankAccount) { + if (report?.isWaitingOnBankAccount) { return `${payerPaidAmountMesssage} • ${Localize.translateLocal('iou.pending')}`; } - if (report.hasOutstandingIOU) { + if (report?.hasOutstandingIOU) { return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName, amount: formattedAmount}); } @@ -1221,14 +1175,13 @@ function getMoneyRequestReportName(report, policy = undefined) { /** * Gets transaction created, amount, currency, comment, and waypoints (for distance request) * into a flat object. Used for displaying transactions and sending them in API commands - * - * @param {Object} transaction - * @returns {Object} */ -function getTransactionDetails(transaction) { + +// TODO: Check if this shouldn't be OnyxEntry +function getTransactionDetails(transaction: Transaction) { const report = getReport(transaction.reportID); return { - created: TransactionUtils.getCreated(transaction), + created: TransactionUtils.getCreated(transaction ?? {}), amount: TransactionUtils.getAmount(transaction, isExpenseReport(report)), currency: TransactionUtils.getCurrency(transaction), comment: TransactionUtils.getDescription(transaction), @@ -1248,22 +1201,19 @@ function getTransactionDetails(transaction) { * - in case of expense report * - the current user is the requestor * - or the user is an admin on the policy the expense report is tied to - * - * @param {Object} reportAction - * @returns {Boolean} */ -function canEditMoneyRequest(reportAction) { +function canEditMoneyRequest(reportAction: OnyxEntry): boolean { // If the report action i snot IOU type, return true early - if (reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { + if (reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { return true; } - const moneyRequestReportID = lodashGet(reportAction, 'originalMessage.IOUReportID', 0); + const moneyRequestReportID = reportAction.originalMessage.IOUReportID ?? 0; if (!moneyRequestReportID) { return false; } - const moneyRequestReport = getReport(moneyRequestReportID); - const isReportSettled = isSettled(moneyRequestReport.reportID); - const isAdmin = isExpenseReport(moneyRequestReport) && lodashGet(getPolicy(moneyRequestReport.policyID), 'role', '') === CONST.POLICY.ROLE.ADMIN; + const moneyRequestReport = getReport(String(moneyRequestReportID)); + const isReportSettled = isSettled(moneyRequestReport?.reportID ?? ''); + const isAdmin = isExpenseReport(moneyRequestReport) && (getPolicy(moneyRequestReport?.policyID ?? '')?.role ?? '') === CONST.POLICY.ROLE.ADMIN; const isRequestor = currentUserAccountID === reportAction.actorAccountID; return !isReportSettled && (isAdmin || isRequestor); } @@ -1275,14 +1225,11 @@ function canEditMoneyRequest(reportAction) { * - It's an ADDCOMMENT that is not an attachment * - It's money request where conditions for editability are defined in canEditMoneyRequest method * - It's not pending deletion - * - * @param {Object} reportAction - * @returns {Boolean} */ -function canEditReportAction(reportAction) { - const isCommentOrIOU = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; +function canEditReportAction(reportAction: OnyxEntry): boolean { + const isCommentOrIOU = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; return ( - reportAction.actorAccountID === currentUserAccountID && + reportAction?.actorAccountID === currentUserAccountID && isCommentOrIOU && canEditMoneyRequest(reportAction) && // Returns true for non-IOU actions !isReportMessageAttachment(lodashGet(reportAction, ['message', 0], {})) && @@ -1294,13 +1241,10 @@ function canEditReportAction(reportAction) { /** * Gets all transactions on an IOU report with a receipt - * - * @param {Object|null} iouReportID - * @returns {[Object]} */ -function getTransactionsWithReceipts(iouReportID) { +function getTransactionsWithReceipts(iouReportID: string | undefined) { const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); - return _.filter(allTransactions, (transaction) => TransactionUtils.hasReceipt(transaction)); + return allTransactions.filter((transaction) => TransactionUtils.hasReceipt(transaction)); } /** @@ -1309,18 +1253,14 @@ function getTransactionsWithReceipts(iouReportID) { * all requests are receipts that are being SmartScanned. As soon as we have a non-receipt request, * or as soon as one receipt request is done scanning, we have at least one * "ready" money request, and we remove this indicator to show the partial report total. - * - * @param {Object|null} iouReportID - * @param {Object|null} reportPreviewAction the preview action associated with the IOU report - * @returns {Boolean} */ -function areAllRequestsBeingSmartScanned(iouReportID, reportPreviewAction) { +function areAllRequestsBeingSmartScanned(iouReportID: string | undefined, reportPreviewAction: OnyxEntry): boolean { const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); // If we have more requests than requests with receipts, we have some manual requests if (ReportActionsUtils.getNumberOfMoneyRequests(reportPreviewAction) > transactionsWithReceipts.length) { return false; } - return _.all(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)); + return transactionsWithReceipts.every((transaction) => TransactionUtils.isReceiptBeingScanned(transaction)); } /** @@ -1329,18 +1269,15 @@ function areAllRequestsBeingSmartScanned(iouReportID, reportPreviewAction) { * @param {Object|null} iouReportID * @returns {Boolean} */ -function hasMissingSmartscanFields(iouReportID) { +function hasMissingSmartscanFields(iouReportID?: string) { const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); - return _.some(transactionsWithReceipts, (transaction) => TransactionUtils.hasMissingSmartscanFields(transaction)); + return transactionsWithReceipts.some((transaction) => TransactionUtils.hasMissingSmartscanFields(transaction)); } /** * Given a parent IOU report action get report name for the LHN. - * - * @param {Object} reportAction - * @returns {String} */ -function getTransactionReportName(reportAction) { +function getTransactionReportName(reportAction: OnyxEntry): string { if (ReportActionsUtils.isDeletedParentAction(reportAction)) { return Localize.translateLocal('parentReportAction.deletedRequest'); } @@ -1370,17 +1307,17 @@ function getTransactionReportName(reportAction) { * @param {Boolean} [shouldConsiderReceiptBeingScanned=false] * @returns {String} */ -function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceiptBeingScanned = false) { +function getReportPreviewMessage(report: OnyxEntry, reportAction?: OnyxEntry, shouldConsiderReceiptBeingScanned = false) { const reportActionMessage = lodashGet(reportAction, 'message[0].html', ''); - if (_.isEmpty(report) || !report.reportID) { + if (Object.keys(report ?? {}).length === 0 || !report?.reportID) { // The iouReport is not found locally after SignIn because the OpenApp API won't return iouReports if they're settled // As a temporary solution until we know how to solve this the best, we just use the message that returned from BE return reportActionMessage; } const totalAmount = getMoneyRequestTotal(report); - const payerName = isExpenseReport(report) ? getPolicyName(report) : getDisplayNameForParticipant(report.managerID, true); + const payerName = isExpenseReport(report) ? getPolicyName(report) : getDisplayNameForParticipant(report.managerID ?? -1, true); const formattedAmount = CurrencyUtils.convertToDisplayString(totalAmount, report.currency); if (isReportApproved(report) && getPolicyType(report, allPolicies) === CONST.POLICY.TYPE.CORPORATE) { @@ -1390,7 +1327,7 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip if (shouldConsiderReceiptBeingScanned && ReportActionsUtils.isMoneyRequestAction(reportAction)) { const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); - if (!_.isEmpty(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + if (Object.keys(linkedTransaction).length !== 0 && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { return Localize.translateLocal('iou.receiptScanning'); } } @@ -1399,7 +1336,7 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip // A settled report preview message can come in three formats "paid ... elsewhere" or "paid ... with Expensify" let translatePhraseKey = 'iou.paidElsewhereWithAmount'; if ( - _.contains([CONST.IOU.PAYMENT_TYPE.VBBA, CONST.IOU.PAYMENT_TYPE.EXPENSIFY], reportAction.originalMessage.paymentType) || + [CONST.IOU.PAYMENT_TYPE.VBBA, CONST.IOU.PAYMENT_TYPE.EXPENSIFY].includes(reportAction?.originalMessage?.paymentType) || reportActionMessage.match(/ (with Expensify|using Expensify)$/) ) { translatePhraseKey = 'iou.paidWithExpensifyWithAmount'; @@ -1408,7 +1345,7 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip } if (report.isWaitingOnBankAccount) { - const submitterDisplayName = getDisplayNameForParticipant(report.ownerAccountID, true); + const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID ?? -1, true); return Localize.translateLocal('iou.waitingOnBankAccount', {submitterDisplayName}); } @@ -1417,15 +1354,9 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip /** * Get the proper message schema for modified expense message. - * - * @param {String} newValue - * @param {String} oldValue - * @param {String} valueName - * @param {Boolean} valueInQuotes - * @returns {String} */ -function getProperSchemaForModifiedExpenseMessage(newValue, oldValue, valueName, valueInQuotes) { +function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean) { const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; const displayValueName = valueName.toLowerCase(); @@ -1441,15 +1372,8 @@ function getProperSchemaForModifiedExpenseMessage(newValue, oldValue, valueName, /** * 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) { +function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { if (!oldDistance) { return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); } @@ -1466,23 +1390,21 @@ function getProperSchemaForModifiedDistanceMessage(newDistance, oldDistance, new * * 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)) { +function getModifiedExpenseMessage(reportAction: OnyxEntry): string { + const reportActionOriginalMessage = reportAction?.originalMessage ?? {}; + if (Object.keys(reportActionOriginalMessage).length === 0) { return Localize.translateLocal('iou.changedTheRequest'); } const hasModifiedAmount = - _.has(reportActionOriginalMessage, 'oldAmount') && - _.has(reportActionOriginalMessage, 'oldCurrency') && - _.has(reportActionOriginalMessage, 'amount') && - _.has(reportActionOriginalMessage, 'currency'); + Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldAmount') && + Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldCurrency') && + Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'amount') && + Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'currency'); - const hasModifiedMerchant = _.has(reportActionOriginalMessage, 'oldMerchant') && _.has(reportActionOriginalMessage, 'merchant'); + const hasModifiedMerchant = + Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldMerchant') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'merchant'); if (hasModifiedAmount) { const oldCurrency = reportActionOriginalMessage.oldCurrency; const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.oldAmount, oldCurrency); @@ -1499,34 +1421,38 @@ function getModifiedExpenseMessage(reportAction) { return getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); } - const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); + const hasModifiedComment = + Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldComment') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true); } - const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); + const hasModifiedCreated = + Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldCreated') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'created'); if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp let formattedOldCreated = parseISO(reportActionOriginalMessage.oldCreated); formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); + return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated.toDateString(), Localize.translateLocal('common.date'), false); } if (hasModifiedMerchant) { return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true); } - const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); + const hasModifiedCategory = + Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldCategory') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true); } - const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); + const hasModifiedTag = Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldTag') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'tag'); if (hasModifiedTag) { return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, Localize.translateLocal('common.tag'), true); } - const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); + const hasModifiedBillable = + Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldBillable') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true); } @@ -1537,50 +1463,45 @@ function getModifiedExpenseMessage(reportAction) { * object of the modified expense action. * * At the moment, we only allow changing one transaction field at a time. - * - * @param {Object} oldTransaction - * @param {Object} transactionChanges - * @param {Boolean} isFromExpenseReport - * @returns {Object} */ -function getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport) { - const originalMessage = {}; +function getModifiedExpenseOriginalMessage(oldTransaction: Transaction, transactionChanges: Transaction, isFromExpenseReport: boolean) { + const originalMessage: ExpanseOriginalMessage = {}; // Remark: Comment field is the only one which has new/old prefixes for the keys (newComment/ oldComment), // all others have old/- pattern such as oldCreated/created - if (_.has(transactionChanges, 'comment')) { + if (Object.prototype.hasOwnProperty.call(transactionChanges, 'comment')) { originalMessage.oldComment = TransactionUtils.getDescription(oldTransaction); originalMessage.newComment = transactionChanges.comment; } - if (_.has(transactionChanges, 'created')) { + if (Object.prototype.hasOwnProperty.call(transactionChanges, 'created')) { originalMessage.oldCreated = TransactionUtils.getCreated(oldTransaction); originalMessage.created = transactionChanges.created; } - if (_.has(transactionChanges, 'merchant')) { + if (Object.prototype.hasOwnProperty.call(transactionChanges, 'merchant')) { originalMessage.oldMerchant = TransactionUtils.getMerchant(oldTransaction); originalMessage.merchant = transactionChanges.merchant; } // The amount is always a combination of the currency and the number value so when one changes we need to store both // to match how we handle the modified expense action in oldDot - if (_.has(transactionChanges, 'amount') || _.has(transactionChanges, 'currency')) { + if (Object.prototype.hasOwnProperty.call(transactionChanges, 'amount') || _.has(transactionChanges, 'currency')) { originalMessage.oldAmount = TransactionUtils.getAmount(oldTransaction, isFromExpenseReport); originalMessage.amount = lodashGet(transactionChanges, 'amount', originalMessage.oldAmount); originalMessage.oldCurrency = TransactionUtils.getCurrency(oldTransaction); originalMessage.currency = lodashGet(transactionChanges, 'currency', originalMessage.oldCurrency); } - if (_.has(transactionChanges, 'category')) { + if (Object.prototype.hasOwnProperty.call(transactionChanges, 'category')) { originalMessage.oldCategory = TransactionUtils.getCategory(oldTransaction); originalMessage.category = transactionChanges.category; } - if (_.has(transactionChanges, 'tag')) { + if (Object.prototype.hasOwnProperty.call(transactionChanges, 'tag')) { originalMessage.oldTag = TransactionUtils.getTag(oldTransaction); originalMessage.tag = transactionChanges.tag; } - if (_.has(transactionChanges, 'billable')) { + if (Object.prototype.hasOwnProperty.call(transactionChanges, 'billable')) { const oldBillable = TransactionUtils.getBillable(oldTransaction); originalMessage.oldBillable = oldBillable ? Localize.translateLocal('common.billable').toLowerCase() : Localize.translateLocal('common.nonBillable').toLowerCase(); originalMessage.billable = transactionChanges.billable ? Localize.translateLocal('common.billable').toLowerCase() : Localize.translateLocal('common.nonBillable').toLowerCase(); @@ -1591,15 +1512,12 @@ function getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, i /** * Returns the parentReport if the given report is a thread. - * - * @param {Object} report - * @returns {Object} */ -function getParentReport(report) { - if (!report || !report.parentReportID) { +function getParentReport(report: OnyxEntry): OnyxEntry | undefined { + if (!report?.parentReportID) { return {}; } - return lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, {}); + return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]; } /** @@ -3029,7 +2947,7 @@ function getNewMarkerReportActionID(report, sortedAndFilteredReportActions) { const newMarkerIndex = _.findLastIndex(sortedAndFilteredReportActions, (reportAction) => (reportAction.created || '') > report.lastReadTime); - return _.has(sortedAndFilteredReportActions[newMarkerIndex], 'reportActionID') ? sortedAndFilteredReportActions[newMarkerIndex].reportActionID : ''; + return Object.prototype.hasOwnProperty.call(sortedAndFilteredReportActions[newMarkerIndex], 'reportActionID') ? sortedAndFilteredReportActions[newMarkerIndex].reportActionID : ''; } /** @@ -3204,7 +3122,7 @@ function getMoneyRequestOptions(report, reportParticipants, betas) { return []; } - const participants = _.filter(reportParticipants, (accountID) => currentUserPersonalDetails.accountID !== accountID); + const participants = _.filter(reportParticipants, (accountID) => currentUserPersonalDetails?.accountID !== accountID); // Verify if there is any of the expensify accounts amongst the participants in which case user cannot take IOU actions on such report const hasExcludedIOUAccountIDs = lodashIntersection(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS).length > 0; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index df2043ce44d5..5e8c663b7a11 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -1,4 +1,4 @@ -import Onyx, {OnyxCollection} from 'react-native-onyx'; +import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {format, parseISO, isValid} from 'date-fns'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; @@ -294,7 +294,7 @@ function hasRoute(transaction: Transaction): boolean { * * @deprecated Use Onyx.connect() or withOnyx() instead */ -function getLinkedTransaction(reportAction: ReportAction): Transaction | Record { +function getLinkedTransaction(reportAction?: OnyxEntry): Transaction | Record { let transactionID = ''; if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 243990a72874..04113032ff43 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -78,6 +78,7 @@ type Report = { isWaitingOnBankAccount?: boolean; visibility?: ValueOf; preexistingReportID?: string; + iouReportID?: number; }; export default Report; From e9ddcbec5392f67b0ed45964deb3914e17d0d1fe Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 6 Oct 2023 17:36:25 +0200 Subject: [PATCH 04/63] ref: keep migrating ReportUtils --- src/libs/ReportUtils.ts | 988 +++++++++++++----------------- src/libs/TransactionUtils.ts | 30 +- src/types/onyx/OriginalMessage.ts | 33 +- src/types/onyx/PersonalDetails.ts | 2 + src/types/onyx/Report.ts | 7 + src/types/onyx/ReportAction.ts | 12 +- 6 files changed, 466 insertions(+), 606 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index cb796be9716d..46bc8d22704c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1,11 +1,12 @@ /* eslint-disable rulesdir/prefer-underscore-method */ -import _ from 'underscore'; import {format, parseISO} from 'date-fns'; import {SvgProps} from 'react-native-svg'; import Str from 'expensify-common/lib/str'; -import lodashGet from 'lodash/get'; +import lodashEscape from 'lodash/escape'; +import lodashIsEqual from 'lodash/isEqual'; +import lodashFindLastIndex from 'lodash/findLastIndex'; import lodashIntersection from 'lodash/intersection'; -import {EmptyObject, ValueOf} from 'type-fest'; +import {ValueOf} from 'type-fest'; import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import ONYXKEYS from '../ONYXKEYS'; @@ -25,9 +26,10 @@ import isReportMessageAttachment from './isReportMessageAttachment'; import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAvatars'; import * as CurrencyUtils from './CurrencyUtils'; import * as UserUtils from './UserUtils'; -import {Login, PersonalDetails, Policy, Report, ReportAction, Transaction} from '../types/onyx'; -import OriginalMessage from '../types/onyx/OriginalMessage'; -import {Comment} from '../types/onyx/Transaction'; +import {Beta, Login, PersonalDetails, Policy, Report, ReportAction, Transaction} from '../types/onyx'; +import {Comment, Receipt} from '../types/onyx/Transaction'; +import DeepValueOf from '../types/utils/DeepValueOf'; +import {IOUMessage} from '../types/onyx/OriginalMessage'; type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string}; type Avatar = { @@ -55,6 +57,20 @@ type ExpanseOriginalMessage = { billable?: string; oldBillable?: string; }; +type Participant = { + accountID: number; + alternateText: string; + firstName: string; + icons: Avatar[]; + keyForList: string; + lastName: string; + login: string; + phoneNumber: string; + searchText: string; + selected: boolean; + text: string; +}; + let currentUserEmail: string | undefined; let currentUserAccountID: number | undefined; let isAnonymousUser = false; @@ -134,7 +150,7 @@ function getPolicyType(report: OnyxEntry, policies: OnyxCollection, returnEmptyIfNotFound = false, policy: OnyxEntry = undefined): string { +function getPolicyName(report?: OnyxEntry, returnEmptyIfNotFound = false, policy: OnyxEntry = undefined): string { const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); if (Object.keys(report ?? {}).length === 0) { return noPolicyFound; @@ -194,7 +210,7 @@ function isTaskReport(report: OnyxEntry): boolean { * There's another situation where you don't have access to the parentReportAction (because it was created in a chat you don't have access to) * In this case, we have added the key to the report itself */ -function isCanceledTaskReport(report: OnyxEntry, parentReportAction: OnyxEntry): boolean { +function isCanceledTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry): boolean { if (Object.keys(parentReportAction ?? {}).length > 0 && (parentReportAction?.message?.[0].isDeletedParentAction ?? false)) { return true; } @@ -212,7 +228,7 @@ function isCanceledTaskReport(report: OnyxEntry, parentReportAction: Ony * @param report * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ -function isOpenTaskReport(report: OnyxEntry, parentReportAction: OnyxEntry): boolean { +function isOpenTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry): boolean { return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; } @@ -240,7 +256,7 @@ function isReportApproved(report: OnyxEntry): boolean { /** * Given a collection of reports returns them sorted by last read */ -function sortReportsByLastRead(reports: OnyxCollection): OnyxEntry[] { +function sortReportsByLastRead(reports: OnyxCollection): Array> { return Object.values(reports ?? {}) .filter((report) => report?.reportID && report?.lastReadTime) .sort((a, b) => { @@ -484,11 +500,11 @@ function findLastAccessedReport( // Check where ReportUtils.findLastAccessedReport is called in MainDrawerNavigator.js for more context. // Domain rooms are now the only type of default room that are on the defaultRooms beta. sortedReports = sortedReports.filter( - (report) => !isDomainRoom(report) || getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE || hasExpensifyGuidesEmails(lodashGet(report, ['participantAccountIDs'], [])), + (report) => !isDomainRoom(report) || getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE || hasExpensifyGuidesEmails(report?.participantAccountIDs ?? []), ); } - return adminReport || sortedReports[sortedReports.length - 1]; + return adminReport ?? sortedReports[sortedReports.length - 1]; } /** @@ -734,17 +750,13 @@ function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAcc /** * Whether the time row should be shown for a report. - * @param {Array} personalDetails - * @param {Object} report - * @param {Number} accountID - * @return {Boolean} */ function canShowReportRecipientLocalTime(personalDetails: OnyxCollection, report: OnyxEntry, accountID: number): boolean { const reportRecipientAccountIDs = getReportRecipientAccountIDs(report, accountID); const hasMultipleParticipants = reportRecipientAccountIDs.length > 1; - const reportRecipient = personalDetails?.[reportRecipientAccountIDs[0]]; + const reportRecipient = personalDetails?.[reportRecipientAccountIDs[0] ?? -1]; const reportRecipientTimezone = reportRecipient?.timezone ?? CONST.DEFAULT_TIME_ZONE; - const isReportParticipantValidated = lodashGet(reportRecipient, 'validated', false); + const isReportParticipantValidated = reportRecipient?.validated ?? false; return Boolean(!hasMultipleParticipants && !isChatRoom(report) && !isPolicyExpenseChat(report) && reportRecipient && reportRecipientTimezone?.selected && isReportParticipantValidated); } @@ -787,13 +799,13 @@ function getWorkspaceAvatar(report: OnyxEntry) { * The Avatar sources can be URLs or Icon components according to the chat type. */ function getIconsForParticipants(participants: number[], personalDetails: OnyxCollection) { - const participantDetails: Array<[number, string, string | React.FC, React.FC]> = []; + const participantDetails: Array<[number, string, string | React.FC, React.FC | string]> = []; const participantsList = participants || []; for (const accountID of participantsList) { - const avatarSource = UserUtils.getAvatar(lodashGet(personalDetails, [accountID, 'avatar'], ''), accountID); + const avatarSource = UserUtils.getAvatar(personalDetails?.[accountID]?.avatar ?? '', accountID); const displayNameLogin = personalDetails?.[accountID]?.displayName ?? personalDetails?.[accountID]?.login ?? ''; - participantDetails.push([accountID, displayNameLogin, avatarSource, personalDetails?.[accountID]?.fallBackIcon]); + participantDetails.push([accountID, displayNameLogin, avatarSource, personalDetails?.[accountID]?.fallBackIcon ?? '']); } const sortedParticipantDetails = participantDetails.sort((first, second) => { @@ -887,7 +899,7 @@ function getIcons( source: UserUtils.getAvatar(personalDetails?.[actorAccountID]?.avatar ?? '', actorAccountID), name: actorDisplayName, type: CONST.ICON_TYPE_AVATAR, - fallbackIcon: personalDetails?.[parentReportAction.actorAccountID]?.fallbackIcon, + fallbackIcon: personalDetails?.[parentReportAction.actorAccountID]?.fallBackIcon, }; if (isWorkspaceThread(report)) { @@ -902,7 +914,7 @@ function getIcons( source: UserUtils.getAvatar(personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? '', report?.ownerAccountID ?? -1), type: CONST.ICON_TYPE_AVATAR, name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '', - fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon, + fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallBackIcon, }; if (isWorkspaceTaskReport(report)) { @@ -1045,7 +1057,7 @@ function getDisplayNamesStringFromTooltips(displayNamesWithTooltips: PersonalDet /** * Get the report given a reportID */ -function getReport(reportID: string): OnyxEntry { +function getReport(reportID: string | undefined): OnyxEntry { // Deleted reports are set to null and lodashGet will still return null in that case, so we need to add an extra check return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? ({} as OnyxEntry); } @@ -1097,7 +1109,7 @@ function isWaitingForIOUActionFromCurrentUser(report: OnyxEntry): boolea * @param report * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ -function isWaitingForTaskCompleteFromAssignee(report: OnyxEntry, parentReportAction: OnyxEntry): boolean { +function isWaitingForTaskCompleteFromAssignee(report: OnyxEntry, parentReportAction?: OnyxEntry): boolean { return isTaskReport(report) && isReportManager(report) && isOpenTaskReport(report, parentReportAction); } @@ -1178,10 +1190,10 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< */ // TODO: Check if this shouldn't be OnyxEntry -function getTransactionDetails(transaction: Transaction) { - const report = getReport(transaction.reportID); +function getTransactionDetails(transaction: OnyxEntry) { + const report = getReport(transaction?.reportID); return { - created: TransactionUtils.getCreated(transaction ?? {}), + created: TransactionUtils.getCreated(transaction), amount: TransactionUtils.getAmount(transaction, isExpenseReport(report)), currency: TransactionUtils.getCurrency(transaction), comment: TransactionUtils.getDescription(transaction), @@ -1228,14 +1240,17 @@ function canEditMoneyRequest(reportAction: OnyxEntry): boolean { */ function canEditReportAction(reportAction: OnyxEntry): boolean { const isCommentOrIOU = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; - return ( + + console.log('canEditReportAction', reportAction?.message); + return Boolean( reportAction?.actorAccountID === currentUserAccountID && - isCommentOrIOU && - canEditMoneyRequest(reportAction) && // Returns true for non-IOU actions - !isReportMessageAttachment(lodashGet(reportAction, ['message', 0], {})) && - !ReportActionsUtils.isDeletedAction(reportAction) && - !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && - reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE + isCommentOrIOU && + canEditMoneyRequest(reportAction) && // Returns true for non-IOU actions + reportAction?.message && + !isReportMessageAttachment({text: reportAction?.message?.[0].text, html: reportAction?.message?.[0].html, translationKey: reportAction?.message?.[0].translationKey}) && + !ReportActionsUtils.isDeletedAction(reportAction) && + !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && + reportAction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); } @@ -1308,7 +1323,7 @@ function getTransactionReportName(reportAction: OnyxEntry): string * @returns {String} */ function getReportPreviewMessage(report: OnyxEntry, reportAction?: OnyxEntry, shouldConsiderReceiptBeingScanned = false) { - const reportActionMessage = lodashGet(reportAction, 'message[0].html', ''); + const reportActionMessage = reportAction?.message?.[0].html ?? ''; if (Object.keys(report ?? {}).length === 0 || !report?.reportID) { // The iouReport is not found locally after SignIn because the OpenApp API won't return iouReports if they're settled @@ -1327,7 +1342,7 @@ function getReportPreviewMessage(report: OnyxEntry, reportAction?: OnyxE if (shouldConsiderReceiptBeingScanned && ReportActionsUtils.isMoneyRequestAction(reportAction)) { const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); - if (Object.keys(linkedTransaction).length !== 0 && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + if (Object.keys(linkedTransaction ?? {}).length !== 0 && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { return Localize.translateLocal('iou.receiptScanning'); } } @@ -1464,47 +1479,47 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin * * At the moment, we only allow changing one transaction field at a time. */ -function getModifiedExpenseOriginalMessage(oldTransaction: Transaction, transactionChanges: Transaction, isFromExpenseReport: boolean) { +function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry, transactionChanges: OnyxEntry, isFromExpenseReport: boolean) { const originalMessage: ExpanseOriginalMessage = {}; - + console.log('getModifiedExpenseOriginalMessage', transactionChanges); // Remark: Comment field is the only one which has new/old prefixes for the keys (newComment/ oldComment), // all others have old/- pattern such as oldCreated/created if (Object.prototype.hasOwnProperty.call(transactionChanges, 'comment')) { originalMessage.oldComment = TransactionUtils.getDescription(oldTransaction); - originalMessage.newComment = transactionChanges.comment; + originalMessage.newComment = transactionChanges?.comment; } if (Object.prototype.hasOwnProperty.call(transactionChanges, 'created')) { originalMessage.oldCreated = TransactionUtils.getCreated(oldTransaction); - originalMessage.created = transactionChanges.created; + originalMessage.created = transactionChanges?.created; } if (Object.prototype.hasOwnProperty.call(transactionChanges, 'merchant')) { originalMessage.oldMerchant = TransactionUtils.getMerchant(oldTransaction); - originalMessage.merchant = transactionChanges.merchant; + originalMessage.merchant = transactionChanges?.merchant; } // The amount is always a combination of the currency and the number value so when one changes we need to store both // to match how we handle the modified expense action in oldDot - if (Object.prototype.hasOwnProperty.call(transactionChanges, 'amount') || _.has(transactionChanges, 'currency')) { + if (Object.prototype.hasOwnProperty.call(transactionChanges, 'amount') || Object.prototype.hasOwnProperty.call(transactionChanges, 'currency')) { originalMessage.oldAmount = TransactionUtils.getAmount(oldTransaction, isFromExpenseReport); - originalMessage.amount = lodashGet(transactionChanges, 'amount', originalMessage.oldAmount); + originalMessage.amount = transactionChanges?.amount.originalMessage.oldAmount; originalMessage.oldCurrency = TransactionUtils.getCurrency(oldTransaction); - originalMessage.currency = lodashGet(transactionChanges, 'currency', originalMessage.oldCurrency); + originalMessage.currency = transactionChanges?.currency.originalMessage.oldCurrency; } if (Object.prototype.hasOwnProperty.call(transactionChanges, 'category')) { originalMessage.oldCategory = TransactionUtils.getCategory(oldTransaction); - originalMessage.category = transactionChanges.category; + originalMessage.category = transactionChanges?.category; } if (Object.prototype.hasOwnProperty.call(transactionChanges, 'tag')) { originalMessage.oldTag = TransactionUtils.getTag(oldTransaction); - originalMessage.tag = transactionChanges.tag; + originalMessage.tag = transactionChanges?.tag; } if (Object.prototype.hasOwnProperty.call(transactionChanges, 'billable')) { const oldBillable = TransactionUtils.getBillable(oldTransaction); originalMessage.oldBillable = oldBillable ? Localize.translateLocal('common.billable').toLowerCase() : Localize.translateLocal('common.nonBillable').toLowerCase(); - originalMessage.billable = transactionChanges.billable ? Localize.translateLocal('common.billable').toLowerCase() : Localize.translateLocal('common.nonBillable').toLowerCase(); + originalMessage.billable = transactionChanges?.billable ? Localize.translateLocal('common.billable').toLowerCase() : Localize.translateLocal('common.nonBillable').toLowerCase(); } return originalMessage; @@ -1513,7 +1528,7 @@ function getModifiedExpenseOriginalMessage(oldTransaction: Transaction, transact /** * Returns the parentReport if the given report is a thread. */ -function getParentReport(report: OnyxEntry): OnyxEntry | undefined { +function getParentReport(report: OnyxEntry): OnyxEntry | undefined | Record { if (!report?.parentReportID) { return {}; } @@ -1523,21 +1538,18 @@ function getParentReport(report: OnyxEntry): OnyxEntry | undefin /** * Returns the root parentReport if the given report is nested. * Uses recursion to iterate any depth of nested reports. - * - * @param {Object} report - * @returns {Object} */ -function getRootParentReport(report) { +function getRootParentReport(report: OnyxEntry): OnyxEntry | Record { if (!report) { return {}; } // Returns the current report as the root report, because it does not have a parentReportID - if (!report.parentReportID) { + if (!report?.parentReportID) { return report; } - const parentReport = getReport(report.parentReportID); + const parentReport = getReport(report?.parentReportID); // Runs recursion to iterate a parent report return getRootParentReport(parentReport); @@ -1545,12 +1557,8 @@ function getRootParentReport(report) { /** * Get the title for a report. - * - * @param {Object} report - * @param {Object} [policy] - * @returns {String} */ -function getReportName(report, policy = undefined) { +function getReportName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string { let formattedName; const parentReportAction = ReportActionsUtils.getParentReportAction(report); if (isChatThread(report)) { @@ -1559,13 +1567,13 @@ function getReportName(report, policy = undefined) { } const isAttachment = ReportActionsUtils.isReportActionAttachment(parentReportAction); - const parentReportActionMessage = lodashGet(parentReportAction, ['message', 0, 'text'], '').replace(/(\r\n|\n|\r)/gm, ' '); + const parentReportActionMessage = (parentReportAction?.message?.[0].text ?? '').replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { return `[${Localize.translateLocal('common.attachment')}]`; } if ( - lodashGet(parentReportAction, 'message[0].moderationDecision.decision') === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || - lodashGet(parentReportAction, 'message[0].moderationDecision.decision') === CONST.MODERATION.MODERATOR_DECISION_HIDDEN + parentReportAction?.message?.[0]?.moderationDecision.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || + parentReportAction?.message?.[0]?.moderationDecision.decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN ) { return Localize.translateLocal('parentReportAction.hiddenMessage'); } @@ -1577,7 +1585,7 @@ function getReportName(report, policy = undefined) { } if (isChatRoom(report) || isTaskReport(report)) { - formattedName = report.reportName; + formattedName = report?.reportName; } if (isPolicyExpenseChat(report)) { @@ -1597,22 +1605,26 @@ function getReportName(report, policy = undefined) { } // Not a room or PolicyExpenseChat, generate title from participants - const participantAccountIDs = (report && report.participantAccountIDs) || []; - const participantsWithoutCurrentUser = _.without(participantAccountIDs, currentUserAccountID); + const participantAccountIDs = report?.participantAccountIDs ?? []; + const participantsWithoutCurrentUser = participantAccountIDs.filter((accountID) => accountID !== currentUserAccountID); const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1; - return _.map(participantsWithoutCurrentUser, (accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport)).join(', '); + return participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport)).join(', '); } /** * Recursively navigates through thread parents to get the root report and workspace name. * The recursion stops when we find a non thread or money request report, whichever comes first. - * @param {Object} report - * @returns {Object} */ -function getRootReportAndWorkspaceName(report) { +function getRootReportAndWorkspaceName(report?: OnyxEntry) { + if (!report) { + return { + rootReportName: '', + workspaceName: '', + }; + } if (isChildReport(report) && !isMoneyRequestReport(report) && !isTaskReport(report)) { - const parentReport = lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]); + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; return getRootReportAndWorkspaceName(parentReport); } @@ -1636,10 +1648,8 @@ function getRootReportAndWorkspaceName(report) { /** * Get either the policyName or domainName the chat is tied to - * @param {Object} report - * @returns {String} */ -function getChatRoomSubtitle(report) { +function getChatRoomSubtitle(report: OnyxEntry): string { if (isChatThread(report)) { return ''; } @@ -1648,27 +1658,25 @@ function getChatRoomSubtitle(report) { } if (getChatType(report) === CONST.REPORT.CHAT_TYPE.DOMAIN_ALL) { // The domainAll rooms are just #domainName, so we ignore the prefix '#' to get the domainName - return report.reportName.substring(1); + return report?.reportName?.substring(1) ?? ''; } - if ((isPolicyExpenseChat(report) && report.isOwnPolicyExpenseChat) || isExpenseReport(report)) { + if ((isPolicyExpenseChat(report) && report?.isOwnPolicyExpenseChat) || isExpenseReport(report)) { return Localize.translateLocal('workspace.common.workspace'); } if (isArchivedRoom(report)) { - return report.oldPolicyName || ''; + return report?.oldPolicyName ?? ''; } return getPolicyName(report); } /** * Gets the parent navigation subtitle for the report - * @param {Object} report - * @returns {Object} */ -function getParentNavigationSubtitle(report) { +function getParentNavigationSubtitle(report: OnyxEntry) { if (isThread(report)) { - const parentReport = lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]); + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; const {rootReportName, workspaceName} = getRootReportAndWorkspaceName(parentReport); - if (_.isEmpty(rootReportName)) { + if (!rootReportName) { return {}; } @@ -1682,9 +1690,9 @@ function getParentNavigationSubtitle(report) { * * @param {Object} report */ -function navigateToDetailsPage(report) { - const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []); - +function navigateToDetailsPage(report: OnyxEntry) { + const participantAccountIDs = report?.participantAccountIDs ?? []; + // TODO: should we add some default navigation type ? if (isChatRoom(report) || isPolicyExpenseChat(report) || isChatThread(report) || isTaskReport(report) || isMoneyRequestReport(report)) { Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID)); return; @@ -1703,29 +1711,20 @@ function navigateToDetailsPage(report) { * * In a test of 500M reports (28 years of reports at our current max rate) we got 20-40 collisions meaning that * this is more than random enough for our needs. - * - * @returns {String} */ function generateReportID() { return (Math.floor(Math.random() * 2 ** 21) * 2 ** 32 + Math.floor(Math.random() * 2 ** 32)).toString(); } -/** - * @param {Object} report - * @returns {Boolean} - */ -function hasReportNameError(report) { - return !_.isEmpty(lodashGet(report, 'errorFields.reportName', {})); +function hasReportNameError(report: OnyxEntry): boolean { + return Object.keys(report?.errorFields?.reportName ?? {}).length !== 0; } /** * For comments shorter than or equal to 10k chars, convert the comment from MD into HTML because that's how it is stored in the database * For longer comments, skip parsing, but still escape the text, and display plaintext for performance reasons. It takes over 40s to parse a 100k long string!! - * - * @param {String} text - * @returns {String} */ -function getParsedComment(text) { +function getParsedComment(text: string): string { const parser = new ExpensiMark(); return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : _.escape(text); } @@ -1735,16 +1734,15 @@ function getParsedComment(text) { * @param {File} [file] * @returns {Object} */ -function buildOptimisticAddCommentReportAction(text, file) { +function buildOptimisticAddCommentReportAction(text?: string, file?: File & {source: string; uri: string}): {commentText: string; reportAction: ReportAction} { const parser = new ExpensiMark(); - const commentText = getParsedComment(text); - const isAttachment = _.isEmpty(text) && file !== undefined; + const commentText = getParsedComment(text ?? ''); + const isAttachment = !text && file !== undefined; const attachmentInfo = isAttachment ? file : {}; const htmlForNewComment = isAttachment ? CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML : commentText; // Remove HTML from text when applying optimistic offline comment const textForNewComment = isAttachment ? CONST.ATTACHMENT_MESSAGE_TEXT : parser.htmlToText(htmlForNewComment); - return { commentText, reportAction: { @@ -1754,12 +1752,12 @@ function buildOptimisticAddCommentReportAction(text, file) { person: [ { style: 'strong', - text: lodashGet(allPersonalDetails, [currentUserAccountID, 'displayName'], currentUserEmail), + text: allPersonalDetails?.[currentUserAccountID ?? -1]?.displayName ?? currentUserEmail, type: 'TEXT', }, ], automatic: false, - avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatarURL(currentUserAccountID)), + avatar: allPersonalDetails?.[currentUserAccountID ?? -1]?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), created: DateUtils.getDBTime(), message: [ { @@ -1780,25 +1778,24 @@ function buildOptimisticAddCommentReportAction(text, file) { /** * update optimistic parent reportAction when a comment is added or remove in the child report - * @param {String} parentReportAction - Parent report action of the child report - * @param {String} lastVisibleActionCreated - Last visible action created of the child report - * @param {String} type - The type of action in the child report - * @returns {Object} + * @param parentReportAction - Parent report action of the child report + * @param lastVisibleActionCreated - Last visible action created of the child report + * @param type - The type of action in the child report */ -function updateOptimisticParentReportAction(parentReportAction, lastVisibleActionCreated, type) { - let childVisibleActionCount = parentReportAction.childVisibleActionCount || 0; - let childCommenterCount = parentReportAction.childCommenterCount || 0; - let childOldestFourAccountIDs = parentReportAction.childOldestFourAccountIDs; +function updateOptimisticParentReportAction(parentReportAction: OnyxEntry, lastVisibleActionCreated: string, type: string) { + let childVisibleActionCount = parentReportAction?.childVisibleActionCount ?? 0; + let childCommenterCount = parentReportAction?.childCommenterCount ?? 0; + let childOldestFourAccountIDs = parentReportAction?.childOldestFourAccountIDs; if (type === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { childVisibleActionCount += 1; const oldestFourAccountIDs = childOldestFourAccountIDs ? childOldestFourAccountIDs.split(',') : []; if (oldestFourAccountIDs.length < 4) { - const index = _.findIndex(oldestFourAccountIDs, (accountID) => accountID === currentUserAccountID.toString()); + const index = oldestFourAccountIDs.findIndex((accountID) => accountID === currentUserAccountID?.toString()); if (index === -1) { childCommenterCount += 1; - oldestFourAccountIDs.push(currentUserAccountID); + oldestFourAccountIDs.push(currentUserAccountID?.toString() ?? ''); } } childOldestFourAccountIDs = oldestFourAccountIDs.join(','); @@ -1823,49 +1820,49 @@ function updateOptimisticParentReportAction(parentReportAction, lastVisibleActio /** * Get optimistic data of parent report action - * @param {String} reportID The reportID of the report that is updated - * @param {String} lastVisibleActionCreated Last visible action created of the child report - * @param {String} type The type of action in the child report - * @param {String} parentReportID Custom reportID to be updated - * @param {String} parentReportActionID Custom reportActionID to be updated - * @returns {Object} + * @param reportID The reportID of the report that is updated + * @param lastVisibleActionCreated Last visible action created of the child report + * @param type The type of action in the child report + * @param parentReportID Custom reportID to be updated + * @param parentReportActionID Custom reportActionID to be updated */ -function getOptimisticDataForParentReportAction(reportID, lastVisibleActionCreated, type, parentReportID = '', parentReportActionID = '') { +function getOptimisticDataForParentReportAction(reportID: string, lastVisibleActionCreated: string, type: string, parentReportID = '', parentReportActionID = '') { const report = getReport(reportID); const parentReportAction = ReportActionsUtils.getParentReportAction(report); - if (_.isEmpty(parentReportAction)) { + if (!parentReportAction) { return {}; } const optimisticParentReportAction = updateOptimisticParentReportAction(parentReportAction, lastVisibleActionCreated, type); return { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID || report.parentReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID || report?.parentReportID}`, value: { - [parentReportActionID || report.parentReportActionID]: optimisticParentReportAction, + [parentReportActionID || (report?.parentReportActionID ?? '')]: optimisticParentReportAction, }, }; } /** * Builds an optimistic reportAction for the parent report when a task is created - * @param {String} taskReportID - Report ID of the task - * @param {String} taskTitle - Title of the task - * @param {String} taskAssignee - Email of the person assigned to the task - * @param {Number} taskAssigneeAccountID - AccountID of the person assigned to the task - * @param {String} text - Text of the comment - * @param {String} parentReportID - Report ID of the parent report - * @returns {Object} - */ -function buildOptimisticTaskCommentReportAction(taskReportID, taskTitle, taskAssignee, taskAssigneeAccountID, text, parentReportID) { + * @param taskReportID - Report ID of the task + * @param taskTitle - Title of the task + * @param taskAssignee - Email of the person assigned to the task + * @param taskAssigneeAccountID - AccountID of the person assigned to the task + * @param text - Text of the comment + * @param parentReportID - Report ID of the parent report + */ +function buildOptimisticTaskCommentReportAction(taskReportID: string, taskTitle: string, taskAssignee: string, taskAssigneeAccountID: number, text: string, parentReportID: string) { const reportAction = buildOptimisticAddCommentReportAction(text); - reportAction.reportAction.message[0].taskReportID = taskReportID; + if (reportAction.reportAction.message) { + reportAction.reportAction.message[0].taskReportID = taskReportID; + } // These parameters are not saved on the reportAction, but are used to display the task in the UI // Added when we fetch the reportActions on a report reportAction.reportAction.originalMessage = { - html: reportAction.reportAction.message[0].html, - taskReportID: reportAction.reportAction.message[0].taskReportID, + html: reportAction.reportAction.message?.[0].html, + taskReportID: reportAction.reportAction.message?.[0].taskReportID, }; reportAction.reportAction.childReportID = taskReportID; reportAction.reportAction.parentReportID = parentReportID; @@ -1881,16 +1878,14 @@ function buildOptimisticTaskCommentReportAction(taskReportID, taskTitle, taskAss /** * Builds an optimistic IOU report with a randomly generated reportID * - * @param {Number} payeeAccountID - AccountID of the person generating the IOU. - * @param {Number} payerAccountID - AccountID of the other person participating in the IOU. - * @param {Number} total - IOU amount in the smallest unit of the currency. - * @param {String} chatReportID - Report ID of the chat where the IOU is. - * @param {String} currency - IOU currency. - * @param {Boolean} isSendingMoney - If we send money the IOU should be created as settled - * - * @returns {Object} - */ -function buildOptimisticIOUReport(payeeAccountID, payerAccountID, total, chatReportID, currency, isSendingMoney = false) { + * @param payeeAccountID - AccountID of the person generating the IOU. + * @param payerAccountID - AccountID of the other person participating in the IOU. + * @param total - IOU amount in the smallest unit of the currency. + * @param chatReportID - Report ID of the chat where the IOU is. + * @param currency - IOU currency. + * @param isSendingMoney - If we send money the IOU should be created as settled + */ +function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number, total: number, chatReportID: string, currency: string, isSendingMoney = false) { const formattedTotal = CurrencyUtils.convertToDisplayString(total, currency); const personalDetails = getPersonalDetailsForAccountID(payerAccountID); const payerEmail = personalDetails.login; @@ -1919,22 +1914,20 @@ function buildOptimisticIOUReport(payeeAccountID, payerAccountID, total, chatRep /** * Builds an optimistic Expense report with a randomly generated reportID * - * @param {String} chatReportID - Report ID of the PolicyExpenseChat where the Expense Report is - * @param {String} policyID - The policy ID of the PolicyExpenseChat - * @param {Number} payeeAccountID - AccountID of the employee (payee) - * @param {Number} total - Amount in cents - * @param {String} currency - * - * @returns {Object} + * @param chatReportID - Report ID of the PolicyExpenseChat where the Expense Report is + * @param policyID - The policy ID of the PolicyExpenseChat + * @param payeeAccountID - AccountID of the employee (payee) + * @param total - Amount in cents + * @param currency */ -function buildOptimisticExpenseReport(chatReportID, policyID, payeeAccountID, total, currency) { +function buildOptimisticExpenseReport(chatReportID: string, policyID: string, payeeAccountID: number, total: number, currency: string) { // The amount for Expense reports are stored as negative value in the database const storedTotal = total * -1; - const policyName = getPolicyName(allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`]); + const policyName = getPolicyName(allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`]); const formattedTotal = CurrencyUtils.convertToDisplayString(storedTotal, currency); // The expense report is always created with the policy's output currency - const outputCurrency = lodashGet(allPolicies, [`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, 'outputCurrency'], CONST.CURRENCY.USD); + const outputCurrency = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.outputCurrency ?? CONST.CURRENCY.USD; return { reportID: generateReportID(), @@ -1956,16 +1949,15 @@ function buildOptimisticExpenseReport(chatReportID, policyID, payeeAccountID, to } /** - * @param {String} iouReportID - the report ID of the IOU report the action belongs to - * @param {String} type - IOUReportAction type. Can be oneOf(create, decline, cancel, pay, split) - * @param {Number} total - IOU total in cents - * @param {String} comment - IOU comment - * @param {String} currency - IOU currency - * @param {String} paymentType - IOU paymentMethodType. Can be oneOf(Elsewhere, Expensify) - * @param {Boolean} isSettlingUp - Whether we are settling up an IOU - * @returns {Array} + * @param iouReportID - the report ID of the IOU report the action belongs to + * @param type - IOUReportAction type. Can be oneOf(create, decline, cancel, pay, split) + * @param total - IOU total in cents + * @param comment - IOU comment + * @param currency - IOU currency + * @param paymentType - IOU paymentMethodType. Can be oneOf(Elsewhere, Expensify) + * @param isSettlingUp - Whether we are settling up an IOU */ -function getIOUReportActionMessage(iouReportID, type, total, comment, currency, paymentType = '', isSettlingUp = false) { +function getIOUReportActionMessage(iouReportID: string, type: string, total: number, comment: string, currency: string, paymentType = '', isSettlingUp = false) { const amount = type === CONST.IOU.REPORT_ACTION_TYPE.PAY ? CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(getReport(iouReportID)), currency) @@ -2005,7 +1997,7 @@ function getIOUReportActionMessage(iouReportID, type, total, comment, currency, return [ { - html: _.escape(iouMessage), + html: lodashEscape(iouMessage), text: iouMessage, isEdited: false, type: CONST.REPORT.MESSAGE.TYPE.COMMENT, @@ -2016,49 +2008,48 @@ function getIOUReportActionMessage(iouReportID, type, total, comment, currency, /** * Builds an optimistic IOU reportAction object * - * @param {String} type - IOUReportAction type. Can be oneOf(create, delete, pay, split). - * @param {Number} amount - IOU amount in cents. - * @param {String} currency - * @param {String} comment - User comment for the IOU. - * @param {Array} participants - An array with participants details. - * @param {String} [transactionID] - Not required if the IOUReportAction type is 'pay' - * @param {String} [paymentType] - Only required if the IOUReportAction type is 'pay'. Can be oneOf(elsewhere, Expensify). - * @param {String} [iouReportID] - Only required if the IOUReportActions type is oneOf(decline, cancel, pay). Generates a randomID as default. - * @param {Boolean} [isSettlingUp] - Whether we are settling up an IOU. - * @param {Boolean} [isSendMoneyFlow] - Whether this is send money flow - * @param {Object} [receipt] - * @param {Boolean} [isOwnPolicyExpenseChat] - Whether this is an expense report create from the current user's policy expense chat - * @returns {Object} + * @param type - IOUReportAction type. Can be oneOf(create, delete, pay, split). + * @param amount - IOU amount in cents. + * @param currency + * @param comment - User comment for the IOU. + * @param participants - An array with participants details. + * @param [transactionID] - Not required if the IOUReportAction type is 'pay' + * @param [paymentType] - Only required if the IOUReportAction type is 'pay'. Can be oneOf(elsewhere, Expensify). + * @param [iouReportID] - Only required if the IOUReportActions type is oneOf(decline, cancel, pay). Generates a randomID as default. + * @param [isSettlingUp] - Whether we are settling up an IOU. + * @param [isSendMoneyFlow] - Whether this is send money flow + * @param [receipt] + * @param [isOwnPolicyExpenseChat] - Whether this is an expense report create from the current user's policy expense chat */ function buildOptimisticIOUReportAction( - type, - amount, - currency, - comment, - participants, + type: string, + amount: number, + currency: string, + comment: string, + participants: Participant[], transactionID = '', paymentType = '', iouReportID = '', isSettlingUp = false, isSendMoneyFlow = false, - receipt = {}, + receipt: Receipt = {}, isOwnPolicyExpenseChat = false, ) { const IOUReportID = iouReportID || generateReportID(); - const originalMessage = { + const originalMessage: IOUMessage = { amount, comment, currency, IOUTransactionID: transactionID, - IOUReportID, + IOUReportID: Number(IOUReportID), type, }; if (type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { // In send money flow, we store amount, comment, currency in IOUDetails when type = pay if (isSendMoneyFlow) { - _.each(['amount', 'comment', 'currency'], (key) => { + ['amount', 'comment', 'currency'].forEach((key) => { delete originalMessage[key]; }); originalMessage.IOUDetails = {amount, comment, currency}; @@ -2077,9 +2068,9 @@ function buildOptimisticIOUReportAction( delete originalMessage.IOUReportID; // Split bill made from a policy expense chat only have the payee's accountID as the participant because the payer could be any policy admin if (isOwnPolicyExpenseChat) { - originalMessage.participantAccountIDs = [currentUserAccountID]; + originalMessage.participantAccountIDs = [currentUserAccountID ?? -1]; } else { - originalMessage.participantAccountIDs = [currentUserAccountID, ..._.pluck(participants, 'accountID')]; + originalMessage.participantAccountIDs = [currentUserAccountID ?? -1, ..._.pluck(participants, 'accountID')]; } } @@ -2087,14 +2078,14 @@ function buildOptimisticIOUReportAction( actionName: CONST.REPORT.ACTIONS.TYPE.IOU, actorAccountID: currentUserAccountID, automatic: false, - avatar: lodashGet(currentUserPersonalDetails, 'avatar', UserUtils.getDefaultAvatar(currentUserAccountID)), + avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatar(currentUserAccountID), isAttachment: false, originalMessage, message: getIOUReportActionMessage(iouReportID, type, amount, comment, currency, paymentType, isSettlingUp), person: [ { style: 'strong', - text: lodashGet(currentUserPersonalDetails, 'displayName', currentUserEmail), + text: currentUserPersonalDetails?.displayName ?? currentUserEmail, type: 'TEXT', }, ], @@ -2103,19 +2094,13 @@ function buildOptimisticIOUReportAction( created: DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, receipt, - whisperedToAccountIDs: _.contains([CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING], receipt.state) ? [currentUserAccountID] : [], + whisperedToAccountIDs: [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].includes(receipt?.state) ? [currentUserAccountID] : [], }; } /** * Builds an optimistic APPROVED report action with a randomly generated reportActionID. - * - * @param {Number} amount - * @param {String} currency - * @param {Number} expenseReportID - * - * @returns {Object} */ -function buildOptimisticApprovedReportAction(amount, currency, expenseReportID) { +function buildOptimisticApprovedReportAction(amount: number, currency: string, expenseReportID: string) { const originalMessage = { amount, currency, @@ -2126,14 +2111,14 @@ function buildOptimisticApprovedReportAction(amount, currency, expenseReportID) actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED, actorAccountID: currentUserAccountID, automatic: false, - avatar: lodashGet(currentUserPersonalDetails, 'avatar', UserUtils.getDefaultAvatar(currentUserAccountID)), + avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatar(currentUserAccountID), isAttachment: false, originalMessage, message: getIOUReportActionMessage(expenseReportID, CONST.REPORT.ACTIONS.TYPE.APPROVED, Math.abs(amount), '', currency), person: [ { style: 'strong', - text: lodashGet(currentUserPersonalDetails, 'displayName', currentUserEmail), + text: currentUserPersonalDetails?.displayName ?? currentUserEmail, type: 'TEXT', }, ], @@ -2147,24 +2132,22 @@ function buildOptimisticApprovedReportAction(amount, currency, expenseReportID) /** * Builds an optimistic report preview action with a randomly generated reportActionID. * - * @param {Object} chatReport - * @param {Object} iouReport - * @param {String} [comment] - User comment for the IOU. - * @param {Object} [transaction] - optimistic first transaction of preview - * - * @returns {Object} + * @param chatReport + * @param iouReport + * @param [comment] - User comment for the IOU. + * @param [transaction] - optimistic first transaction of preview */ -function buildOptimisticReportPreview(chatReport, iouReport, comment = '', transaction = undefined) { +function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: OnyxEntry, comment = '', transaction: OnyxEntry | undefined = undefined) { const hasReceipt = TransactionUtils.hasReceipt(transaction); const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); const message = getReportPreviewMessage(iouReport); return { reportActionID: NumberUtils.rand64(), - reportID: chatReport.reportID, + reportID: chatReport?.reportID, actionName: CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, originalMessage: { - linkedReportID: iouReport.reportID, + linkedReportID: iouReport?.reportID, }, message: [ { @@ -2175,32 +2158,31 @@ function buildOptimisticReportPreview(chatReport, iouReport, comment = '', trans }, ], created: DateUtils.getDBTime(), - accountID: iouReport.managerID || 0, + accountID: iouReport?.managerID ?? 0, // The preview is initially whispered if created with a receipt, so the actor is the current user as well - actorAccountID: hasReceipt ? currentUserAccountID : iouReport.managerID || 0, + actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.managerID ?? 0, childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, - childLastReceiptTransactionIDs: hasReceipt ? transaction.transactionID : '', + childLastReceiptTransactionIDs: hasReceipt ? transaction?.transactionID : '', whisperedToAccountIDs: isReceiptBeingScanned ? [currentUserAccountID] : [], }; } /** * Builds an optimistic modified expense action with a randomly generated reportActionID. - * - * @param {Object} transactionThread - * @param {Object} oldTransaction - * @param {Object} transactionChanges - * @param {Object} isFromExpenseReport - * @returns {Object} */ -function buildOptimisticModifiedExpenseReportAction(transactionThread, oldTransaction, transactionChanges, isFromExpenseReport) { +function buildOptimisticModifiedExpenseReportAction( + transactionThread: OnyxEntry, + oldTransaction: OnyxEntry, + transactionChanges: OnyxEntry, + isFromExpenseReport: boolean, +) { const originalMessage = getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport); return { actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, actorAccountID: currentUserAccountID, automatic: false, - avatar: lodashGet(currentUserPersonalDetails, 'avatar', UserUtils.getDefaultAvatar(currentUserAccountID)), + avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatar(currentUserAccountID), created: DateUtils.getDBTime(), isAttachment: false, message: [ @@ -2215,13 +2197,13 @@ function buildOptimisticModifiedExpenseReportAction(transactionThread, oldTransa person: [ { style: 'strong', - text: lodashGet(currentUserPersonalDetails, 'displayName', currentUserAccountID), + text: currentUserPersonalDetails?.displayName ?? currentUserAccountID, type: 'TEXT', }, ], pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, reportActionID: NumberUtils.rand64(), - reportID: transactionThread.reportID, + reportID: transactionThread?.reportID, shouldShow: true, }; } @@ -2229,17 +2211,22 @@ function buildOptimisticModifiedExpenseReportAction(transactionThread, oldTransa /** * Updates a report preview action that exists for an IOU report. * - * @param {Object} iouReport - * @param {Object} reportPreviewAction - * @param {Boolean} isPayRequest - * @param {String} [comment] - User comment for the IOU. - * @param {Object} [transaction] - optimistic newest transaction of a report preview + * @param iouReport + * @param reportPreviewAction + * @param [isPayRequest] + * @param [comment] - User comment for the IOU. + * @param [transaction] - optimistic newest transaction of a report preview * - * @returns {Object} */ -function updateReportPreview(iouReport, reportPreviewAction, isPayRequest = false, comment = '', transaction = undefined) { +function updateReportPreview( + iouReport: OnyxEntry, + reportPreviewAction: OnyxEntry, + isPayRequest = false, + comment = '', + transaction: OnyxEntry | undefined = undefined, +) { const hasReceipt = TransactionUtils.hasReceipt(transaction); - const lastReceiptTransactionIDs = lodashGet(reportPreviewAction, 'childLastReceiptTransactionIDs', ''); + const lastReceiptTransactionIDs = reportPreviewAction?.childLastReceiptTransactionIDs ?? ''; const previousTransactionIDs = lastReceiptTransactionIDs.split(',').slice(0, 2); const message = getReportPreviewMessage(iouReport, reportPreviewAction); @@ -2254,16 +2241,16 @@ function updateReportPreview(iouReport, reportPreviewAction, isPayRequest = fals type: CONST.REPORT.MESSAGE.TYPE.COMMENT, }, ], - childLastMoneyRequestComment: comment || reportPreviewAction.childLastMoneyRequestComment, - childMoneyRequestCount: reportPreviewAction.childMoneyRequestCount + (isPayRequest ? 0 : 1), - childLastReceiptTransactionIDs: hasReceipt ? [transaction.transactionID, ...previousTransactionIDs].join(',') : lastReceiptTransactionIDs, + childLastMoneyRequestComment: comment || reportPreviewAction?.childLastMoneyRequestComment, + childMoneyRequestCount: (reportPreviewAction?.childMoneyRequestCount ?? 0) + (isPayRequest ? 0 : 1), + childLastReceiptTransactionIDs: hasReceipt ? [transaction?.transactionID, ...previousTransactionIDs].join(',') : lastReceiptTransactionIDs, // As soon as we add a transaction without a receipt to the report, it will have ready money requests, // so we remove the whisper - whisperedToAccountIDs: hasReceipt ? reportPreviewAction.whisperedToAccountIDs : [], + whisperedToAccountIDs: hasReceipt ? reportPreviewAction?.whisperedToAccountIDs : [], }; } -function buildOptimisticTaskReportAction(taskReportID, actionName, message = '') { +function buildOptimisticTaskReportAction(taskReportID: string, actionName: DeepValueOf, message = '') { const originalMessage = { taskReportID, type: actionName, @@ -2274,7 +2261,7 @@ function buildOptimisticTaskReportAction(taskReportID, actionName, message = '') actionName, actorAccountID: currentUserAccountID, automatic: false, - avatar: lodashGet(currentUserPersonalDetails, 'avatar', UserUtils.getDefaultAvatar(currentUserAccountID)), + avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatar(currentUserAccountID), isAttachment: false, originalMessage, message: [ @@ -2287,7 +2274,7 @@ function buildOptimisticTaskReportAction(taskReportID, actionName, message = '') person: [ { style: 'strong', - text: lodashGet(currentUserPersonalDetails, 'displayName', currentUserAccountID), + text: currentUserPersonalDetails?.displayName ?? currentUserAccountID, type: 'TEXT', }, ], @@ -2301,33 +2288,18 @@ function buildOptimisticTaskReportAction(taskReportID, actionName, message = '') /** * Builds an optimistic chat report with a randomly generated reportID and as much information as we currently have - * - * @param {Array} participantList Array of participant accountIDs - * @param {String} reportName - * @param {String} chatType - * @param {String} policyID - * @param {Number} ownerAccountID - * @param {Boolean} isOwnPolicyExpenseChat - * @param {String} oldPolicyName - * @param {String} visibility - * @param {String} writeCapability - * @param {String} notificationPreference - * @param {String} parentReportActionID - * @param {String} parentReportID - * @param {String} welcomeMessage - * @returns {Object} */ function buildOptimisticChatReport( - participantList, - reportName = CONST.REPORT.DEFAULT_REPORT_NAME, - chatType = '', - policyID = CONST.POLICY.OWNER_EMAIL_FAKE, - ownerAccountID = CONST.REPORT.OWNER_ACCOUNT_ID_FAKE, + participantList: Array, + reportName: string = CONST.REPORT.DEFAULT_REPORT_NAME, + chatType: ValueOf | '' = '', + policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE, + ownerAccountID: number = CONST.REPORT.OWNER_ACCOUNT_ID_FAKE, isOwnPolicyExpenseChat = false, oldPolicyName = '', - visibility = undefined, - writeCapability = undefined, - notificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + visibility: ValueOf | undefined | null = undefined, + writeCapability: ValueOf | undefined = undefined, + notificationPreference: string | number = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, parentReportActionID = '', parentReportID = '', welcomeMessage = '', @@ -2364,10 +2336,8 @@ function buildOptimisticChatReport( /** * Returns the necessary reportAction onyx data to indicate that the chat has been created optimistically - * @param {String} emailCreatingAction - * @returns {Object} */ -function buildOptimisticCreatedReportAction(emailCreatingAction) { +function buildOptimisticCreatedReportAction(emailCreatingAction: string) { return { reportActionID: NumberUtils.rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, @@ -2389,11 +2359,11 @@ function buildOptimisticCreatedReportAction(emailCreatingAction) { { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: lodashGet(allPersonalDetails, [currentUserAccountID, 'displayName'], currentUserEmail), + text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, }, ], automatic: false, - avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatar(currentUserAccountID)), + avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatar(currentUserAccountID), created: DateUtils.getDBTime(), shouldShow: true, }; @@ -2401,12 +2371,9 @@ function buildOptimisticCreatedReportAction(emailCreatingAction) { /** * Returns the necessary reportAction onyx data to indicate that a task report has been edited - * - * @param {String} emailEditingTask - * @returns {Object} */ - -function buildOptimisticEditedTaskReportAction(emailEditingTask) { +function buildOptimisticEditedTaskReportAction(emailEditingTask: string) { + // TODO: create type for return value return { reportActionID: NumberUtils.rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED, @@ -2428,11 +2395,11 @@ function buildOptimisticEditedTaskReportAction(emailEditingTask) { { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: lodashGet(allPersonalDetails, [currentUserAccountID, 'displayName'], currentUserEmail), + text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, }, ], automatic: false, - avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatar(currentUserAccountID)), + avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatar(currentUserAccountID), created: DateUtils.getDBTime(), shouldShow: false, }; @@ -2441,17 +2408,16 @@ function buildOptimisticEditedTaskReportAction(emailEditingTask) { /** * Returns the necessary reportAction onyx data to indicate that a chat has been archived * - * @param {String} emailClosingReport - * @param {String} policyName - * @param {String} reason - A reason why the chat has been archived - * @returns {Object} + * @param emailClosingReport + * @param policyName + * @param reason - A reason why the chat has been archived */ -function buildOptimisticClosedReportAction(emailClosingReport, policyName, reason = CONST.REPORT.ARCHIVE_REASON.DEFAULT) { +function buildOptimisticClosedReportAction(emailClosingReport: string, policyName: string, reason: string = CONST.REPORT.ARCHIVE_REASON.DEFAULT) { return { actionName: CONST.REPORT.ACTIONS.TYPE.CLOSED, actorAccountID: currentUserAccountID, automatic: false, - avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatar(currentUserAccountID)), + avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatar(currentUserAccountID), created: DateUtils.getDBTime(), message: [ { @@ -2474,7 +2440,7 @@ function buildOptimisticClosedReportAction(emailClosingReport, policyName, reaso { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: lodashGet(allPersonalDetails, [currentUserAccountID, 'displayName'], currentUserEmail), + text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, }, ], reportActionID: NumberUtils.rand64(), @@ -2482,14 +2448,9 @@ function buildOptimisticClosedReportAction(emailClosingReport, policyName, reaso }; } -/** - * @param {String} policyID - * @param {String} policyName - * @returns {Object} - */ -function buildOptimisticWorkspaceChats(policyID, policyName) { +function buildOptimisticWorkspaceChats(policyID: string, policyName: string) { const announceChatData = buildOptimisticChatReport( - [currentUserAccountID], + [currentUserAccountID ?? -1], CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policyID, @@ -2509,7 +2470,7 @@ function buildOptimisticWorkspaceChats(policyID, policyName) { }; const adminsChatData = buildOptimisticChatReport( - [currentUserAccountID], + [currentUserAccountID ?? -1], CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, policyID, @@ -2523,9 +2484,9 @@ function buildOptimisticWorkspaceChats(policyID, policyName) { [adminsCreatedAction.reportActionID]: adminsCreatedAction, }; - const expenseChatData = buildOptimisticChatReport([currentUserAccountID], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, currentUserAccountID, true, policyName); + const expenseChatData = buildOptimisticChatReport([currentUserAccountID ?? -1], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, currentUserAccountID, true, policyName); const expenseChatReportID = expenseChatData.reportID; - const expenseReportCreatedAction = buildOptimisticCreatedReportAction(currentUserEmail); + const expenseReportCreatedAction = buildOptimisticCreatedReportAction(currentUserEmail ?? ''); const expenseReportActionData = { [expenseReportCreatedAction.reportActionID]: expenseReportCreatedAction, }; @@ -2549,17 +2510,22 @@ function buildOptimisticWorkspaceChats(policyID, policyName) { /** * Builds an optimistic Task Report with a randomly generated reportID * - * @param {Number} ownerAccountID - Account ID of the person generating the Task. - * @param {String} assigneeAccountID - AccountID of the other person participating in the Task. - * @param {String} parentReportID - Report ID of the chat where the Task is. - * @param {String} title - Task title. - * @param {String} description - Task description. - * @param {String} policyID - PolicyID of the parent report - * - * @returns {Object} - */ - -function buildOptimisticTaskReport(ownerAccountID, assigneeAccountID = 0, parentReportID, title, description, policyID = CONST.POLICY.OWNER_EMAIL_FAKE) { + * @param ownerAccountID - Account ID of the person generating the Task. + * @param assigneeAccountID - AccountID of the other person participating in the Task. + * @param parentReportID - Report ID of the chat where the Task is. + * @param title - Task title. + * @param description - Task description. + * @param policyID - PolicyID of the parent report + */ + +function buildOptimisticTaskReport( + ownerAccountID: number, + assigneeAccountID?: number, + parentReportID?: string, + title?: string, + description?: string, + policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE, +) { return { reportID: generateReportID(), reportName: title, @@ -2578,67 +2544,51 @@ function buildOptimisticTaskReport(ownerAccountID, assigneeAccountID = 0, parent /** * A helper method to create transaction thread * - * @param {Object} reportAction - the parent IOU report action from which to create the thread + * @param reportAction - the parent IOU report action from which to create the thread * - * @param {String} moneyRequestReportID - the reportID which the report action belong to - * - * @returns {Object} + * @param moneyRequestReportID - the reportID which the report action belong to */ -function buildTransactionThread(reportAction, moneyRequestReportID) { - const participantAccountIDs = _.uniq([currentUserAccountID, Number(reportAction.actorAccountID)]); +function buildTransactionThread(reportAction: OnyxEntry, moneyRequestReportID: string) { + const participantAccountIDs = [...new Set([currentUserAccountID, Number(reportAction?.actorAccountID)])]; return buildOptimisticChatReport( participantAccountIDs, getTransactionReportName(reportAction), '', - lodashGet(getReport(moneyRequestReportID), 'policyID', CONST.POLICY.OWNER_EMAIL_FAKE), + getReport(moneyRequestReportID)?.policyID ?? CONST.POLICY.OWNER_EMAIL_FAKE, CONST.POLICY.OWNER_ACCOUNT_ID_FAKE, false, '', undefined, undefined, CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, - reportAction.reportActionID, + reportAction?.reportActionID, moneyRequestReportID, ); } -/** - * @param {Object} report - * @returns {Boolean} - */ -function isUnread(report) { +function isUnread(report: OnyxEntry): boolean { if (!report) { return false; } // lastVisibleActionCreated and lastReadTime are both datetime strings and can be compared directly - const lastVisibleActionCreated = report.lastVisibleActionCreated || ''; - const lastReadTime = report.lastReadTime || ''; + const lastVisibleActionCreated = report.lastVisibleActionCreated ?? ''; + const lastReadTime = report.lastReadTime ?? ''; return lastReadTime < lastVisibleActionCreated; } -/** - * @param {Object} report - * @returns {Boolean} - */ -function isUnreadWithMention(report) { +function isUnreadWithMention(report: OnyxEntry): boolean { if (!report) { return false; } - // lastMentionedTime and lastReadTime are both datetime strings and can be compared directly - const lastMentionedTime = report.lastMentionedTime || ''; - const lastReadTime = report.lastReadTime || ''; + const lastMentionedTime = report?.lastMentionedTime ?? ''; + const lastReadTime = report?.lastReadTime ?? ''; return lastReadTime < lastMentionedTime; } -/** - * @param {Object} report - * @param {Object} allReportsDict - * @returns {Boolean} - */ -function isIOUOwnedByCurrentUser(report, allReportsDict = null) { - const allAvailableReports = allReportsDict || allReports; +function isIOUOwnedByCurrentUser(report: OnyxEntry, allReportsDict: OnyxCollection = null) { + const allAvailableReports = allReportsDict ?? allReports; if (!report || !allAvailableReports) { return false; } @@ -2657,12 +2607,13 @@ function isIOUOwnedByCurrentUser(report, allReportsDict = null) { /** * Should return true only for personal 1:1 report * - * @param {Object} report (chatReport or iouReport) - * @returns {boolean} + * @param report (chatReport or iouReport) */ -function isOneOnOneChat(report) { - const isChatRoomValue = lodashGet(report, 'isChatRoom', false); - const participantsListValue = lodashGet(report, 'participantsList', []); + +// @ts-expect-error Will be fixed when OptionUtils will be merged +function isOneOnOneChat(report): boolean { + const isChatRoomValue = report?.isChatRoom ?? false; + const participantsListValue = report?.participantsList ?? []; return ( !isThread(report) && !isChatRoom(report) && @@ -2680,13 +2631,8 @@ function isOneOnOneChat(report) { /** * Assuming the passed in report is a default room, lets us know whether we can see it or not, based on permissions and * the various subsets of users we've allowed to use default rooms. - * - * @param {Object} report - * @param {Array} policies - * @param {Array} betas - * @return {Boolean} */ -function canSeeDefaultRoom(report, policies, betas) { +function canSeeDefaultRoom(report: OnyxEntry, policies: OnyxCollection, betas: Beta[]): boolean { // Include archived rooms if (isArchivedRoom(report)) { return true; @@ -2698,12 +2644,12 @@ function canSeeDefaultRoom(report, policies, betas) { } // Include domain rooms with Partner Managers (Expensify accounts) in them for accounts that are on a domain with an Approved Accountant - if (isDomainRoom(report) && doesDomainHaveApprovedAccountant && hasExpensifyEmails(lodashGet(report, ['participantAccountIDs'], []))) { + if (isDomainRoom(report) && doesDomainHaveApprovedAccountant && hasExpensifyEmails(report?.participantAccountIDs ?? [])) { return true; } // If the room has an assigned guide, it can be seen. - if (hasExpensifyGuidesEmails(lodashGet(report, ['participantAccountIDs'], []))) { + if (hasExpensifyGuidesEmails(report?.participantAccountIDs ?? [])) { return true; } @@ -2716,14 +2662,7 @@ function canSeeDefaultRoom(report, policies, betas) { return Permissions.canUseDefaultRooms(betas); } -/** - * @param {Object} report - * @param {Array} policies - * @param {Array} betas - * @param {Object} allReportActions - * @returns {Boolean} - */ -function canAccessReport(report, policies, betas, allReportActions) { +function canAccessReport(report: OnyxEntry, policies: OnyxCollection, betas: Beta[], allReportActions?: OnyxCollection): boolean { if (isThread(report) && ReportActionsUtils.isPendingRemove(ReportActionsUtils.getParentReportAction(report, allReportActions))) { return false; } @@ -2737,15 +2676,12 @@ function canAccessReport(report, policies, betas, allReportActions) { } /** * Check if the report is the parent report of the currently viewed report or at least one child report has report action - * @param {Object} report - * @param {String} currentReportId - * @returns {Boolean} */ -function shouldHideReport(report, currentReportId) { +function shouldHideReport(report: OnyxEntry, currentReportId: string): boolean { const parentReport = getParentReport(getReport(currentReportId)); - const reportActions = ReportActionsUtils.getAllReportActions(report.reportID); - const isChildReportHasComment = _.some(reportActions, (reportAction) => (reportAction.childVisibleActionCount || 0) > 0); - return parentReport.reportID !== report.reportID && !isChildReportHasComment; + const reportActions = ReportActionsUtils.getAllReportActions(report?.reportID); + const isChildReportHasComment = Object.values(reportActions ?? {})?.some((reportAction) => (reportAction?.childVisibleActionCount ?? 0) > 0); + return parentReport?.reportID !== report?.reportID && !isChildReportHasComment; } /** @@ -2754,26 +2690,25 @@ function shouldHideReport(report, currentReportId) { * * This logic is very specific and the order of the logic is very important. It should fail quickly in most cases and also * filter out the majority of reports before filtering out very specific minority of reports. - * - * @param {Object} report - * @param {String} currentReportId - * @param {Boolean} isInGSDMode - * @param {String[]} betas - * @param {Object} policies - * @param {Object} allReportActions - * @param {Boolean} excludeEmptyChats - * @returns {boolean} - */ -function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies, allReportActions, excludeEmptyChats = false) { + */ +function shouldReportBeInOptionList( + // TODO: Change to OptionList type when merged + report: OnyxEntry, + currentReportId: string, + isInGSDMode: boolean, + betas: Beta[], + policies: OnyxCollection, + allReportActions: OnyxCollection, + excludeEmptyChats = false, +) { const isInDefaultMode = !isInGSDMode; // Exclude reports that have no data because there wouldn't be anything to show in the option item. // This can happen if data is currently loading from the server or a report is in various stages of being created. // This can also happen for anyone accessing a public room or archived room for which they don't have access to the underlying policy. if ( - !report || - !report.reportID || - report.isHidden || + !report?.reportID || + report?.isHidden || (report.participantAccountIDs && report.participantAccountIDs.length === 0 && !isChatThread(report) && @@ -2811,7 +2746,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, // Include reports that have errors from trying to add a workspace // If we excluded it, then the red-brock-road pattern wouldn't work for the user to resolve the error - if (report.errorFields && report.errorFields.addWorkspaceRoom) { + if (report.errorFields.addWorkspaceRoom) { return true; } @@ -2835,16 +2770,14 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, /** * Attempts to find a report in onyx with the provided list of participants. Does not include threads, task, money request, room, and policy expense chat. - * @param {Array} newParticipantList - * @returns {Array|undefined} */ -function getChatByParticipants(newParticipantList) { +function getChatByParticipants(newParticipantList: number[]) { newParticipantList.sort(); - return _.find(allReports, (report) => { + return Object.values(allReports ?? {}).find((report) => { // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it if ( !report || - _.isEmpty(report.participantAccountIDs) || + report.participantAccountIDs?.length === 0 || isChatThread(report) || isTaskReport(report) || isMoneyRequestReport(report) || @@ -2854,45 +2787,38 @@ function getChatByParticipants(newParticipantList) { return false; } + const sortedParticipanctsAccountIDs = report.parentReportActionIDs?.sort((a, b) => a - b); + // Only return the chat if it has all the participants - return _.isEqual(newParticipantList, _.sortBy(report.participantAccountIDs)); + return lodashIsEqual(newParticipantList, sortedParticipanctsAccountIDs); }); } /** * Attempts to find a report in onyx with the provided list of participants in given policy - * @param {Array} newParticipantList - * @param {String} policyID - * @returns {object|undefined} */ -function getChatByParticipantsAndPolicy(newParticipantList, policyID) { +function getChatByParticipantsAndPolicy(newParticipantList: number[], policyID: string) { newParticipantList.sort(); - return _.find(allReports, (report) => { + return Object.values(allReports ?? {}).find((report) => { // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it - if (!report || !report.participantAccountIDs) { + if (!report?.participantAccountIDs) { return false; } - + const sortedParticipanctsAccountIDs = report.parentReportActionIDs?.sort((a, b) => a - b); // Only return the room if it has all the participants and is not a policy room - return report.policyID === policyID && _.isEqual(newParticipantList, _.sortBy(report.participantAccountIDs)); + return report.policyID === policyID && lodashIsEqual(newParticipantList, sortedParticipanctsAccountIDs); }); } -/** - * @param {String} policyID - * @returns {Array} - */ -function getAllPolicyReports(policyID) { - return _.filter(allReports, (report) => report && report.policyID === policyID); +function getAllPolicyReports(policyID: string): Array> { + return Object.values(allReports ?? {}).filter((report) => report?.policyID === policyID); } /** * Returns true if Chronos is one of the chat participants (1:1) - * @param {Object} report - * @returns {Boolean} */ -function chatIncludesChronos(report) { - return report.participantAccountIDs && _.contains(report.participantAccountIDs, CONST.ACCOUNT_ID.CHRONOS); +function chatIncludesChronos(report: OnyxEntry): boolean { + return Boolean(report?.participantAccountIDs && report.participantAccountIDs.includes(CONST.ACCOUNT_ID.CHRONOS)); } /** @@ -2900,15 +2826,11 @@ function chatIncludesChronos(report) { * * - It was written by someone else * - It's an ADDCOMMENT that is not an attachment - * - * @param {Object} reportAction - * @param {number} reportID - * @returns {Boolean} */ -function canFlagReportAction(reportAction, reportID) { +function canFlagReportAction(reportAction: OnyxEntry, reportID: string | undefined): boolean { return ( - reportAction.actorAccountID !== currentUserAccountID && - reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && + reportAction?.actorAccountID !== currentUserAccountID && + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportActionsUtils.isDeletedAction(reportAction) && !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && isAllowedToComment(getReport(reportID)) @@ -2917,35 +2839,23 @@ function canFlagReportAction(reportAction, reportID) { /** * Whether flag comment page should show - * - * @param {Object} reportAction - * @param {Object} report - * @returns {Boolean} */ - -function shouldShowFlagComment(reportAction, report) { +function shouldShowFlagComment(reportAction: OnyxEntry, report: OnyxEntry): boolean { return ( - canFlagReportAction(reportAction, report.reportID) && + canFlagReportAction(reportAction, report?.reportID) && !isArchivedRoom(report) && !chatIncludesChronos(report) && - !isConciergeChatReport(report.reportID) && - reportAction.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE + !isConciergeChatReport(report) && + reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE ); } -/** - * @param {Object} report - * @param {String} report.lastReadTime - * @param {Array} sortedAndFilteredReportActions - reportActions for the report, sorted newest to oldest, and filtered for only those that should be visible - * - * @returns {String|null} - */ -function getNewMarkerReportActionID(report, sortedAndFilteredReportActions) { +function getNewMarkerReportActionID(report: OnyxEntry, sortedAndFilteredReportActions: ReportAction[]) { if (!isUnread(report)) { return ''; } - const newMarkerIndex = _.findLastIndex(sortedAndFilteredReportActions, (reportAction) => (reportAction.created || '') > report.lastReadTime); + const newMarkerIndex = lodashFindLastIndex(sortedAndFilteredReportActions, (reportAction) => (reportAction.created ?? '') > (report?.lastReadTime ?? '')); return Object.prototype.hasOwnProperty.call(sortedAndFilteredReportActions[newMarkerIndex], 'reportActionID') ? sortedAndFilteredReportActions[newMarkerIndex].reportActionID : ''; } @@ -2953,27 +2863,22 @@ function getNewMarkerReportActionID(report, sortedAndFilteredReportActions) { /** * Performs the markdown conversion, and replaces code points > 127 with C escape sequences * Used for compatibility with the backend auth validator for AddComment, and to account for MD in comments - * @param {String} textComment - * @returns {Number} The comment's total length as seen from the backend + * @returns The comment's total length as seen from the backend */ -function getCommentLength(textComment) { +function getCommentLength(textComment: string): number { return getParsedComment(textComment) .replace(/[^ -~]/g, '\\u????') .trim().length; } -/** - * @param {String|null} url - * @returns {String} - */ -function getRouteFromLink(url) { +function getRouteFromLink(url: string | null): string { if (!url) { return ''; } // Get the reportID from URL let route = url; - _.each(linkingConfig.prefixes, (prefix) => { + linkingConfig.prefixes.forEach((prefix) => { const localWebAndroidRegEx = /^(http:\/\/([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3}))/; if (route.startsWith(prefix)) { route = route.replace(prefix, ''); @@ -2996,11 +2901,7 @@ function getRouteFromLink(url) { return route; } -/** - * @param {String} route - * @returns {Object} - */ -function parseReportRouteParams(route) { +function parseReportRouteParams(route: string) { let parsingRoute = route; if (parsingRoute.at(0) === '/') { // remove the first slash @@ -3018,11 +2919,7 @@ function parseReportRouteParams(route) { }; } -/** - * @param {String|null} url - * @returns {String} - */ -function getReportIDFromLink(url) { +function getReportIDFromLink(url: string | null): string { const route = getRouteFromLink(url); const {reportID, isSubReportPageRoute} = parseReportRouteParams(route); if (isSubReportPageRoute) { @@ -3034,13 +2931,10 @@ function getReportIDFromLink(url) { /** * Check if the chat report is linked to an iou that is waiting for the current user to add a credit bank account. - * - * @param {Object} chatReport - * @returns {Boolean} */ -function hasIOUWaitingOnCurrentUserBankAccount(chatReport) { - if (chatReport.iouReportID) { - const iouReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`]; +function hasIOUWaitingOnCurrentUserBankAccount(chatReport: OnyxEntry): boolean { + if (chatReport?.iouReportID) { + const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReport?.iouReportID}`]; if (iouReport && iouReport.isWaitingOnBankAccount && iouReport.ownerAccountID === currentUserAccountID) { return true; } @@ -3056,12 +2950,8 @@ function hasIOUWaitingOnCurrentUserBankAccount(chatReport) { * - employee can request money in submitted expense report only if the policy has Instant Submit settings turned on * - in an IOU report, which is not settled yet * - in DM chat - * - * @param {Object} report - * @param {Array} participants - * @returns {Boolean} */ -function canRequestMoney(report, participants) { +function canRequestMoney(report: OnyxEntry, participants: number[]) { // User cannot request money in chat thread or in task report if (isChatThread(report) || isTaskReport(report)) { return false; @@ -3073,9 +2963,9 @@ function canRequestMoney(report, participants) { } // In case of expense reports, we have to look at the parent workspace chat to get the isOwnPolicyExpenseChat property - let isOwnPolicyExpenseChat = report.isOwnPolicyExpenseChat || false; + let isOwnPolicyExpenseChat = report?.isOwnPolicyExpenseChat ?? false; if (isExpenseReport(report) && getParentReport(report)) { - isOwnPolicyExpenseChat = getParentReport(report).isOwnPolicyExpenseChat; + isOwnPolicyExpenseChat = Boolean(getParentReport(report)?.isOwnPolicyExpenseChat); } // In case there are no other participants than the current user and it's not user's own policy expense chat, they can't request money from such report @@ -3110,19 +3000,14 @@ function canRequestMoney(report, participants) { * * None of the options should show in chat threads or if there is some special Expensify account * as a participant of the report. - * - * @param {Object} report - * @param {Array} reportParticipants - * @param {Array} betas - * @returns {Array} */ -function getMoneyRequestOptions(report, reportParticipants, betas) { +function getMoneyRequestOptions(report: OnyxEntry, reportParticipants: number[], betas: Beta[]) { // In any thread or task report, we do not allow any new money requests yet if (isChatThread(report) || isTaskReport(report)) { return []; } - const participants = _.filter(reportParticipants, (accountID) => currentUserPersonalDetails?.accountID !== accountID); + const participants = reportParticipants.filter((accountID) => currentUserPersonalDetails?.accountID !== accountID); // Verify if there is any of the expensify accounts amongst the participants in which case user cannot take IOU actions on such report const hasExcludedIOUAccountIDs = lodashIntersection(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS).length > 0; @@ -3163,21 +3048,15 @@ function getMoneyRequestOptions(report, reportParticipants, betas) { * `domain` - Nobody can leave (it's auto-shared with domain members) * `dm` - Nobody can leave (it's auto-shared with users) * `private` - Anybody can leave (though you can only be invited to join) - * - * @param {Object} report - * @param {String} report.visibility - * @param {String} report.chatType - * @param {Boolean} isPolicyMember - * @returns {Boolean} */ -function canLeaveRoom(report, isPolicyMember) { - if (_.isEmpty(report.visibility)) { +function canLeaveRoom(report: OnyxEntry, isPolicyMember: boolean): boolean { + if (!report?.visibility) { if ( - report.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS || - report.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE || - report.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || - report.chatType === CONST.REPORT.CHAT_TYPE.DOMAIN_ALL || - _.isEmpty(report.chatType) + report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS || + report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE || + report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || + report?.chatType === CONST.REPORT.CHAT_TYPE.DOMAIN_ALL || + !report?.chatType ) { // DM chats don't have a chatType return false; @@ -3188,22 +3067,15 @@ function canLeaveRoom(report, isPolicyMember) { return true; } -/** - * @param {Number[]} participantAccountIDs - * @returns {Boolean} - */ -function isCurrentUserTheOnlyParticipant(participantAccountIDs) { - return participantAccountIDs && participantAccountIDs.length === 1 && participantAccountIDs[0] === currentUserAccountID; +function isCurrentUserTheOnlyParticipant(participantAccountIDs?: number[]): boolean { + return Boolean(participantAccountIDs && participantAccountIDs.length === 1 && participantAccountIDs[0] === currentUserAccountID); } /** * Returns display names for those that can see the whisper. * However, it returns "you" if the current user is the only one who can see it besides the person that sent it. - * - * @param {Number[]} participantAccountIDs - * @returns {string} */ -function getWhisperDisplayNames(participantAccountIDs) { +function getWhisperDisplayNames(participantAccountIDs?: number[]): string | undefined { const isWhisperOnlyVisibleToCurrentUser = isCurrentUserTheOnlyParticipant(participantAccountIDs); // When the current user is the only participant, the display name needs to be "you" because that's the only person reading it @@ -3211,20 +3083,18 @@ function getWhisperDisplayNames(participantAccountIDs) { return Localize.translateLocal('common.youAfterPreposition'); } - return _.map(participantAccountIDs, (accountID) => getDisplayNameForParticipant(accountID, !isWhisperOnlyVisibleToCurrentUser)).join(', '); + return participantAccountIDs?.map((accountID) => getDisplayNameForParticipant(accountID, !isWhisperOnlyVisibleToCurrentUser)).join(', '); } /** * Show subscript on workspace chats / threads and expense requests - * @param {Object} report - * @returns {Boolean} */ -function shouldReportShowSubscript(report) { +function shouldReportShowSubscript(report: OnyxEntry): boolean { if (isArchivedRoom(report)) { return false; } - if (isPolicyExpenseChat(report) && !isChatThread(report) && !isTaskReport(report) && !report.isOwnPolicyExpenseChat) { + if (isPolicyExpenseChat(report) && !isChatThread(report) && !isTaskReport(report) && !report?.isOwnPolicyExpenseChat) { return true; } @@ -3249,97 +3119,75 @@ function shouldReportShowSubscript(report) { /** * Return true if reports data exists - * @returns {Boolean} */ -function isReportDataReady() { - return !_.isEmpty(allReports) && _.some(_.keys(allReports), (key) => allReports[key] && allReports[key].reportID); +function isReportDataReady(): boolean { + return Object.keys(allReports ?? {}).length !== 0 && Object.keys(allReports ?? {}).some((key) => allReports?.[key]?.reportID); } /** * Return true if reportID from path is valid - * @param {String} reportIDFromPath - * @returns {Boolean} */ -function isValidReportIDFromPath(reportIDFromPath) { +function isValidReportIDFromPath(reportIDFromPath: string): boolean { return typeof reportIDFromPath === 'string' && !['', 'null', '0'].includes(reportIDFromPath); } /** * Return the errors we have when creating a chat or a workspace room - * @param {Object} report - * @returns {Object} errors */ -function getAddWorkspaceRoomOrChatReportErrors(report) { +function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry) { // We are either adding a workspace room, or we're creating a chat, it isn't possible for both of these to have errors for the same report at the same time, so // simply looking up the first truthy value will get the relevant property if it's set. - return lodashGet(report, 'errorFields.addWorkspaceRoom') || lodashGet(report, 'errorFields.createChat'); + return report?.errorFields?.addWorkspaceRoom ?? report?.errorFields?.createChat; } /** * Returns true if write actions like assign task, money request, send message should be disabled on a report - * @param {Object} report - * @returns {Boolean} */ -function shouldDisableWriteActions(report) { +function shouldDisableWriteActions(report: OnyxEntry): boolean { const reportErrors = getAddWorkspaceRoomOrChatReportErrors(report); - return isArchivedRoom(report) || !_.isEmpty(reportErrors) || !isAllowedToComment(report) || isAnonymousUser; + return isArchivedRoom(report) || Object.keys(reportErrors ?? {}).length !== 0 || !isAllowedToComment(report) || isAnonymousUser; } /** * Returns ID of the original report from which the given reportAction is first created. - * - * @param {String} reportID - * @param {Object} reportAction - * @returns {String} */ -function getOriginalReportID(reportID, reportAction) { - return isThreadFirstChat(reportAction, reportID) ? lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, 'parentReportID']) : reportID; +function getOriginalReportID(reportID: string, reportAction: OnyxEntry): string | undefined { + return isThreadFirstChat(reportAction, reportID) ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.parentReportID : reportID; } /** * Return the pendingAction and the errors we have when creating a chat or a workspace room offline - * @param {Object} report - * @returns {Object} pending action , errors */ -function getReportOfflinePendingActionAndErrors(report) { +function getReportOfflinePendingActionAndErrors(report: OnyxEntry) { // We are either adding a workspace room, or we're creating a chat, it isn't possible for both of these to be pending, or to have errors for the same report at the same time, so // simply looking up the first truthy value for each case will get the relevant property if it's set. - const addWorkspaceRoomOrChatPendingAction = lodashGet(report, 'pendingFields.addWorkspaceRoom') || lodashGet(report, 'pendingFields.createChat'); + const addWorkspaceRoomOrChatPendingAction = report?.pendingFields?.addWorkspaceRoom ?? report?.pendingFields?.createChat; const addWorkspaceRoomOrChatErrors = getAddWorkspaceRoomOrChatReportErrors(report); return {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors}; } -/** - * @param {String} policyOwner - * @returns {String|null} - */ -function getPolicyExpenseChatReportIDByOwner(policyOwner) { - const policyWithOwner = _.find(allPolicies, (policy) => policy.owner === policyOwner); +function getPolicyExpenseChatReportIDByOwner(policyOwner: string) { + const policyWithOwner = Object.values(allPolicies ?? {}).find((policy) => policy?.owner === policyOwner); if (!policyWithOwner) { return null; } - const expenseChat = _.find(allReports, (report) => isPolicyExpenseChat(report) && report.policyID === policyWithOwner.id); + const expenseChat = Object.values(allReports ?? {}).find((report) => isPolicyExpenseChat(report) && report?.policyID === policyWithOwner.id); if (!expenseChat) { return null; } return expenseChat.reportID; } -/* - * @param {Object|null} report - * @returns {Boolean} - */ -function shouldDisableSettings(report) { +function shouldDisableSettings(report: OnyxEntry): boolean { return !isMoneyRequestReport(report) && !isPolicyExpenseChat(report) && !isChatRoom(report) && !isChatThread(report); } /** - * @param {Object|null} report - * @param {Object|null} policy - the workspace the report is on, null if the user isn't a member of the workspace - * @returns {Boolean} + * @param report + * @param policy - the workspace the report is on, null if the user isn't a member of the workspace */ -function shouldDisableRename(report, policy) { +function shouldDisableRename(report: OnyxEntry, policy: OnyxEntry): boolean { if (isDefaultRoom(report) || isArchivedRoom(report) || isChatThread(report) || isMoneyRequestReport(report) || isPolicyExpenseChat(report)) { return true; } @@ -3352,22 +3200,22 @@ function shouldDisableRename(report, policy) { // If there is a linked workspace, that means the user is a member of the workspace the report is in. // Still, we only want policy owners and admins to be able to modify the name. - return !_.keys(loginList).includes(policy.owner) && policy.role !== CONST.POLICY.ROLE.ADMIN; + return !Object.keys(loginList ?? {}).includes(policy.owner) && policy.role !== CONST.POLICY.ROLE.ADMIN; } /** * Returns the onyx data needed for the task assignee chat - * @param {Number} accountID - * @param {String} assigneeEmail - * @param {Number} assigneeAccountID - * @param {String} taskReportID - * @param {String} assigneeChatReportID - * @param {String} parentReportID - * @param {String} title - * @param {Object} assigneeChatReport - * @returns {Object} */ -function getTaskAssigneeChatOnyxData(accountID, assigneeEmail, assigneeAccountID, taskReportID, assigneeChatReportID, parentReportID, title, assigneeChatReport) { +function getTaskAssigneeChatOnyxData( + accountID: number, + assigneeEmail: string, + assigneeAccountID: number, + taskReportID: string, + assigneeChatReportID: string, + parentReportID: string, + title: string, + assigneeChatReport: OnyxEntry, +) { // Set if we need to add a comment to the assignee chat notifying them that they have been assigned a task let optimisticAssigneeAddComment; // Set if this is a new chat that needs to be created for the assignee @@ -3379,7 +3227,7 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeEmail, assigneeAccountID // You're able to assign a task to someone you haven't chatted with before - so we need to optimistically create the chat and the chat reportActions // Only add the assignee chat report to onyx if we haven't already set it optimistically - if (assigneeChatReport.isOptimisticReport && lodashGet(assigneeChatReport, 'pendingFields.createChat') !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { + if (assigneeChatReport?.isOptimisticReport && assigneeChatReport.pendingFields?.createChat !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { optimisticChatCreatedReportAction = buildOptimisticCreatedReportAction(assigneeChatReportID); optimisticData.push( { @@ -3434,9 +3282,9 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeEmail, assigneeAccountID // If you're choosing to share the task in the same DM as the assignee then we don't need to create another reportAction indicating that you've been assigned if (assigneeChatReportID !== parentReportID) { - const displayname = lodashGet(allPersonalDetails, [assigneeAccountID, 'displayName']) || lodashGet(allPersonalDetails, [assigneeAccountID, 'login'], ''); + const displayname = allPersonalDetails?.[assigneeAccountID]?.displayName ?? allPersonalDetails?.[assigneeAccountID]?.login ?? ''; optimisticAssigneeAddComment = buildOptimisticTaskCommentReportAction(taskReportID, title, assigneeEmail, assigneeAccountID, `assigned to ${displayname}`, parentReportID); - const lastAssigneeCommentText = formatReportLastMessageText(optimisticAssigneeAddComment.reportAction.message[0].text); + const lastAssigneeCommentText = formatReportLastMessageText(optimisticAssigneeAddComment.reportAction.message?.[0].text ?? ''); const optimisticAssigneeReport = { lastVisibleActionCreated: currentTime, lastMessageText: lastAssigneeCommentText, @@ -3448,7 +3296,7 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeEmail, assigneeAccountID { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, - value: {[optimisticAssigneeAddComment.reportAction.reportActionID]: optimisticAssigneeAddComment.reportAction}, + value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '']: optimisticAssigneeAddComment.reportAction}, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -3459,7 +3307,7 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeEmail, assigneeAccountID failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, - value: {[optimisticAssigneeAddComment.reportAction.reportActionID]: {pendingAction: null}}, + value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '']: {pendingAction: null}}, }); } @@ -3474,58 +3322,44 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeEmail, assigneeAccountID /** * Returns an array of the participants Ids of a report - * - * @param {Object} report - * @returns {Array} */ -function getParticipantsIDs(report) { +function getParticipantsIDs(report: OnyxEntry): Array { if (!report) { return []; } - const participants = report.participantAccountIDs || []; + const participants = report.participantAccountIDs ?? []; // Build participants list for IOU/expense reports if (isMoneyRequestReport(report)) { - return _.chain([report.managerID, report.ownerAccountID, ...participants]) - .compact() - .uniq() - .value(); + const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...participants].filter(Boolean); + const onlyUnique = [...new Set([...onlyTruthyValues])]; + return onlyUnique; } return participants; } /** * Get the last 3 transactions with receipts of an IOU report that will be displayed on the report preview - * - * @param {Object} reportPreviewAction - * @returns {Object} */ -function getReportPreviewDisplayTransactions(reportPreviewAction) { - const transactionIDs = lodashGet(reportPreviewAction, ['childLastReceiptTransactionIDs'], '').split(','); - return _.reduce( - transactionIDs, - (transactions, transactionID) => { - const transaction = TransactionUtils.getTransaction(transactionID); - if (TransactionUtils.hasReceipt(transaction)) { - transactions.push(transaction); - } - return transactions; - }, - [], - ); +function getReportPreviewDisplayTransactions(reportPreviewAction: OnyxEntry) { + const transactionIDs = (reportPreviewAction?.childLastReceiptTransactionIDs ?? '').split(','); + return transactionIDs.reduce((transactions: Array>, transactionID: string) => { + const transaction = TransactionUtils.getTransaction(transactionID); + if (TransactionUtils.hasReceipt(transaction)) { + transactions.push(transaction); + } + return transactions; + }, []); } /** * Return iou report action display message - * - * @param {Object} reportAction report action - * @returns {String} */ -function getIOUReportActionDisplayMessage(reportAction) { - const originalMessage = _.get(reportAction, 'originalMessage', {}); +function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) { + const originalMessage = reportAction?.originalMessage; let displayMessage; - if (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { + if (originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { const {amount, currency, IOUReportID} = originalMessage; const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency); const iouReport = getReport(IOUReportID); diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 5e8c663b7a11..0ed788e088b0 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -167,7 +167,7 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra * * @deprecated Use withOnyx() or Onyx.connect() instead */ -function getTransaction(transactionID: string): Transaction | Record { +function getTransaction(transactionID: string): OnyxEntry { return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {}; } @@ -175,14 +175,14 @@ function getTransaction(transactionID: string): Transaction | Record): string { return transaction?.comment?.comment ?? ''; } /** * Return the amount field from the transaction, return the modifiedAmount if present. */ -function getAmount(transaction: Transaction, isFromExpenseReport: boolean): number { +function getAmount(transaction: OnyxEntry, isFromExpenseReport: boolean): number { // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value if (!isFromExpenseReport) { const amount = transaction?.modifiedAmount ?? 0; @@ -208,7 +208,7 @@ function getAmount(transaction: Transaction, isFromExpenseReport: boolean): numb /** * Return the currency field from the transaction, return the modifiedCurrency if present. */ -function getCurrency(transaction: Transaction): string { +function getCurrency(transaction: OnyxEntry): string { const currency = transaction?.modifiedCurrency ?? ''; if (currency) { return currency; @@ -219,42 +219,42 @@ function getCurrency(transaction: Transaction): string { /** * Return the merchant field from the transaction, return the modifiedMerchant if present. */ -function getMerchant(transaction: Transaction): string { +function getMerchant(transaction: OnyxEntry): string { return transaction?.modifiedMerchant ? transaction.modifiedMerchant : transaction?.merchant || ''; } /** * Return the waypoints field from the transaction, return the modifiedWaypoints if present. */ -function getWaypoints(transaction: Transaction): WaypointCollection | undefined { +function getWaypoints(transaction: OnyxEntry): WaypointCollection | undefined { return transaction?.modifiedWaypoints ?? transaction?.comment?.waypoints; } /** * Return the category from the transaction. This "category" field has no "modified" complement. */ -function getCategory(transaction: Transaction): string { +function getCategory(transaction: OnyxEntry): string { return transaction?.category ?? ''; } /** * Return the billable field from the transaction. This "billable" field has no "modified" complement. */ -function getBillable(transaction: Transaction): boolean { +function getBillable(transaction: OnyxEntry): boolean { return transaction?.billable ?? false; } /** * Return the tag from the transaction. This "tag" field has no "modified" complement. */ -function getTag(transaction: Transaction): string { +function getTag(transaction: OnyxEntry): string { return transaction?.tag ?? ''; } /** * Return the created field from the transaction, return the modifiedCreated if present. */ -function getCreated(transaction: Transaction): string { +function getCreated(transaction: OnyxEntry): string { const created = transaction?.modifiedCreated ? transaction.modifiedCreated : transaction?.created || ''; const createdDate = parseISO(created); if (isValid(createdDate)) { @@ -264,20 +264,20 @@ function getCreated(transaction: Transaction): string { return ''; } -function isDistanceRequest(transaction: Transaction): boolean { +function isDistanceRequest(transaction: OnyxEntry): boolean { const type = transaction?.comment?.type; const customUnitName = transaction?.comment?.customUnit?.name; return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE; } -function isReceiptBeingScanned(transaction: Transaction): boolean { - return [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === transaction.receipt.state); +function isReceiptBeingScanned(transaction?: OnyxEntry): boolean { + return [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === transaction?.receipt.state); } /** * Check if the transaction has a non-smartscanning receipt and is missing required fields */ -function hasMissingSmartscanFields(transaction: Transaction): boolean { +function hasMissingSmartscanFields(transaction: OnyxEntry): boolean { return hasReceipt(transaction) && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction); } @@ -294,7 +294,7 @@ function hasRoute(transaction: Transaction): boolean { * * @deprecated Use Onyx.connect() or withOnyx() instead */ -function getLinkedTransaction(reportAction?: OnyxEntry): Transaction | Record { +function getLinkedTransaction(reportAction?: OnyxEntry): OnyxEntry { let transactionID = ''; if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 143b70127de5..864a09873790 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -1,20 +1,27 @@ import {ValueOf} from 'type-fest'; import CONST from '../../CONST'; +type IOUDetails = { + amount?: number; + comment?: string; + currency?: string; +}; +type IOUMessage = { + /** The ID of the iou transaction */ + IOUTransactionID?: string; + IOUReportID?: number; + amount: number; + comment?: string; + currency: string; + lastModified?: string; + participantAccountIDs?: number[]; + type: string; + paymentType?: string; + IOUDetails?: IOUDetails; +}; type OriginalMessageIOU = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.IOU; - originalMessage: { - /** The ID of the iou transaction */ - IOUTransactionID?: string; - - IOUReportID?: number; - amount: number; - comment?: string; - currency: string; - lastModified?: string; - participantAccountIDs?: number[]; - type: string; - }; + originalMessage: IOUMessage; }; type FlagSeverityName = 'spam' | 'inconsiderate' | 'bullying' | 'intimidation' | 'harassment' | 'assault'; @@ -136,4 +143,4 @@ type OriginalMessage = | OriginalMessagePolicyTask; export default OriginalMessage; -export type {Reaction, ChronosOOOEvent}; +export type {Reaction, ChronosOOOEvent, IOUMessage}; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index fcbff2e0e366..f78bc39d229e 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -45,6 +45,8 @@ type PersonalDetails = { /** If trying to get PersonalDetails from the server and user is offling */ isOptimisticPersonalDetail?: boolean; + + fallBackIcon?: string; }; export type {Timezone}; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 04113032ff43..2c95a014053e 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -79,6 +79,13 @@ type Report = { visibility?: ValueOf; preexistingReportID?: string; iouReportID?: number; + lastMentionedTime?: string | null; + parentReportActionIDs?: number[]; + errorFields?: OnyxCommon.ErrorFields; + pendingFields?: { + createChat: ValueOf; + addWorkspaceRoom: ValueOf; + }; }; export default Report; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index ec505a7e8d07..fb8bacfb3766 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -1,5 +1,7 @@ +import {ValueOf} from 'type-fest'; import OriginalMessage, {Reaction} from './OriginalMessage'; import * as OnyxCommon from './OnyxCommon'; +import CONST from '../../CONST'; type Message = { /** The type of the action item fragment. Used to render a corresponding component */ @@ -34,6 +36,7 @@ type Message = { isDeletedParentAction: boolean; whisperedTo: number[]; reactions: Reaction[]; + taskReportID?: string; }; type Person = { @@ -79,8 +82,15 @@ type ReportActionBase = { childCommenterCount?: number; childLastVisibleActionCreated?: string; childVisibleActionCount?: number; - + parentReportID?: string; + childReportName?: string; + childManagerAccountID?: number; + childStatusNum?: ValueOf; + childStateNum?: ValueOf; pendingAction?: OnyxCommon.PendingAction; + childLastReceiptTransactionIDs?: string; + childLastMoneyRequestComment?: string; + childMoneyRequestCount?: number; }; type ReportAction = ReportActionBase & OriginalMessage; From dfffe5fa77b77fc6a4c3c5c084e940730209d663 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 10 Oct 2023 12:51:10 +0200 Subject: [PATCH 05/63] fix: merge conflicts, few adjustments --- src/libs/ReportUtils.ts | 273 +++++++++++++++++------------- src/types/onyx/PersonalDetails.ts | 4 +- src/types/onyx/Report.ts | 3 + src/types/onyx/ReportAction.ts | 16 +- 4 files changed, 178 insertions(+), 118 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 308125fa5730..5d446e50a172 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1,4 +1,3 @@ -/* eslint-disable rulesdir/prefer-underscore-method */ import {format, parseISO} from 'date-fns'; import {SvgProps} from 'react-native-svg'; import Str from 'expensify-common/lib/str'; @@ -7,7 +6,7 @@ import lodashIsEqual from 'lodash/isEqual'; import lodashFindLastIndex from 'lodash/findLastIndex'; import lodashIntersection from 'lodash/intersection'; import {ValueOf} from 'type-fest'; -import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; @@ -30,6 +29,7 @@ import {Beta, Login, PersonalDetails, Policy, Report, ReportAction, Transaction} import {Comment, Receipt} from '../types/onyx/Transaction'; import DeepValueOf from '../types/utils/DeepValueOf'; import {IOUMessage} from '../types/onyx/OriginalMessage'; +import {Message} from '../types/onyx/ReportAction'; type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string}; type Avatar = { @@ -37,7 +37,7 @@ type Avatar = { source: React.FC | string; type: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; name: string; - fallBackIcon?: React.FC | string; + fallbackIcon?: React.FC | string; }; type ExpanseOriginalMessage = { oldComment?: string; @@ -249,7 +249,7 @@ function isReportManager(report: OnyxEntry): boolean { /** * Checks if the supplied report has been approved */ -function isReportApproved(report: OnyxEntry): boolean { +function isReportApproved(report: OnyxEntry | undefined): boolean { return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report.statusNum === CONST.REPORT.STATUS.APPROVED; } @@ -317,7 +317,7 @@ function isAnnounceRoom(report: OnyxEntry): boolean { * Whether the provided report is a default room */ function isDefaultRoom(report: OnyxEntry): boolean { - return [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL].indexOf(getChatType(report)) > -1; + return [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL].indexOf(getChatType(report) ?? '') > -1; } /** @@ -448,9 +448,6 @@ function isExpensifyOnlyParticipantInReport(report: OnyxEntry): boolean /** * Returns true if there are any Expensify accounts (i.e. with domain 'expensify.com') in the set of accountIDs * by cross-referencing the accountIDs with personalDetails. - * - * @param {Array} accountIDs - * @return {Boolean} */ function hasExpensifyEmails(accountIDs: number[]): boolean { return accountIDs.some((accountID) => Str.extractCompanyNameFromEmailDomain(allPersonalDetails?.[accountID]?.login ?? '') === CONST.EXPENSIFY_PARTNER_NAME); @@ -492,7 +489,7 @@ function findLastAccessedReport( return sortedReports[0]; } - return adminReport || sortedReports.find((report) => !isConciergeChatReport(report)); + return adminReport ?? sortedReports.find((report) => !isConciergeChatReport(report)); } if (ignoreDomainRooms) { @@ -510,14 +507,14 @@ function findLastAccessedReport( /** * Whether the provided report is an archived room */ -function isArchivedRoom(report: OnyxEntry): boolean { +function isArchivedRoom(report: OnyxEntry | undefined): boolean { return report?.statusNum === CONST.REPORT.STATUS.CLOSED && report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED; } /** * Checks if the current user is allowed to comment on the given report. */ -function isAllowedToComment(report: OnyxEntry): boolean { +function isAllowedToComment(report: OnyxEntry | undefined): boolean { // Default to allowing all users to post const capability = (report?.writeCapability ?? CONST.REPORT.WRITE_CAPABILITIES.ALL) || CONST.REPORT.WRITE_CAPABILITIES.ALL; @@ -645,29 +642,26 @@ function isMoneyRequestReport(reportOrID: OnyxEntry | string): boolean { /** * Get the report given a reportID - * - * @param {String} reportID - * @returns {Object} */ -function getReport(reportID: string) { +function getReport(reportID: string | undefined): OnyxEntry | undefined { // Deleted reports are set to null and lodashGet will still return null in that case, so we need to add an extra check - return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; + return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; } /** * Can only delete if the author is this user and the action is an ADDCOMMENT action or an IOU action in an unsettled report, or if the user is a * policy admin */ -function canDeleteReportAction(reportAction: OnyxEntry, reportID: string) { +function canDeleteReportAction(reportAction: OnyxEntry, reportID: string): boolean { const report = getReport(reportID); - const isActionOwner = reportAction.actorAccountID === currentUserAccountID; + const isActionOwner = reportAction?.actorAccountID === currentUserAccountID; if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { // For now, users cannot delete split actions - const isSplitAction = lodashGet(reportAction, 'originalMessage.type') === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; + const isSplitAction = reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; - if (isSplitAction || isSettled(reportAction.originalMessage.IOUReportID) || isReportApproved(report)) { + if (isSplitAction || isSettled(reportAction?.originalMessage?.IOUReportID) || isReportApproved(report)) { return false; } @@ -685,8 +679,8 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: return false; } - const policy = lodashGet(allPolicies, `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`) || {}; - const isAdmin = policy.role === CONST.POLICY.ROLE.ADMIN && !isDM(report); + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN && !isDM(report); return isActionOwner || isAdmin; } @@ -694,7 +688,7 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: /** * Get welcome message based on room type */ -function getRoomWelcomeMessage(report: OnyxEntry, isUserPolicyAdmin: boolean) { +function getRoomWelcomeMessage(report: OnyxEntry, isUserPolicyAdmin: boolean): WelcomeMessage { const welcomeMessage: WelcomeMessage = {showReportName: true}; const workspaceName = getPolicyName(report); @@ -791,7 +785,7 @@ function formatReportLastMessageText(lastMessageText: string, isModifiedExpenseM /** * Helper method to return the default avatar associated with the given login */ -function getDefaultWorkspaceAvatar(workspaceName?: string): string { +function getDefaultWorkspaceAvatar(workspaceName?: string): React.FC { if (!workspaceName) { return defaultWorkspaceAvatars.WorkspaceBuilding; } @@ -802,7 +796,7 @@ function getDefaultWorkspaceAvatar(workspaceName?: string): string { .replace(/[^0-9a-z]/gi, '') .toUpperCase(); - const defaultWorkspaceAvatar = defaultWorkspaceAvatars[`Workspace${alphaNumeric[0]}`] as React.FC; + const defaultWorkspaceAvatar = defaultWorkspaceAvatars[`Workspace${alphaNumeric[0]}`]; return !alphaNumeric ? defaultWorkspaceAvatars.WorkspaceBuilding : defaultWorkspaceAvatar; } @@ -823,7 +817,7 @@ function getIconsForParticipants(participants: number[], personalDetails: OnyxCo for (const accountID of participantsList) { const avatarSource = UserUtils.getAvatar(personalDetails?.[accountID]?.avatar ?? '', accountID); const displayNameLogin = personalDetails?.[accountID]?.displayName ?? personalDetails?.[accountID]?.login ?? ''; - participantDetails.push([accountID, displayNameLogin, avatarSource, personalDetails?.[accountID]?.fallBackIcon ?? '']); + participantDetails.push([accountID, displayNameLogin, avatarSource, personalDetails?.[accountID]?.fallbackIcon ?? '']); } const sortedParticipantDetails = participantDetails.sort((first, second) => { @@ -848,7 +842,7 @@ function getIconsForParticipants(participants: number[], personalDetails: OnyxCo source: sortedParticipantDetail[2], type: CONST.ICON_TYPE_AVATAR, name: sortedParticipantDetail[1], - fallBackIcon: sortedParticipantDetail[3], + fallbackIcon: sortedParticipantDetail[3], }; avatars.push(userIcon); } @@ -910,14 +904,14 @@ function getIcons( if (isChatThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const actorAccountID = parentReportAction[actorAccountID] ?? -1; + const actorAccountID = parentReportAction[actorAccountID ?? -1] ?? -1; const actorDisplayName = allPersonalDetails?.[actorAccountID]?.displayName ?? ''; const actorIcon = { id: actorAccountID, source: UserUtils.getAvatar(personalDetails?.[actorAccountID]?.avatar ?? '', actorAccountID), name: actorDisplayName, type: CONST.ICON_TYPE_AVATAR, - fallbackIcon: personalDetails?.[parentReportAction.actorAccountID]?.fallBackIcon, + fallbackIcon: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.fallbackIcon, }; if (isWorkspaceThread(report)) { @@ -932,7 +926,7 @@ function getIcons( source: UserUtils.getAvatar(personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? '', report?.ownerAccountID ?? -1), type: CONST.ICON_TYPE_AVATAR, name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '', - fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallBackIcon, + fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon, }; if (isWorkspaceTaskReport(report)) { @@ -996,7 +990,7 @@ function getIcons( * Gets the personal details for a login by looking in the ONYXKEYS.PERSONAL_DETAILS_LIST Onyx key (stored in the local variable, allPersonalDetails). If it doesn't exist in Onyx, * then a default object is constructed. */ -function getPersonalDetailsForAccountID(accountID: number): PersonalDetails { +function getPersonalDetailsForAccountID(accountID: number): PersonalDetails | Record | {avatar: string | React.FC} { if (!accountID) { return {}; } @@ -1023,21 +1017,17 @@ function getDisplayNameForParticipant(accountID: number, shouldUseShortForm = fa return ''; } const personalDetails = getPersonalDetailsForAccountID(accountID); - const longName = personalDetails.displayName; - // TODO: Check why ?? is not working - const shortName = personalDetails?.firstName || longName; - return shouldUseShortForm ? shortName : longName; + if ('displayName' in personalDetails) { + const longName = personalDetails.displayName; + const shortName = personalDetails.firstName ? personalDetails?.firstName : longName; + return shouldUseShortForm ? shortName : longName; + } } -/** - * @param {Object} personalDetailsList - * @param {Boolean} isMultipleParticipantReport - * @returns {Array} - */ -function getDisplayNamesWithTooltips(personalDetailsList, isMultipleParticipantReport) { - return _.map(personalDetailsList, (user) => { +function getDisplayNamesWithTooltips(personalDetailsList: PersonalDetails[], isMultipleParticipantReport: boolean) { + return personalDetailsList?.map?.((user) => { const accountID = Number(user.accountID); - const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport) || user.login || ''; + const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport) ?? user.login ?? ''; const avatar = UserUtils.getDefaultAvatar(accountID); let pronouns = user.pronouns; @@ -1049,7 +1039,7 @@ function getDisplayNamesWithTooltips(personalDetailsList, isMultipleParticipantR return { displayName, avatar, - login: user.login || '', + login: user.login ?? '', accountID, pronouns, }; @@ -1132,7 +1122,7 @@ function getMoneyRequestTotal(report: OnyxEntry, allReportsDict: OnyxCol /** * Get the title for a policy expense chat which depends on the role of the policy member seeing this report */ -function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string { +function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string | undefined { const reportOwnerDisplayName = getDisplayNameForParticipant(report?.ownerAccountID ?? -1) ?? allPersonalDetails?.[report?.ownerAccountID ?? -1]?.login ?? report?.reportName; // If the policy expense chat is owned by this user, use the name of the policy as the report name. @@ -1235,7 +1225,6 @@ function canEditMoneyRequest(reportAction: OnyxEntry): boolean { function canEditReportAction(reportAction: OnyxEntry): boolean { const isCommentOrIOU = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; - console.log('canEditReportAction', reportAction?.message); return Boolean( reportAction?.actorAccountID === currentUserAccountID && isCommentOrIOU && @@ -1311,12 +1300,12 @@ function getTransactionReportName(reportAction: OnyxEntry): string /** * Get money request message for an IOU report * - * @param {Object} report - * @param {Object} [reportAction={}] This can be either a report preview action or the IOU action - * @param {Boolean} [shouldConsiderReceiptBeingScanned=false] - * @returns {String} + * @param report + * @param [reportAction] This can be either a report preview action or the IOU action + * @param [shouldConsiderReceiptBeingScanned=false] + * @returns */ -function getReportPreviewMessage(report: OnyxEntry, reportAction?: OnyxEntry, shouldConsiderReceiptBeingScanned = false) { +function getReportPreviewMessage(report: OnyxEntry, reportAction?: OnyxEntry, shouldConsiderReceiptBeingScanned = false): string { const reportActionMessage = reportAction?.message?.[0].html ?? ''; if (Object.keys(report ?? {}).length === 0 || !report?.reportID) { @@ -1400,8 +1389,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. */ -function getModifiedExpenseMessage(reportAction: OnyxEntry): string { +function getModifiedExpenseMessage(reportAction: OnyxEntry): string | undefined { const reportActionOriginalMessage = reportAction?.originalMessage ?? {}; + console.log({reportActionOriginalMessage}); if (Object.keys(reportActionOriginalMessage).length === 0) { return Localize.translateLocal('iou.changedTheRequest'); } @@ -1415,16 +1405,16 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin const hasModifiedMerchant = Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldMerchant') && Object.prototype.hasOwnProperty.call(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, oldCurrency); - const currency = reportActionOriginalMessage.currency; - const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.amount, currency); + 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); + if (hasModifiedMerchant && reportActionOriginalMessage?.merchant?.includes('@')) { + return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage?.merchant, reportActionOriginalMessage?.oldMerchant, amount, oldAmount); } return getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); @@ -1433,37 +1423,42 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin const hasModifiedComment = Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldComment') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true); + return getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage?.newComment, + reportActionOriginalMessage?.oldComment, + Localize.translateLocal('common.description'), + true, + ); } const hasModifiedCreated = Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldCreated') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'created'); if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp - let formattedOldCreated = parseISO(reportActionOriginalMessage.oldCreated); + let formattedOldCreated: Date | string = parseISO(reportActionOriginalMessage?.oldCreated); formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated.toDateString(), Localize.translateLocal('common.date'), false); + return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.created, formattedOldCreated?.toDateString?.(), Localize.translateLocal('common.date'), false); } if (hasModifiedMerchant) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true); + return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.merchant, reportActionOriginalMessage?.oldMerchant, Localize.translateLocal('common.merchant'), true); } const hasModifiedCategory = Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldCategory') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true); + return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.category, reportActionOriginalMessage?.oldCategory, Localize.translateLocal('common.category'), true); } const hasModifiedTag = Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldTag') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'tag'); if (hasModifiedTag) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, Localize.translateLocal('common.tag'), true); + return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.tag, reportActionOriginalMessage?.oldTag, Localize.translateLocal('common.tag'), true); } const hasModifiedBillable = Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldBillable') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true); + return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.billable, reportActionOriginalMessage?.oldBillable, Localize.translateLocal('iou.request'), true); } } @@ -1495,9 +1490,9 @@ function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry): OnyxEntry | undefined | Record { +function getParentReport(report: OnyxEntry | undefined): OnyxEntry | undefined | Record { if (!report?.parentReportID) { return {}; } @@ -1720,15 +1715,14 @@ function hasReportNameError(report: OnyxEntry): boolean { */ function getParsedComment(text: string): string { const parser = new ExpensiMark(); - return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : _.escape(text); + return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : lodashEscape(text); } -/** - * @param {String} [text] - * @param {File} [file] - * @returns {Object} - */ -function buildOptimisticAddCommentReportAction(text?: string, file?: File & {source: string; uri: string}): {commentText: string; reportAction: ReportAction} { +type OptimisticReportAction = { + commentText: string; + reportAction: Partial; +}; +function buildOptimisticAddCommentReportAction(text?: string, file?: File & {source: string; uri: string}): OptimisticReportAction { const parser = new ExpensiMark(); const commentText = getParsedComment(text ?? ''); const isAttachment = !text && file !== undefined; @@ -1776,8 +1770,13 @@ function buildOptimisticAddCommentReportAction(text?: string, file?: File & {sou * @param lastVisibleActionCreated - Last visible action created of the child report * @param type - The type of action in the child report */ - -function updateOptimisticParentReportAction(parentReportAction: OnyxEntry, lastVisibleActionCreated: string, type: string) { +type UpdateOptimisticParentReportAction = { + childVisibleActionCount: number; + childCommenterCount: number; + childLastVisibleActionCreated: string; + childOldestFourAccountIDs: string | undefined; +}; +function updateOptimisticParentReportAction(parentReportAction: OnyxEntry, lastVisibleActionCreated: string, type: string): UpdateOptimisticParentReportAction { let childVisibleActionCount = parentReportAction?.childVisibleActionCount ?? 0; let childCommenterCount = parentReportAction?.childCommenterCount ?? 0; let childOldestFourAccountIDs = parentReportAction?.childOldestFourAccountIDs; @@ -1820,7 +1819,13 @@ function updateOptimisticParentReportAction(parentReportAction: OnyxEntry { const report = getReport(reportID); const parentReportAction = ReportActionsUtils.getParentReportAction(report); if (!parentReportAction) { @@ -1879,10 +1884,29 @@ function buildOptimisticTaskCommentReportAction(taskReportID: string, taskTitle: * @param currency - IOU currency. * @param isSendingMoney - If we send money the IOU should be created as settled */ -function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number, total: number, chatReportID: string, currency: string, isSendingMoney = false) { + +type OptimisticIOUReport = Pick< + Report, + | 'cachedTotal' + | 'hasOutstandingIOU' + | 'type' + | 'chatReportID' + | 'currency' + | 'managerID' + | 'ownerAccountID' + | 'participantAccountIDs' + | 'reportID' + | 'state' + | 'stateNum' + | 'total' + | 'reportName' + | 'notificationPreference' + | 'parentReportID' +>; +function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number, total: number, chatReportID: string, currency: string, isSendingMoney = false): OptimisticIOUReport { const formattedTotal = CurrencyUtils.convertToDisplayString(total, currency); const personalDetails = getPersonalDetailsForAccountID(payerAccountID); - const payerEmail = personalDetails.login; + const payerEmail = 'login' in personalDetails ? personalDetails.login : ''; return { // If we're sending money, hasOutstandingIOU should be false hasOutstandingIOU: !isSendingMoney, @@ -1914,7 +1938,24 @@ function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number * @param total - Amount in cents * @param currency */ -function buildOptimisticExpenseReport(chatReportID: string, policyID: string, payeeAccountID: number, total: number, currency: string) { + +type OptimisticExpanseReport = Pick< + Report, + | 'reportID' + | 'chatReportID' + | 'policyID' + | 'type' + | 'ownerAccountID' + | 'hasOutstandingIOU' + | 'currency' + | 'reportName' + | 'state' + | 'stateNum' + | 'total' + | 'notificationPreference' + | 'parentReportID' +>; +function buildOptimisticExpenseReport(chatReportID: string, policyID: string, payeeAccountID: number, total: number, currency: string): OptimisticExpanseReport { // The amount for Expense reports are stored as negative value in the database const storedTotal = total * -1; const policyName = getPolicyName(allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`]); @@ -1951,7 +1992,7 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa * @param paymentType - IOU paymentMethodType. Can be oneOf(Elsewhere, Expensify) * @param isSettlingUp - Whether we are settling up an IOU */ -function getIOUReportActionMessage(iouReportID: string, type: string, total: number, comment: string, currency: string, paymentType = '', isSettlingUp = false) { +function getIOUReportActionMessage(iouReportID: string, type: string, total: number, comment: string, currency: string, paymentType = '', isSettlingUp = false): [Message] { const amount = type === CONST.IOU.REPORT_ACTION_TYPE.PAY ? CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(getReport(iouReportID)), currency) @@ -1995,7 +2036,7 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num return [ { html: lodashEscape(iouMessage), - text: iouMessage, + text: iouMessage ?? '', isEdited: false, type: CONST.REPORT.MESSAGE.TYPE.COMMENT, }, @@ -2018,6 +2059,24 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num * @param [receipt] * @param [isOwnPolicyExpenseChat] - Whether this is an expense report create from the current user's policy expense chat */ + +type OptimisticIOUReportAction = Pick< + ReportAction, + | 'actionName' + | 'actorAccountID' + | 'automatic' + | 'avatar' + | 'isAttachment' + | 'originalMessage' + | 'message' + | 'person' + | 'reportActionID' + | 'shouldShow' + | 'created' + | 'pendingAction' + | 'receipt' + | 'whisperedToAccountIDs' +>; function buildOptimisticIOUReportAction( type: string, amount: number, @@ -2031,7 +2090,7 @@ function buildOptimisticIOUReportAction( isSendMoneyFlow = false, receipt: Receipt = {}, isOwnPolicyExpenseChat = false, -) { +): OptimisticIOUReportAction { const IOUReportID = iouReportID || generateReportID(); const originalMessage: IOUMessage = { @@ -2046,7 +2105,8 @@ function buildOptimisticIOUReportAction( if (type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { // In send money flow, we store amount, comment, currency in IOUDetails when type = pay if (isSendMoneyFlow) { - ['amount', 'comment', 'currency'].forEach((key) => { + const keys = ['amount', 'comment', 'currency'] as const; + keys.forEach((key) => { delete originalMessage[key]; }); originalMessage.IOUDetails = {amount, comment, currency}; @@ -2067,7 +2127,7 @@ function buildOptimisticIOUReportAction( if (isOwnPolicyExpenseChat) { originalMessage.participantAccountIDs = [currentUserAccountID ?? -1]; } else { - originalMessage.participantAccountIDs = [currentUserAccountID ?? -1, ..._.pluck(participants, 'accountID')]; + originalMessage.participantAccountIDs = [currentUserAccountID ?? -1, ...participants.map((participant) => participant.accountID)]; } } @@ -2129,13 +2189,8 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e /** * Builds an optimistic SUBMITTED report action with a randomly generated reportActionID. * - * @param {Number} amount - * @param {String} currency - * @param {Number} expenseReportID - * - * @returns {Object} */ -function buildOptimisticSubmittedReportAction(amount, currency, expenseReportID) { +function buildOptimisticSubmittedReportAction(amount: number, currency: string, expenseReportID: string) { const originalMessage = { amount, currency, @@ -2146,14 +2201,14 @@ function buildOptimisticSubmittedReportAction(amount, currency, expenseReportID) actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, actorAccountID: currentUserAccountID, automatic: false, - avatar: lodashGet(currentUserPersonalDetails, 'avatar', UserUtils.getDefaultAvatar(currentUserAccountID)), + avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatar(currentUserAccountID), isAttachment: false, originalMessage, message: getIOUReportActionMessage(expenseReportID, CONST.REPORT.ACTIONS.TYPE.SUBMITTED, Math.abs(amount), '', currency), person: [ { style: 'strong', - text: lodashGet(currentUserPersonalDetails, 'displayName', currentUserEmail), + text: currentUserPersonalDetails?.displayName ?? currentUserEmail, type: 'TEXT', }, ], @@ -2780,7 +2835,7 @@ function shouldReportBeInOptionList( // Include reports that have errors from trying to add a workspace // If we excluded it, then the red-brock-road pattern wouldn't work for the user to resolve the error - if (report.errorFields.addWorkspaceRoom) { + if (report.errorFields?.addWorkspaceRoom) { return true; } @@ -2805,8 +2860,8 @@ function shouldReportBeInOptionList( /** * Attempts to find a report in onyx with the provided list of participants. Does not include threads, task, money request, room, and policy expense chat. */ -function getChatByParticipants(newParticipantList: number[]) { - const sortedNewParticipantList = _.sortBy(newParticipantList); +function getChatByParticipants(newParticipantList: number[]): OnyxEntry | undefined { + const sortedNewParticipantList = newParticipantList.sort((a, b) => a - b); return Object.values(allReports ?? {}).find((report) => { // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it if ( @@ -2821,10 +2876,11 @@ function getChatByParticipants(newParticipantList: number[]) { return false; } - const sortedParticipanctsAccountIDs = report.parentReportActionIDs?.sort((a, b) => a - b); - // Only return the chat if it has all the participants - return _.isEqual(sortedNewParticipantList, _.sortBy(report.participantAccountIDs)); + return lodashIsEqual( + sortedNewParticipantList, + report.participantAccountIDs?.sort((a, b) => a - b), + ); }); } @@ -3217,13 +3273,8 @@ function shouldDisableSettings(report: OnyxEntry): boolean { return !isMoneyRequestReport(report) && !isPolicyExpenseChat(report) && !isChatRoom(report) && !isChatThread(report); } -/** - * @param {String} policyID - * @param {Array} accountIDs - * @returns {Array} - */ -function getWorkspaceChats(policyID, accountIDs) { - return _.filter(allReports, (report) => isPolicyExpenseChat(report) && lodashGet(report, 'policyID', '') === policyID && _.contains(accountIDs, lodashGet(report, 'ownerAccountID', ''))); +function getWorkspaceChats(policyID: string, accountIDs: number[]) { + return Object.values(allReports ?? {})?.filter((report) => isPolicyExpenseChat(report) && (report?.policyID ?? '') === policyID && accountIDs.includes(report?.ownerAccountID ?? -1)); } /** @@ -3433,12 +3484,8 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) return displayMessage; } -/** - * @param {Object} report - * @returns {Boolean} - */ -function isReportDraft(report) { - return lodashGet(report, 'stateNum') === CONST.REPORT.STATE_NUM.OPEN && lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.OPEN; +function isReportDraft(report: OnyxEntry): boolean { + return report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; } export { diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index f78bc39d229e..d495eb1c6c52 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -1,3 +1,5 @@ +import {SvgProps} from 'react-native-svg'; + type Timezone = { /** Value of selected timezone */ selected?: string; @@ -46,7 +48,7 @@ type PersonalDetails = { /** If trying to get PersonalDetails from the server and user is offling */ isOptimisticPersonalDetail?: boolean; - fallBackIcon?: string; + fallbackIcon?: string; }; export type {Timezone}; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 833a0c75295b..432119330cbb 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -88,6 +88,9 @@ type Report = { }; /** If the report contains nonreimbursable expenses, send the nonreimbursable total */ nonReimbursableTotal?: number; + cachedTotal?: string; + chatReportID?: string; + state?: ValueOf; }; export default Report; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index fb8bacfb3766..8c17bb5111b1 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -2,6 +2,7 @@ import {ValueOf} from 'type-fest'; import OriginalMessage, {Reaction} from './OriginalMessage'; import * as OnyxCommon from './OnyxCommon'; import CONST from '../../CONST'; +import {Receipt} from './Transaction'; type Message = { /** The type of the action item fragment. Used to render a corresponding component */ @@ -31,12 +32,14 @@ type Message = { iconUrl?: string; /** Fragment edited flag */ - isEdited: boolean; + isEdited?: boolean; - isDeletedParentAction: boolean; - whisperedTo: number[]; - reactions: Reaction[]; + isDeletedParentAction?: boolean; + whisperedTo?: number[]; + reactions?: Reaction[]; taskReportID?: string; + html?: string; + translationKey?: string; }; type Person = { @@ -91,8 +94,13 @@ type ReportActionBase = { childLastReceiptTransactionIDs?: string; childLastMoneyRequestComment?: string; childMoneyRequestCount?: number; + isFirstItem?: boolean; + isAttachment?: boolean; + attachmentInfo?: (File & {source: string; uri: string}) | Record; + receipt?: Receipt; }; type ReportAction = ReportActionBase & OriginalMessage; export default ReportAction; +export type {Message}; From 4ad36b4cfd04d84dfae226a64e1aa4ff08056f1b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 12 Oct 2023 16:50:59 +0200 Subject: [PATCH 06/63] fix: ts fixes for ReportUtils --- src/libs/ReportUtils.ts | 104 ++++++++++++++++++++------------- src/types/onyx/Report.ts | 2 + src/types/onyx/ReportAction.ts | 5 +- 3 files changed, 68 insertions(+), 43 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5d446e50a172..7a1f7c66a614 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -26,7 +26,7 @@ import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAva import * as CurrencyUtils from './CurrencyUtils'; import * as UserUtils from './UserUtils'; import {Beta, Login, PersonalDetails, Policy, Report, ReportAction, Transaction} from '../types/onyx'; -import {Comment, Receipt} from '../types/onyx/Transaction'; +import {Receipt} from '../types/onyx/Transaction'; import DeepValueOf from '../types/utils/DeepValueOf'; import {IOUMessage} from '../types/onyx/OriginalMessage'; import {Message} from '../types/onyx/ReportAction'; @@ -41,7 +41,8 @@ type Avatar = { }; type ExpanseOriginalMessage = { oldComment?: string; - newComment?: Comment; + newComment?: string; + comment?: string; merchant?: string; oldCreated?: string; created?: string; @@ -56,6 +57,7 @@ type ExpanseOriginalMessage = { oldTag?: string; billable?: string; oldBillable?: string; + }; type Participant = { accountID: number; @@ -150,7 +152,7 @@ function getPolicyType(report: OnyxEntry, policies: OnyxCollection, returnEmptyIfNotFound = false, policy: OnyxEntry = undefined): string { +function getPolicyName(report?: OnyxEntry, returnEmptyIfNotFound = false, policy: OnyxEntry = undefined): string | undefined { const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); if (Object.keys(report ?? {}).length === 0) { return noPolicyFound; @@ -269,7 +271,7 @@ function sortReportsByLastRead(reports: OnyxCollection): Array | string): boolean { /** * Checks if a report is an IOU or expense report. */ -function isMoneyRequestReport(reportOrID: OnyxEntry | string): boolean { +function isMoneyRequestReport(reportOrID?: OnyxEntry | string): boolean { const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`]; return isIOUReport(report) || isExpenseReport(report); } @@ -856,7 +858,9 @@ function getIconsForParticipants(participants: number[], personalDetails: OnyxCo function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): Avatar { const workspaceName = getPolicyName(report, false, policy); // TODO: Check why ?? is not working here - const policyExpenseChatAvatarSource = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar || getDefaultWorkspaceAvatar(workspaceName); + const policyExpenseChatAvatarSource = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar + ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar + : getDefaultWorkspaceAvatar(workspaceName); const workspaceIcon = { source: policyExpenseChatAvatarSource, @@ -1012,7 +1016,7 @@ function getPersonalDetailsForAccountID(accountID: number): PersonalDetails | Re /** * Get the displayName for a single report participant. */ -function getDisplayNameForParticipant(accountID: number, shouldUseShortForm = false) { +function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = false) { if (!accountID) { return ''; } @@ -1097,7 +1101,7 @@ function isWaitingForTaskCompleteFromAssignee(report: OnyxEntry, parentR return isTaskReport(report) && isReportManager(report) && isOpenTaskReport(report, parentReportAction); } -function getMoneyRequestTotal(report: OnyxEntry, allReportsDict: OnyxCollection = null): number { +function getMoneyRequestTotal(report?: OnyxEntry, allReportsDict: OnyxCollection = null): number { const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport; if (isMoneyRequestReport(report)) { @@ -1230,7 +1234,11 @@ function canEditReportAction(reportAction: OnyxEntry): boolean { isCommentOrIOU && canEditMoneyRequest(reportAction) && // Returns true for non-IOU actions reportAction?.message && - !isReportMessageAttachment({text: reportAction?.message?.[0].text, html: reportAction?.message?.[0].html, translationKey: reportAction?.message?.[0].translationKey}) && + !isReportMessageAttachment({ + text: reportAction?.message?.[0].text, + html: reportAction?.message?.[0].html ?? '', + translationKey: reportAction?.message?.[0].translationKey ?? '', + }) && !ReportActionsUtils.isDeletedAction(reportAction) && !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && reportAction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, @@ -1264,8 +1272,7 @@ function areAllRequestsBeingSmartScanned(iouReportID: string | undefined, report /** * Check if any of the transactions in the report has required missing fields * - * @param {Object|null} iouReportID - * @returns {Boolean} + * @param iouReportID */ function hasMissingSmartscanFields(iouReportID?: string) { const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); @@ -1275,7 +1282,7 @@ function hasMissingSmartscanFields(iouReportID?: string) { /** * Given a parent IOU report action get report name for the LHN. */ -function getTransactionReportName(reportAction: OnyxEntry): string { +function getTransactionReportName(reportAction?: OnyxEntry): string { if (ReportActionsUtils.isDeletedParentAction(reportAction)) { return Localize.translateLocal('parentReportAction.deletedRequest'); } @@ -1390,9 +1397,8 @@ function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDista * If we change this function be sure to update the backend as well. */ function getModifiedExpenseMessage(reportAction: OnyxEntry): string | undefined { - const reportActionOriginalMessage = reportAction?.originalMessage ?? {}; - console.log({reportActionOriginalMessage}); - if (Object.keys(reportActionOriginalMessage).length === 0) { + const reportActionOriginalMessage = reportAction?.originalMessage as ExpanseOriginalMessage; + if (Object.keys(reportActionOriginalMessage ?? {}).length === 0) { return Localize.translateLocal('iou.changedTheRequest'); } @@ -1406,15 +1412,15 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldMerchant') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'merchant'); if (hasModifiedAmount) { const oldCurrency = reportActionOriginalMessage?.oldCurrency; - const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.oldAmount, oldCurrency); + const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency ?? ''); const currency = reportActionOriginalMessage?.currency; - const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.amount, 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 getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage?.merchant, reportActionOriginalMessage?.oldMerchant ?? '', amount, oldAmount); } return getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); @@ -1424,8 +1430,8 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldComment') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage?.newComment, - reportActionOriginalMessage?.oldComment, + reportActionOriginalMessage?.newComment ?? '', + reportActionOriginalMessage?.oldComment ?? '', Localize.translateLocal('common.description'), true, ); @@ -1435,30 +1441,46 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldCreated') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'created'); if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp - let formattedOldCreated: Date | string = parseISO(reportActionOriginalMessage?.oldCreated); + let formattedOldCreated: Date | string = parseISO(reportActionOriginalMessage?.oldCreated ?? ''); formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.created, formattedOldCreated?.toDateString?.(), Localize.translateLocal('common.date'), false); + + return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.created ?? '', formattedOldCreated?.toString?.(), Localize.translateLocal('common.date'), false); } if (hasModifiedMerchant) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.merchant, reportActionOriginalMessage?.oldMerchant, Localize.translateLocal('common.merchant'), true); + return getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage?.merchant ?? '', + reportActionOriginalMessage?.oldMerchant ?? '', + Localize.translateLocal('common.merchant'), + true, + ); } const hasModifiedCategory = Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldCategory') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.category, reportActionOriginalMessage?.oldCategory, Localize.translateLocal('common.category'), true); + return getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage?.category ?? '', + reportActionOriginalMessage?.oldCategory ?? '', + Localize.translateLocal('common.category'), + true, + ); } const hasModifiedTag = Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldTag') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'tag'); if (hasModifiedTag) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.tag, reportActionOriginalMessage?.oldTag, Localize.translateLocal('common.tag'), true); + return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.tag ?? '', reportActionOriginalMessage?.oldTag ?? '', Localize.translateLocal('common.tag'), true); } const hasModifiedBillable = Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldBillable') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.billable, reportActionOriginalMessage?.oldBillable, Localize.translateLocal('iou.request'), true); + return getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage?.billable ?? '', + reportActionOriginalMessage?.oldBillable ?? '', + Localize.translateLocal('iou.request'), + true, + ); } } @@ -1468,9 +1490,8 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin * * At the moment, we only allow changing one transaction field at a time. */ -function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry, transactionChanges: OnyxEntry, isFromExpenseReport: boolean) { +function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry, transactionChanges: ExpanseOriginalMessage, isFromExpenseReport: boolean) { const originalMessage: ExpanseOriginalMessage = {}; - console.log('getModifiedExpenseOriginalMessage', transactionChanges); // Remark: Comment field is the only one which has new/old prefixes for the keys (newComment/ oldComment), // all others have old/- pattern such as oldCreated/created if (Object.prototype.hasOwnProperty.call(transactionChanges, 'comment')) { @@ -1490,9 +1511,9 @@ function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry | undefined): OnyxEntry): OnyxEntry | Record { +function getRootParentReport(report?: OnyxEntry): OnyxEntry | Record { if (!report) { return {}; } @@ -1561,8 +1582,8 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry | un return `[${Localize.translateLocal('common.attachment')}]`; } if ( - parentReportAction?.message?.[0]?.moderationDecision.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || - parentReportAction?.message?.[0]?.moderationDecision.decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN + parentReportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || + parentReportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN ) { return Localize.translateLocal('parentReportAction.hiddenMessage'); } @@ -1638,7 +1659,7 @@ function getRootReportAndWorkspaceName(report?: OnyxEntry) { /** * Get either the policyName or domainName the chat is tied to */ -function getChatRoomSubtitle(report: OnyxEntry): string { +function getChatRoomSubtitle(report: OnyxEntry): string | undefined { if (isChatThread(report)) { return ''; } @@ -2151,7 +2172,7 @@ function buildOptimisticIOUReportAction( created: DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, receipt, - whisperedToAccountIDs: [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].includes(receipt?.state) ? [currentUserAccountID] : [], + whisperedToAccountIDs: [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === receipt?.state) ? [currentUserAccountID] : [], }; } /** @@ -2791,12 +2812,12 @@ function shouldReportBeInOptionList( excludeEmptyChats = false, ) { const isInDefaultMode = !isInGSDMode; - // Exclude reports that have no data because there wouldn't be anything to show in the option item. // This can happen if data is currently loading from the server or a report is in various stages of being created. // This can also happen for anyone accessing a public room or archived room for which they don't have access to the underlying policy. if ( !report?.reportID || + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing report?.isHidden || (report.participantAccountIDs && report.participantAccountIDs.length === 0 && @@ -2821,6 +2842,7 @@ function shouldReportBeInOptionList( } // Include reports that are relevant to the user in any view mode. Criteria include having a draft, having an outstanding IOU, or being assigned to an open task. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (report.hasDraft || isWaitingForIOUActionFromCurrentUser(report) || isWaitingForTaskCompleteFromAssignee(report)) { return true; } @@ -3025,7 +3047,7 @@ function getReportIDFromLink(url: string | null): string { function hasIOUWaitingOnCurrentUserBankAccount(chatReport: OnyxEntry): boolean { if (chatReport?.iouReportID) { const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReport?.iouReportID}`]; - if (iouReport && iouReport.isWaitingOnBankAccount && iouReport.ownerAccountID === currentUserAccountID) { + if (iouReport?.isWaitingOnBankAccount && iouReport?.ownerAccountID === currentUserAccountID) { return true; } } @@ -3066,7 +3088,7 @@ function canRequestMoney(report: OnyxEntry, participants: number[]) { // User can request money in any IOU report, unless paid, but user can only request money in an expense report // which is tied to their workspace chat. if (isMoneyRequestReport(report)) { - return ((isExpenseReport(report) && isOwnPolicyExpenseChat) || isIOUReport(report)) && !isReportApproved(report) && !isSettled(report.reportID); + return ((isExpenseReport(report) && isOwnPolicyExpenseChat) || isIOUReport(report)) && !isReportApproved(report) && !isSettled(report?.reportID); } // In case of policy expense chat, users can only request money from their own policy expense chat @@ -3451,13 +3473,13 @@ function getReportPreviewDisplayTransactions(reportPreviewAction: OnyxEntry) { - const originalMessage = reportAction?.originalMessage; + const originalMessage = reportAction?.originalMessage as IOUMessage; let displayMessage; if (originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { const {amount, currency, IOUReportID} = originalMessage; const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency); - const iouReport = getReport(IOUReportID); - const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport.managerID); + const iouReport = getReport(String(IOUReportID)); + const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport?.managerID); let translationKey; switch (originalMessage.paymentType) { case CONST.IOU.PAYMENT_TYPE.ELSEWHERE: diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 432119330cbb..2fa9f4155fe2 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -91,6 +91,8 @@ type Report = { cachedTotal?: string; chatReportID?: string; state?: ValueOf; + isHidden?: boolean; + lastMessageTranslationKey?: string; }; export default Report; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 8c17bb5111b1..230f5edfd2aa 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -1,4 +1,5 @@ import {ValueOf} from 'type-fest'; +import {SvgProps} from 'react-native-svg'; import OriginalMessage, {Reaction} from './OriginalMessage'; import * as OnyxCommon from './OnyxCommon'; import CONST from '../../CONST'; @@ -73,9 +74,9 @@ type ReportActionBase = { error?: string; /** accountIDs of the people to which the whisper was sent to (if any). Returns empty array if it is not a whisper */ - whisperedToAccountIDs?: number[]; + whisperedToAccountIDs?: Array; - avatar?: string; + avatar?: string | React.FC; automatic?: boolean; shouldShow?: boolean; childReportID?: string; From a36ff94468e2306fef37269478ec94c1c7693032 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 19 Oct 2023 16:36:24 +0200 Subject: [PATCH 07/63] fix: ts errors --- src/libs/ReportActionsUtils.ts | 7 +- src/libs/ReportUtils.ts | 201 +++++++++++++++++---------------- src/libs/TransactionUtils.ts | 4 +- src/types/onyx/ReportAction.ts | 4 +- src/types/onyx/Session.ts | 2 + 5 files changed, 115 insertions(+), 103 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 1f71b290e386..2b7174d40be0 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -67,7 +67,10 @@ function isReversedTransaction(reportAction: OnyxEntry) { return (reportAction?.message?.[0].isReversedTransaction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0; } -function isPendingRemove(reportAction: OnyxEntry): boolean { +function isPendingRemove(reportAction: OnyxEntry | Record): boolean { + if (!reportAction) { + return false; + } return reportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE; } @@ -120,7 +123,7 @@ function isSentMoneyReportAction(reportAction: OnyxEntry): boolean * Returns whether the thread is a transaction thread, which is any thread with IOU parent * report action from requesting money (type - create) or from sending money (type - pay with IOUDetails field) */ -function isTransactionThread(parentReportAction: OnyxEntry): boolean { +function isTransactionThread(parentReportAction: ReportAction): boolean { return ( parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && (parentReportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 09cd29bd9169..780b8b0efc9e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5,6 +5,7 @@ import lodashEscape from 'lodash/escape'; import lodashIsEqual from 'lodash/isEqual'; import lodashFindLastIndex from 'lodash/findLastIndex'; import lodashIntersection from 'lodash/intersection'; + import {ValueOf} from 'type-fest'; import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; @@ -21,6 +22,7 @@ import * as Url from './Url'; import Permissions from './Permissions'; import DateUtils from './DateUtils'; import linkingConfig from './Navigation/linkingConfig'; +import isReportMessageAttachment from './isReportMessageAttachment'; import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAvatars'; import * as CurrencyUtils from './CurrencyUtils'; import * as UserUtils from './UserUtils'; @@ -28,9 +30,10 @@ import {Beta, Login, PersonalDetails, Policy, Report, ReportAction, Transaction} import {Receipt} from '../types/onyx/Transaction'; import DeepValueOf from '../types/utils/DeepValueOf'; import {IOUMessage} from '../types/onyx/OriginalMessage'; -import {Message} from '../types/onyx/ReportAction'; +import {Message, ReportActions} from '../types/onyx/ReportAction'; type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string}; + type Avatar = { id: number; source: React.FC | string; @@ -38,6 +41,7 @@ type Avatar = { name: string; fallbackIcon?: React.FC | string; }; + type ExpanseOriginalMessage = { oldComment?: string; newComment?: string; @@ -56,8 +60,8 @@ type ExpanseOriginalMessage = { oldTag?: string; billable?: string; oldBillable?: string; - }; + type Participant = { accountID: number; alternateText: string; @@ -72,6 +76,14 @@ type Participant = { text: string; }; +function isTypeTransaction(arg: Transaction | Record): arg is Transaction { + return arg !== undefined; // Customize this type guard as needed +} + +function isTypeReportAction(arg: ReportAction | Record): arg is ReportAction { + return arg !== undefined; // Customize this type guard as needed +} + let currentUserEmail: string | undefined; let currentUserAccountID: number | undefined; let isAnonymousUser = false; @@ -86,7 +98,6 @@ Onyx.connect({ currentUserEmail = value.email; currentUserAccountID = value.accountID; - // TODO: There is no such a field so it will always be false should we remove it? isAnonymousUser = value.authTokenType === 'anonymousAccount'; }, }); @@ -111,7 +122,7 @@ Onyx.connect({ let doesDomainHaveApprovedAccountant = false; Onyx.connect({ key: ONYXKEYS.ACCOUNT, - waitForCollectionCallback: true, + // waitForCollectionCallback: true, callback: (value) => (doesDomainHaveApprovedAccountant = value?.doesDomainHaveApprovedAccountant ?? false), }); @@ -318,7 +329,7 @@ function isAnnounceRoom(report: OnyxEntry): boolean { * Whether the provided report is a default room */ function isDefaultRoom(report: OnyxEntry): boolean { - return [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL].indexOf(getChatType(report) ?? '') > -1; + return [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL].indexOf(getChatType(report)) > -1; } /** @@ -577,14 +588,6 @@ function isWorkspaceThread(report: OnyxEntry): boolean { return Boolean(isThread(report) && !isDM(report)); } -/** - * Returns true if reportAction has a child. - */ -// TODO: It's not used anywhere should I remove it? -function isThreadParent(reportAction: OnyxEntry): boolean { - return reportAction?.childReportID !== 0; -} - /** * Returns true if reportAction is the first chat preview of a Thread */ @@ -607,7 +610,7 @@ function isExpenseRequest(report?: OnyxEntry): boolean { if (report && isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; - return isExpenseReport(parentReport) && ReportActionsUtils.isTransactionThread(parentReportAction); + return isExpenseReport(parentReport) && isTypeReportAction(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); } return false; } @@ -620,7 +623,7 @@ function isIOURequest(report?: OnyxEntry): boolean { if (report && isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; - return isIOUReport(parentReport) && ReportActionsUtils.isTransactionThread(parentReportAction); + return isIOUReport(parentReport) && isTypeReportAction(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); } return false; } @@ -757,7 +760,7 @@ function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAcc } const reportParticipants = finalParticipantAccountIDs?.filter((accountID) => accountID !== currentLoginAccountID) ?? []; - const participantsWithoutExpensifyAccountIDs = _.difference(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS); + const participantsWithoutExpensifyAccountIDs = reportParticipants.filter((participant) => !CONST.EXPENSIFY_ACCOUNT_IDS.includes(participant ?? 0)); return participantsWithoutExpensifyAccountIDs; } @@ -895,11 +898,11 @@ function getIcons( const parentReportAction = ReportActionsUtils.getParentReportAction(report); const workspaceIcon = getWorkspaceIcon(report, policy); const memberIcon = { - source: UserUtils.getAvatar(personalDetails?.[parentReportAction.actorAccountID]?.avatar ?? '', parentReportAction.actorAccountID), + source: UserUtils.getAvatar(personalDetails?.[parentReportAction.actorAccountID ?? -1]?.avatar ?? '', parentReportAction.actorAccountID ?? -1), id: parentReportAction.actorAccountID, type: CONST.ICON_TYPE_AVATAR, - name: personalDetails?.[parentReportAction.actorAccountID]?.displayName ?? '', - fallbackIcon: personalDetails?.[parentReportAction.actorAccountID]?.fallbackIcon, + name: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.displayName ?? '', + fallbackIcon: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.fallbackIcon, }; return [memberIcon, workspaceIcon]; @@ -907,11 +910,11 @@ function getIcons( if (isChatThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const actorAccountID = parentReportAction[actorAccountID ?? -1] ?? -1; - const actorDisplayName = allPersonalDetails?.[actorAccountID]?.displayName ?? ''; + const actorAccountID = parentReportAction.actorAccountID; + const actorDisplayName = allPersonalDetails?.[actorAccountID ?? -1]?.displayName ?? ''; const actorIcon = { id: actorAccountID, - source: UserUtils.getAvatar(personalDetails?.[actorAccountID]?.avatar ?? '', actorAccountID), + source: UserUtils.getAvatar(personalDetails?.[actorAccountID ?? -1]?.avatar ?? '', actorAccountID ?? -1), name: actorDisplayName, type: CONST.ICON_TYPE_AVATAR, fallbackIcon: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.fallbackIcon, @@ -1053,10 +1056,9 @@ function getDisplayNamesWithTooltips(personalDetailsList: PersonalDetails[], isM * For a deleted parent report action within a chat report, * let us return the appropriate display message * - * @param {Object} reportAction - The deleted report action of a chat report for which we need to return message. - * @return {String} + * @param reportAction - The deleted report action of a chat report for which we need to return message. */ -function getDeletedParentActionMessageForChatReport(reportAction) { +function getDeletedParentActionMessageForChatReport(reportAction: OnyxEntry): string { // By default, let us display [Deleted message] let deletedMessageText = Localize.translateLocal('parentReportAction.deletedMessage'); if (ReportActionsUtils.isCreatedTaskReportAction(reportAction)) { @@ -1069,16 +1071,16 @@ function getDeletedParentActionMessageForChatReport(reportAction) { /** * Returns the last visible message for a given report after considering the given optimistic actions * - * @param {String} reportID - the report for which last visible message has to be fetched - * @param {Object} [actionsToMerge] - the optimistic merge actions that needs to be considered while fetching last visible message - * @return {Object} + * @param reportID - the report for which last visible message has to be fetched + * @param [actionsToMerge] - the optimistic merge actions that needs to be considered while fetching last visible message + */ -function getLastVisibleMessage(reportID, actionsToMerge = {}) { +function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: ReportActions = {}) { const report = getReport(reportID); - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, actionsToMerge); + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID ?? '', actionsToMerge); // For Chat Report with deleted parent actions, let us fetch the correct message - if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && isChatReport(report)) { + if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && report && isChatReport(report)) { const lastMessageText = getDeletedParentActionMessageForChatReport(lastVisibleAction); return { lastMessageText, @@ -1086,7 +1088,7 @@ function getLastVisibleMessage(reportID, actionsToMerge = {}) { } // Fetch the last visible message for report represented by reportID and based on actions to merge. - return ReportActionsUtils.getLastVisibleMessage(reportID, actionsToMerge); + return ReportActionsUtils.getLastVisibleMessage(reportID ?? '', actionsToMerge); } /** @@ -1140,26 +1142,17 @@ function isWaitingForTaskCompleteFromAssignee(report: OnyxEntry, parentR return isTaskReport(report) && isReportManager(report) && isOpenTaskReport(report, parentReportAction); } -function getMoneyRequestTotal(report?: OnyxEntry, allReportsDict: OnyxCollection = null): number { - const allAvailableReports = allReportsDict ?? allReports; /** * Returns number of transactions that are nonReimbursable * - * @param {Object|null} iouReportID - * @returns {Number} */ -function hasNonReimbursableTransactions(iouReportID) { +function hasNonReimbursableTransactions(iouReportID: string | undefined) { const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); - return _.filter(allTransactions, (transaction) => transaction.reimbursable === false).length > 0; + return allTransactions.filter((transaction) => transaction.reimbursable === false).length > 0; } -/** - * @param {Object} report - * @param {Object} allReportsDict - * @returns {Number} - */ -function getMoneyRequestReimbursableTotal(report, allReportsDict = null) { - const allAvailableReports = allReportsDict || allReports; +function getMoneyRequestReimbursableTotal(report: OnyxEntry | undefined, allReportsDict?: OnyxCollection): number { + const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport; if (isMoneyRequestReport(report)) { moneyRequestReport = report; @@ -1180,23 +1173,18 @@ function getMoneyRequestReimbursableTotal(report, allReportsDict = null) { return 0; } -/** - * @param {Object} report - * @param {Object} allReportsDict - * @returns {Object} - */ -function getMoneyRequestSpendBreakdown(report, allReportsDict = null) { - const allAvailableReports = allReportsDict || allReports; +function getMoneyRequestSpendBreakdown(report: OnyxEntry, allReportsDict: OnyxCollection = null) { + const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport; if (isMoneyRequestReport(report)) { moneyRequestReport = report; } - if (allAvailableReports && report.hasOutstandingIOU && report.iouReportID) { + if (allAvailableReports && report?.hasOutstandingIOU && report?.iouReportID) { moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; } if (moneyRequestReport) { - let nonReimbursableSpend = lodashGet(moneyRequestReport, 'nonReimbursableTotal', 0); - let reimbursableSpend = lodashGet(moneyRequestReport, 'total', 0); + let nonReimbursableSpend = moneyRequestReport.nonReimbursableTotal ?? 0; + let reimbursableSpend = moneyRequestReport.total ?? 0; if (nonReimbursableSpend + reimbursableSpend !== 0) { // There is a possibility that if the Expense report has a negative total. @@ -1277,9 +1265,11 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< * into a flat object. Used for displaying transactions and sending them in API commands */ -// TODO: Check if this shouldn't be OnyxEntry -function getTransactionDetails(transaction: OnyxEntry, createdDateFormat:string = CONST.DATE.FNS_FORMAT_STRING) { +function getTransactionDetails(transaction: OnyxEntry, createdDateFormat: string = CONST.DATE.FNS_FORMAT_STRING) { const report = getReport(transaction?.reportID); + if (!transaction) { + return; + } return { created: TransactionUtils.getCreated(transaction, createdDateFormat), amount: TransactionUtils.getAmount(transaction, isExpenseReport(report)), @@ -1338,11 +1328,7 @@ function canEditReportAction(reportAction: OnyxEntry): boolean { isCommentOrIOU && canEditMoneyRequest(reportAction) && // Returns true for non-IOU actions reportAction?.message && - !isReportMessageAttachment({ - text: reportAction?.message?.[0].text, - html: reportAction?.message?.[0].html ?? '', - translationKey: reportAction?.message?.[0].translationKey ?? '', - }) && + !isReportMessageAttachment(reportAction.message[0] ?? {}) && !ReportActionsUtils.isDeletedAction(reportAction) && !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && reportAction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, @@ -1396,6 +1382,9 @@ function getTransactionReportName(reportAction: OnyxEntry) { } const transaction = TransactionUtils.getLinkedTransaction(reportAction); + if (!isTypeTransaction(transaction)) { + return ''; + } if (TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { return Localize.translateLocal('iou.receiptScanning'); } @@ -1404,11 +1393,11 @@ function getTransactionReportName(reportAction: OnyxEntry) { return Localize.translateLocal('iou.receiptMissingDetails'); } - const {amount, currency, comment} = getTransactionDetails(transaction); + const transactionDetails = getTransactionDetails(transaction); return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) ? 'iou.threadSentMoneyReportName' : 'iou.threadRequestReportName', { - formattedAmount: CurrencyUtils.convertToDisplayString(amount, currency), - comment, + formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''), + comment: transactionDetails?.comment, }); } @@ -1429,18 +1418,22 @@ function getReportPreviewMessage(report: OnyxEntry, reportAction?: OnyxE return reportActionMessage; } - if (!isIOUReport(report) && ReportActionsUtils.isSplitBillAction(reportAction)) { + if (!isIOUReport(report) && reportAction && ReportActionsUtils.isSplitBillAction(reportAction)) { // This covers group chats where the last action is a split bill action const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); - if (_.isEmpty(linkedTransaction)) { + if (Object.keys(linkedTransaction ?? {}).length === 0) { return reportActionMessage; } - if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { - return Localize.translateLocal('iou.receiptScanning'); + + if (isTypeTransaction(linkedTransaction)) { + if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + return Localize.translateLocal('iou.receiptScanning'); + } + + const transactionDetails = getTransactionDetails(linkedTransaction); + const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); + return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment: transactionDetails?.comment}); } - const {amount, currency, comment} = getTransactionDetails(linkedTransaction); - const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency); - return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment}); } const totalAmount = getMoneyRequestReimbursableTotal(report); @@ -1451,10 +1444,10 @@ function getReportPreviewMessage(report: OnyxEntry, reportAction?: OnyxE return `approved ${formattedAmount}`; } - if (shouldConsiderReceiptBeingScanned && ReportActionsUtils.isMoneyRequestAction(reportAction)) { + if (shouldConsiderReceiptBeingScanned && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); - if (Object.keys(linkedTransaction ?? {}).length !== 0 && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + if (isTypeTransaction(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { return Localize.translateLocal('iou.receiptScanning'); } } @@ -1694,6 +1687,9 @@ function getRootParentReport(report?: OnyxEntry): OnyxEntry | Re function getReportName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string { let formattedName; const parentReportAction = ReportActionsUtils.getParentReportAction(report); + if (!isTypeReportAction(parentReportAction)) { + return ''; + } if (isChatThread(report)) { if (ReportActionsUtils.isTransactionThread(parentReportAction)) { return getTransactionReportName(parentReportAction); @@ -1793,6 +1789,7 @@ function getChatRoomSubtitle(report: OnyxEntry): string | undefined { // The domainAll rooms are just #domainName, so we ignore the prefix '#' to get the domainName return report?.reportName?.substring(1) ?? ''; } + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if ((isPolicyExpenseChat(report) && report?.isOwnPolicyExpenseChat) || isExpenseReport(report)) { return Localize.translateLocal('workspace.common.workspace'); } @@ -1821,7 +1818,6 @@ function getParentNavigationSubtitle(report: OnyxEntry) { /** * Navigate to the details page of a given report * - * @param {Object} report */ function navigateToDetailsPage(report: OnyxEntry) { const participantAccountIDs = report?.participantAccountIDs ?? []; @@ -1830,7 +1826,7 @@ function navigateToDetailsPage(report: OnyxEntry) { Navigation.navigate(ROUTES.PROFILE.getRoute(participantAccountIDs[0])); return; } - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? "")); + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '')); } /** @@ -1967,8 +1963,11 @@ function getOptimisticDataForParentReportAction( parentReportActionID = '', ): OnyxUpdate | Record { const report = getReport(reportID); + if (!report) { + return {}; + } const parentReportAction = ReportActionsUtils.getParentReportAction(report); - if (!parentReportAction) { + if (!parentReportAction || !isTypeReportAction(parentReportAction)) { return {}; } @@ -2042,6 +2041,7 @@ type OptimisticIOUReport = Pick< | 'reportName' | 'notificationPreference' | 'parentReportID' + | 'statusNum' >; function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number, total: number, chatReportID: string, currency: string, isSendingMoney = false): OptimisticIOUReport { const formattedTotal = CurrencyUtils.convertToDisplayString(total, currency); @@ -2134,9 +2134,10 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa * @param isSettlingUp - Whether we are settling up an IOU */ function getIOUReportActionMessage(iouReportID: string, type: string, total: number, comment: string, currency: string, paymentType = '', isSettlingUp = false): [Message] { + const report = getReport(iouReportID); const amount = type === CONST.IOU.REPORT_ACTION_TYPE.PAY - ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(getReport(iouReportID)), currency) + ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(report), currency) : CurrencyUtils.convertToDisplayString(total, currency); let paymentMethodMessage; @@ -2368,9 +2369,9 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string, * @param [comment] - User comment for the IOU. * @param [transaction] - optimistic first transaction of preview */ -function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: OnyxEntry, comment = '', transaction: OnyxEntry | undefined = undefined) { +function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: OnyxEntry, comment = '', transaction: Transaction | undefined = undefined) { const hasReceipt = TransactionUtils.hasReceipt(transaction); - const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); + const isReceiptBeingScanned = hasReceipt && transaction && TransactionUtils.isReceiptBeingScanned(transaction); const message = getReportPreviewMessage(iouReport); const created = DateUtils.getDBTime(); return { @@ -2395,7 +2396,7 @@ function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.managerID ?? 0, childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, - childRecentReceiptTransactionIDs: hasReceipt ? {[transaction.transactionID]: created} : [], + childRecentReceiptTransactionIDs: hasReceipt && transaction ? {[transaction.transactionID]: created} : [], whisperedToAccountIDs: isReceiptBeingScanned ? [currentUserAccountID] : [], }; } @@ -2406,7 +2407,7 @@ function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: function buildOptimisticModifiedExpenseReportAction( transactionThread: OnyxEntry, oldTransaction: OnyxEntry, - transactionChanges: OnyxEntry, + transactionChanges: ExpanseOriginalMessage, isFromExpenseReport: boolean, ) { const originalMessage = getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport); @@ -2460,7 +2461,10 @@ function updateReportPreview( const hasReceipt = TransactionUtils.hasReceipt(transaction); const recentReceiptTransactions = reportPreviewAction?.childRecentReceiptTransactionIDs ?? {}; const transactionsToKeep = TransactionUtils.getRecentTransactions(recentReceiptTransactions); - const previousTransactions = _.mapObject(recentReceiptTransactions, (value, key) => (_.contains(transactionsToKeep, key) ? value : null)); + const previousTransactions = Object.entries(recentReceiptTransactions ?? {}).map((item) => { + const [key, value] = item; + return transactionsToKeep.includes(key) ? value : null; + }); const message = getReportPreviewMessage(iouReport, reportPreviewAction); return { @@ -2478,7 +2482,7 @@ function updateReportPreview( childMoneyRequestCount: (reportPreviewAction?.childMoneyRequestCount ?? 0) + (isPayRequest ? 0 : 1), childRecentReceiptTransactionIDs: hasReceipt ? { - [transaction.transactionID]: transaction?.created, + ...(transaction && {[transaction.transactionID]: transaction?.created}), ...previousTransactions, } : recentReceiptTransactions, @@ -2901,7 +2905,7 @@ function canSeeDefaultRoom(report: OnyxEntry, policies: OnyxCollection

, policies: OnyxCollection, betas: Beta[], allReportActions?: OnyxCollection): boolean { +function canAccessReport(report: OnyxEntry, policies: OnyxCollection, betas: Beta[], allReportActions?: OnyxCollection): boolean { if (isThread(report) && ReportActionsUtils.isPendingRemove(ReportActionsUtils.getParentReportAction(report, allReportActions))) { return false; } @@ -2918,7 +2922,7 @@ function canAccessReport(report: OnyxEntry, policies: OnyxCollection, currentReportId: string): boolean { const parentReport = getParentReport(getReport(currentReportId)); - const reportActions = ReportActionsUtils.getAllReportActions(report?.reportID); + const reportActions = ReportActionsUtils.getAllReportActions(report?.reportID ?? ''); const isChildReportHasComment = Object.values(reportActions ?? {})?.some((reportAction) => (reportAction?.childVisibleActionCount ?? 0) > 0); return parentReport?.reportID !== report?.reportID && !isChildReportHasComment; } @@ -2937,7 +2941,7 @@ function shouldReportBeInOptionList( isInGSDMode: boolean, betas: Beta[], policies: OnyxCollection, - allReportActions: OnyxCollection, + allReportActions: OnyxCollection, excludeEmptyChats = false, ) { const isInDefaultMode = !isInGSDMode; @@ -2945,9 +2949,9 @@ function shouldReportBeInOptionList( // This can happen if data is currently loading from the server or a report is in various stages of being created. // This can also happen for anyone accessing a public room or archived room for which they don't have access to the underlying policy. if ( - !report || - !report.reportID || + !report?.reportID || !report.type || + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing report.isHidden || (report.participantAccountIDs && report.participantAccountIDs.length === 0 && @@ -3398,8 +3402,10 @@ function shouldDisableWriteActions(report: OnyxEntry): boolean { * Returns ID of the original report from which the given reportAction is first created. */ function getOriginalReportID(reportID: string, reportAction: OnyxEntry): string | undefined { - const currentReportAction = ReportActionsUtils.getReportAction(reportID, reportAction?.reportActionID); - return isThreadFirstChat(reportAction, reportID) && _.isEmpty(currentReportAction) ? lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, 'parentReportID']) : reportID; + const currentReportAction = ReportActionsUtils.getReportAction(reportID, reportAction?.reportActionID ?? ''); + return isThreadFirstChat(reportAction, reportID) && Object.keys(currentReportAction ?? {}).length === 0 + ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.parentReportID + : reportID; } /** @@ -3595,8 +3601,8 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) if (originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { const {amount, currency, IOUReportID} = originalMessage; const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency); - const iouReport = getReport(IOUReportID); - const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport.managerID, true); + const iouReport = getReport(String(IOUReportID) ?? ''); + const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport?.managerID, true); let translationKey; switch (originalMessage.paymentType) { case CONST.IOU.PAYMENT_TYPE.ELSEWHERE: @@ -3612,12 +3618,12 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) } displayMessage = Localize.translateLocal(translationKey, {amount: formattedAmount, payer: payerName}); } else { - const transaction = TransactionUtils.getTransaction(originalMessage.IOUTransactionID); - const {amount, currency, comment} = getTransactionDetails(transaction); - const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency); + const transaction = TransactionUtils.getTransaction(originalMessage.IOUTransactionID ?? ''); + const transactionDetails = getTransactionDetails(transaction); + const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); displayMessage = Localize.translateLocal('iou.requestedAmount', { formattedAmount, - comment, + comment: transactionDetails?.comment, }); } return displayMessage; @@ -3740,7 +3746,6 @@ export { getWorkspaceAvatar, isThread, isChatThread, - isThreadParent, isThreadFirstChat, isChildReport, shouldReportShowSubscript, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index b5afd52a136f..0be06ed554ba 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -308,7 +308,7 @@ function getTag(transaction: OnyxEntry): string { /** * Return the created field from the transaction, return the modifiedCreated if present. */ -function getCreated(transaction: Transaction, dateFormat: string = CONST.DATE.FNS_FORMAT_STRING): string { +function getCreated(transaction: OnyxEntry, dateFormat: string = CONST.DATE.FNS_FORMAT_STRING): string { const created = transaction?.modifiedCreated ? transaction.modifiedCreated : transaction?.created || ''; const createdDate = parseISO(created); if (isValid(createdDate)) { @@ -378,7 +378,7 @@ function hasRoute(transaction: Transaction): boolean { * * @deprecated Use Onyx.connect() or withOnyx() instead */ -function getLinkedTransaction(reportAction?: OnyxEntry): OnyxEntry { +function getLinkedTransaction(reportAction?: OnyxEntry): Transaction | Record { let transactionID = ''; if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 8736ea06e97d..f589b15a9552 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -1,6 +1,6 @@ import {ValueOf} from 'type-fest'; import {SvgProps} from 'react-native-svg'; -import OriginalMessage, {Reaction} from './OriginalMessage'; +import OriginalMessage, {Decision, Reaction} from './OriginalMessage'; import * as OnyxCommon from './OnyxCommon'; import CONST from '../../CONST'; import {Receipt} from './Transaction'; @@ -43,6 +43,7 @@ type Message = { reactions?: Reaction[]; taskReportID?: string; translationKey?: string; + moderationDecision?: Decision; }; type Person = { @@ -113,6 +114,7 @@ type ReportActionBase = { errors?: OnyxCommon.Errors; isAttachment?: boolean; + childRecentReceiptTransactionIDs?: Record; }; type ReportAction = ReportActionBase & OriginalMessage; diff --git a/src/types/onyx/Session.ts b/src/types/onyx/Session.ts index 62930e3b2c27..e6658ff04835 100644 --- a/src/types/onyx/Session.ts +++ b/src/types/onyx/Session.ts @@ -7,6 +7,8 @@ type Session = { /** Currently logged in user authToken */ authToken?: string; + authTokenType?: string; + supportAuthToken?: string; /** Currently logged in user encrypted authToken */ From ba218c5c44f5c59fb18e3231c7b6ca9dfee35862 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 20 Oct 2023 15:22:41 +0200 Subject: [PATCH 08/63] fix: fixing type issues --- src/libs/ReportUtils.ts | 100 +++++++++++++++--------------- src/libs/TransactionUtils.ts | 12 ++-- src/libs/UserUtils.ts | 4 +- src/libs/actions/ReportActions.ts | 2 +- src/types/onyx/OriginalMessage.ts | 12 +++- src/types/onyx/PersonalDetails.ts | 2 +- src/types/onyx/Report.ts | 1 - src/types/onyx/ReportAction.ts | 1 + 8 files changed, 71 insertions(+), 63 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 780b8b0efc9e..c5d914311a06 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -29,14 +29,14 @@ import * as UserUtils from './UserUtils'; import {Beta, Login, PersonalDetails, Policy, Report, ReportAction, Transaction} from '../types/onyx'; import {Receipt} from '../types/onyx/Transaction'; import DeepValueOf from '../types/utils/DeepValueOf'; -import {IOUMessage} from '../types/onyx/OriginalMessage'; +import {Closed, IOUMessage} from '../types/onyx/OriginalMessage'; import {Message, ReportActions} from '../types/onyx/ReportAction'; type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string}; type Avatar = { id: number; - source: React.FC | string; + source: React.FC | string | undefined; type: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; name: string; fallbackIcon?: React.FC | string; @@ -162,7 +162,7 @@ function getPolicyType(report: OnyxEntry, policies: OnyxCollection, returnEmptyIfNotFound = false, policy: OnyxEntry = undefined): string | undefined { +function getPolicyName(report?: OnyxEntry, returnEmptyIfNotFound = false, policy: OnyxEntry = undefined): string { const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); if (Object.keys(report ?? {}).length === 0) { return noPolicyFound; @@ -223,7 +223,7 @@ function isTaskReport(report: OnyxEntry): boolean { * In this case, we have added the key to the report itself */ function isCanceledTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry): boolean { - if (Object.keys(parentReportAction ?? {}).length > 0 && (parentReportAction?.message?.[0].isDeletedParentAction ?? false)) { + if (Object.keys(parentReportAction ?? {}).length > 0 && (parentReportAction?.message?.[0]?.isDeletedParentAction ?? false)) { return true; } @@ -329,7 +329,7 @@ function isAnnounceRoom(report: OnyxEntry): boolean { * Whether the provided report is a default room */ function isDefaultRoom(report: OnyxEntry): boolean { - return [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL].indexOf(getChatType(report)) > -1; + return [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL].some((type) => type === getChatType(report)); } /** @@ -578,7 +578,7 @@ function isDM(report?: OnyxEntry): boolean { * Returns true if report has a single participant. */ function hasSingleParticipant(report?: OnyxEntry): boolean { - return Boolean(report?.participantAccountIDs?.length === 1); + return report?.participantAccountIDs?.length === 1; } /** @@ -662,10 +662,11 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: const isActionOwner = reportAction?.actorAccountID === currentUserAccountID; if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { + const originalMessage = reportAction?.originalMessage as IOUMessage; // For now, users cannot delete split actions - const isSplitAction = reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; + const isSplitAction = originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; - if (isSplitAction || isSettled(reportAction?.originalMessage?.IOUReportID) || isReportApproved(report)) { + if (isSplitAction || isSettled(String(originalMessage?.IOUReportID)) || isReportApproved(report)) { return false; } @@ -734,7 +735,7 @@ function hasAutomatedExpensifyAccountIDs(accountIDs: number[]): boolean { return accountIDs.filter((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)).length > 0; } -function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAccountID: number): Array { +function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAccountID: number): number[] { let finalReport: OnyxEntry | undefined = report; // In 1:1 chat threads, the participants will be the same as parent report. If a report is specifically a 1:1 chat thread then we will // get parent report and use its participants array. @@ -745,16 +746,16 @@ function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAcc } } - let finalParticipantAccountIDs: Array | undefined = []; + let finalParticipantAccountIDs: number[] | undefined = []; if (isMoneyRequestReport(report)) { // For money requests i.e the IOU (1:1 person) and Expense (1:* person) reports, use the full `initialParticipantAccountIDs` array // and add the `ownerAccountId`. Money request reports don't add `ownerAccountId` in `participantAccountIDs` array const defaultParticipantAccountIDs = finalReport?.participantAccountIDs ?? []; - const setOfParticipantAccountIDs = new Set([...defaultParticipantAccountIDs, ...[report?.ownerAccountID]]); + const setOfParticipantAccountIDs = new Set(report?.ownerAccountID ? [...defaultParticipantAccountIDs, report.ownerAccountID] : defaultParticipantAccountIDs); finalParticipantAccountIDs = [...setOfParticipantAccountIDs]; // Task reports `managerID` will change when assignee is changed, in that case the old `managerID` is still present in `participantAccountIDs` // array along with the new one. We only need the `managerID` as a participant here. - finalParticipantAccountIDs = [report?.managerID]; + finalParticipantAccountIDs = report?.managerID ? [report?.managerID] : []; } else { finalParticipantAccountIDs = finalReport?.participantAccountIDs; } @@ -800,7 +801,8 @@ function getDefaultWorkspaceAvatar(workspaceName?: string): React.FC { .replace(/[^0-9a-z]/gi, '') .toUpperCase(); - const defaultWorkspaceAvatar = defaultWorkspaceAvatars[`Workspace${alphaNumeric[0]}`]; + const workspace = `Workspace${alphaNumeric[0]}` as keyof typeof defaultWorkspaceAvatars; + const defaultWorkspaceAvatar = defaultWorkspaceAvatars[workspace]; return !alphaNumeric ? defaultWorkspaceAvatars.WorkspaceBuilding : defaultWorkspaceAvatar; } @@ -815,13 +817,13 @@ function getWorkspaceAvatar(report: OnyxEntry) { * The Avatar sources can be URLs or Icon components according to the chat type. */ function getIconsForParticipants(participants: number[], personalDetails: OnyxCollection) { - const participantDetails: Array<[number, string, string | React.FC, React.FC | string]> = []; + const participantDetails: Array<[number, string, string | React.FC, string | React.FC]> = []; const participantsList = participants || []; for (const accountID of participantsList) { const avatarSource = UserUtils.getAvatar(personalDetails?.[accountID]?.avatar ?? '', accountID); - const displayNameLogin = personalDetails?.[accountID]?.displayName ?? personalDetails?.[accountID]?.login ?? ''; - participantDetails.push([accountID, displayNameLogin, avatarSource, personalDetails?.[accountID]?.fallbackIcon ?? '']); + const displayNameLogin = personalDetails?.[accountID]?.displayName ? personalDetails?.[accountID]?.displayName : personalDetails?.[accountID]?.login; + participantDetails.push([accountID, displayNameLogin ?? '', avatarSource, personalDetails?.[accountID]?.fallbackIcon ?? '']); } const sortedParticipantDetails = participantDetails.sort((first, second) => { @@ -834,7 +836,7 @@ function getIconsForParticipants(participants: number[], personalDetails: OnyxCo // Then fallback on accountID as the final sorting criteria. // This will ensure that the order of avatars with same login/displayName // stay consistent across all users and devices - return first[0] > second[0]; + return first[0] - second[0]; }); // Now that things are sorted, gather only the avatars (second element in the array) and return those @@ -1030,7 +1032,10 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f } } -function getDisplayNamesWithTooltips(personalDetailsList: PersonalDetails[], isMultipleParticipantReport: boolean) { +function getDisplayNamesWithTooltips( + personalDetailsList: PersonalDetails[], + isMultipleParticipantReport: boolean, +): Array> { return personalDetailsList?.map?.((user) => { const accountID = Number(user.accountID); const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport) ?? user.login ?? ''; @@ -1099,7 +1104,7 @@ function isWaitingForIOUActionFromCurrentUser(report: OnyxEntry): boolea return false; } - if (isArchivedRoom(getReport(report.parentReportID ?? ''))) { + if (isArchivedRoom(getReport(report.parentReportID))) { return false; } @@ -1224,7 +1229,8 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry

): boolean { /** * Gets all transactions on an IOU report with a receipt */ -function getTransactionsWithReceipts(iouReportID: string | undefined) { +function getTransactionsWithReceipts(iouReportID: string | undefined): Transaction[] { const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); return allTransactions.filter((transaction) => TransactionUtils.hasReceipt(transaction)); } @@ -1455,8 +1461,9 @@ function getReportPreviewMessage(report: OnyxEntry, reportAction?: OnyxE if (isSettled(report.reportID)) { // A settled report preview message can come in three formats "paid ... elsewhere" or "paid ... with Expensify" let translatePhraseKey = 'iou.paidElsewhereWithAmount'; + const originalMessage = reportAction?.originalMessage as IOUMessage; if ( - [CONST.IOU.PAYMENT_TYPE.VBBA, CONST.IOU.PAYMENT_TYPE.EXPENSIFY].includes(reportAction?.originalMessage?.paymentType) || + [CONST.IOU.PAYMENT_TYPE.VBBA, CONST.IOU.PAYMENT_TYPE.EXPENSIFY].some((paymentType) => paymentType === originalMessage?.paymentType) || reportActionMessage.match(/ (with Expensify|using Expensify)$/) ) { translatePhraseKey = 'iou.paidWithExpensifyWithAmount'; @@ -1519,13 +1526,12 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin } const hasModifiedAmount = - Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldAmount') && - Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldCurrency') && - Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'amount') && - Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'currency'); + Object.hasOwn(reportActionOriginalMessage, 'oldAmount') && + Object.hasOwn(reportActionOriginalMessage, 'oldCurrency') && + Object.hasOwn(reportActionOriginalMessage, 'amount') && + Object.hasOwn(reportActionOriginalMessage, 'currency'); - const hasModifiedMerchant = - Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldMerchant') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'merchant'); + const hasModifiedMerchant = Object.hasOwn(reportActionOriginalMessage, 'oldMerchant') && Object.hasOwn(reportActionOriginalMessage, 'merchant'); if (hasModifiedAmount) { const oldCurrency = reportActionOriginalMessage?.oldCurrency; const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency ?? ''); @@ -1542,8 +1548,7 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin return getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); } - const hasModifiedComment = - Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldComment') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'newComment'); + const hasModifiedComment = Object.hasOwn(reportActionOriginalMessage, 'oldComment') && Object.hasOwn(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { return getProperSchemaForModifiedExpenseMessage( reportActionOriginalMessage?.newComment ?? '', @@ -1553,8 +1558,7 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin ); } - const hasModifiedCreated = - Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldCreated') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'created'); + const hasModifiedCreated = Object.hasOwn(reportActionOriginalMessage, 'oldCreated') && Object.hasOwn(reportActionOriginalMessage, 'created'); if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp let formattedOldCreated: Date | string = parseISO(reportActionOriginalMessage?.oldCreated ?? ''); @@ -1572,8 +1576,7 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin ); } - const hasModifiedCategory = - Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldCategory') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'category'); + const hasModifiedCategory = Object.hasOwn(reportActionOriginalMessage, 'oldCategory') && Object.hasOwn(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { return getProperSchemaForModifiedExpenseMessage( reportActionOriginalMessage?.category ?? '', @@ -1583,13 +1586,12 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin ); } - const hasModifiedTag = Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldTag') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'tag'); + const hasModifiedTag = Object.hasOwn(reportActionOriginalMessage, 'oldTag') && Object.hasOwn(reportActionOriginalMessage, 'tag'); if (hasModifiedTag) { return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.tag ?? '', reportActionOriginalMessage?.oldTag ?? '', Localize.translateLocal('common.tag'), true); } - const hasModifiedBillable = - Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'oldBillable') && Object.prototype.hasOwnProperty.call(reportActionOriginalMessage, 'billable'); + const hasModifiedBillable = Object.hasOwn(reportActionOriginalMessage, 'oldBillable') && Object.hasOwn(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { return getProperSchemaForModifiedExpenseMessage( reportActionOriginalMessage?.billable ?? '', @@ -1610,39 +1612,39 @@ function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry; function buildOptimisticIOUReportAction( - type: string, + type: ValueOf, amount: number, currency: string, comment: string, participants: Participant[], - transactionID = '', - paymentType = '', + transactionID: string, + paymentType: DeepValueOf, iouReportID = '', isSettlingUp = false, isSendMoneyFlow = false, @@ -3103,7 +3105,7 @@ function getNewMarkerReportActionID(report: OnyxEntry, sortedAndFiltered const newMarkerIndex = lodashFindLastIndex(sortedAndFilteredReportActions, (reportAction) => (reportAction.created ?? '') > (report?.lastReadTime ?? '')); - return Object.prototype.hasOwnProperty.call(sortedAndFilteredReportActions[newMarkerIndex], 'reportActionID') ? sortedAndFilteredReportActions[newMarkerIndex].reportActionID : ''; + return Object.hasOwn(sortedAndFilteredReportActions[newMarkerIndex], 'reportActionID') ? sortedAndFilteredReportActions[newMarkerIndex].reportActionID : ''; } /** @@ -3619,7 +3621,7 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) displayMessage = Localize.translateLocal(translationKey, {amount: formattedAmount, payer: payerName}); } else { const transaction = TransactionUtils.getTransaction(originalMessage.IOUTransactionID ?? ''); - const transactionDetails = getTransactionDetails(transaction); + const transactionDetails = transaction && isTypeTransaction(transaction) ? getTransactionDetails(transaction) : undefined; const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); displayMessage = Localize.translateLocal('iou.requestedAmount', { formattedAmount, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 0be06ed554ba..71d0b8000801 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -192,7 +192,7 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra * * @deprecated Use withOnyx() or Onyx.connect() instead */ -function getTransaction(transactionID: string): OnyxEntry { +function getTransaction(transactionID: string): OnyxEntry | Record { return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {}; } @@ -260,7 +260,7 @@ function getOriginalAmount(transaction: Transaction): number { * Return the merchant field from the transaction, return the modifiedMerchant if present. */ function getMerchant(transaction: OnyxEntry): string { - return transaction?.modifiedMerchant ? transaction.modifiedMerchant : transaction?.merchant || ''; + return transaction?.modifiedMerchant ? transaction.modifiedMerchant : transaction?.merchant ?? ''; } /** @@ -309,7 +309,7 @@ function getTag(transaction: OnyxEntry): string { * Return the created field from the transaction, return the modifiedCreated if present. */ function getCreated(transaction: OnyxEntry, dateFormat: string = CONST.DATE.FNS_FORMAT_STRING): string { - const created = transaction?.modifiedCreated ? transaction.modifiedCreated : transaction?.created || ''; + const created = transaction?.modifiedCreated ? transaction.modifiedCreated : transaction?.created ?? ''; const createdDate = parseISO(created); if (isValid(createdDate)) { return format(createdDate, dateFormat); @@ -354,15 +354,15 @@ function isPosted(transaction: Transaction): boolean { return transaction.status === CONST.TRANSACTION.STATUS.POSTED; } -function isReceiptBeingScanned(transaction: Transaction): boolean { - return [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === transaction.receipt.state); +function isReceiptBeingScanned(transaction: OnyxEntry): boolean { + return [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === transaction?.receipt.state); } /** * Check if the transaction has a non-smartscanning receipt and is missing required fields */ function hasMissingSmartscanFields(transaction: OnyxEntry): boolean { - return hasReceipt(transaction) && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction); + return Boolean(transaction && hasReceipt(transaction) && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction)); } /** diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 15bf3c0f1029..193b0a1f641b 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -104,7 +104,7 @@ function getDefaultAvatarURL(accountID: string | number = '', isNewDot = false): * Given a user's avatar path, returns true if user doesn't have an avatar or if URL points to a default avatar * @param [avatarURL] - the avatar source from user's personalDetails */ -function isDefaultAvatar(avatarURL?: string): boolean { +function isDefaultAvatar(avatarURL?: string | React.FC): boolean { if (typeof avatarURL === 'string') { if (avatarURL.includes('images/avatars/avatar_') || avatarURL.includes('images/avatars/default-avatar_') || avatarURL.includes('images/avatars/user/default')) { return true; @@ -131,7 +131,7 @@ function isDefaultAvatar(avatarURL?: string): boolean { * @param avatarURL - the avatar source from user's personalDetails * @param accountID - the accountID of the user */ -function getAvatar(avatarURL: string, accountID: number): React.FC | string { +function getAvatar(avatarURL: string | React.FC, accountID: number): React.FC | string { return isDefaultAvatar(avatarURL) ? getDefaultAvatar(accountID) : avatarURL; } diff --git a/src/libs/actions/ReportActions.ts b/src/libs/actions/ReportActions.ts index 3faa1dbe3574..af9e87224096 100644 --- a/src/libs/actions/ReportActions.ts +++ b/src/libs/actions/ReportActions.ts @@ -19,7 +19,7 @@ function clearReportActionErrors(reportID: string, reportAction: ReportAction) { }); // If there's a linked transaction, delete that too - const linkedTransactionID = ReportActionUtils.getLinkedTransactionID(originalReportID, reportAction.reportActionID); + const linkedTransactionID = ReportActionUtils.getLinkedTransactionID(originalReportID ?? '', reportAction.reportActionID); if (linkedTransactionID) { Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID}`, null); } diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index cbcb042b6bd2..069b4768cafa 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -24,8 +24,8 @@ type IOUMessage = { currency: string; lastModified?: string; participantAccountIDs?: number[]; - type: string; - paymentType?: string; + type: ValueOf; + paymentType?: DeepValueOf; /** Only exists when we are sending money */ IOUDetails?: IOUDetails; }; @@ -66,6 +66,12 @@ type Reaction = { users: User[]; }; +type Closed = { + policyName: string; + reason: ValueOf; + lastModified?: string; +}; + type OriginalMessageAddComment = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT; originalMessage: { @@ -182,4 +188,4 @@ type OriginalMessage = | OriginalMessageReimbursementQueued; export default OriginalMessage; -export type {ChronosOOOEvent, Decision, Reaction, ActionName, IOUMessage}; +export type {ChronosOOOEvent, Decision, Reaction, ActionName, IOUMessage, Closed}; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index d495eb1c6c52..118881a2551b 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -28,7 +28,7 @@ type PersonalDetails = { phoneNumber?: string; /** Avatar URL of the current user from their personal details */ - avatar: string; + avatar: string | React.FC; /** Flag to set when Avatar uploading */ avatarUploading?: boolean; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index e2cd8bb51665..438a0335d452 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -93,7 +93,6 @@ type Report = { chatReportID?: string; state?: ValueOf; isHidden?: boolean; - lastMessageTranslationKey?: string; }; export default Report; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index f589b15a9552..84f5cd987a76 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -44,6 +44,7 @@ type Message = { taskReportID?: string; translationKey?: string; moderationDecision?: Decision; + isReversedTransaction?: boolean; }; type Person = { From 57b42e844fbad216690bf2ed497479e8e8be492a Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 23 Oct 2023 12:12:49 +0200 Subject: [PATCH 09/63] fix: added some return types --- src/libs/ReportUtils.ts | 47 +++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c5d914311a06..cf95905df3e1 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2301,7 +2301,14 @@ function buildOptimisticIOUReportAction( /** * Builds an optimistic APPROVED report action with a randomly generated reportActionID. */ -function buildOptimisticApprovedReportAction(amount: number, currency: string, expenseReportID: string) { +function buildOptimisticApprovedReportAction( + amount: number, + currency: string, + expenseReportID: string, +): Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' +> { const originalMessage = { amount, currency, @@ -2363,6 +2370,20 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string, }; } +type OptimisticReportPreviw = Pick< + ReportAction, + | 'actionName' + | 'reportActionID' + | 'pendingAction' + | 'originalMessage' + | 'message' + | 'created' + | 'actorAccountID' + | 'childMoneyRequestCount' + | 'childLastMoneyRequestComment' + | 'childRecentReceiptTransactionIDs' + | 'whisperedToAccountIDs' +> & {reportID?: string; accountID?: number}; /** * Builds an optimistic report preview action with a randomly generated reportActionID. * @@ -2371,7 +2392,7 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string, * @param [comment] - User comment for the IOU. * @param [transaction] - optimistic first transaction of preview */ -function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: OnyxEntry, comment = '', transaction: Transaction | undefined = undefined) { +function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: OnyxEntry, comment = '', transaction: Transaction | undefined = undefined): OptimisticReportPreviw { const hasReceipt = TransactionUtils.hasReceipt(transaction); const isReceiptBeingScanned = hasReceipt && transaction && TransactionUtils.isReceiptBeingScanned(transaction); const message = getReportPreviewMessage(iouReport); @@ -2398,7 +2419,7 @@ function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.managerID ?? 0, childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, - childRecentReceiptTransactionIDs: hasReceipt && transaction ? {[transaction.transactionID]: created} : [], + childRecentReceiptTransactionIDs: hasReceipt && transaction ? {[transaction.transactionID]: created} : undefined, whisperedToAccountIDs: isReceiptBeingScanned ? [currentUserAccountID] : [], }; } @@ -2442,7 +2463,10 @@ function buildOptimisticModifiedExpenseReportAction( shouldShow: true, }; } - +type UpdateReportPreview = Pick< + ReportAction, + 'created' | 'message' | 'childLastMoneyRequestComment' | 'childMoneyRequestCount' | 'childRecentReceiptTransactionIDs' | 'whisperedToAccountIDs' +>; /** * Updates a report preview action that exists for an IOU report. * @@ -2459,14 +2483,23 @@ function updateReportPreview( isPayRequest = false, comment = '', transaction: OnyxEntry | undefined = undefined, -) { +): UpdateReportPreview { const hasReceipt = TransactionUtils.hasReceipt(transaction); const recentReceiptTransactions = reportPreviewAction?.childRecentReceiptTransactionIDs ?? {}; const transactionsToKeep = TransactionUtils.getRecentTransactions(recentReceiptTransactions); - const previousTransactions = Object.entries(recentReceiptTransactions ?? {}).map((item) => { + const previousTransactionsArray = Object.entries(recentReceiptTransactions ?? {}).map((item) => { const [key, value] = item; - return transactionsToKeep.includes(key) ? value : null; + return transactionsToKeep.includes(key) ? {[key]: value} : null; }); + const previousTransactions: Record = {}; + + for (const obj of previousTransactionsArray) { + for (const key in obj) { + if (Object.hasOwn(obj, key)) { + previousTransactions[key] = obj[key]; + } + } + } const message = getReportPreviewMessage(iouReport, reportPreviewAction); return { From 5aaf3a34186b7da6b6f67f14075cb7a13c70cddb Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 23 Oct 2023 14:37:05 +0200 Subject: [PATCH 10/63] fix: removed usused argument from canCreateRequest and make some ts fixes --- src/libs/ReportUtils.ts | 26 ++++++++++++----------- src/libs/TransactionUtils.ts | 2 +- src/pages/iou/MoneyRequestSelectorPage.js | 9 ++++---- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e030c56fbf59..d5541d2d1d5c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -272,8 +272,8 @@ function sortReportsByLastRead(reports: OnyxCollection): Array report?.reportID && report?.lastReadTime) .sort((a, b) => { - const aTime = a?.lastReadTime ? parseISO(a.lastReadTime) : 0; - const bTime = b?.lastReadTime ? parseISO(b.lastReadTime) : 0; + const aTime = a?.lastReadTime ? a.lastReadTime : 0; + const bTime = b?.lastReadTime ? b.lastReadTime : 0; return Number(aTime) - Number(bTime); }); } @@ -584,12 +584,10 @@ function hasSingleParticipant(report?: OnyxEntry): boolean { /** * Checks whether all the transactions linked to the IOU report are of the Distance Request type * - * @param {string|null} iouReportID - * @returns {boolean} */ -function hasOnlyDistanceRequestTransactions(iouReportID) { +function hasOnlyDistanceRequestTransactions(iouReportID: string | undefined): boolean { const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); - return _.all(allTransactions, (transaction) => TransactionUtils.isDistanceRequest(transaction)); + return allTransactions.every((transaction) => TransactionUtils.isDistanceRequest(transaction)); } /** @@ -1272,6 +1270,7 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName, amount: formattedAmount}); } + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (report?.hasOutstandingIOU || moneyRequestTotal === 0) { return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName, amount: formattedAmount}); } @@ -1576,7 +1575,7 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin // Take only the YYYY-MM-DD value as the original date includes timestamp let formattedOldCreated: Date | string = new Date(reportActionOriginalMessage?.oldCreated ?? ''); formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - g; + return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.created ?? '', formattedOldCreated?.toString?.(), Localize.translateLocal('common.date'), false); } @@ -3124,10 +3123,13 @@ function chatIncludesChronos(report: OnyxEntry): boolean { function canFlagReportAction(reportAction: OnyxEntry, reportID: string | undefined): boolean { const report = getReport(reportID); const isCurrentUserAction = reportAction?.actorAccountID === currentUserAccountID; - + const isOriginalMessageHaveHtml = + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED || + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST; if (ReportActionsUtils.isWhisperAction(reportAction)) { // Allow flagging welcome message whispers as they can be set by any room creator - if (report?.welcomeMessage && !isCurrentUserAction && reportAction?.originalMessage?.html === report.welcomeMessage) { + if (report?.welcomeMessage && !isCurrentUserAction && isOriginalMessageHaveHtml && reportAction?.originalMessage?.html === report.welcomeMessage) { return true; } @@ -3316,7 +3318,7 @@ function canRequestMoney(report: OnyxEntry, participants: number[]) { * None of the options should show in chat threads or if there is some special Expensify account * as a participant of the report. */ -function getMoneyRequestOptions(report: OnyxEntry, reportParticipants: number[]): (typeof CONST.IOU.TYPE)[keyof typeof CONST.IOU.TYPE][] { +function getMoneyRequestOptions(report: OnyxEntry, reportParticipants: number[]): Array<(typeof CONST.IOU.TYPE)[keyof typeof CONST.IOU.TYPE]> { // In any thread or task report, we do not allow any new money requests yet if (isChatThread(report) || isTaskReport(report)) { return []; @@ -3501,12 +3503,12 @@ function getPolicyExpenseChatReportIDByOwner(policyOwner: string) { return expenseChat.reportID; } -function canCreateRequest(report: OnyxEntry, betas: Beta[], iouType: (typeof CONST.IOU.TYPE)[keyof typeof CONST.IOU.TYPE]): boolean { +function canCreateRequest(report: OnyxEntry, iouType: (typeof CONST.IOU.TYPE)[keyof typeof CONST.IOU.TYPE]): boolean { const participantAccountIDs = report?.participantAccountIDs ?? []; if (shouldDisableWriteActions(report)) { return false; } - return getMoneyRequestOptions(report, participantAccountIDs, betas).includes(iouType); + return getMoneyRequestOptions(report, participantAccountIDs).includes(iouType); } function getWorkspaceChats(policyID: string, accountIDs: number[]) { diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 8eb2e287b832..68769e4274ee 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -309,7 +309,7 @@ function getTag(transaction: OnyxEntry): string { * Return the created field from the transaction, return the modifiedCreated if present. */ function getCreated(transaction: OnyxEntry, dateFormat: string = CONST.DATE.FNS_FORMAT_STRING): string { - const created = transaction?.modifiedCreated ? transaction.modifiedCreated : transaction?.created || ''; + const created = transaction?.modifiedCreated ? transaction.modifiedCreated : transaction?.created ?? ''; const createdDate = new Date(created); if (isValid(createdDate)) { return format(createdDate, dateFormat); diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index 979be64f68e9..60069ea13e5a 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -45,14 +45,15 @@ const propTypes = { /** Which tab has been selected */ selectedTab: PropTypes.string, - /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), + // Commenting it for the future migration to TS as its not used now but we have it in Props + // /** Beta features list */ + // betas: PropTypes.arrayOf(PropTypes.string), }; const defaultProps = { selectedTab: CONST.TAB.SCAN, report: {}, - betas: [], + // betas: [], }; function MoneyRequestSelectorPage(props) { @@ -77,7 +78,7 @@ function MoneyRequestSelectorPage(props) { }; // Allow the user to create the request if we are creating the request in global menu or the report can create the request - const isAllowedToCreateRequest = _.isEmpty(props.report.reportID) || ReportUtils.canCreateRequest(props.report, props.betas, iouType); + const isAllowedToCreateRequest = _.isEmpty(props.report.reportID) || ReportUtils.canCreateRequest(props.report, iouType); const prevSelectedTab = usePrevious(props.selectedTab); useEffect(() => { From 0ae6b036cfc1f73a4b35a8d5ef5202042e3c0303 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 24 Oct 2023 10:03:44 +0200 Subject: [PATCH 11/63] fix: resolved few comments from code review --- src/libs/ReportActionsUtils.ts | 2 +- src/libs/ReportUtils.ts | 4 ++-- src/libs/TransactionUtils.ts | 2 +- src/types/onyx/Report.ts | 6 +++--- src/types/onyx/Session.ts | 1 + 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 39650adc63d4..8161086be960 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -127,7 +127,7 @@ function isSentMoneyReportAction(reportAction: OnyxEntry): boolean * Returns whether the thread is a transaction thread, which is any thread with IOU parent * report action from requesting money (type - create) or from sending money (type - pay with IOUDetails field) */ -function isTransactionThread(parentReportAction: ReportAction): boolean { +function isTransactionThread(parentReportAction: OnyxEntry): boolean { return ( parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && (parentReportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0359fad05855..ab90ef5da5db 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1332,7 +1332,7 @@ function canEditMoneyRequest(reportAction: OnyxEntry) { return false; } - const moneyRequestReport = getReport(moneyRequestReportID); + const moneyRequestReport = getReport(String(moneyRequestReportID)); const isReportSettled = isSettled(moneyRequestReport?.reportID); const isAdmin = ((isExpenseReport(moneyRequestReport) && getPolicy(moneyRequestReport?.policyID ?? '')?.role) ?? '') === CONST.POLICY.ROLE.ADMIN; const isRequestor = currentUserAccountID === reportAction?.actorAccountID; @@ -1890,7 +1890,7 @@ function navigateToDetailsPage(report: OnyxEntry) { * this is more than random enough for our needs. */ function generateReportID() { - return (Math.floor(Math.random() * 2 ** 21) * 2 ** 32 + Math.floor(Math.random() * 2 ** 32)).toString(); + return Math.floor(Math.random() * 2 ** 21) * 2 ** 32 + Math.floor(Math.random() * 2 ** 32).toString(); } function hasReportNameError(report: OnyxEntry): boolean { diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 68769e4274ee..73ffb12c437f 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -378,7 +378,7 @@ function hasRoute(transaction: Transaction): boolean { * * @deprecated Use Onyx.connect() or withOnyx() instead */ -function getLinkedTransaction(reportAction?: OnyxEntry): Transaction | Record { +function getLinkedTransaction(reportAction: OnyxEntry): Transaction | Record { let transactionID = ''; if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 438a0335d452..8b99ec8aff68 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -79,13 +79,13 @@ type Report = { isWaitingOnBankAccount?: boolean; visibility?: ValueOf; preexistingReportID?: string; - iouReportID?: number; + iouReportID?: string; lastMentionedTime?: string | null; parentReportActionIDs?: number[]; errorFields?: OnyxCommon.ErrorFields; pendingFields?: { - createChat: ValueOf; - addWorkspaceRoom: ValueOf; + createChat: OnyxCommon.PendingAction; + addWorkspaceRoom: OnyxCommon.PendingAction; }; /** If the report contains nonreimbursable expenses, send the nonreimbursable total */ nonReimbursableTotal?: number; diff --git a/src/types/onyx/Session.ts b/src/types/onyx/Session.ts index e6658ff04835..da61034ff831 100644 --- a/src/types/onyx/Session.ts +++ b/src/types/onyx/Session.ts @@ -7,6 +7,7 @@ type Session = { /** Currently logged in user authToken */ authToken?: string; + /** Type of token for currently logged user */ authTokenType?: string; supportAuthToken?: string; From e8945738a039babe95b546661e886ca996bda2ca Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 24 Oct 2023 16:50:41 +0200 Subject: [PATCH 12/63] fix: fixed ReportUtilsTest --- src/libs/ReportUtils.ts | 63 ++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 3b249d252ab0..f3dadf5f9ca4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5,6 +5,7 @@ import lodashEscape from 'lodash/escape'; import lodashIsEqual from 'lodash/isEqual'; import lodashFindLastIndex from 'lodash/findLastIndex'; import lodashIntersection from 'lodash/intersection'; +import lodashMap from 'lodash/map'; import {ValueOf} from 'type-fest'; import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; @@ -272,9 +273,9 @@ function sortReportsByLastRead(reports: OnyxCollection): Array report?.reportID && report?.lastReadTime) .sort((a, b) => { - const aTime = a?.lastReadTime ? a.lastReadTime : 0; - const bTime = b?.lastReadTime ? b.lastReadTime : 0; - return Number(aTime) - Number(bTime); + const aTime = new Date(a?.lastReadTime ?? ''); + const bTime = new Date(b?.lastReadTime ?? ''); + return aTime - bTime; }); } @@ -1048,39 +1049,37 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f } function getDisplayNamesWithTooltips( - personalDetailsList: PersonalDetails[], + personalDetailsList: PersonalDetails[] | Record, isMultipleParticipantReport: boolean, ): Array> { - return personalDetailsList - ?.map?.((user) => { - const accountID = Number(user.accountID); - const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport) ?? user.login ?? ''; - const avatar = UserUtils.getDefaultAvatar(accountID); - - let pronouns = user.pronouns; - if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) { - const pronounTranslationKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); - pronouns = Localize.translateLocal(`pronouns.${pronounTranslationKey}`); - } + return lodashMap(personalDetailsList, (user: PersonalDetails) => { + const accountID = Number(user.accountID); + const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport) ?? user.login ?? ''; + const avatar = UserUtils.getDefaultAvatar(accountID); + + let pronouns = user.pronouns; + if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) { + const pronounTranslationKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); + pronouns = Localize.translateLocal(`pronouns.${pronounTranslationKey}`); + } - return { - displayName, - avatar, - login: user.login ?? '', - accountID, - pronouns, - }; - }) - .sort((first, second) => { - // First sort by displayName/login - const displayNameLoginOrder = first.displayName.localeCompare(second.displayName); - if (displayNameLoginOrder !== 0) { - return displayNameLoginOrder; - } + return { + displayName, + avatar, + login: user.login ?? '', + accountID, + pronouns, + }; + }).sort((first: PersonalDetails, second: PersonalDetails) => { + // First sort by displayName/login + const displayNameLoginOrder = first.displayName.localeCompare(second.displayName); + if (displayNameLoginOrder !== 0) { + return displayNameLoginOrder; + } - // Then fallback on accountID as the final sorting criteria. - return first.accountID - second.accountID; - }); + // Then fallback on accountID as the final sorting criteria. + return first.accountID - second.accountID; + }); } /** From 4d10611c680d92c1c58b300f94161bb5c7a32a78 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 25 Oct 2023 13:43:57 +0200 Subject: [PATCH 13/63] fix: resolving comments --- src/libs/ReportUtils.ts | 442 ++++++++++++++++-------------- src/libs/TransactionUtils.ts | 5 +- src/types/onyx/OriginalMessage.ts | 8 +- 3 files changed, 248 insertions(+), 207 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f3dadf5f9ca4..bfd22ea25fc5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6,7 +6,6 @@ import lodashIsEqual from 'lodash/isEqual'; import lodashFindLastIndex from 'lodash/findLastIndex'; import lodashIntersection from 'lodash/intersection'; import lodashMap from 'lodash/map'; - import {ValueOf} from 'type-fest'; import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; @@ -28,22 +27,23 @@ import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAva import * as CurrencyUtils from './CurrencyUtils'; import * as UserUtils from './UserUtils'; import {Beta, Login, PersonalDetails, Policy, Report, ReportAction, Transaction} from '../types/onyx'; -import {Receipt} from '../types/onyx/Transaction'; +import {Receipt, WaypointCollection} from '../types/onyx/Transaction'; import DeepValueOf from '../types/utils/DeepValueOf'; -import {Closed, IOUMessage} from '../types/onyx/OriginalMessage'; +import {IOUMessage} from '../types/onyx/OriginalMessage'; import {Message, ReportActions} from '../types/onyx/ReportAction'; +import {PendingAction} from '../types/onyx/OnyxCommon'; type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string}; type Avatar = { - id: number; + id?: number; source: React.FC | string | undefined; type: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; name: string; fallbackIcon?: React.FC | string; }; -type ExpanseOriginalMessage = { +type ExpenseOriginalMessage = { oldComment?: string; newComment?: string; comment?: string; @@ -77,12 +77,111 @@ type Participant = { text: string; }; +type SpendBreakdown = { + nonReimbursableSpend: number; + reimbursableSpend: number; + totalDisplaySpend: number; +}; + +type ParticipantDetails = [number, string, string | React.FC, string | React.FC]; + +type ReportAndWorkspaceName = { + rootReportName: string; + workspaceName?: string; +}; + +type OptimisticReportAction = { + commentText: string; + reportAction: Partial; +}; + +type UpdateOptimisticParentReportAction = { + childVisibleActionCount: number; + childCommenterCount: number; + childLastVisibleActionCreated: string; + childOldestFourAccountIDs: string | undefined; +}; + +type OptimisticExpenseReport = Pick< + Report, + | 'reportID' + | 'chatReportID' + | 'policyID' + | 'type' + | 'ownerAccountID' + | 'hasOutstandingIOU' + | 'currency' + | 'reportName' + | 'state' + | 'stateNum' + | 'total' + | 'notificationPreference' + | 'parentReportID' +>; + +type OptimisticIOUReportAction = Pick< + ReportAction, + | 'actionName' + | 'actorAccountID' + | 'automatic' + | 'avatar' + | 'isAttachment' + | 'originalMessage' + | 'message' + | 'person' + | 'reportActionID' + | 'shouldShow' + | 'created' + | 'pendingAction' + | 'receipt' + | 'whisperedToAccountIDs' +>; + +type OptimisticReportPreview = Pick< + ReportAction, + | 'actionName' + | 'reportActionID' + | 'pendingAction' + | 'originalMessage' + | 'message' + | 'created' + | 'actorAccountID' + | 'childMoneyRequestCount' + | 'childLastMoneyRequestComment' + | 'childRecentReceiptTransactionIDs' + | 'whisperedToAccountIDs' +> & {reportID?: string; accountID?: number}; + +type UpdateReportPreview = Pick< + ReportAction, + 'created' | 'message' | 'childLastMoneyRequestComment' | 'childMoneyRequestCount' | 'childRecentReceiptTransactionIDs' | 'whisperedToAccountIDs' +>; + +type ReportRouteParams = { + reportID: string; + isSubReportPageRoute: boolean; +}; + +type ReportOfflinePendingActionAndErrors = { + addWorkspaceRoomOrChatPendingAction: PendingAction | undefined; + addWorkspaceRoomOrChatErrors: Record | null | undefined; +}; + +type OptimisticApprovedReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' +>; + function isTypeTransaction(arg: Transaction | Record): arg is Transaction { - return arg !== undefined; // Customize this type guard as needed + return arg !== undefined; } function isTypeReportAction(arg: ReportAction | Record): arg is ReportAction { - return arg !== undefined; // Customize this type guard as needed + return arg !== undefined; +} + +function isReportType(arg: OnyxEntry | Record): arg is OnyxEntry { + return arg !== undefined; } let currentUserEmail: string | undefined; @@ -104,11 +203,11 @@ Onyx.connect({ }); let allPersonalDetails: OnyxCollection; -let currentUserPersonalDetails: OnyxEntry; +let currentUserPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => { - currentUserPersonalDetails = value?.[currentUserAccountID ?? '']; + currentUserPersonalDetails = value?.[currentUserAccountID ?? ''] ?? null; allPersonalDetails = value ?? {}; }, }); @@ -123,7 +222,7 @@ Onyx.connect({ let doesDomainHaveApprovedAccountant = false; Onyx.connect({ key: ONYXKEYS.ACCOUNT, - // waitForCollectionCallback: true, + waitForCollectionCallback: true, callback: (value) => (doesDomainHaveApprovedAccountant = value?.doesDomainHaveApprovedAccountant ?? false), }); @@ -144,16 +243,15 @@ function getChatType(report?: OnyxEntry): ValueOf { +function getPolicy(policyID: string): OnyxEntry | Record { if (!allPolicies || !policyID) { - return null; + return {}; } - return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}; } /** * Get the policy type from a given report - * @param report * @param policies must have Onyxkey prefix (i.e 'policy_') for keys */ function getPolicyType(report: OnyxEntry, policies: OnyxCollection): string { @@ -163,7 +261,7 @@ function getPolicyType(report: OnyxEntry, policies: OnyxCollection, returnEmptyIfNotFound = false, policy: OnyxEntry = undefined): string { +function getPolicyName(report: OnyxEntry | undefined, returnEmptyIfNotFound = false, policy: OnyxEntry = null): string { const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); if (Object.keys(report ?? {}).length === 0) { return noPolicyFound; @@ -185,6 +283,7 @@ function getPolicyName(report?: OnyxEntry, returnEmptyIfNotFound = false * Returns the concatenated title for the PrimaryLogins of a report */ function getReportParticipantsTitle(accountIDs: number[]): string { + // Somehow it's possible for the logins coming from report.participantAccountIDs to contain undefined values so we use .filter(Boolean) to remove them. return accountIDs.filter(Boolean).join(', '); } @@ -238,7 +337,6 @@ function isCanceledTaskReport(report: OnyxEntry, parentReportAction?: On /** * Checks if a report is an open task report. * - * @param report * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ function isOpenTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry): boolean { @@ -262,14 +360,14 @@ function isReportManager(report: OnyxEntry): boolean { /** * Checks if the supplied report has been approved */ -function isReportApproved(report: OnyxEntry | undefined): boolean { +function isReportApproved(report: OnyxEntry): boolean { return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report.statusNum === CONST.REPORT.STATUS.APPROVED; } /** * Given a collection of reports returns them sorted by last read */ -function sortReportsByLastRead(reports: OnyxCollection): Array> { +function sortReportsByLastRead(reports: OnyxCollection): Report[] { return Object.values(reports ?? {}) .filter((report) => report?.reportID && report?.lastReadTime) .sort((a, b) => { @@ -282,7 +380,7 @@ function sortReportsByLastRead(reports: OnyxCollection): Array): boolean { /** * Whether the provided report is a Policy Expense chat. */ -function isPolicyExpenseChat(report?: OnyxEntry): boolean { +function isPolicyExpenseChat(report: OnyxEntry | undefined): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT; } @@ -469,7 +567,7 @@ function isExpensifyOnlyParticipantInReport(report: OnyxEntry): boolean * by cross-referencing the accountIDs with personalDetails. */ function hasExpensifyEmails(accountIDs: number[]): boolean { - return accountIDs.some((accountID) => Str.extractCompanyNameFromEmailDomain(allPersonalDetails?.[accountID]?.login ?? '') === CONST.EXPENSIFY_PARTNER_NAME); + return accountIDs.some((accountID) => Str.extractEmailDomain(allPersonalDetails?.[accountID]?.login ?? '') === CONST.EXPENSIFY_PARTNER_NAME); } /** @@ -478,7 +576,7 @@ function hasExpensifyEmails(accountIDs: number[]): boolean { * of the user's chats should have their personal details in Onyx. */ function hasExpensifyGuidesEmails(accountIDs: number[]): boolean { - return accountIDs.some((accountID) => Str.extractCompanyNameFromEmailDomain(allPersonalDetails?.[accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN); + return accountIDs.some((accountID) => Str.extractEmailDomain(allPersonalDetails?.[accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN); } function findLastAccessedReport( @@ -520,20 +618,20 @@ function findLastAccessedReport( ); } - return adminReport ?? sortedReports[sortedReports.length - 1]; + return adminReport ?? sortedReports.at(-1); } /** * Whether the provided report is an archived room */ -function isArchivedRoom(report: OnyxEntry | undefined): boolean { +function isArchivedRoom(report: OnyxEntry): boolean { return report?.statusNum === CONST.REPORT.STATUS.CLOSED && report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED; } /** * Checks if the current user is allowed to comment on the given report. */ -function isAllowedToComment(report: OnyxEntry | undefined): boolean { +function isAllowedToComment(report: OnyxEntry): boolean { // Default to allowing all users to post const capability = (report?.writeCapability ?? CONST.REPORT.WRITE_CAPABILITIES.ALL) || CONST.REPORT.WRITE_CAPABILITIES.ALL; @@ -549,7 +647,7 @@ function isAllowedToComment(report: OnyxEntry | undefined): boolean { // If we've made it here, commenting on this report is restricted. // If the user is an admin, allow them to post. const policy = allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; - return (policy?.role ?? '') === CONST.POLICY.ROLE.ADMIN; + return policy?.role === CONST.POLICY.ROLE.ADMIN; } /** @@ -560,7 +658,7 @@ function isPolicyExpenseChatAdmin(report: OnyxEntry, policies: OnyxColle return false; } - const policyRole = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.role ?? ''; + const policyRole = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.role; return policyRole === CONST.POLICY.ROLE.ADMIN; } @@ -569,7 +667,7 @@ function isPolicyExpenseChatAdmin(report: OnyxEntry, policies: OnyxColle * Checks if the current user is the admin of the policy. */ function isPolicyAdmin(policyID: string, policies: OnyxCollection): boolean { - const policyRole = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.role ?? ''; + const policyRole = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.role; return policyRole === CONST.POLICY.ROLE.ADMIN; } @@ -648,7 +746,7 @@ function isIOURequest(report?: OnyxEntry): boolean { * Checks if a report is an IOU or expense request. */ function isMoneyRequest(reportOrID: OnyxEntry | string): boolean { - const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`]; + const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID; return isIOURequest(report) || isExpenseRequest(report); } @@ -663,9 +761,9 @@ function isMoneyRequestReport(reportOrID?: OnyxEntry | string): boolean /** * Get the report given a reportID */ -function getReport(reportID: string | undefined): OnyxEntry | undefined { +function getReport(reportID: string | undefined): OnyxEntry | Record { // Deleted reports are set to null and lodashGet will still return null in that case, so we need to add an extra check - return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; } /** @@ -682,7 +780,7 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: // For now, users cannot delete split actions const isSplitAction = originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; - if (isSplitAction || isSettled(String(originalMessage?.IOUReportID)) || isReportApproved(report)) { + if (isSplitAction || isSettled(String(originalMessage?.IOUReportID)) || (isReportType(report) && isReportApproved(report))) { return false; } @@ -701,7 +799,7 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: } const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; - const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN && !isDM(report); + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN && isReportType(report) && !isDM(report); return isActionOwner || isAdmin; } @@ -769,6 +867,7 @@ function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAcc const defaultParticipantAccountIDs = finalReport?.participantAccountIDs ?? []; const setOfParticipantAccountIDs = new Set(report?.ownerAccountID ? [...defaultParticipantAccountIDs, report.ownerAccountID] : defaultParticipantAccountIDs); finalParticipantAccountIDs = [...setOfParticipantAccountIDs]; + } else if (isTaskReport(report)) { // Task reports `managerID` will change when assignee is changed, in that case the old `managerID` is still present in `participantAccountIDs` // array along with the new one. We only need the `managerID` as a participant here. finalParticipantAccountIDs = report?.managerID ? [report?.managerID] : []; @@ -787,7 +886,7 @@ function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAcc function canShowReportRecipientLocalTime(personalDetails: OnyxCollection, report: OnyxEntry, accountID: number): boolean { const reportRecipientAccountIDs = getReportRecipientAccountIDs(report, accountID); const hasMultipleParticipants = reportRecipientAccountIDs.length > 1; - const reportRecipient = personalDetails?.[reportRecipientAccountIDs[0] ?? -1]; + const reportRecipient = personalDetails?.[reportRecipientAccountIDs[0]]; const reportRecipientTimezone = reportRecipient?.timezone ?? CONST.DEFAULT_TIME_ZONE; const isReportParticipantValidated = reportRecipient?.validated ?? false; return Boolean(!hasMultipleParticipants && !isChatRoom(report) && !isPolicyExpenseChat(report) && reportRecipient && reportRecipientTimezone?.selected && isReportParticipantValidated); @@ -832,8 +931,8 @@ function getWorkspaceAvatar(report: OnyxEntry) { * Returns the appropriate icons for the given chat report using the stored personalDetails. * The Avatar sources can be URLs or Icon components according to the chat type. */ -function getIconsForParticipants(participants: number[], personalDetails: OnyxCollection) { - const participantDetails: Array<[number, string, string | React.FC, string | React.FC]> = []; +function getIconsForParticipants(participants: number[], personalDetails: OnyxCollection): Avatar[] { + const participantDetails: ParticipantDetails[] = []; const participantsList = participants || []; for (const accountID of participantsList) { @@ -875,9 +974,8 @@ function getIconsForParticipants(participants: number[], personalDetails: OnyxCo /** * Given a report, return the associated workspace icon. */ -function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): Avatar { +function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry = null): Avatar { const workspaceName = getPolicyName(report, false, policy); - // TODO: Check why ?? is not working here const policyExpenseChatAvatarSource = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar : getDefaultWorkspaceAvatar(workspaceName); @@ -902,7 +1000,7 @@ function getIcons( defaultName = '', defaultAccountID = -1, policy: OnyxEntry | undefined = undefined, -) { +): Avatar[] { if (Object.keys(report ?? {}).length === 0) { const fallbackIcon: Avatar = { source: defaultIcon ?? Expensicons.FallbackAvatar, @@ -969,7 +1067,7 @@ function getIcons( type: CONST.ICON_TYPE_WORKSPACE, name: domainName, id: -1, - }; + } as Avatar; return [domainIcon]; } if (isAdminRoom(report) || isAnnounceRoom(report) || isChatRoom(report) || isArchivedRoom(report)) { @@ -1014,7 +1112,7 @@ function getIcons( * Gets the personal details for a login by looking in the ONYXKEYS.PERSONAL_DETAILS_LIST Onyx key (stored in the local variable, allPersonalDetails). If it doesn't exist in Onyx, * then a default object is constructed. */ -function getPersonalDetailsForAccountID(accountID: number): PersonalDetails | Record | {avatar: string | React.FC} { +function getPersonalDetailsForAccountID(accountID: number): PersonalDetails | Partial> { if (!accountID) { return {}; } @@ -1036,7 +1134,7 @@ function getPersonalDetailsForAccountID(accountID: number): PersonalDetails | Re /** * Get the displayName for a single report participant. */ -function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = false) { +function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = false): string | undefined { if (!accountID) { return ''; } @@ -1157,8 +1255,8 @@ function isWaitingForIOUActionFromCurrentUser(report: OnyxEntry): boolea if (!report) { return false; } - - if (isArchivedRoom(getReport(report.parentReportID))) { + const parentReport = getReport(report.parentReportID); + if (parentReport && isArchivedRoom(parentReport)) { return false; } @@ -1195,7 +1293,6 @@ function isWaitingForIOUActionFromCurrentUser(report: OnyxEntry): boolea /** * Checks if a report is an open task report assigned to current user. * - * @param report * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ function isWaitingForTaskCompleteFromAssignee(report: OnyxEntry, parentReportAction?: OnyxEntry): boolean { @@ -1206,7 +1303,7 @@ function isWaitingForTaskCompleteFromAssignee(report: OnyxEntry, parentR * Returns number of transactions that are nonReimbursable * */ -function hasNonReimbursableTransactions(iouReportID: string | undefined) { +function hasNonReimbursableTransactions(iouReportID: string | undefined): boolean { const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); return allTransactions.filter((transaction) => transaction.reimbursable === false).length > 0; } @@ -1233,7 +1330,7 @@ function getMoneyRequestReimbursableTotal(report: OnyxEntry | undefined, return 0; } -function getMoneyRequestSpendBreakdown(report: OnyxEntry, allReportsDict: OnyxCollection = null) { +function getMoneyRequestSpendBreakdown(report: OnyxEntry, allReportsDict: OnyxCollection = null): SpendBreakdown { const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport; if (isMoneyRequestReport(report)) { @@ -1270,7 +1367,7 @@ function getMoneyRequestSpendBreakdown(report: OnyxEntry, allReportsDict /** * Get the title for a policy expense chat which depends on the role of the policy member seeing this report */ -function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string | undefined { +function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry | undefined): string | undefined { const reportOwnerDisplayName = getDisplayNameForParticipant(report?.ownerAccountID ?? -1) ?? allPersonalDetails?.[report?.ownerAccountID ?? -1]?.login ?? report?.reportName; // If the policy expense chat is owned by this user, use the name of the policy as the report name. @@ -1284,8 +1381,7 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry

, policy: OnyxEntry

, policy: OnyxEntry | undefined = undefined) { +function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry | undefined): string { const moneyRequestTotal = getMoneyRequestReimbursableTotal(report); const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID)); const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID); @@ -1324,12 +1420,29 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< return payerPaidAmountMessage; } +type TransactionDetails = + | { + created: string; + amount: number; + currency: string; + merchant: string; + waypoints?: WaypointCollection; + comment: string; + category: string; + billable: boolean; + tag: string; + mccGroup?: ValueOf; + cardID: number; + originalAmount: number; + originalCurrency: string; + } + | undefined; /** * Gets transaction created, amount, currency, comment, and waypoints (for distance request) * into a flat object. Used for displaying transactions and sending them in API commands */ -function getTransactionDetails(transaction: OnyxEntry, createdDateFormat: string = CONST.DATE.FNS_FORMAT_STRING) { +function getTransactionDetails(transaction: OnyxEntry, createdDateFormat: string = CONST.DATE.FNS_FORMAT_STRING): TransactionDetails { const report = getReport(transaction?.reportID); if (!transaction) { return; @@ -1466,7 +1579,7 @@ function areAllRequestsBeingSmartScanned(iouReportID: string | undefined, report * * @param iouReportID */ -function hasMissingSmartscanFields(iouReportID?: string) { +function hasMissingSmartscanFields(iouReportID: string | undefined): boolean { const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); return transactionsWithReceipts.some((transaction) => TransactionUtils.hasMissingSmartscanFields(transaction)); } @@ -1474,7 +1587,7 @@ function hasMissingSmartscanFields(iouReportID?: string) { /** * Given a parent IOU report action get report name for the LHN. */ -function getTransactionReportName(reportAction: OnyxEntry) { +function getTransactionReportName(reportAction: OnyxEntry): string { if (ReportActionsUtils.isReversedTransaction(reportAction)) { return Localize.translateLocal('parentReportAction.reversedTransaction'); } @@ -1506,10 +1619,8 @@ function getTransactionReportName(reportAction: OnyxEntry) { /** * Get money request message for an IOU report * - * @param report * @param [reportAction] This can be either a report preview action or the IOU action * @param [shouldConsiderReceiptBeingScanned=false] - * @returns */ function getReportPreviewMessage( report: OnyxEntry, @@ -1587,7 +1698,7 @@ function getReportPreviewMessage( * Get the proper message schema for modified expense message. */ -function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean) { +function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean): string { const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; const displayValueName = valueName.toLowerCase(); @@ -1623,18 +1734,15 @@ function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDista * If we change this function be sure to update the backend as well. */ function getModifiedExpenseMessage(reportAction: OnyxEntry): string | undefined { - const reportActionOriginalMessage = reportAction?.originalMessage as ExpanseOriginalMessage; + const reportActionOriginalMessage = reportAction?.originalMessage as ExpenseOriginalMessage; if (Object.keys(reportActionOriginalMessage ?? {}).length === 0) { return Localize.translateLocal('iou.changedTheRequest'); } const hasModifiedAmount = - Object.hasOwn(reportActionOriginalMessage, 'oldAmount') && - Object.hasOwn(reportActionOriginalMessage, 'oldCurrency') && - Object.hasOwn(reportActionOriginalMessage, 'amount') && - Object.hasOwn(reportActionOriginalMessage, 'currency'); + 'oldAmount' in reportActionOriginalMessage && 'oldCurrency' in reportActionOriginalMessage && 'amount' in reportActionOriginalMessage && 'currency' in reportActionOriginalMessage; - const hasModifiedMerchant = Object.hasOwn(reportActionOriginalMessage, 'oldMerchant') && Object.hasOwn(reportActionOriginalMessage, 'merchant'); + const hasModifiedMerchant = 'oldMerchant' in reportActionOriginalMessage && 'merchant' in reportActionOriginalMessage; if (hasModifiedAmount) { const oldCurrency = reportActionOriginalMessage?.oldCurrency; const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency ?? ''); @@ -1651,7 +1759,7 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin return getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); } - const hasModifiedComment = Object.hasOwn(reportActionOriginalMessage, 'oldComment') && Object.hasOwn(reportActionOriginalMessage, 'newComment'); + const hasModifiedComment = 'oldComment' in reportActionOriginalMessage && 'newComment' in reportActionOriginalMessage; if (hasModifiedComment) { return getProperSchemaForModifiedExpenseMessage( reportActionOriginalMessage?.newComment ?? '', @@ -1661,7 +1769,7 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin ); } - const hasModifiedCreated = Object.hasOwn(reportActionOriginalMessage, 'oldCreated') && Object.hasOwn(reportActionOriginalMessage, 'created'); + const hasModifiedCreated = '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 ?? ''); @@ -1679,7 +1787,7 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin ); } - const hasModifiedCategory = Object.hasOwn(reportActionOriginalMessage, 'oldCategory') && Object.hasOwn(reportActionOriginalMessage, 'category'); + const hasModifiedCategory = 'oldCategory' in reportActionOriginalMessage && 'category' in reportActionOriginalMessage; if (hasModifiedCategory) { return getProperSchemaForModifiedExpenseMessage( reportActionOriginalMessage?.category ?? '', @@ -1689,12 +1797,12 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin ); } - const hasModifiedTag = Object.hasOwn(reportActionOriginalMessage, 'oldTag') && Object.hasOwn(reportActionOriginalMessage, 'tag'); + const hasModifiedTag = 'oldTag' in reportActionOriginalMessage && 'tag' in reportActionOriginalMessage; if (hasModifiedTag) { return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.tag ?? '', reportActionOriginalMessage?.oldTag ?? '', Localize.translateLocal('common.tag'), true); } - const hasModifiedBillable = Object.hasOwn(reportActionOriginalMessage, 'oldBillable') && Object.hasOwn(reportActionOriginalMessage, 'billable'); + const hasModifiedBillable = 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage; if (hasModifiedBillable) { return getProperSchemaForModifiedExpenseMessage( reportActionOriginalMessage?.billable ?? '', @@ -1711,43 +1819,43 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin * * At the moment, we only allow changing one transaction field at a time. */ -function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry, transactionChanges: ExpanseOriginalMessage, isFromExpenseReport: boolean) { - const originalMessage: ExpanseOriginalMessage = {}; +function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry, transactionChanges: ExpenseOriginalMessage, isFromExpenseReport: boolean): ExpenseOriginalMessage { + const originalMessage: ExpenseOriginalMessage = {}; // Remark: Comment field is the only one which has new/old prefixes for the keys (newComment/ oldComment), // all others have old/- pattern such as oldCreated/created - if (Object.hasOwn(transactionChanges, 'comment')) { + if ('comment' in transactionChanges) { originalMessage.oldComment = TransactionUtils.getDescription(oldTransaction); originalMessage.newComment = transactionChanges?.comment; } - if (Object.hasOwn(transactionChanges, 'created')) { + if ('created' in transactionChanges) { originalMessage.oldCreated = TransactionUtils.getCreated(oldTransaction); originalMessage.created = transactionChanges?.created; } - if (Object.hasOwn(transactionChanges, 'merchant')) { + if ('merchant' in transactionChanges) { originalMessage.oldMerchant = TransactionUtils.getMerchant(oldTransaction); originalMessage.merchant = transactionChanges?.merchant; } // The amount is always a combination of the currency and the number value so when one changes we need to store both // to match how we handle the modified expense action in oldDot - if (Object.hasOwn(transactionChanges, 'amount') || Object.hasOwn(transactionChanges, 'currency')) { + if ('amount' in transactionChanges || 'currency' in transactionChanges) { originalMessage.oldAmount = TransactionUtils.getAmount(oldTransaction, isFromExpenseReport); originalMessage.amount = transactionChanges?.amount; originalMessage.oldCurrency = TransactionUtils.getCurrency(oldTransaction); originalMessage.currency = transactionChanges?.currency; } - if (Object.hasOwn(transactionChanges, 'category')) { + if ('category' in transactionChanges) { originalMessage.oldCategory = TransactionUtils.getCategory(oldTransaction); originalMessage.category = transactionChanges?.category; } - if (Object.hasOwn(transactionChanges, 'tag')) { + if ('tag' in transactionChanges) { originalMessage.oldTag = TransactionUtils.getTag(oldTransaction); originalMessage.tag = transactionChanges?.tag; } - if (Object.hasOwn(transactionChanges, 'billable')) { + if ('billable' in transactionChanges) { const oldBillable = TransactionUtils.getBillable(oldTransaction); originalMessage.oldBillable = oldBillable ? Localize.translateLocal('common.billable').toLowerCase() : Localize.translateLocal('common.nonBillable').toLowerCase(); originalMessage.billable = transactionChanges?.billable ? Localize.translateLocal('common.billable').toLowerCase() : Localize.translateLocal('common.nonBillable').toLowerCase(); @@ -1759,11 +1867,11 @@ function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry | undefined): OnyxEntry | undefined | Record { +function getParentReport(report: OnyxEntry | undefined): OnyxEntry | Record { if (!report?.parentReportID) { return {}; } - return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]; + return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`] ?? {}; } /** @@ -1789,7 +1897,7 @@ function getRootParentReport(report?: OnyxEntry): OnyxEntry | Re /** * Get the title for a report. */ -function getReportName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string { +function getReportName(report: OnyxEntry, policy: OnyxEntry = null): string { let formattedName; const parentReportAction = ReportActionsUtils.getParentReportAction(report); if (!isTypeReportAction(parentReportAction)) { @@ -1850,7 +1958,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry | un * Recursively navigates through thread parents to get the root report and workspace name. * The recursion stops when we find a non thread or money request report, whichever comes first. */ -function getRootReportAndWorkspaceName(report?: OnyxEntry) { +function getRootReportAndWorkspaceName(report: OnyxEntry | undefined): ReportAndWorkspaceName { if (!report) { return { rootReportName: '', @@ -1907,7 +2015,7 @@ function getChatRoomSubtitle(report: OnyxEntry): string | undefined { /** * Gets the parent navigation subtitle for the report */ -function getParentNavigationSubtitle(report: OnyxEntry) { +function getReportAndWorkspaceName(report: OnyxEntry): ReportAndWorkspaceName | Record { if (isThread(report)) { const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; const {rootReportName, workspaceName} = getRootReportAndWorkspaceName(parentReport); @@ -1942,7 +2050,7 @@ function navigateToDetailsPage(report: OnyxEntry) { * In a test of 500M reports (28 years of reports at our current max rate) we got 20-40 collisions meaning that * this is more than random enough for our needs. */ -function generateReportID() { +function generateReportID(): string { return Math.floor(Math.random() * 2 ** 21) * 2 ** 32 + Math.floor(Math.random() * 2 ** 32).toString(); } @@ -1959,10 +2067,6 @@ function getParsedComment(text: string): string { return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : lodashEscape(text); } -type OptimisticReportAction = { - commentText: string; - reportAction: Partial; -}; function buildOptimisticAddCommentReportAction(text?: string, file?: File & {source: string; uri: string}): OptimisticReportAction { const parser = new ExpensiMark(); const commentText = getParsedComment(text ?? ''); @@ -2011,12 +2115,7 @@ function buildOptimisticAddCommentReportAction(text?: string, file?: File & {sou * @param lastVisibleActionCreated - Last visible action created of the child report * @param type - The type of action in the child report */ -type UpdateOptimisticParentReportAction = { - childVisibleActionCount: number; - childCommenterCount: number; - childLastVisibleActionCreated: string; - childOldestFourAccountIDs: string | undefined; -}; + function updateOptimisticParentReportAction(parentReportAction: OnyxEntry, lastVisibleActionCreated: string, type: string): UpdateOptimisticParentReportAction { let childVisibleActionCount = parentReportAction?.childVisibleActionCount ?? 0; let childCommenterCount = parentReportAction?.childCommenterCount ?? 0; @@ -2095,7 +2194,14 @@ function getOptimisticDataForParentReportAction( * @param text - Text of the comment * @param parentReportID - Report ID of the parent report */ -function buildOptimisticTaskCommentReportAction(taskReportID: string, taskTitle: string, taskAssignee: string, taskAssigneeAccountID: number, text: string, parentReportID: string) { +function buildOptimisticTaskCommentReportAction( + taskReportID: string, + taskTitle: string, + taskAssignee: string, + taskAssigneeAccountID: number, + text: string, + parentReportID: string, +): OptimisticReportAction { const reportAction = buildOptimisticAddCommentReportAction(text); if (reportAction.reportAction.message) { reportAction.reportAction.message[0].taskReportID = taskReportID; @@ -2185,23 +2291,7 @@ function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number * @param currency */ -type OptimisticExpanseReport = Pick< - Report, - | 'reportID' - | 'chatReportID' - | 'policyID' - | 'type' - | 'ownerAccountID' - | 'hasOutstandingIOU' - | 'currency' - | 'reportName' - | 'state' - | 'stateNum' - | 'total' - | 'notificationPreference' - | 'parentReportID' ->; -function buildOptimisticExpenseReport(chatReportID: string, policyID: string, payeeAccountID: number, total: number, currency: string): OptimisticExpanseReport { +function buildOptimisticExpenseReport(chatReportID: string, policyID: string, payeeAccountID: number, total: number, currency: string): OptimisticExpenseReport { // The amount for Expense reports are stored as negative value in the database const storedTotal = total * -1; const policyName = getPolicyName(allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`]); @@ -2230,13 +2320,13 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa } /** - * @param iouReportID - the report ID of the IOU report the action belongs to - * @param type - IOUReportAction type. Can be oneOf(create, decline, cancel, pay, split) - * @param total - IOU total in cents - * @param comment - IOU comment - * @param currency - IOU currency - * @param paymentType - IOU paymentMethodType. Can be oneOf(Elsewhere, Expensify) - * @param isSettlingUp - Whether we are settling up an IOU + * @param iouReportID - the report ID of the IOU report the action belongs to + * @param type - IOUReportAction type. Can be oneOf(create, decline, cancel, pay, split) + * @param total - IOU total in cents + * @param comment - IOU comment + * @param currency - IOU currency + * @param paymentType - IOU paymentMethodType. Can be oneOf(Elsewhere, Expensify) + * @param isSettlingUp - Whether we are settling up an IOU */ function getIOUReportActionMessage(iouReportID: string, type: string, total: number, comment: string, currency: string, paymentType = '', isSettlingUp = false): [Message] { const report = getReport(iouReportID); @@ -2307,23 +2397,6 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num * @param [isOwnPolicyExpenseChat] - Whether this is an expense report create from the current user's policy expense chat */ -type OptimisticIOUReportAction = Pick< - ReportAction, - | 'actionName' - | 'actorAccountID' - | 'automatic' - | 'avatar' - | 'isAttachment' - | 'originalMessage' - | 'message' - | 'person' - | 'reportActionID' - | 'shouldShow' - | 'created' - | 'pendingAction' - | 'receipt' - | 'whisperedToAccountIDs' ->; function buildOptimisticIOUReportAction( type: ValueOf, amount: number, @@ -2345,7 +2418,7 @@ function buildOptimisticIOUReportAction( comment, currency, IOUTransactionID: transactionID, - IOUReportID: Number(IOUReportID), + IOUReportID, type, }; @@ -2372,9 +2445,9 @@ function buildOptimisticIOUReportAction( delete originalMessage.IOUReportID; // Split bill made from a policy expense chat only have the payee's accountID as the participant because the payer could be any policy admin if (isOwnPolicyExpenseChat) { - originalMessage.participantAccountIDs = [currentUserAccountID ?? -1]; + originalMessage.participantAccountIDs = currentUserAccountID ? [currentUserAccountID] : []; } else { - originalMessage.participantAccountIDs = [currentUserAccountID ?? -1, ...participants.map((participant) => participant.accountID)]; + originalMessage.participantAccountIDs = currentUserAccountID ? [currentUserAccountID, ...participants.map((participant) => participant.accountID)] : []; } } @@ -2400,17 +2473,11 @@ function buildOptimisticIOUReportAction( whisperedToAccountIDs: [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === receipt?.state) ? [currentUserAccountID] : [], }; } + /** * Builds an optimistic APPROVED report action with a randomly generated reportActionID. */ -function buildOptimisticApprovedReportAction( - amount: number, - currency: string, - expenseReportID: string, -): Pick< - ReportAction, - 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' -> { +function buildOptimisticApprovedReportAction(amount: number, currency: string, expenseReportID: string): OptimisticApprovedReportAction { const originalMessage = { amount, currency, @@ -2472,20 +2539,6 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string, }; } -type OptimisticReportPreviw = Pick< - ReportAction, - | 'actionName' - | 'reportActionID' - | 'pendingAction' - | 'originalMessage' - | 'message' - | 'created' - | 'actorAccountID' - | 'childMoneyRequestCount' - | 'childLastMoneyRequestComment' - | 'childRecentReceiptTransactionIDs' - | 'whisperedToAccountIDs' -> & {reportID?: string; accountID?: number}; /** * Builds an optimistic report preview action with a randomly generated reportActionID. * @@ -2494,7 +2547,7 @@ type OptimisticReportPreviw = Pick< * @param [comment] - User comment for the IOU. * @param [transaction] - optimistic first transaction of preview */ -function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: OnyxEntry, comment = '', transaction: Transaction | undefined = undefined): OptimisticReportPreviw { +function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: OnyxEntry, comment = '', transaction: Transaction | undefined = undefined): OptimisticReportPreview { const hasReceipt = TransactionUtils.hasReceipt(transaction); const isReceiptBeingScanned = hasReceipt && transaction && TransactionUtils.isReceiptBeingScanned(transaction); const message = getReportPreviewMessage(iouReport); @@ -2532,7 +2585,7 @@ function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: function buildOptimisticModifiedExpenseReportAction( transactionThread: OnyxEntry, oldTransaction: OnyxEntry, - transactionChanges: ExpanseOriginalMessage, + transactionChanges: ExpenseOriginalMessage, isFromExpenseReport: boolean, ) { const originalMessage = getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport); @@ -2565,16 +2618,10 @@ function buildOptimisticModifiedExpenseReportAction( shouldShow: true, }; } -type UpdateReportPreview = Pick< - ReportAction, - 'created' | 'message' | 'childLastMoneyRequestComment' | 'childMoneyRequestCount' | 'childRecentReceiptTransactionIDs' | 'whisperedToAccountIDs' ->; + /** * Updates a report preview action that exists for an IOU report. * - * @param iouReport - * @param reportPreviewAction - * @param [isPayRequest] * @param [comment] - User comment for the IOU. * @param [transaction] - optimistic newest transaction of a report preview * @@ -2584,7 +2631,7 @@ function updateReportPreview( reportPreviewAction: OnyxEntry, isPayRequest = false, comment = '', - transaction: OnyxEntry | undefined = undefined, + transaction?: OnyxEntry, ): UpdateReportPreview { const hasReceipt = TransactionUtils.hasReceipt(transaction); const recentReceiptTransactions = reportPreviewAction?.childRecentReceiptTransactionIDs ?? {}; @@ -2829,7 +2876,7 @@ function buildOptimisticClosedReportAction(emailClosingReport: string, policyNam function buildOptimisticWorkspaceChats(policyID: string, policyName: string) { const announceChatData = buildOptimisticChatReport( - [currentUserAccountID ?? -1], + currentUserAccountID ? [currentUserAccountID] : [], CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policyID, @@ -2967,7 +3014,7 @@ function isUnreadWithMention(report: OnyxEntry): boolean { return lastReadTime < lastMentionedTime; } -function isIOUOwnedByCurrentUser(report: OnyxEntry, allReportsDict: OnyxCollection = null) { +function isIOUOwnedByCurrentUser(report: OnyxEntry, allReportsDict: OnyxCollection = null): boolean { const allAvailableReports = allReportsDict ?? allReports; if (!report || !allAvailableReports) { return false; @@ -2991,7 +3038,7 @@ function isIOUOwnedByCurrentUser(report: OnyxEntry, allReportsDict: Onyx */ // @ts-expect-error Will be fixed when OptionUtils will be merged -function isOneOnOneChat(report): boolean { +function isOneOnOneChat(report: OnyxEntry): boolean { const isChatRoomValue = report?.isChatRoom ?? false; const participantsListValue = report?.participantsList ?? []; return ( @@ -3154,7 +3201,7 @@ function shouldReportBeInOptionList( * Attempts to find a report in onyx with the provided list of participants. Does not include threads, task, money request, room, and policy expense chat. */ function getChatByParticipants(newParticipantList: number[]): OnyxEntry | undefined { - const sortedNewParticipantList = newParticipantList.sort((a, b) => a - b); + const sortedNewParticipantList = newParticipantList.sort(); return Object.values(allReports ?? {}).find((report) => { // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it if ( @@ -3170,24 +3217,21 @@ function getChatByParticipants(newParticipantList: number[]): OnyxEntry } // Only return the chat if it has all the participants - return lodashIsEqual( - sortedNewParticipantList, - report.participantAccountIDs?.sort((a, b) => a - b), - ); + return lodashIsEqual(sortedNewParticipantList, report.participantAccountIDs?.sort()); }); } /** * Attempts to find a report in onyx with the provided list of participants in given policy */ -function getChatByParticipantsAndPolicy(newParticipantList: number[], policyID: string) { +function getChatByParticipantsAndPolicy(newParticipantList: number[], policyID: string): OnyxEntry | undefined { newParticipantList.sort(); return Object.values(allReports ?? {}).find((report) => { // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it if (!report?.participantAccountIDs) { return false; } - const sortedParticipanctsAccountIDs = report.parentReportActionIDs?.sort((a, b) => a - b); + const sortedParticipanctsAccountIDs = report.parentReportActionIDs?.sort(); // Only return the room if it has all the participants and is not a policy room return report.policyID === policyID && lodashIsEqual(newParticipantList, sortedParticipanctsAccountIDs); }); @@ -3228,12 +3272,13 @@ function canFlagReportAction(reportAction: OnyxEntry, reportID: st return false; } - return ( + return Boolean( !isCurrentUserAction && - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && - !ReportActionsUtils.isDeletedAction(reportAction) && - !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && - isAllowedToComment(report) + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && + !ReportActionsUtils.isDeletedAction(reportAction) && + !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && + isReportType(report) && + isAllowedToComment(report), ); } @@ -3250,14 +3295,14 @@ function shouldShowFlagComment(reportAction: OnyxEntry, report: On ); } -function getNewMarkerReportActionID(report: OnyxEntry, sortedAndFilteredReportActions: ReportAction[]) { +function getNewMarkerReportActionID(report: OnyxEntry, sortedAndFilteredReportActions: ReportAction[]): string { if (!isUnread(report)) { return ''; } const newMarkerIndex = lodashFindLastIndex(sortedAndFilteredReportActions, (reportAction) => (reportAction.created ?? '') > (report?.lastReadTime ?? '')); - return Object.hasOwn(sortedAndFilteredReportActions[newMarkerIndex], 'reportActionID') ? sortedAndFilteredReportActions[newMarkerIndex].reportActionID : ''; + return 'reportActionID' in sortedAndFilteredReportActions[newMarkerIndex] ? sortedAndFilteredReportActions[newMarkerIndex].reportActionID : ''; } /** @@ -3301,7 +3346,7 @@ function getRouteFromLink(url: string | null): string { return route; } -function parseReportRouteParams(route: string) { +function parseReportRouteParams(route: string): ReportRouteParams { let parsingRoute = route; if (parsingRoute.at(0) === '/') { // remove the first slash @@ -3359,7 +3404,7 @@ function hasIOUWaitingOnCurrentUserBankAccount(chatReport: OnyxEntry): b * - in an IOU report, which is not settled yet * - in DM chat */ -function canRequestMoney(report: OnyxEntry, participants: number[]) { +function canRequestMoney(report: OnyxEntry, participants: number[]): boolean { // User cannot request money in chat thread or in task report if (isChatThread(report) || isTaskReport(report)) { return false; @@ -3409,7 +3454,7 @@ function canRequestMoney(report: OnyxEntry, participants: number[]) { * None of the options should show in chat threads or if there is some special Expensify account * as a participant of the report. */ -function getMoneyRequestOptions(report: OnyxEntry, reportParticipants: number[]): Array<(typeof CONST.IOU.TYPE)[keyof typeof CONST.IOU.TYPE]> { +function getMoneyRequestOptions(report: OnyxEntry, reportParticipants: number[]): Array> { // In any thread or task report, we do not allow any new money requests yet if (isChatThread(report) || isTaskReport(report)) { return []; @@ -3546,7 +3591,7 @@ function isValidReportIDFromPath(reportIDFromPath: string): boolean { /** * Return the errors we have when creating a chat or a workspace room */ -function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry) { +function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry): Record | null | undefined { // We are either adding a workspace room, or we're creating a chat, it isn't possible for both of these to have errors for the same report at the same time, so // simply looking up the first truthy value will get the relevant property if it's set. return report?.errorFields?.addWorkspaceRoom ?? report?.errorFields?.createChat; @@ -3573,7 +3618,7 @@ function getOriginalReportID(reportID: string, reportAction: OnyxEntry) { +function getReportOfflinePendingActionAndErrors(report: OnyxEntry): ReportOfflinePendingActionAndErrors { // We are either adding a workspace room, or we're creating a chat, it isn't possible for both of these to be pending, or to have errors for the same report at the same time, so // simply looking up the first truthy value for each case will get the relevant property if it's set. const addWorkspaceRoomOrChatPendingAction = report?.pendingFields?.addWorkspaceRoom ?? report?.pendingFields?.createChat; @@ -3581,7 +3626,7 @@ function getReportOfflinePendingActionAndErrors(report: OnyxEntry) { return {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors}; } -function getPolicyExpenseChatReportIDByOwner(policyOwner: string) { +function getPolicyExpenseChatReportIDByOwner(policyOwner: string): string | null { const policyWithOwner = Object.values(allPolicies ?? {}).find((policy) => policy?.owner === policyOwner); if (!policyWithOwner) { return null; @@ -3602,12 +3647,11 @@ function canCreateRequest(report: OnyxEntry, iouType: (typeof CONST.IOU. return getMoneyRequestOptions(report, participantAccountIDs).includes(iouType); } -function getWorkspaceChats(policyID: string, accountIDs: number[]) { +function getWorkspaceChats(policyID: string, accountIDs: number[]): Array> { return Object.values(allReports ?? {})?.filter((report) => isPolicyExpenseChat(report) && (report?.policyID ?? '') === policyID && accountIDs.includes(report?.ownerAccountID ?? -1)); } /** - * @param report * @param policy - the workspace the report is on, null if the user isn't a member of the workspace */ function shouldDisableRename(report: OnyxEntry, policy: OnyxEntry): boolean { @@ -3746,7 +3790,7 @@ function getTaskAssigneeChatOnyxData( /** * Returns an array of the participants Ids of a report */ -function getParticipantsIDs(report: OnyxEntry): Array { +function getParticipantsIDs(report: OnyxEntry): number[] { if (!report) { return []; } @@ -3765,7 +3809,7 @@ function getParticipantsIDs(report: OnyxEntry): Array) { +function getIOUReportActionDisplayMessage(reportAction: OnyxEntry): string { const originalMessage = reportAction?.originalMessage as IOUMessage; let displayMessage; if (originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { @@ -3827,7 +3871,7 @@ function isGroupChat(report: OnyxEntry): boolean { ); } -function shouldUseFullTitleToDisplay(report: OnyxEntry) { +function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report); } @@ -3848,7 +3892,7 @@ export { isUserCreatedPolicyRoom, isChatRoom, getChatRoomSubtitle, - getParentNavigationSubtitle, + getReportAndWorkspaceName, getPolicyName, getPolicyType, isArchivedRoom, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 73ffb12c437f..2cdc2af100b8 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -1,5 +1,6 @@ import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {format, isValid} from 'date-fns'; +import {ValueOf} from 'type-fest'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; import DateUtils from './DateUtils'; @@ -266,8 +267,8 @@ function getMerchant(transaction: OnyxEntry): string { /** * Return the mccGroup field from the transaction, return the modifiedMCCGroup if present. */ -function getMCCGroup(transaction: Transaction): string { - return transaction?.modifiedMCCGroup ? transaction.modifiedMCCGroup : transaction?.mccGroup ?? ''; +function getMCCGroup(transaction: Transaction): ValueOf | undefined { + return transaction?.modifiedMCCGroup ? transaction.modifiedMCCGroup : transaction?.mccGroup; } /** diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 069b4768cafa..90553b70c26d 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -18,7 +18,7 @@ type IOUDetails = { type IOUMessage = { /** The ID of the iou transaction */ IOUTransactionID?: string; - IOUReportID?: number; + IOUReportID?: string; amount: number; comment?: string; currency: string; @@ -94,11 +94,7 @@ type OriginalMessageSubmitted = { type OriginalMessageClosed = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.CLOSED; - originalMessage: { - policyName: string; - reason: ValueOf; - lastModified?: string; - }; + originalMessage: Closed; }; type OriginalMessageCreated = { From 7f5c5577f15f46f6cf958d7ab95c17f3ea270668 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 25 Oct 2023 22:44:02 +0200 Subject: [PATCH 14/63] fix: resolving comments --- src/libs/ReportUtils.ts | 290 ++++++++++++++++++++++----------- src/types/onyx/Report.ts | 4 + src/types/onyx/ReportAction.ts | 2 +- 3 files changed, 203 insertions(+), 93 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bfe1450b1d0f..aef93332c057 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5,7 +5,6 @@ import lodashEscape from 'lodash/escape'; import lodashIsEqual from 'lodash/isEqual'; import lodashFindLastIndex from 'lodash/findLastIndex'; import lodashIntersection from 'lodash/intersection'; -import lodashMap from 'lodash/map'; import {ValueOf} from 'type-fest'; import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; @@ -172,15 +171,72 @@ type OptimisticApprovedReportAction = Pick< 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' >; -function isTypeTransaction(arg: Transaction | Record): arg is Transaction { - return arg !== undefined; -} +type OptimisticSubmittedReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' +>; -function isTypeReportAction(arg: ReportAction | Record): arg is ReportAction { - return arg !== undefined; -} +type OptimisticEditedTaskReportAction = Pick< + ReportAction, + 'reportActionID' | 'actionName' | 'pendingAction' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'shouldShow' | 'message' | 'person' +>; + +type OptimisticClosedReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'originalMessage' | 'pendingAction' | 'person' | 'reportActionID' | 'shouldShow' +>; + +type OptimisticCreatedReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' +>; -function isReportType(arg: OnyxEntry | Record): arg is OnyxEntry { +type OptimisticChatReport = Pick< + Report, + | 'type' + | 'chatType' + | 'hasOutstandingIOU' + | 'isOwnPolicyExpenseChat' + | 'isPinned' + | 'lastActorAccountID' + | 'lastMessageTranslationKey' + | 'lastMessageHtml' + | 'lastMessageText' + | 'lastReadTime' + | 'lastVisibleActionCreated' + | 'notificationPreference' + | 'oldPolicyName' + | 'ownerAccountID' + | 'parentReportActionID' + | 'parentReportID' + | 'participantAccountIDs' + | 'policyID' + | 'reportID' + | 'reportName' + | 'stateNum' + | 'statusNum' + | 'visibility' + | 'welcomeMessage' + | 'writeCapability' +>; + +type OptimisticWorkspaceChats = { + announceChatReportID: string; + announceChatData: OptimisticChatReport; + announceReportActionData: Record; + announceCreatedReportActionID: string; + adminsChatReportID: string; + adminsChatData: OptimisticChatReport; + adminsReportActionData: Record; + adminsCreatedReportActionID: string; + expenseChatReportID: string; + expenseChatData: OptimisticChatReport; + expenseReportActionData: Record; + expenseCreatedReportActionID: string; +}; + +function checkIfCorrectType(arg: T | Record): arg is T { + // TODO: change it to correct type guard return arg !== undefined; } @@ -222,6 +278,7 @@ Onyx.connect({ let doesDomainHaveApprovedAccountant = false; Onyx.connect({ key: ONYXKEYS.ACCOUNT, + // Check if I remove that will cause regressions waitForCollectionCallback: true, callback: (value) => (doesDomainHaveApprovedAccountant = value?.doesDomainHaveApprovedAccountant ?? false), }); @@ -239,7 +296,7 @@ Onyx.connect({ callback: (val) => (loginList = val), }); -function getChatType(report?: OnyxEntry): ValueOf | undefined { +function getChatType(report: OnyxEntry): ValueOf | undefined { return report?.chatType; } @@ -297,14 +354,14 @@ function isChatReport(report: OnyxEntry): boolean { /** * Checks if a report is an Expense report. */ -function isExpenseReport(report?: OnyxEntry): boolean { +function isExpenseReport(report: OnyxEntry): boolean { return report?.type === CONST.REPORT.TYPE.EXPENSE; } /** * Checks if a report is an IOU report. */ -function isIOUReport(report?: OnyxEntry): boolean { +function isIOUReport(report: OnyxEntry): boolean { return report?.type === CONST.REPORT.TYPE.IOU; } @@ -367,7 +424,7 @@ function isReportApproved(report: OnyxEntry): boolean { /** * Given a collection of reports returns them sorted by last read */ -function sortReportsByLastRead(reports: OnyxCollection): Report[] { +function sortReportsByLastRead(reports: OnyxCollection): Array> { return Object.values(reports ?? {}) .filter((report) => report?.reportID && report?.lastReadTime) .sort((a, b) => { @@ -454,7 +511,7 @@ function isUserCreatedPolicyRoom(report: OnyxEntry): boolean { /** * Whether the provided report is a Policy Expense chat. */ -function isPolicyExpenseChat(report: OnyxEntry | undefined): boolean { +function isPolicyExpenseChat(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT; } @@ -513,7 +570,7 @@ function isWorkspaceTaskReport(report: OnyxEntry): boolean { if (!isTaskReport(report)) { return false; } - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; return isPolicyExpenseChat(parentReport); } @@ -675,14 +732,14 @@ function isPolicyAdmin(policyID: string, policies: OnyxCollection): bool /** * Returns true if report is a DM/Group DM chat. */ -function isDM(report?: OnyxEntry): boolean { +function isDM(report: OnyxEntry): boolean { return !getChatType(report); } /** * Returns true if report has a single participant. */ -function hasSingleParticipant(report?: OnyxEntry): boolean { +function hasSingleParticipant(report: OnyxEntry): boolean { return report?.participantAccountIDs?.length === 1; } @@ -723,8 +780,8 @@ function isChildReport(report: OnyxEntry): boolean { function isExpenseRequest(report?: OnyxEntry): boolean { if (report && isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; - return isExpenseReport(parentReport) && isTypeReportAction(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; + return isExpenseReport(parentReport) && checkIfCorrectType(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); } return false; } @@ -736,8 +793,8 @@ function isExpenseRequest(report?: OnyxEntry): boolean { function isIOURequest(report?: OnyxEntry): boolean { if (report && isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; - return isIOUReport(parentReport) && isTypeReportAction(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; + return isIOUReport(parentReport) && checkIfCorrectType(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); } return false; } @@ -754,7 +811,7 @@ function isMoneyRequest(reportOrID: OnyxEntry | string): boolean { * Checks if a report is an IOU or expense report. */ function isMoneyRequestReport(reportOrID?: OnyxEntry | string): boolean { - const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`]; + const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null; return isIOUReport(report) || isExpenseReport(report); } @@ -775,12 +832,11 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: const isActionOwner = reportAction?.actorAccountID === currentUserAccountID; - if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { - const originalMessage = reportAction?.originalMessage as IOUMessage; + if (ReportActionsUtils.isMoneyRequestAction(reportAction) && reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { // For now, users cannot delete split actions - const isSplitAction = originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; + const isSplitAction = reportAction.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; - if (isSplitAction || isSettled(String(originalMessage?.IOUReportID)) || (isReportType(report) && isReportApproved(report))) { + if (isSplitAction || isSettled(String(reportAction?.originalMessage?.IOUReportID)) || (checkIfCorrectType(report) && isReportApproved(report))) { return false; } @@ -799,7 +855,7 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: } const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; - const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN && isReportType(report) && !isDM(report); + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN && checkIfCorrectType(report) && !isDM(report); return isActionOwner || isAdmin; } @@ -854,7 +910,7 @@ function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAcc // In 1:1 chat threads, the participants will be the same as parent report. If a report is specifically a 1:1 chat thread then we will // get parent report and use its participants array. if (isThread(report) && !(isTaskReport(report) || isMoneyRequestReport(report))) { - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; if (hasSingleParticipant(parentReport)) { finalReport = parentReport; } @@ -1150,34 +1206,38 @@ function getDisplayNamesWithTooltips( personalDetailsList: PersonalDetails[] | Record, isMultipleParticipantReport: boolean, ): Array> { - return lodashMap(personalDetailsList, (user: PersonalDetails) => { - const accountID = Number(user.accountID); - const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport) ?? user.login ?? ''; - const avatar = UserUtils.getDefaultAvatar(accountID); - - let pronouns = user.pronouns; - if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) { - const pronounTranslationKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); - pronouns = Localize.translateLocal(`pronouns.${pronounTranslationKey}`); - } + const personalDetailsListArray = Array.isArray(personalDetailsList) ? personalDetailsList : Object.values(personalDetailsList); + + return personalDetailsListArray + ?.map((user) => { + const accountID = Number(user.accountID); + const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport) ?? user.login ?? ''; + const avatar = UserUtils.getDefaultAvatar(accountID); + + let pronouns = user.pronouns; + if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) { + const pronounTranslationKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); + pronouns = Localize.translateLocal(`pronouns.${pronounTranslationKey}`); + } - return { - displayName, - avatar, - login: user.login ?? '', - accountID, - pronouns, - }; - }).sort((first: PersonalDetails, second: PersonalDetails) => { - // First sort by displayName/login - const displayNameLoginOrder = first.displayName.localeCompare(second.displayName); - if (displayNameLoginOrder !== 0) { - return displayNameLoginOrder; - } + return { + displayName, + avatar, + login: user.login ?? '', + accountID, + pronouns, + }; + }) + .sort((first, second) => { + // First sort by displayName/login + const displayNameLoginOrder = first.displayName.localeCompare(second.displayName); + if (displayNameLoginOrder !== 0) { + return displayNameLoginOrder; + } - // Then fallback on accountID as the final sorting criteria. - return first.accountID - second.accountID; - }); + // Then fallback on accountID as the final sorting criteria. + return first.accountID - second.accountID; + }); } /** @@ -1237,7 +1297,7 @@ function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: Rep const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID ?? '', actionsToMerge); // For Chat Report with deleted parent actions, let us fetch the correct message - if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && report && isChatReport(report)) { + if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && report && checkIfCorrectType(report) && isChatReport(report)) { const lastMessageText = getDeletedParentActionMessageForChatReport(lastVisibleAction); return { lastMessageText, @@ -1256,7 +1316,7 @@ function isWaitingForIOUActionFromCurrentUser(report: OnyxEntry): boolea return false; } const parentReport = getReport(report.parentReportID); - if (parentReport && isArchivedRoom(parentReport)) { + if (parentReport && checkIfCorrectType(parentReport) && isArchivedRoom(parentReport)) { return false; } @@ -1367,7 +1427,7 @@ function getMoneyRequestSpendBreakdown(report: OnyxEntry, allReportsDict /** * Get the title for a policy expense chat which depends on the role of the policy member seeing this report */ -function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry | undefined): string | undefined { +function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry): string | undefined { const reportOwnerDisplayName = getDisplayNameForParticipant(report?.ownerAccountID ?? -1) ?? allPersonalDetails?.[report?.ownerAccountID ?? -1]?.login ?? report?.reportName; // If the policy expense chat is owned by this user, use the name of the policy as the report name. @@ -1395,7 +1455,7 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry

, policy: OnyxEntry | undefined): string { +function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry): string { const moneyRequestTotal = getMoneyRequestReimbursableTotal(report); const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID)); const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID); @@ -1449,7 +1509,7 @@ function getTransactionDetails(transaction: OnyxEntry, createdDateF } return { created: TransactionUtils.getCreated(transaction, createdDateFormat), - amount: TransactionUtils.getAmount(transaction, isExpenseReport(report)), + amount: TransactionUtils.getAmount(transaction, checkIfCorrectType(report) && isExpenseReport(report)), currency: TransactionUtils.getCurrency(transaction), comment: TransactionUtils.getDescription(transaction), merchant: TransactionUtils.getMerchant(transaction), @@ -1493,7 +1553,8 @@ function canEditMoneyRequest(reportAction: OnyxEntry) { const moneyRequestReport = getReport(String(moneyRequestReportID)); const isReportSettled = isSettled(moneyRequestReport?.reportID); - const isAdmin = ((isExpenseReport(moneyRequestReport) && getPolicy(moneyRequestReport?.policyID ?? '')?.role) ?? '') === CONST.POLICY.ROLE.ADMIN; + const isAdmin = + ((checkIfCorrectType(moneyRequestReport) && isExpenseReport(moneyRequestReport) && getPolicy(moneyRequestReport?.policyID ?? '')?.role) ?? '') === CONST.POLICY.ROLE.ADMIN; const isRequestor = currentUserAccountID === reportAction?.actorAccountID; if (isAdmin) { @@ -1565,7 +1626,7 @@ function getTransactionsWithReceipts(iouReportID: string | undefined): Transacti * or as soon as one receipt request is done scanning, we have at least one * "ready" money request, and we remove this indicator to show the partial report total. */ -function areAllRequestsBeingSmartScanned(iouReportID: string | undefined, reportPreviewAction: OnyxEntry): boolean { +function areAllRequestsBeingSmartScanned(iouReportID: string, reportPreviewAction: OnyxEntry): boolean { const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); // If we have more requests than requests with receipts, we have some manual requests if (ReportActionsUtils.getNumberOfMoneyRequests(reportPreviewAction) > transactionsWithReceipts.length) { @@ -1579,7 +1640,7 @@ function areAllRequestsBeingSmartScanned(iouReportID: string | undefined, report * * @param iouReportID */ -function hasMissingSmartscanFields(iouReportID: string | undefined): boolean { +function hasMissingSmartscanFields(iouReportID: string): boolean { const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); return transactionsWithReceipts.some((transaction) => TransactionUtils.hasMissingSmartscanFields(transaction)); } @@ -1597,7 +1658,7 @@ function getTransactionReportName(reportAction: OnyxEntry): string } const transaction = TransactionUtils.getLinkedTransaction(reportAction); - if (!isTypeTransaction(transaction)) { + if (!checkIfCorrectType(transaction)) { return ''; } if (TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { @@ -1642,7 +1703,7 @@ function getReportPreviewMessage( return reportActionMessage; } - if (isTypeTransaction(linkedTransaction)) { + if (checkIfCorrectType(linkedTransaction)) { if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { return Localize.translateLocal('iou.receiptScanning'); } @@ -1664,7 +1725,7 @@ function getReportPreviewMessage( if (shouldConsiderReceiptBeingScanned && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); - if (isTypeTransaction(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + if (checkIfCorrectType(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { return Localize.translateLocal('iou.receiptScanning'); } } @@ -1867,7 +1928,7 @@ function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry | undefined): OnyxEntry | Record { +function getParentReport(report: OnyxEntry): OnyxEntry | Record { if (!report?.parentReportID) { return {}; } @@ -1891,7 +1952,7 @@ function getRootParentReport(report?: OnyxEntry): OnyxEntry | Re const parentReport = getReport(report?.parentReportID); // Runs recursion to iterate a parent report - return getRootParentReport(parentReport); + return getRootParentReport(checkIfCorrectType(parentReport) ? parentReport : null); } /** @@ -1900,7 +1961,7 @@ function getRootParentReport(report?: OnyxEntry): OnyxEntry | Re function getReportName(report: OnyxEntry, policy: OnyxEntry = null): string { let formattedName; const parentReportAction = ReportActionsUtils.getParentReportAction(report); - if (!isTypeReportAction(parentReportAction)) { + if (!checkIfCorrectType(parentReportAction)) { return ''; } if (isChatThread(report)) { @@ -2028,6 +2089,22 @@ function getReportAndWorkspaceName(report: OnyxEntry): ReportAndWorkspac return {}; } +/** + * Gets the parent navigation subtitle for the report + */ +function getParentNavigationSubtitle(report: OnyxEntry) { + if (isThread(report)) { + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; + const {rootReportName, workspaceName} = getRootReportAndWorkspaceName(parentReport); + if (!rootReportName) { + return {}; + } + + return {rootReportName, workspaceName}; + } + return {}; +} + /** * Navigate to the details page of a given report * @@ -2167,11 +2244,11 @@ function getOptimisticDataForParentReportAction( parentReportActionID = '', ): OnyxUpdate | Record { const report = getReport(reportID); - if (!report) { + if (!report || !checkIfCorrectType(report)) { return {}; } const parentReportAction = ReportActionsUtils.getParentReportAction(report); - if (!parentReportAction || !isTypeReportAction(parentReportAction)) { + if (!parentReportAction || !checkIfCorrectType(parentReportAction)) { return {}; } @@ -2332,7 +2409,7 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num const report = getReport(iouReportID); const amount = type === CONST.IOU.REPORT_ACTION_TYPE.PAY - ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(report), currency) + ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(checkIfCorrectType(report) ? report : null), currency) : CurrencyUtils.convertToDisplayString(total, currency); let paymentMethodMessage; @@ -2510,7 +2587,7 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e * Builds an optimistic SUBMITTED report action with a randomly generated reportActionID. * */ -function buildOptimisticSubmittedReportAction(amount: number, currency: string, expenseReportID: string) { +function buildOptimisticSubmittedReportAction(amount: number, currency: string, expenseReportID: string): OptimisticSubmittedReportAction { const originalMessage = { amount, currency, @@ -2547,7 +2624,7 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string, * @param [comment] - User comment for the IOU. * @param [transaction] - optimistic first transaction of preview */ -function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: OnyxEntry, comment = '', transaction: Transaction | undefined = undefined): OptimisticReportPreview { +function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: OnyxEntry, comment = '', transaction: OnyxEntry = null): OptimisticReportPreview { const hasReceipt = TransactionUtils.hasReceipt(transaction); const isReceiptBeingScanned = hasReceipt && transaction && TransactionUtils.isReceiptBeingScanned(transaction); const message = getReportPreviewMessage(iouReport); @@ -2716,20 +2793,20 @@ function buildOptimisticTaskReportAction(taskReportID: string, actionName: DeepV * Builds an optimistic chat report with a randomly generated reportID and as much information as we currently have */ function buildOptimisticChatReport( - participantList: Array, + participantList: number[], reportName: string = CONST.REPORT.DEFAULT_REPORT_NAME, - chatType: ValueOf | '' = '', + chatType: ValueOf | undefined = undefined, policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE, ownerAccountID: number = CONST.REPORT.OWNER_ACCOUNT_ID_FAKE, isOwnPolicyExpenseChat = false, oldPolicyName = '', - visibility: ValueOf | undefined | null = undefined, + visibility: ValueOf | undefined = undefined, writeCapability: ValueOf | undefined = undefined, notificationPreference: string | number = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, parentReportActionID = '', parentReportID = '', welcomeMessage = '', -) { +): OptimisticChatReport { const currentTime = DateUtils.getDBTime(); return { type: CONST.REPORT.TYPE.CHAT, @@ -2740,7 +2817,7 @@ function buildOptimisticChatReport( lastActorAccountID: 0, lastMessageTranslationKey: '', lastMessageHtml: '', - lastMessageText: null, + lastMessageText: undefined, lastReadTime: currentTime, lastVisibleActionCreated: currentTime, notificationPreference, @@ -2763,7 +2840,7 @@ function buildOptimisticChatReport( /** * Returns the necessary reportAction onyx data to indicate that the chat has been created optimistically */ -function buildOptimisticCreatedReportAction(emailCreatingAction: string) { +function buildOptimisticCreatedReportAction(emailCreatingAction: string): OptimisticCreatedReportAction { return { reportActionID: NumberUtils.rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, @@ -2798,8 +2875,7 @@ function buildOptimisticCreatedReportAction(emailCreatingAction: string) { /** * Returns the necessary reportAction onyx data to indicate that a task report has been edited */ -function buildOptimisticEditedTaskReportAction(emailEditingTask: string) { - // TODO: create type for return value +function buildOptimisticEditedTaskReportAction(emailEditingTask: string): OptimisticEditedTaskReportAction { return { reportActionID: NumberUtils.rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED, @@ -2838,7 +2914,7 @@ function buildOptimisticEditedTaskReportAction(emailEditingTask: string) { * @param policyName * @param reason - A reason why the chat has been archived */ -function buildOptimisticClosedReportAction(emailClosingReport: string, policyName: string, reason: string = CONST.REPORT.ARCHIVE_REASON.DEFAULT) { +function buildOptimisticClosedReportAction(emailClosingReport: string, policyName: string, reason: string = CONST.REPORT.ARCHIVE_REASON.DEFAULT): OptimisticClosedReportAction { return { actionName: CONST.REPORT.ACTIONS.TYPE.CLOSED, actorAccountID: currentUserAccountID, @@ -2874,7 +2950,7 @@ function buildOptimisticClosedReportAction(emailClosingReport: string, policyNam }; } -function buildOptimisticWorkspaceChats(policyID: string, policyName: string) { +function buildOptimisticWorkspaceChats(policyID: string, policyName: string): OptimisticWorkspaceChats { const announceChatData = buildOptimisticChatReport( currentUserAccountID ? [currentUserAccountID] : [], CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE, @@ -2883,7 +2959,7 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string) { CONST.POLICY.OWNER_ACCOUNT_ID_FAKE, false, policyName, - null, + undefined, undefined, // #announce contains all policy members so notifying always should be opt-in only. @@ -2933,6 +3009,22 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string) { }; } +type OptimisticTaskReport = Pick< + Report, + | 'reportID' + | 'reportName' + | 'description' + | 'ownerAccountID' + | 'participantAccountIDs' + | 'managerID' + | 'type' + | 'parentReportID' + | 'policyID' + | 'stateNum' + | 'statusNum' + | 'notificationPreference' +>; + /** * Builds an optimistic Task Report with a randomly generated reportID * @@ -2951,7 +3043,7 @@ function buildOptimisticTaskReport( title?: string, description?: string, policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE, -) { +): OptimisticTaskReport { return { reportID: generateReportID(), reportName: title, @@ -2976,7 +3068,7 @@ function buildOptimisticTaskReport( * @param moneyRequestReportID - the reportID which the report action belong to */ function buildTransactionThread(reportAction: OnyxEntry, moneyRequestReportID: string) { - const participantAccountIDs = [...new Set([currentUserAccountID, Number(reportAction?.actorAccountID)])]; + const participantAccountIDs = [...new Set([currentUserAccountID, Number(reportAction?.actorAccountID)])].filter(Boolean) as number[]; return buildOptimisticChatReport( participantAccountIDs, getTransactionReportName(reportAction), @@ -3037,7 +3129,6 @@ function isIOUOwnedByCurrentUser(report: OnyxEntry, allReportsDict: Onyx * @param report (chatReport or iouReport) */ -// @ts-expect-error Will be fixed when OptionUtils will be merged function isOneOnOneChat(report: OnyxEntry): boolean { const isChatRoomValue = report?.isChatRoom ?? false; const participantsListValue = report?.participantsList ?? []; @@ -3105,7 +3196,8 @@ function canAccessReport(report: OnyxEntry, policies: OnyxCollection, currentReportId: string): boolean { - const parentReport = getParentReport(getReport(currentReportId)); + const currentReport = getReport(currentReportId); + const parentReport = getParentReport(checkIfCorrectType(currentReport) ? currentReport : null); const reportActions = ReportActionsUtils.getAllReportActions(report?.reportID ?? ''); const isChildReportHasComment = Object.values(reportActions ?? {})?.some((reportAction) => (reportAction?.childVisibleActionCount ?? 0) > 0); return parentReport?.reportID !== report?.reportID && !isChildReportHasComment; @@ -3119,7 +3211,6 @@ function shouldHideReport(report: OnyxEntry, currentReportId: string): b * filter out the majority of reports before filtering out very specific minority of reports. */ function shouldReportBeInOptionList( - // TODO: Change to OptionList type when merged report: OnyxEntry, currentReportId: string, isInGSDMode: boolean, @@ -3282,7 +3373,7 @@ function canFlagReportAction(reportAction: OnyxEntry, reportID: st reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportActionsUtils.isDeletedAction(reportAction) && !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && - isReportType(report) && + checkIfCorrectType(report) && isAllowedToComment(report), ); } @@ -3675,6 +3766,17 @@ function shouldDisableRename(report: OnyxEntry, policy: OnyxEntry; +}; + /** * Returns the onyx data needed for the task assignee chat */ @@ -3804,7 +3906,7 @@ function getParticipantsIDs(report: OnyxEntry): number[] { // Build participants list for IOU/expense reports if (isMoneyRequestReport(report)) { - const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...participants].filter(Boolean); + const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...participants].filter(Boolean) as number[]; const onlyUnique = [...new Set([...onlyTruthyValues])]; return onlyUnique; } @@ -3821,7 +3923,10 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) const {amount, currency, IOUReportID} = originalMessage; const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency); const iouReport = getReport(String(IOUReportID) ?? ''); - const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport?.managerID, true); + + const payerName = isExpenseReport(checkIfCorrectType(iouReport) ? iouReport : null) + ? getPolicyName(checkIfCorrectType(iouReport) ? iouReport : null) + : getDisplayNameForParticipant(iouReport?.managerID, true); let translationKey; switch (originalMessage.paymentType) { case CONST.IOU.PAYMENT_TYPE.ELSEWHERE: @@ -3838,7 +3943,7 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) displayMessage = Localize.translateLocal(translationKey, {amount: formattedAmount, payer: payerName}); } else { const transaction = TransactionUtils.getTransaction(originalMessage.IOUTransactionID ?? ''); - const transactionDetails = transaction && isTypeTransaction(transaction) ? getTransactionDetails(transaction) : undefined; + const transactionDetails = transaction && checkIfCorrectType(transaction) ? getTransactionDetails(transaction) : undefined; const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); displayMessage = Localize.translateLocal('iou.requestedAmount', { formattedAmount, @@ -3897,6 +4002,7 @@ export { isUserCreatedPolicyRoom, isChatRoom, getChatRoomSubtitle, + getParentNavigationSubtitle, getReportAndWorkspaceName, getPolicyName, getPolicyType, diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 8b99ec8aff68..2c756608107d 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -1,6 +1,7 @@ import {ValueOf} from 'type-fest'; import CONST from '../../CONST'; import * as OnyxCommon from './OnyxCommon'; +import PersonalDetails from './PersonalDetails'; type Report = { /** The specific type of chat */ @@ -93,6 +94,9 @@ type Report = { chatReportID?: string; state?: ValueOf; isHidden?: boolean; + isChatRoom?: boolean; + participantsList?: Array>; + description?: string; }; export default Report; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 84f5cd987a76..11525b3d25d3 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -10,7 +10,7 @@ type Message = { type: string; /** The html content of the fragment. */ - html: string; + html?: string; /** The text content of the fragment. */ text: string; From 557fbc1e6b209385e59f3c2941e6e9873e1c778f Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 26 Oct 2023 13:10:47 +0200 Subject: [PATCH 15/63] fix: adding return types --- src/libs/ReportActionsUtils.ts | 2 +- src/libs/ReportUtils.ts | 181 +++++++++++++++++------------- src/types/onyx/OriginalMessage.ts | 20 +++- 3 files changed, 122 insertions(+), 81 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 4cc997919aee..9211bc2508d8 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -369,7 +369,7 @@ function replaceBaseURL(reportAction: ReportAction): ReportAction { if (!updatedReportAction.message) { return updatedReportAction; } - updatedReportAction.message[0].html = reportAction.message[0].html.replace('%baseURL', environmentURL); + updatedReportAction.message[0].html = reportAction.message[0].html?.replace('%baseURL', environmentURL); return updatedReportAction; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index aef93332c057..d1d3a4c7fa5e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -28,7 +28,7 @@ import * as UserUtils from './UserUtils'; import {Beta, Login, PersonalDetails, Policy, Report, ReportAction, Transaction} from '../types/onyx'; import {Receipt, WaypointCollection} from '../types/onyx/Transaction'; import DeepValueOf from '../types/utils/DeepValueOf'; -import {IOUMessage} from '../types/onyx/OriginalMessage'; +import {IOUMessage, OriginalMessageActionName} from '../types/onyx/OriginalMessage'; import {Message, ReportActions} from '../types/onyx/ReportAction'; import {PendingAction} from '../types/onyx/OnyxCommon'; @@ -220,6 +220,23 @@ type OptimisticChatReport = Pick< | 'writeCapability' >; +type OptimisticTaskReportAction = Pick< + ReportAction, + | 'actionName' + | 'actorAccountID' + | 'automatic' + | 'avatar' + | 'created' + | 'isAttachment' + | 'message' + | 'originalMessage' + | 'person' + | 'pendingAction' + | 'reportActionID' + | 'shouldShow' + | 'isFirstItem' +>; + type OptimisticWorkspaceChats = { announceChatReportID: string; announceChatData: OptimisticChatReport; @@ -235,6 +252,65 @@ type OptimisticWorkspaceChats = { expenseCreatedReportActionID: string; }; +type OptimisticModifiedExpenseReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'isAttachment' | 'message' | 'originalMessage' | 'person' | 'pendingAction' | 'reportActionID' | 'shouldShow' +> & {reportID?: string}; + +type OptimisticTaskReport = Pick< + Report, + | 'reportID' + | 'reportName' + | 'description' + | 'ownerAccountID' + | 'participantAccountIDs' + | 'managerID' + | 'type' + | 'parentReportID' + | 'policyID' + | 'stateNum' + | 'statusNum' + | 'notificationPreference' +>; + +type TransactionDetails = + | { + created: string; + amount: number; + currency: string; + merchant: string; + waypoints?: WaypointCollection; + comment: string; + category: string; + billable: boolean; + tag: string; + mccGroup?: ValueOf; + cardID: number; + originalAmount: number; + originalCurrency: string; + } + | undefined; + +type OptimisticIOUReport = Pick< + Report, + | 'cachedTotal' + | 'hasOutstandingIOU' + | 'type' + | 'chatReportID' + | 'currency' + | 'managerID' + | 'ownerAccountID' + | 'participantAccountIDs' + | 'reportID' + | 'state' + | 'stateNum' + | 'total' + | 'reportName' + | 'notificationPreference' + | 'parentReportID' + | 'statusNum' +>; + function checkIfCorrectType(arg: T | Record): arg is T { // TODO: change it to correct type guard return arg !== undefined; @@ -279,7 +355,7 @@ let doesDomainHaveApprovedAccountant = false; Onyx.connect({ key: ONYXKEYS.ACCOUNT, // Check if I remove that will cause regressions - waitForCollectionCallback: true, + // waitForCollectionCallback: true, callback: (value) => (doesDomainHaveApprovedAccountant = value?.doesDomainHaveApprovedAccountant ?? false), }); @@ -430,6 +506,7 @@ function sortReportsByLastRead(reports: OnyxCollection): Array { const aTime = new Date(a?.lastReadTime ?? ''); const bTime = new Date(b?.lastReadTime ?? ''); + // @ts-expect-error It's ok to subtract dates return aTime - bTime; }); } @@ -777,7 +854,7 @@ function isChildReport(report: OnyxEntry): boolean { * An Expense Request is a thread where the parent report is an Expense Report and * the parentReportAction is a transaction. */ -function isExpenseRequest(report?: OnyxEntry): boolean { +function isExpenseRequest(report: OnyxEntry): boolean { if (report && isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; @@ -790,7 +867,7 @@ function isExpenseRequest(report?: OnyxEntry): boolean { * An IOU Request is a thread where the parent report is an IOU Report and * the parentReportAction is a transaction. */ -function isIOURequest(report?: OnyxEntry): boolean { +function isIOURequest(report: OnyxEntry): boolean { if (report && isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; @@ -803,14 +880,14 @@ function isIOURequest(report?: OnyxEntry): boolean { * Checks if a report is an IOU or expense request. */ function isMoneyRequest(reportOrID: OnyxEntry | string): boolean { - const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID; + const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; return isIOURequest(report) || isExpenseRequest(report); } /** * Checks if a report is an IOU or expense report. */ -function isMoneyRequestReport(reportOrID?: OnyxEntry | string): boolean { +function isMoneyRequestReport(reportOrID: OnyxEntry | string): boolean { const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null; return isIOUReport(report) || isExpenseReport(report); } @@ -1355,6 +1432,7 @@ function isWaitingForIOUActionFromCurrentUser(report: OnyxEntry): boolea * * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ +// TODO: TEST IT CAREFULLY function isWaitingForTaskCompleteFromAssignee(report: OnyxEntry, parentReportAction?: OnyxEntry): boolean { return isTaskReport(report) && isReportManager(report) && isOpenTaskReport(report, parentReportAction); } @@ -1368,7 +1446,7 @@ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolea return allTransactions.filter((transaction) => transaction.reimbursable === false).length > 0; } -function getMoneyRequestReimbursableTotal(report: OnyxEntry | undefined, allReportsDict?: OnyxCollection): number { +function getMoneyRequestReimbursableTotal(report: OnyxEntry, allReportsDict?: OnyxCollection): number { const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport; if (isMoneyRequestReport(report)) { @@ -1480,23 +1558,6 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< return payerPaidAmountMessage; } -type TransactionDetails = - | { - created: string; - amount: number; - currency: string; - merchant: string; - waypoints?: WaypointCollection; - comment: string; - category: string; - billable: boolean; - tag: string; - mccGroup?: ValueOf; - cardID: number; - originalAmount: number; - originalCurrency: string; - } - | undefined; /** * Gets transaction created, amount, currency, comment, and waypoints (for distance request) * into a flat object. Used for displaying transactions and sending them in API commands @@ -1776,7 +1837,7 @@ function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: st /** * Get the proper message schema for modified distance message. */ -function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { +function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string): string { if (!oldDistance) { return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); } @@ -1939,7 +2000,7 @@ function getParentReport(report: OnyxEntry): OnyxEntry | Record< * Returns the root parentReport if the given report is nested. * Uses recursion to iterate any depth of nested reports. */ -function getRootParentReport(report?: OnyxEntry): OnyxEntry | Record { +function getRootParentReport(report: OnyxEntry): OnyxEntry | Record { if (!report) { return {}; } @@ -2019,7 +2080,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu * Recursively navigates through thread parents to get the root report and workspace name. * The recursion stops when we find a non thread or money request report, whichever comes first. */ -function getRootReportAndWorkspaceName(report: OnyxEntry | undefined): ReportAndWorkspaceName { +function getRootReportAndWorkspaceName(report: OnyxEntry): ReportAndWorkspaceName { if (!report) { return { rootReportName: '', @@ -2027,7 +2088,7 @@ function getRootReportAndWorkspaceName(report: OnyxEntry | undefined): R }; } if (isChildReport(report) && !isMoneyRequestReport(report) && !isTaskReport(report)) { - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; return getRootReportAndWorkspaceName(parentReport); } @@ -2078,7 +2139,7 @@ function getChatRoomSubtitle(report: OnyxEntry): string | undefined { */ function getReportAndWorkspaceName(report: OnyxEntry): ReportAndWorkspaceName | Record { if (isThread(report)) { - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; const {rootReportName, workspaceName} = getRootReportAndWorkspaceName(parentReport); if (!rootReportName) { return {}; @@ -2094,7 +2155,7 @@ function getReportAndWorkspaceName(report: OnyxEntry): ReportAndWorkspac */ function getParentNavigationSubtitle(report: OnyxEntry) { if (isThread(report)) { - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; const {rootReportName, workspaceName} = getRootReportAndWorkspaceName(parentReport); if (!rootReportName) { return {}; @@ -2312,25 +2373,6 @@ function buildOptimisticTaskCommentReportAction( * @param isSendingMoney - If we send money the IOU should be created as settled */ -type OptimisticIOUReport = Pick< - Report, - | 'cachedTotal' - | 'hasOutstandingIOU' - | 'type' - | 'chatReportID' - | 'currency' - | 'managerID' - | 'ownerAccountID' - | 'participantAccountIDs' - | 'reportID' - | 'state' - | 'stateNum' - | 'total' - | 'reportName' - | 'notificationPreference' - | 'parentReportID' - | 'statusNum' ->; function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number, total: number, chatReportID: string, currency: string, isSendingMoney = false): OptimisticIOUReport { const formattedTotal = CurrencyUtils.convertToDisplayString(total, currency); const personalDetails = getPersonalDetailsForAccountID(payerAccountID); @@ -2664,7 +2706,7 @@ function buildOptimisticModifiedExpenseReportAction( oldTransaction: OnyxEntry, transactionChanges: ExpenseOriginalMessage, isFromExpenseReport: boolean, -) { +): OptimisticModifiedExpenseReportAction { const originalMessage = getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport); return { actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, @@ -2685,7 +2727,7 @@ function buildOptimisticModifiedExpenseReportAction( person: [ { style: 'strong', - text: currentUserPersonalDetails?.displayName ?? currentUserAccountID, + text: currentUserPersonalDetails?.displayName ?? String(currentUserAccountID), type: 'TEXT', }, ], @@ -2753,13 +2795,12 @@ function updateReportPreview( }; } -function buildOptimisticTaskReportAction(taskReportID: string, actionName: DeepValueOf, message = '') { +function buildOptimisticTaskReportAction(taskReportID: string, actionName: OriginalMessageActionName, message = ''): OptimisticTaskReportAction { const originalMessage = { taskReportID, type: actionName, text: message, }; - return { actionName, actorAccountID: currentUserAccountID, @@ -2777,7 +2818,7 @@ function buildOptimisticTaskReportAction(taskReportID: string, actionName: DeepV person: [ { style: 'strong', - text: currentUserPersonalDetails?.displayName ?? currentUserAccountID, + text: currentUserPersonalDetails?.displayName ?? String(currentUserAccountID), type: 'TEXT', }, ], @@ -3009,22 +3050,6 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string): Op }; } -type OptimisticTaskReport = Pick< - Report, - | 'reportID' - | 'reportName' - | 'description' - | 'ownerAccountID' - | 'participantAccountIDs' - | 'managerID' - | 'type' - | 'parentReportID' - | 'policyID' - | 'stateNum' - | 'statusNum' - | 'notificationPreference' ->; - /** * Builds an optimistic Task Report with a randomly generated reportID * @@ -3067,12 +3092,12 @@ function buildOptimisticTaskReport( * * @param moneyRequestReportID - the reportID which the report action belong to */ -function buildTransactionThread(reportAction: OnyxEntry, moneyRequestReportID: string) { +function buildTransactionThread(reportAction: OnyxEntry, moneyRequestReportID: string): OptimisticChatReport { const participantAccountIDs = [...new Set([currentUserAccountID, Number(reportAction?.actorAccountID)])].filter(Boolean) as number[]; return buildOptimisticChatReport( participantAccountIDs, getTransactionReportName(reportAction), - '', + undefined, getReport(moneyRequestReportID)?.policyID ?? CONST.POLICY.OWNER_EMAIL_FAKE, CONST.POLICY.OWNER_ACCOUNT_ID_FAKE, false, @@ -3770,8 +3795,8 @@ type OnyxDataTaskAssigneeChat = { optimisticData: OnyxUpdate[]; successData: OnyxUpdate[]; failureData: OnyxUpdate[]; - optimisticAssigneeAddComment: OptimisticReportAction; - optimisticChatCreatedReportAction: Pick< + optimisticAssigneeAddComment?: OptimisticReportAction; + optimisticChatCreatedReportAction?: Pick< ReportAction, 'reportActionID' | 'actionName' | 'pendingAction' | 'actorAccountID' | 'message' | 'person' | 'automatic' | 'avatar' | 'created' | 'shouldShow' >; @@ -3789,15 +3814,15 @@ function getTaskAssigneeChatOnyxData( parentReportID: string, title: string, assigneeChatReport: OnyxEntry, -) { +): OnyxDataTaskAssigneeChat { // Set if we need to add a comment to the assignee chat notifying them that they have been assigned a task let optimisticAssigneeAddComment; // Set if this is a new chat that needs to be created for the assignee let optimisticChatCreatedReportAction; const currentTime = DateUtils.getDBTime(); - const optimisticData = []; - const successData = []; - const failureData = []; + const optimisticData: OnyxUpdate[] = []; + const successData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; // You're able to assign a task to someone you haven't chatted with before - so we need to optimistically create the chat and the chat reportActions // Only add the assignee chat report to onyx if we haven't already set it optimistically diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 90553b70c26d..679f29abc6a1 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -3,7 +3,23 @@ import CONST from '../../CONST'; import DeepValueOf from '../utils/DeepValueOf'; type ActionName = DeepValueOf; - +type OriginalMessageActionName = + | 'ADDCOMMENT' + | 'APPROVED' + | 'CHRONOSOOOLIST' + | 'CLOSED' + | 'CREATED' + | 'IOU' + | 'MODIFIEDEXPENSE' + | 'REIMBURSEMENTQUEUED' + | 'RENAMED' + | 'REPORTPREVIEW' + | 'SUBMITTED' + | 'TASKCANCELLED' + | 'TASKCOMPLETED' + | 'TASKEDITED' + | 'TASKREOPENED' + | ValueOf; type OriginalMessageApproved = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.APPROVED; originalMessage: unknown; @@ -184,4 +200,4 @@ type OriginalMessage = | OriginalMessageReimbursementQueued; export default OriginalMessage; -export type {ChronosOOOEvent, Decision, Reaction, ActionName, IOUMessage, Closed}; +export type {ChronosOOOEvent, Decision, Reaction, ActionName, IOUMessage, Closed, OriginalMessageActionName}; From 27ed0e53197ca25519e4a3332aee446cc22a0653 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 26 Oct 2023 13:58:53 +0200 Subject: [PATCH 16/63] fix: fixed last TS issue --- src/libs/ReportUtils.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c8a08d01b5b5..ad449e13e2a8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3797,10 +3797,7 @@ type OnyxDataTaskAssigneeChat = { successData: OnyxUpdate[]; failureData: OnyxUpdate[]; optimisticAssigneeAddComment?: OptimisticReportAction; - optimisticChatCreatedReportAction?: Pick< - ReportAction, - 'reportActionID' | 'actionName' | 'pendingAction' | 'actorAccountID' | 'message' | 'person' | 'automatic' | 'avatar' | 'created' | 'shouldShow' - >; + optimisticChatCreatedReportAction?: OptimisticCreatedReportAction; }; /** @@ -3843,7 +3840,7 @@ function getTaskAssigneeChatOnyxData( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, - value: {[optimisticChatCreatedReportAction.reportActionID]: optimisticChatCreatedReportAction}, + value: {[optimisticChatCreatedReportAction.reportActionID ?? '']: optimisticChatCreatedReportAction as Partial}, }, ); @@ -3867,7 +3864,7 @@ function getTaskAssigneeChatOnyxData( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, - value: {[optimisticChatCreatedReportAction.reportActionID]: {pendingAction: null}}, + value: {[optimisticChatCreatedReportAction.reportActionID ?? '']: {pendingAction: null}}, }, // If we failed, we want to remove the optimistic personal details as it was likely due to an invalid login { From dae29531bbe30d305a4ef0f10a2efc8cfbd24919 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 26 Oct 2023 17:26:23 +0200 Subject: [PATCH 17/63] fix: fix ts problems --- src/libs/Permissions.ts | 9 +++++---- src/libs/ReportUtils.ts | 15 ++++++--------- src/pages/home/report/withReportOrNotFound.tsx | 4 ++-- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 13489c396c3c..cc2ecb26ad76 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -1,8 +1,9 @@ +import {OnyxEntry} from 'react-native-onyx'; import CONST from '../CONST'; import Beta from '../types/onyx/Beta'; -function canUseAllBetas(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.ALL); +function canUseAllBetas(betas: OnyxEntry): boolean { + return Boolean(betas?.includes(CONST.BETAS.ALL)); } function canUseChronos(betas: Beta[]): boolean { @@ -13,8 +14,8 @@ function canUsePayWithExpensify(betas: Beta[]): boolean { return betas?.includes(CONST.BETAS.PAY_WITH_EXPENSIFY) || canUseAllBetas(betas); } -function canUseDefaultRooms(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas(betas); +function canUseDefaultRooms(betas: OnyxEntry): boolean { + return betas?.includes(CONST.BETAS.DEFAULT_ROOMS) ?? canUseAllBetas(betas); } function canUseWallet(betas: Beta[]): boolean { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ad449e13e2a8..d50616a8271b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -355,7 +355,7 @@ let doesDomainHaveApprovedAccountant = false; Onyx.connect({ key: ONYXKEYS.ACCOUNT, // Check if I remove that will cause regressions - // waitForCollectionCallback: true, + waitForCollectionCallback: true, callback: (value) => (doesDomainHaveApprovedAccountant = value?.doesDomainHaveApprovedAccountant ?? false), }); @@ -2022,15 +2022,12 @@ function getRootParentReport(report: OnyxEntry): OnyxEntry | Rec function getReportName(report: OnyxEntry, policy: OnyxEntry = null): string { let formattedName; const parentReportAction = ReportActionsUtils.getParentReportAction(report); - if (!checkIfCorrectType(parentReportAction)) { - return ''; - } if (isChatThread(report)) { - if (ReportActionsUtils.isTransactionThread(parentReportAction)) { + if (checkIfCorrectType(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction)) { return getTransactionReportName(parentReportAction); } - const isAttachment = ReportActionsUtils.isReportActionAttachment(parentReportAction); + const isAttachment = ReportActionsUtils.isReportActionAttachment(checkIfCorrectType(parentReportAction) ? parentReportAction : null); const parentReportActionMessage = (parentReportAction?.message?.[0].text ?? '').replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { return `[${Localize.translateLocal('common.attachment')}]`; @@ -2044,7 +2041,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu return parentReportActionMessage || Localize.translateLocal('parentReportAction.deletedMessage'); } - if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { + if (isTaskReport(report) && checkIfCorrectType(parentReportAction) && isCanceledTaskReport(report, parentReportAction)) { return Localize.translateLocal('parentReportAction.deletedTask'); } @@ -3175,7 +3172,7 @@ function isOneOnOneChat(report: OnyxEntry): boolean { * Assuming the passed in report is a default room, lets us know whether we can see it or not, based on permissions and * the various subsets of users we've allowed to use default rooms. */ -function canSeeDefaultRoom(report: OnyxEntry, policies: OnyxCollection, betas: Beta[]): boolean { +function canSeeDefaultRoom(report: OnyxEntry, policies: OnyxCollection, betas: OnyxEntry): boolean { // Include archived rooms if (isArchivedRoom(report)) { return true; @@ -3205,7 +3202,7 @@ function canSeeDefaultRoom(report: OnyxEntry, policies: OnyxCollection

, policies: OnyxCollection, betas: Beta[], allReportActions?: OnyxCollection): boolean { +function canAccessReport(report: OnyxEntry, policies: OnyxCollection, betas: OnyxEntry, allReportActions?: OnyxCollection): boolean { if (isThread(report) && ReportActionsUtils.isPendingRemove(ReportActionsUtils.getParentReportAction(report, allReportActions))) { return false; } diff --git a/src/pages/home/report/withReportOrNotFound.tsx b/src/pages/home/report/withReportOrNotFound.tsx index 28d6707b085f..cec451b038a3 100644 --- a/src/pages/home/report/withReportOrNotFound.tsx +++ b/src/pages/home/report/withReportOrNotFound.tsx @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/no-negated-variables */ import React, {ComponentType, ForwardedRef, RefAttributes} from 'react'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {OnyxCollection, OnyxEntry, withOnyx} from 'react-native-onyx'; import {RouteProp} from '@react-navigation/native'; import getComponentDisplayName from '../../../libs/getComponentDisplayName'; import NotFoundPage from '../../ErrorPage/NotFoundPage'; @@ -13,7 +13,7 @@ type OnyxProps = { /** The report currently being looked at */ report: OnyxEntry; /** The policies which the user has access to */ - policies: OnyxEntry; + policies: OnyxCollection; /** Beta features list */ betas: OnyxEntry; /** Indicated whether the report data is loading */ From 1c2fcb73e11440897f239847d16034f9ebee99f6 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 26 Oct 2023 19:11:53 +0200 Subject: [PATCH 18/63] fix: adjusted type comment --- src/types/onyx/PersonalDetails.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 118881a2551b..89706fe47419 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -45,7 +45,7 @@ type PersonalDetails = { /** Timezone of the current user from their personal details */ timezone?: Timezone; - /** If trying to get PersonalDetails from the server and user is offling */ + /** Flag for checking if data is from optimistic data */ isOptimisticPersonalDetail?: boolean; fallbackIcon?: string; From e3eb38d318ff8a73d2ef4f1c312660336056a9bd Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 26 Oct 2023 20:29:25 +0200 Subject: [PATCH 19/63] fix: fixed issue with not displaying displayName for Reports --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d50616a8271b..70f3067f1826 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2186,7 +2186,7 @@ function navigateToDetailsPage(report: OnyxEntry) { * this is more than random enough for our needs. */ function generateReportID(): string { - return Math.floor(Math.random() * 2 ** 21) * 2 ** 32 + Math.floor(Math.random() * 2 ** 32).toString(); + return (Math.floor(Math.random() * 2 ** 21) * 2 ** 32 + Math.floor(Math.random() * 2 ** 32)).toString(); } function hasReportNameError(report: OnyxEntry): boolean { From a951d657f99eecb2efc998c0398ceb4d781549f6 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 27 Oct 2023 12:40:48 +0200 Subject: [PATCH 20/63] fix: remove waitForCollectionCallback from Onyx.connect where its not used on collection key --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 70f3067f1826..be716f2c2651 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -355,7 +355,7 @@ let doesDomainHaveApprovedAccountant = false; Onyx.connect({ key: ONYXKEYS.ACCOUNT, // Check if I remove that will cause regressions - waitForCollectionCallback: true, + // waitForCollectionCallback: true, callback: (value) => (doesDomainHaveApprovedAccountant = value?.doesDomainHaveApprovedAccountant ?? false), }); From 9ce8236ba28148a51f68c7a7f78890458db3253a Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 27 Oct 2023 14:33:06 +0200 Subject: [PATCH 21/63] fix: added return types --- src/libs/ReportActionsUtils.ts | 2 ++ src/libs/ReportUtils.ts | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 9211bc2508d8..bfdd9466c3c6 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -661,3 +661,5 @@ export { shouldReportActionBeVisibleAsLastAction, getFirstVisibleReportActionID, }; + +export type {LastVisibleMessage}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index be716f2c2651..1f88ab349a91 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -31,6 +31,7 @@ import DeepValueOf from '../types/utils/DeepValueOf'; import {IOUMessage, OriginalMessageActionName} from '../types/onyx/OriginalMessage'; import {Message, ReportActions} from '../types/onyx/ReportAction'; import {PendingAction} from '../types/onyx/OnyxCommon'; +import {LastVisibleMessage} from './ReportActionsUtils'; type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string}; @@ -479,7 +480,7 @@ function isOpenTaskReport(report: OnyxEntry, parentReportAction?: OnyxEn /** * Checks if a report is a completed task report. */ -function isCompletedTaskReport(report: OnyxEntry) { +function isCompletedTaskReport(report: OnyxEntry): boolean { return isTaskReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS.APPROVED; } @@ -1369,7 +1370,7 @@ function getReimbursementQueuedActionMessage(reportAction: OnyxEntry, createdDateF * - the current user is the requestor and is not settled yet * - or the user is an admin on the policy the expense report is tied to */ -function canEditMoneyRequest(reportAction: OnyxEntry) { +function canEditMoneyRequest(reportAction: OnyxEntry): boolean { const isDeleted = ReportActionsUtils.isDeletedAction(reportAction); if (isDeleted) { @@ -2150,7 +2151,7 @@ function getReportAndWorkspaceName(report: OnyxEntry): ReportAndWorkspac /** * Gets the parent navigation subtitle for the report */ -function getParentNavigationSubtitle(report: OnyxEntry) { +function getParentNavigationSubtitle(report: OnyxEntry): ReportAndWorkspaceName | Record { if (isThread(report)) { const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; const {rootReportName, workspaceName} = getRootReportAndWorkspaceName(parentReport); From 0c1af91dc3a492e8ebd3924399fc3bcd8d032b5b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 27 Oct 2023 14:42:11 +0200 Subject: [PATCH 22/63] fix: adjusted type guard --- src/libs/ReportUtils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1f88ab349a91..0fe8daf4eb43 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -313,8 +313,7 @@ type OptimisticIOUReport = Pick< >; function checkIfCorrectType(arg: T | Record): arg is T { - // TODO: change it to correct type guard - return arg !== undefined; + return Object.keys(arg ?? {}).length > 0; } let currentUserEmail: string | undefined; @@ -1433,7 +1432,6 @@ function isWaitingForIOUActionFromCurrentUser(report: OnyxEntry): boolea * * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ -// TODO: TEST IT CAREFULLY function isWaitingForTaskCompleteFromAssignee(report: OnyxEntry, parentReportAction?: OnyxEntry): boolean { return isTaskReport(report) && isReportManager(report) && isOpenTaskReport(report, parentReportAction); } From e45f4012afd9db7dd329812dd770a416394cc8d5 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 30 Oct 2023 09:02:55 +0100 Subject: [PATCH 23/63] fix: adjusted types in SidebarUtils --- src/libs/ReportUtils.ts | 3 ++- src/libs/SidebarUtils.ts | 33 ++++++++++++--------------------- src/types/onyx/Report.ts | 1 + 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 772ad942bc82..d5f8b1a51076 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3262,7 +3262,6 @@ function shouldReportBeInOptionList( ) { return false; } - if (!canAccessReport(report, policies, betas, allReportActions)) { return false; } @@ -4163,3 +4162,5 @@ export { parseReportRouteParams, getReimbursementQueuedActionMessage, }; + +export type {Avatar}; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index bfe7d2281049..73ef32d40b45 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -18,6 +18,7 @@ import Policy from '../types/onyx/Policy'; import Report from '../types/onyx/Report'; import {PersonalDetails} from '../types/onyx'; import * as OnyxCommon from '../types/onyx/OnyxCommon'; +import type {Avatar} from './ReportUtils'; const visibleReportActionItems: ReportActions = {}; const lastReportActions: ReportActions = {}; @@ -146,7 +147,10 @@ function getOrderedReportIDs( const isInDefaultMode = !isInGSDMode; const allReportsDictValues = Object.values(allReports); // Filter out all the reports that shouldn't be displayed - const reportsToDisplay = allReportsDictValues.filter((report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies, allReportActions, true)); + const reportsToDisplay = allReportsDictValues.filter((report) => + // TODO: Talk with Fabio about that + ReportUtils.shouldReportBeInOptionList(report, currentReportId ?? '', isInGSDMode, betas, policies, allReportActions, true), + ); if (reportsToDisplay.length === 0) { // Display Concierge chat report when there is no report to be displayed @@ -232,7 +236,7 @@ type OptionData = { pendingAction?: OnyxCommon.PendingAction | null; allReportErrors?: OnyxCommon.Errors | null; brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | '' | null; - icons?: Icon[] | null; + icons?: Avatar[] | null; tooltipText?: string | null; ownerAccountID?: number | null; subtitle?: string | null; @@ -270,30 +274,15 @@ type OptionData = { isWaitingForTaskCompleteFromAssignee?: boolean | null; parentReportID?: string | null; notificationPreference?: string | number | null; - displayNamesWithTooltips?: DisplayNamesWithTooltip[] | null; + displayNamesWithTooltips?: Array> | null; chatType?: ValueOf | null; }; -type DisplayNamesWithTooltip = { - displayName?: string; - avatar?: string; - login?: string; - accountID?: number; - pronouns?: string; -}; - type ActorDetails = { displayName?: string; accountID?: number; }; -type Icon = { - source?: string; - id?: number; - type?: string; - name?: string; -}; - /** * Gets all the data necessary for rendering an OptionRowLHN component */ @@ -396,7 +385,7 @@ function getOptionData( const formattedLogin = Str.isSMSLogin(login) ? LocalePhoneNumber.formatPhoneNumber(login) : login; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. - const displayNamesWithTooltips: DisplayNamesWithTooltip[] = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants); const lastMessageTextFromReport = OptionsListUtils.getLastMessageTextForReport(report); // If the last actor's details are not currently saved in Onyx Collection, @@ -489,8 +478,8 @@ function getOptionData( result.alternateText = lastMessageText || formattedLogin; } - result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); - result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result); + result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result as Report); + result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result as Report); if (!hasMultipleParticipants) { result.accountID = personalDetail.accountID; @@ -523,3 +512,5 @@ export default { isSidebarLoadedReady, resetIsSidebarLoadedReadyPromise, }; + +export type {OptionData}; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index dc3c71e259a7..8e62619144e3 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -109,6 +109,7 @@ type Report = { isChatRoom?: boolean; participantsList?: Array>; description?: string; + text?: string; }; export default Report; From c68dc77e017abdd04451b1ea7cd1ce1fca6163bf Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 30 Oct 2023 10:54:10 +0100 Subject: [PATCH 24/63] fix: linter --- src/libs/TransactionUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index a4c839b48bc7..01976d806284 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -1,5 +1,6 @@ import {format, isValid} from 'date-fns'; import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {RecentWaypoint, ReportAction, Transaction} from '@src/types/onyx'; @@ -7,7 +8,6 @@ import {Comment, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Tr import {isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; import * as NumberUtils from './NumberUtils'; -import {ValueOf} from 'type-fest'; type AdditionalTransactionChanges = {comment?: string; waypoints?: WaypointCollection}; From 19a07f6e3903176374213bed6e6bd1a1f55bd493 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 30 Oct 2023 10:54:49 +0100 Subject: [PATCH 25/63] fix: linter --- src/types/onyx/ReportAction.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index dac39591f622..ba29ae73893a 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -1,8 +1,8 @@ -import {ValueOf} from 'type-fest'; import {SvgProps} from 'react-native-svg'; -import OriginalMessage, {Decision, Reaction} from './OriginalMessage'; +import {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; import * as OnyxCommon from './OnyxCommon'; -import CONST from '../../CONST'; +import OriginalMessage, {Decision, Reaction} from './OriginalMessage'; import {Receipt} from './Transaction'; type Message = { From e61b8da071c2486027a55e9ab400b16e61b4c40c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 30 Oct 2023 11:55:16 +0100 Subject: [PATCH 26/63] fix: linter --- src/libs/ReportUtils.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 894637908f97..21b581bf53cc 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1,19 +1,24 @@ -import {SvgProps} from 'react-native-svg'; import {format} from 'date-fns'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import lodashEscape from 'lodash/escape'; -import lodashIsEqual from 'lodash/isEqual'; import lodashFindLastIndex from 'lodash/findLastIndex'; import lodashIntersection from 'lodash/intersection'; -import {ValueOf} from 'type-fest'; +import lodashIsEqual from 'lodash/isEqual'; import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; -import _ from 'underscore'; +import {SvgProps} from 'react-native-svg'; +import {ValueOf} from 'type-fest'; import * as Expensicons from '@components/Icon/Expensicons'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {Beta, Login, PersonalDetails, Policy, Report, ReportAction, Transaction} from '@src/types/onyx'; +import {PendingAction} from '@src/types/onyx/OnyxCommon'; +import {IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage'; +import {Message, ReportActions} from '@src/types/onyx/ReportAction'; +import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; +import DeepValueOf from '@src/types/utils/DeepValueOf'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import isReportMessageAttachment from './isReportMessageAttachment'; @@ -23,16 +28,10 @@ import Navigation from './Navigation/Navigation'; import * as NumberUtils from './NumberUtils'; import Permissions from './Permissions'; import * as ReportActionsUtils from './ReportActionsUtils'; +import {LastVisibleMessage} from './ReportActionsUtils'; import * as TransactionUtils from './TransactionUtils'; import * as Url from './Url'; import * as UserUtils from './UserUtils'; -import {Beta, Login, PersonalDetails, Policy, Report, ReportAction, Transaction} from '../types/onyx'; -import {Receipt, WaypointCollection} from '../types/onyx/Transaction'; -import DeepValueOf from '../types/utils/DeepValueOf'; -import {IOUMessage, OriginalMessageActionName} from '../types/onyx/OriginalMessage'; -import {Message, ReportActions} from '../types/onyx/ReportAction'; -import {PendingAction} from '../types/onyx/OnyxCommon'; -import {LastVisibleMessage} from './ReportActionsUtils'; type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string}; @@ -3248,7 +3247,7 @@ function shouldReportBeInOptionList( isInGSDMode: boolean, betas: Beta[], policies: OnyxCollection, - allReportActions: OnyxCollection, + allReportActions?: OnyxCollection, excludeEmptyChats = false, ) { const isInDefaultMode = !isInGSDMode; From a953f81dea1c738f13d3f2fa93976f13355a6094 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 30 Oct 2023 13:54:27 +0100 Subject: [PATCH 27/63] fix: removed unnecessary comment --- src/pages/iou/MoneyRequestSelectorPage.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index 0dcd1d8b8f1a..46ddd3f7785c 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -44,16 +44,11 @@ const propTypes = { /** Which tab has been selected */ selectedTab: PropTypes.string, - - // Commenting it for the future migration to TS as its not used now but we have it in Props - // /** Beta features list */ - // betas: PropTypes.arrayOf(PropTypes.string), }; const defaultProps = { selectedTab: CONST.TAB.SCAN, report: {}, - // betas: [], }; function MoneyRequestSelectorPage(props) { From 8b081b2f798ede1865e3400dc61ed7cd76ff5f41 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 31 Oct 2023 08:32:15 +0100 Subject: [PATCH 28/63] fix: resolve comments --- src/libs/Permissions.ts | 4 +- src/libs/ReportUtils.ts | 165 +++++++++++++++++++++------------------- src/libs/UserUtils.ts | 4 + 3 files changed, 91 insertions(+), 82 deletions(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 5ebe02ccddfe..92227ac8c956 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -14,8 +14,8 @@ function canUsePayWithExpensify(betas: Beta[]): boolean { return betas?.includes(CONST.BETAS.PAY_WITH_EXPENSIFY) || canUseAllBetas(betas); } -function canUseDefaultRooms(betas: OnyxEntry): boolean { - return betas?.includes(CONST.BETAS.DEFAULT_ROOMS) ?? canUseAllBetas(betas); +function canUseDefaultRooms(betas: Beta[]): boolean { + return betas?.includes(CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas(betas); } function canUseWallet(betas: Beta[]): boolean { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 21b581bf53cc..1cf2a00a5f4d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -32,15 +32,16 @@ import {LastVisibleMessage} from './ReportActionsUtils'; import * as TransactionUtils from './TransactionUtils'; import * as Url from './Url'; import * as UserUtils from './UserUtils'; +import {AvatarSource} from './UserUtils'; type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string}; type Avatar = { id?: number; - source: React.FC | string | undefined; + source: AvatarSource | undefined; type: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; name: string; - fallbackIcon?: React.FC | string; + fallbackIcon?: AvatarSource; }; type ExpenseOriginalMessage = { @@ -83,7 +84,7 @@ type SpendBreakdown = { totalDisplaySpend: number; }; -type ParticipantDetails = [number, string, string | React.FC, string | React.FC]; +type ParticipantDetails = [number, string, AvatarSource, AvatarSource]; type ReportAndWorkspaceName = { rootReportName: string; @@ -312,7 +313,8 @@ type OptimisticIOUReport = Pick< | 'statusNum' >; -function checkIfCorrectType(arg: T | Record): arg is T { +// eslint-disable-next-line rulesdir/no-negated-variables +function isNotEmptyObject(arg: T | Record): arg is T { return Object.keys(arg ?? {}).length > 0; } @@ -339,7 +341,7 @@ let currentUserPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => { - currentUserPersonalDetails = value?.[currentUserAccountID ?? ''] ?? null; + currentUserPersonalDetails = value?.[currentUserAccountID ?? -1] ?? null; allPersonalDetails = value ?? {}; }, }); @@ -354,8 +356,6 @@ Onyx.connect({ let doesDomainHaveApprovedAccountant = false; Onyx.connect({ key: ONYXKEYS.ACCOUNT, - // Check if I remove that will cause regressions - // waitForCollectionCallback: true, callback: (value) => (doesDomainHaveApprovedAccountant = value?.doesDomainHaveApprovedAccountant ?? false), }); @@ -506,8 +506,8 @@ function sortReportsByLastRead(reports: OnyxCollection): Array { const aTime = new Date(a?.lastReadTime ?? ''); const bTime = new Date(b?.lastReadTime ?? ''); - // @ts-expect-error It's ok to subtract dates - return aTime - bTime; + + return aTime.valueOf() - bTime.valueOf(); }); } @@ -719,7 +719,7 @@ function findLastAccessedReport( policies: OnyxCollection, isFirstTimeNewExpensifyUser: boolean, openOnAdminRoom = false, -): OnyxEntry | undefined { +): OnyxEntry { // If it's the user's first time using New Expensify, then they could either have: // - just a Concierge report, if so we'll return that // - their Concierge report, and a separate report that must have deeplinked them to the app before they created their account. @@ -740,7 +740,7 @@ function findLastAccessedReport( return sortedReports[0]; } - return adminReport ?? sortedReports.find((report) => !isConciergeChatReport(report)); + return adminReport ?? sortedReports.find((report) => !isConciergeChatReport(report)) ?? null; } if (ignoreDomainRooms) { @@ -752,7 +752,7 @@ function findLastAccessedReport( ); } - return adminReport ?? sortedReports.at(-1); + return adminReport ?? sortedReports.at(-1) ?? null; } /** @@ -858,7 +858,7 @@ function isExpenseRequest(report: OnyxEntry): boolean { if (report && isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; - return isExpenseReport(parentReport) && checkIfCorrectType(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); + return isExpenseReport(parentReport) && isNotEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); } return false; } @@ -871,7 +871,7 @@ function isIOURequest(report: OnyxEntry): boolean { if (report && isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; - return isIOUReport(parentReport) && checkIfCorrectType(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); + return isIOUReport(parentReport) && isNotEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); } return false; } @@ -921,7 +921,7 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: // For now, users cannot delete split actions const isSplitAction = reportAction.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; - if (isSplitAction || isSettled(String(reportAction?.originalMessage?.IOUReportID)) || (checkIfCorrectType(report) && isReportApproved(report))) { + if (isSplitAction || isSettled(String(reportAction?.originalMessage?.IOUReportID)) || (isNotEmptyObject(report) && isReportApproved(report))) { return false; } @@ -940,7 +940,7 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: } const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; - const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN && checkIfCorrectType(report) && !isDM(report); + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN && isNotEmptyObject(report) && !isDM(report); return isActionOwner || isAdmin; } @@ -980,7 +980,7 @@ function getRoomWelcomeMessage(report: OnyxEntry, isUserPolicyAdmin: boo * Returns true if Concierge is one of the chat participants (1:1 as well as group chats) */ function chatIncludesConcierge(report: OnyxEntry): boolean { - return Boolean((report?.participantAccountIDs?.length ?? 0) > 0 && report?.participantAccountIDs?.includes(CONST.ACCOUNT_ID.CONCIERGE)); + return Boolean(report?.participantAccountIDs?.length && report?.participantAccountIDs?.includes(CONST.ACCOUNT_ID.CONCIERGE)); } /** @@ -991,7 +991,7 @@ function hasAutomatedExpensifyAccountIDs(accountIDs: number[]): boolean { } function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAccountID: number): number[] { - let finalReport: OnyxEntry | undefined = report; + let finalReport: OnyxEntry = report; // In 1:1 chat threads, the participants will be the same as parent report. If a report is specifically a 1:1 chat thread then we will // get parent report and use its participants array. if (isThread(report) && !(isTaskReport(report) || isMoneyRequestReport(report))) { @@ -1063,7 +1063,7 @@ function getDefaultWorkspaceAvatar(workspaceName?: string): React.FC { return !alphaNumeric ? defaultWorkspaceAvatars.WorkspaceBuilding : defaultWorkspaceAvatar; } -function getWorkspaceAvatar(report: OnyxEntry) { +function getWorkspaceAvatar(report: OnyxEntry): AvatarSource { const workspaceName = getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]); return allPolicies?.[`policy${report?.policyID}`]?.avatar ?? getDefaultWorkspaceAvatar(workspaceName); } @@ -1137,10 +1137,10 @@ function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry = function getIcons( report: OnyxEntry, personalDetails: OnyxCollection, - defaultIcon: string | React.FC | null = null, + defaultIcon: AvatarSource | null = null, defaultName = '', defaultAccountID = -1, - policy: OnyxEntry | undefined = undefined, + policy: OnyxEntry = null, ): Avatar[] { if (Object.keys(report ?? {}).length === 0) { const fallbackIcon: Avatar = { @@ -1203,12 +1203,12 @@ function getIcons( // Get domain name after the #. Domain Rooms use our default workspace avatar pattern. const domainName = report?.reportName?.substring(1); const policyExpenseChatAvatarSource = getDefaultWorkspaceAvatar(domainName); - const domainIcon = { + const domainIcon: Avatar = { source: policyExpenseChatAvatarSource, type: CONST.ICON_TYPE_WORKSPACE, - name: domainName, + name: domainName ?? '', id: -1, - } as Avatar; + }; return [domainIcon]; } if (isAdminRoom(report) || isAnnounceRoom(report) || isChatRoom(report) || isArchivedRoom(report)) { @@ -1361,7 +1361,7 @@ function getReimbursementQueuedActionMessage(reportAction: OnyxEntry): boolea return false; } const parentReport = getReport(report.parentReportID); - if (parentReport && checkIfCorrectType(parentReport) && isArchivedRoom(parentReport)) { + if (parentReport && isNotEmptyObject(parentReport) && isArchivedRoom(parentReport)) { return false; } @@ -1577,7 +1577,7 @@ function getTransactionDetails(transaction: OnyxEntry, createdDateF } return { created: TransactionUtils.getCreated(transaction, createdDateFormat), - amount: TransactionUtils.getAmount(transaction, checkIfCorrectType(report) && isExpenseReport(report)), + amount: TransactionUtils.getAmount(transaction, isNotEmptyObject(report) && isExpenseReport(report)), currency: TransactionUtils.getCurrency(transaction), comment: TransactionUtils.getDescription(transaction), merchant: TransactionUtils.getMerchant(transaction), @@ -1621,8 +1621,7 @@ function canEditMoneyRequest(reportAction: OnyxEntry): boolean { const moneyRequestReport = getReport(String(moneyRequestReportID)); const isReportSettled = isSettled(moneyRequestReport?.reportID); - const isAdmin = - ((checkIfCorrectType(moneyRequestReport) && isExpenseReport(moneyRequestReport) && getPolicy(moneyRequestReport?.policyID ?? '')?.role) ?? '') === CONST.POLICY.ROLE.ADMIN; + const isAdmin = ((isNotEmptyObject(moneyRequestReport) && isExpenseReport(moneyRequestReport) && getPolicy(moneyRequestReport?.policyID ?? '')?.role) ?? '') === CONST.POLICY.ROLE.ADMIN; const isRequestor = currentUserAccountID === reportAction?.actorAccountID; if (isAdmin) { @@ -1726,7 +1725,7 @@ function getTransactionReportName(reportAction: OnyxEntry): string } const transaction = TransactionUtils.getLinkedTransaction(reportAction); - if (!checkIfCorrectType(transaction)) { + if (!isNotEmptyObject(transaction)) { return ''; } if (TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { @@ -1771,7 +1770,7 @@ function getReportPreviewMessage( return reportActionMessage; } - if (checkIfCorrectType(linkedTransaction)) { + if (isNotEmptyObject(linkedTransaction)) { if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { return Localize.translateLocal('iou.receiptScanning'); } @@ -1793,7 +1792,7 @@ function getReportPreviewMessage( if (shouldConsiderReceiptBeingScanned && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); - if (checkIfCorrectType(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + if (isNotEmptyObject(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { return Localize.translateLocal('iou.receiptScanning'); } } @@ -2020,7 +2019,7 @@ function getRootParentReport(report: OnyxEntry): OnyxEntry | Rec const parentReport = getReport(report?.parentReportID); // Runs recursion to iterate a parent report - return getRootParentReport(checkIfCorrectType(parentReport) ? parentReport : null); + return getRootParentReport(isNotEmptyObject(parentReport) ? parentReport : null); } /** @@ -2030,12 +2029,12 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu let formattedName; const parentReportAction = ReportActionsUtils.getParentReportAction(report); if (isChatThread(report)) { - if (checkIfCorrectType(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction)) { + if (isNotEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction)) { return getTransactionReportName(parentReportAction); } - const isAttachment = ReportActionsUtils.isReportActionAttachment(checkIfCorrectType(parentReportAction) ? parentReportAction : null); - const parentReportActionMessage = (parentReportAction?.message?.[0].text ?? '').replace(/(\r\n|\n|\r)/gm, ' '); + const isAttachment = ReportActionsUtils.isReportActionAttachment(isNotEmptyObject(parentReportAction) ? parentReportAction : null); + const parentReportActionMessage = (parentReportAction?.message?.[0]?.text ?? '').replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { return `[${Localize.translateLocal('common.attachment')}]`; } @@ -2048,7 +2047,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu return parentReportActionMessage || Localize.translateLocal('parentReportAction.deletedMessage'); } - if (isTaskReport(report) && checkIfCorrectType(parentReportAction) && isCanceledTaskReport(report, parentReportAction)) { + if (isTaskReport(report) && isNotEmptyObject(parentReportAction) && isCanceledTaskReport(report, parentReportAction)) { return Localize.translateLocal('parentReportAction.deletedTask'); } @@ -2309,11 +2308,11 @@ function getOptimisticDataForParentReportAction( parentReportActionID = '', ): OnyxUpdate | Record { const report = getReport(reportID); - if (!report || !checkIfCorrectType(report)) { + if (!report || !isNotEmptyObject(report)) { return {}; } const parentReportAction = ReportActionsUtils.getParentReportAction(report); - if (!parentReportAction || !checkIfCorrectType(parentReportAction)) { + if (!parentReportAction || !isNotEmptyObject(parentReportAction)) { return {}; } @@ -2455,7 +2454,7 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num const report = getReport(iouReportID); const amount = type === CONST.IOU.REPORT_ACTION_TYPE.PAY - ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(checkIfCorrectType(report) ? report : null), currency) + ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(isNotEmptyObject(report) ? report : null), currency) : CurrencyUtils.convertToDisplayString(total, currency); let paymentMethodMessage; @@ -3208,7 +3207,7 @@ function canSeeDefaultRoom(report: OnyxEntry, policies: OnyxCollection

, policies: OnyxCollection, betas: OnyxEntry, allReportActions?: OnyxCollection): boolean { @@ -3228,7 +3227,7 @@ function canAccessReport(report: OnyxEntry, policies: OnyxCollection, currentReportId: string): boolean { const currentReport = getReport(currentReportId); - const parentReport = getParentReport(checkIfCorrectType(currentReport) ? currentReport : null); + const parentReport = getParentReport(isNotEmptyObject(currentReport) ? currentReport : null); const reportActions = ReportActionsUtils.getAllReportActions(report?.reportID ?? ''); const isChildReportHasComment = Object.values(reportActions ?? {})?.some((reportAction) => (reportAction?.childVisibleActionCount ?? 0) > 0); return parentReport?.reportID !== report?.reportID && !isChildReportHasComment; @@ -3327,41 +3326,45 @@ function shouldReportBeInOptionList( /** * Attempts to find a report in onyx with the provided list of participants. Does not include threads, task, money request, room, and policy expense chat. */ -function getChatByParticipants(newParticipantList: number[]): OnyxEntry | undefined { +function getChatByParticipants(newParticipantList: number[]): OnyxEntry { const sortedNewParticipantList = newParticipantList.sort(); - return Object.values(allReports ?? {}).find((report) => { - // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it - if ( - !report || - report.participantAccountIDs?.length === 0 || - isChatThread(report) || - isTaskReport(report) || - isMoneyRequestReport(report) || - isChatRoom(report) || - isPolicyExpenseChat(report) - ) { - return false; - } + return ( + Object.values(allReports ?? {}).find((report) => { + // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it + if ( + !report || + report.participantAccountIDs?.length === 0 || + isChatThread(report) || + isTaskReport(report) || + isMoneyRequestReport(report) || + isChatRoom(report) || + isPolicyExpenseChat(report) + ) { + return false; + } - // Only return the chat if it has all the participants - return lodashIsEqual(sortedNewParticipantList, report.participantAccountIDs?.sort()); - }); + // Only return the chat if it has all the participants + return lodashIsEqual(sortedNewParticipantList, report.participantAccountIDs?.sort()); + }) ?? null + ); } /** * Attempts to find a report in onyx with the provided list of participants in given policy */ -function getChatByParticipantsAndPolicy(newParticipantList: number[], policyID: string): OnyxEntry | undefined { +function getChatByParticipantsAndPolicy(newParticipantList: number[], policyID: string): OnyxEntry { newParticipantList.sort(); - return Object.values(allReports ?? {}).find((report) => { - // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it - if (!report?.participantAccountIDs) { - return false; - } - const sortedParticipanctsAccountIDs = report.parentReportActionIDs?.sort(); - // Only return the room if it has all the participants and is not a policy room - return report.policyID === policyID && lodashIsEqual(newParticipantList, sortedParticipanctsAccountIDs); - }); + return ( + Object.values(allReports ?? {}).find((report) => { + // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it + if (!report?.participantAccountIDs) { + return false; + } + const sortedParticipanctsAccountIDs = report.parentReportActionIDs?.sort(); + // Only return the room if it has all the participants and is not a policy room + return report.policyID === policyID && lodashIsEqual(newParticipantList, sortedParticipanctsAccountIDs); + }) ?? null + ); } function getAllPolicyReports(policyID: string): Array> { @@ -3404,7 +3407,7 @@ function canFlagReportAction(reportAction: OnyxEntry, reportID: st reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportActionsUtils.isDeletedAction(reportAction) && !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && - checkIfCorrectType(report) && + isNotEmptyObject(report) && isAllowedToComment(report), ); } @@ -3898,7 +3901,7 @@ function getTaskAssigneeChatOnyxData( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, - value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '']: optimisticAssigneeAddComment.reportAction}, + value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? -1]: optimisticAssigneeAddComment.reportAction}, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -3945,18 +3948,20 @@ function getParticipantsIDs(report: OnyxEntry): number[] { * Return iou report action display message */ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry): string { - const originalMessage = reportAction?.originalMessage as IOUMessage; + if (reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { + return ''; + } let displayMessage; - if (originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { - const {amount, currency, IOUReportID} = originalMessage; + if (reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { + const {amount, currency, IOUReportID} = reportAction?.originalMessage; const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency); const iouReport = getReport(String(IOUReportID) ?? ''); - const payerName = isExpenseReport(checkIfCorrectType(iouReport) ? iouReport : null) - ? getPolicyName(checkIfCorrectType(iouReport) ? iouReport : null) + const payerName = isExpenseReport(isNotEmptyObject(iouReport) ? iouReport : null) + ? getPolicyName(isNotEmptyObject(iouReport) ? iouReport : null) : getDisplayNameForParticipant(iouReport?.managerID, true); let translationKey; - switch (originalMessage.paymentType) { + switch (reportAction?.originalMessage.paymentType) { case CONST.IOU.PAYMENT_TYPE.ELSEWHERE: translationKey = 'iou.paidElsewhereWithAmount'; break; @@ -3970,8 +3975,8 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) } displayMessage = Localize.translateLocal(translationKey, {amount: formattedAmount, payer: payerName}); } else { - const transaction = TransactionUtils.getTransaction(originalMessage.IOUTransactionID ?? ''); - const transactionDetails = transaction && checkIfCorrectType(transaction) ? getTransactionDetails(transaction) : undefined; + const transaction = TransactionUtils.getTransaction(reportAction?.originalMessage.IOUTransactionID ?? ''); + const transactionDetails = transaction && isNotEmptyObject(transaction) ? getTransactionDetails(transaction) : undefined; const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); displayMessage = Localize.translateLocal('iou.requestedAmount', { formattedAmount, diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 5fb3343cb189..be292494abe3 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -6,6 +6,8 @@ import CONST from '@src/CONST'; import Login from '@src/types/onyx/Login'; import hashCode from './hashCode'; +type AvatarSource = React.FC | string; + type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24; type LoginListIndicator = ValueOf | ''; @@ -202,3 +204,5 @@ export { getFullSizeAvatar, generateAccountID, }; + +export type {AvatarSource}; From f0ebbde89fa04e480c84cd0bc16ca959a1a430ed Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 31 Oct 2023 10:34:13 +0100 Subject: [PATCH 29/63] fix: liner --- src/libs/TransactionUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 9c75004e8014..5ab030fc0abd 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -1,4 +1,3 @@ -import {format, isValid} from 'date-fns'; import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; From 859ce75404a8ab865b5d0dd7fb6f1f5608858038 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 31 Oct 2023 10:58:36 +0100 Subject: [PATCH 30/63] fix: resolve comments --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 3635e2e870f2..a014b7d41d67 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1253,7 +1253,7 @@ function getIcons( * Gets the personal details for a login by looking in the ONYXKEYS.PERSONAL_DETAILS_LIST Onyx key (stored in the local variable, allPersonalDetails). If it doesn't exist in Onyx, * then a default object is constructed. */ -function getPersonalDetailsForAccountID(accountID: number): PersonalDetails | Partial> { +function getPersonalDetailsForAccountID(accountID: number): Partial { if (!accountID) { return {}; } From 2bc245627461ac6be46d41324b901581fcd887b5 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 31 Oct 2023 11:00:31 +0100 Subject: [PATCH 31/63] fix: prettier --- src/libs/SidebarUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index f25f20675f50..029fb919d21f 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -15,8 +15,8 @@ import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; import * as OptionsListUtils from './OptionsListUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; -import type {Avatar} from './ReportUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; +import type {Avatar} from './ReportUtils'; import * as ReportUtils from './ReportUtils'; import * as UserUtils from './UserUtils'; From 7d308eba006a250b7eff5f96f9b56cba78f2f5a2 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 2 Nov 2023 13:35:02 +0100 Subject: [PATCH 32/63] fix: types issues --- src/libs/ReportUtils.ts | 68 +++++++++++++++++---------- src/libs/SidebarUtils.ts | 90 ++++++++---------------------------- src/types/onyx/OnyxCommon.ts | 8 ++-- 3 files changed, 66 insertions(+), 100 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 077091167522..fdcffc440ab3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -14,7 +14,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {Beta, Login, PersonalDetails, Policy, Report, ReportAction, Transaction} from '@src/types/onyx'; -import {PendingAction} from '@src/types/onyx/OnyxCommon'; +import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import {IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage'; import {Message, ReportActions} from '@src/types/onyx/ReportAction'; import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; @@ -29,22 +29,12 @@ import * as NumberUtils from './NumberUtils'; import Permissions from './Permissions'; import * as ReportActionsUtils from './ReportActionsUtils'; import {LastVisibleMessage} from './ReportActionsUtils'; -import * as SidebarUtils from './SidebarUtils'; import * as TransactionUtils from './TransactionUtils'; import * as Url from './Url'; import * as UserUtils from './UserUtils'; -import {AvatarSource} from './UserUtils'; type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string}; -type Avatar = { - id?: number; - source: AvatarSource | undefined; - type: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; - name: string; - fallbackIcon?: AvatarSource; -}; - type ExpenseOriginalMessage = { oldComment?: string; newComment?: string; @@ -69,7 +59,7 @@ type Participant = { accountID: number; alternateText: string; firstName: string; - icons: Avatar[]; + icons: Icon[]; keyForList: string; lastName: string; login: string; @@ -85,7 +75,7 @@ type SpendBreakdown = { totalDisplaySpend: number; }; -type ParticipantDetails = [number, string, AvatarSource, AvatarSource]; +type ParticipantDetails = [number, string, UserUtils.AvatarSource, UserUtils.AvatarSource]; type ReportAndWorkspaceName = { rootReportName: string; @@ -314,6 +304,34 @@ type OptimisticIOUReport = Pick< | 'statusNum' >; +type OptionData = { + alternateText?: string | null; + pendingAction?: PendingAction | null; + allReportErrors?: Errors | null; + brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | '' | null; + tooltipText?: string | null; + subtitle?: string | null; + login?: string | null; + accountID?: number | null; + status?: string | null; + phoneNumber?: string | null; + isUnread?: boolean | null; + isUnreadWithMention?: boolean | null; + hasDraftComment?: boolean | null; + keyForList?: string | null; + searchText?: string | null; + isIOUReportOwner?: boolean | null; + isArchivedRoom?: boolean | null; + shouldShowSubscript?: boolean | null; + isPolicyExpenseChat?: boolean | null; + isMoneyRequestReport?: boolean | null; + isExpenseRequest?: boolean | null; + isAllowedToComment?: boolean | null; + isThread?: boolean | null; + isTaskReport?: boolean | null; + parentReportAction?: ReportAction; + displayNamesWithTooltips?: Array> | null; +} & Report; // eslint-disable-next-line rulesdir/no-negated-variables function isNotEmptyObject(arg: T | Record): arg is T { return Object.keys(arg ?? {}).length > 0; @@ -1098,7 +1116,7 @@ function getDefaultWorkspaceAvatar(workspaceName?: string): React.FC { return !alphaNumeric ? defaultWorkspaceAvatars.WorkspaceBuilding : defaultWorkspaceAvatar; } -function getWorkspaceAvatar(report: OnyxEntry): AvatarSource { +function getWorkspaceAvatar(report: OnyxEntry): UserUtils.AvatarSource { const workspaceName = getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]); return allPolicies?.[`policy${report?.policyID}`]?.avatar ?? getDefaultWorkspaceAvatar(workspaceName); } @@ -1107,7 +1125,7 @@ function getWorkspaceAvatar(report: OnyxEntry): AvatarSource { * Returns the appropriate icons for the given chat report using the stored personalDetails. * The Avatar sources can be URLs or Icon components according to the chat type. */ -function getIconsForParticipants(participants: number[], personalDetails: OnyxCollection): Avatar[] { +function getIconsForParticipants(participants: number[], personalDetails: OnyxCollection): Icon[] { const participantDetails: ParticipantDetails[] = []; const participantsList = participants || []; @@ -1131,7 +1149,7 @@ function getIconsForParticipants(participants: number[], personalDetails: OnyxCo }); // Now that things are sorted, gather only the avatars (second element in the array) and return those - const avatars: Avatar[] = []; + const avatars: Icon[] = []; for (const sortedParticipantDetail of sortedParticipantDetails) { const userIcon = { @@ -1150,14 +1168,14 @@ function getIconsForParticipants(participants: number[], personalDetails: OnyxCo /** * Given a report, return the associated workspace icon. */ -function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry = null): Avatar { +function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry = null): Icon { const workspaceName = getPolicyName(report, false, policy); const policyExpenseChatAvatarSource = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar : getDefaultWorkspaceAvatar(workspaceName); - const workspaceIcon = { - source: policyExpenseChatAvatarSource, + const workspaceIcon: Icon = { + source: policyExpenseChatAvatarSource ?? '', type: CONST.ICON_TYPE_WORKSPACE, name: workspaceName, id: -1, @@ -1172,13 +1190,13 @@ function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry = function getIcons( report: OnyxEntry, personalDetails: OnyxCollection, - defaultIcon: AvatarSource | null = null, + defaultIcon: UserUtils.AvatarSource | null = null, defaultName = '', defaultAccountID = -1, policy: OnyxEntry = null, -): Avatar[] { +): Icon[] { if (Object.keys(report ?? {}).length === 0) { - const fallbackIcon: Avatar = { + const fallbackIcon: Icon = { source: defaultIcon ?? Expensicons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: defaultName, @@ -1238,7 +1256,7 @@ function getIcons( // Get domain name after the #. Domain Rooms use our default workspace avatar pattern. const domainName = report?.reportName?.substring(1); const policyExpenseChatAvatarSource = getDefaultWorkspaceAvatar(domainName); - const domainIcon: Avatar = { + const domainIcon: Icon = { source: policyExpenseChatAvatarSource, type: CONST.ICON_TYPE_WORKSPACE, name: domainName ?? '', @@ -3609,7 +3627,7 @@ function getMoneyRequestOptions(report: OnyxEntry, reportParticipants: n const participants = reportParticipants.filter((accountID) => currentUserPersonalDetails?.accountID !== accountID); // We don't allow IOU actions if an Expensify account is a participant of the report, unless the policy that the report is on is owned by an Expensify account const doParticipantsIncludeExpensifyAccounts = lodashIntersection(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS).length > 0; - const isPolicyOwnedByExpensifyAccounts = report?.policyID ? CONST.EXPENSIFY_ACCOUNT_IDS.includes(getPolicy(report?.policyID ?? '')?.ownerAccountID || 0) : false; + const isPolicyOwnedByExpensifyAccounts = report?.policyID ? CONST.EXPENSIFY_ACCOUNT_IDS.includes(getPolicy(report?.policyID ?? '')?.ownerAccountID ?? 0) : false; if (doParticipantsIncludeExpensifyAccounts && !isPolicyOwnedByExpensifyAccounts) { return []; } @@ -4202,4 +4220,4 @@ export { getReimbursementQueuedActionMessage, }; -export type {Avatar}; +export type {OptionData}; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 32c197ab5191..3d4c2c944996 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/prefer-underscore-method */ import Str from 'expensify-common/lib/str'; -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxCollection} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -16,7 +16,6 @@ import * as Localize from './Localize'; import * as OptionsListUtils from './OptionsListUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; -import type {Avatar} from './ReportUtils'; import * as ReportUtils from './ReportUtils'; import * as UserUtils from './UserUtils'; @@ -116,11 +115,11 @@ function getOrderedReportIDs( betas: Beta[], policies: Record, priorityMode: ValueOf, - allReportActions: Record, + allReportActions: OnyxCollection, ): string[] { // Generate a unique cache key based on the function arguments const cachedReportsKey = JSON.stringify( - [currentReportId, allReports, betas, policies, priorityMode, allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], + [currentReportId, allReports, betas, policies, priorityMode, allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length ?? 1], (key, value: unknown) => { /** * Exclude 'participantAccountIDs', 'participants' and 'lastMessageText' not to overwhelm a cached key value with huge data, @@ -148,7 +147,6 @@ function getOrderedReportIDs( const allReportsDictValues = Object.values(allReports); // Filter out all the reports that shouldn't be displayed const reportsToDisplay = allReportsDictValues.filter((report) => - // TODO: Talk with Fabio about that ReportUtils.shouldReportBeInOptionList(report, currentReportId ?? '', isInGSDMode, betas, policies, allReportActions, true), ); @@ -222,56 +220,6 @@ function getOrderedReportIDs( return LHNReports; } -type OptionData = { - alternateText?: string | null; - pendingAction?: OnyxCommon.PendingAction | null; - allReportErrors?: OnyxCommon.Errors | null; - brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | '' | null; - icons?: Avatar[] | null; - tooltipText?: string | null; - ownerAccountID?: number | null; - subtitle?: string | null; - participantsList?: PersonalDetails[] | null; - login?: string | null; - accountID?: number | null; - managerID?: number | null; - reportID?: string | null; - policyID?: string | null; - status?: string | null; - type?: string | null; - stateNum?: ValueOf | null; - statusNum?: ValueOf | null; - phoneNumber?: string | null; - isUnread?: boolean | null; - isUnreadWithMention?: boolean | null; - hasDraftComment?: boolean | null; - keyForList?: string | null; - searchText?: string | null; - isPinned?: boolean | null; - hasOutstandingIOU?: boolean | null; - hasOutstandingChildRequest?: boolean | null; - iouReportID?: string | null; - isIOUReportOwner?: boolean | null; - iouReportAmount?: number | null; - isChatRoom?: boolean | null; - isArchivedRoom?: boolean | null; - shouldShowSubscript?: boolean | null; - isPolicyExpenseChat?: boolean | null; - isMoneyRequestReport?: boolean | null; - isExpenseRequest?: boolean | null; - isWaitingOnBankAccount?: boolean | null; - isAllowedToComment?: boolean | null; - isThread?: boolean | null; - isTaskReport?: boolean | null; - parentReportID?: string | null; - parentReportAction?: ReportAction; - notificationPreference?: string | number | null; - displayNamesWithTooltips?: Array> | null; - chatType?: ValueOf | null; - lastMentionedTime?: string; - lastReadTime?: string; -} & Report; - type ActorDetails = { displayName?: string; accountID?: number; @@ -287,7 +235,7 @@ function getOptionData( preferredLocale: ValueOf, policy: Policy, parentReportAction: ReportAction, -): OptionData | undefined { +): ReportUtils.OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do // a null check here and return early. @@ -295,24 +243,24 @@ function getOptionData( return; } - const result: OptionData = { - text: null, + const result: ReportUtils.OptionData = { + // text: null, alternateText: null, pendingAction: null, allReportErrors: null, brickRoadIndicator: null, - icons: null, + // icons: null, tooltipText: null, - ownerAccountID: null, + // ownerAccountID: null, subtitle: null, - participantsList: null, + // participantsList: null, login: null, accountID: null, - managerID: null, - reportID: null, - policyID: null, - statusNum: null, - stateNum: null, + // managerID: null, + reportID: '', + // policyID: null, + // statusNum: null, + // stateNum: null, phoneNumber: null, isUnread: null, isUnreadWithMention: null, @@ -322,7 +270,7 @@ function getOptionData( isPinned: false, hasOutstandingIOU: false, hasOutstandingChildRequest: false, - iouReportID: null, + // iouReportID: null, isIOUReportOwner: null, iouReportAmount: 0, isChatRoom: false, @@ -333,7 +281,7 @@ function getOptionData( isExpenseRequest: false, isWaitingOnBankAccount: false, isAllowedToComment: true, - chatType: null, + // chatType: null, }; const participantPersonalDetailList: PersonalDetails[] = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs ?? [], personalDetails)); const personalDetail = participantPersonalDetailList[0] ?? {}; @@ -365,9 +313,9 @@ function getOptionData( result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs ?? []); result.hasOutstandingIOU = report.hasOutstandingIOU; result.hasOutstandingChildRequest = report.hasOutstandingChildRequest; - result.parentReportID = report.parentReportID ?? null; + result.parentReportID = report.parentReportID ?? ''; result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; - result.notificationPreference = report.notificationPreference ?? null; + result.notificationPreference = report.notificationPreference ?? ''; result.isAllowedToComment = ReportUtils.canUserPerformWriteAction(report); result.chatType = report.chatType; @@ -506,5 +454,3 @@ export default { isSidebarLoadedReady, resetIsSidebarLoadedReadyPromise, }; - -export type {OptionData}; diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index bafd5e8cbbf0..bfd95f418677 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -1,5 +1,5 @@ -import * as React from 'react'; import {ValueOf} from 'type-fest'; +import {AvatarSource} from '@libs/UserUtils'; import CONST from '@src/CONST'; type PendingAction = ValueOf; @@ -9,9 +9,11 @@ type ErrorFields = Record | null>; type Errors = Record; type Icon = { - source: React.ReactNode | string; - type: 'avatar' | 'workspace'; + source: AvatarSource; + type: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; name: string; + id?: number; + fallbackIcon?: AvatarSource; }; export type {Icon, PendingAction, ErrorFields, Errors}; From 223e3c73ac806740d0a245b6bd7211ff1cfe0a6b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 2 Nov 2023 13:57:28 +0100 Subject: [PATCH 33/63] fix: last issues from review --- src/libs/ReportUtils.ts | 4 ++-- src/types/onyx/ReportAction.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fdcffc440ab3..b90a778e3f0f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2660,7 +2660,7 @@ function buildOptimisticIOUReportAction( shouldShow: true, created, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - whisperedToAccountIDs: [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === receipt?.state) ? [currentUserAccountID] : [], + whisperedToAccountIDs: [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === receipt?.state) ? [currentUserAccountID ?? -1] : [], }; } @@ -2765,7 +2765,7 @@ function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, childRecentReceiptTransactionIDs: hasReceipt && transaction ? {[transaction.transactionID]: created} : undefined, - whisperedToAccountIDs: isReceiptBeingScanned ? [currentUserAccountID] : [], + whisperedToAccountIDs: isReceiptBeingScanned ? [currentUserAccountID ?? -1] : [], }; } diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index ba29ae73893a..726243ead6d4 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -78,7 +78,7 @@ type ReportActionBase = { isLoading?: boolean; /** accountIDs of the people to which the whisper was sent to (if any). Returns empty array if it is not a whisper */ - whisperedToAccountIDs?: Array; + whisperedToAccountIDs?: number[]; avatar?: string | React.FC; automatic?: boolean; From 499fb360ed205f4f5ed9d1c102ac38c40a33d088 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 2 Nov 2023 15:15:01 +0100 Subject: [PATCH 34/63] ref: rerun test jobs --- src/libs/ReportUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b90a778e3f0f..a9ed880179b3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1498,6 +1498,7 @@ function requiresAttentionFromCurrentUser(option: OnyxEntry | OptionData if (('isUnreadWithMention' in option && option.isUnreadWithMention) || isUnreadWithMention(option)) { return true; } + console.log('hej'); if (isWaitingForAssigneeToCompleteTask(option, parentReportAction)) { return true; From 257f6922b0720a3513e301d352d1b351d2e46ad2 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 2 Nov 2023 15:16:37 +0100 Subject: [PATCH 35/63] fix: removed log --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a9ed880179b3..b90a778e3f0f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1498,7 +1498,6 @@ function requiresAttentionFromCurrentUser(option: OnyxEntry | OptionData if (('isUnreadWithMention' in option && option.isUnreadWithMention) || isUnreadWithMention(option)) { return true; } - console.log('hej'); if (isWaitingForAssigneeToCompleteTask(option, parentReportAction)) { return true; From e86a618769ed90fd79cb51075d71a62562df6c0a Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 2 Nov 2023 15:42:46 +0100 Subject: [PATCH 36/63] fix: removed commented code --- src/libs/SidebarUtils.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 3d4c2c944996..a6c25d1d46df 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -244,23 +244,15 @@ function getOptionData( } const result: ReportUtils.OptionData = { - // text: null, alternateText: null, pendingAction: null, allReportErrors: null, brickRoadIndicator: null, - // icons: null, tooltipText: null, - // ownerAccountID: null, subtitle: null, - // participantsList: null, login: null, accountID: null, - // managerID: null, reportID: '', - // policyID: null, - // statusNum: null, - // stateNum: null, phoneNumber: null, isUnread: null, isUnreadWithMention: null, @@ -270,7 +262,6 @@ function getOptionData( isPinned: false, hasOutstandingIOU: false, hasOutstandingChildRequest: false, - // iouReportID: null, isIOUReportOwner: null, iouReportAmount: 0, isChatRoom: false, @@ -281,7 +272,6 @@ function getOptionData( isExpenseRequest: false, isWaitingOnBankAccount: false, isAllowedToComment: true, - // chatType: null, }; const participantPersonalDetailList: PersonalDetails[] = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs ?? [], personalDetails)); const personalDetail = participantPersonalDetailList[0] ?? {}; From 478967988f1b6054870707aa27a2eb9b6d19eb4c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 2 Nov 2023 19:21:26 +0100 Subject: [PATCH 37/63] fix: fixed comments --- src/libs/ReportUtils.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 56aa4053a312..486991afa817 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -303,6 +303,7 @@ type OptimisticIOUReport = Pick< | 'parentReportID' | 'statusNum' >; +type DisplayNameWithTooltips = Array>; type OptionData = { alternateText?: string | null; @@ -330,8 +331,9 @@ type OptionData = { isThread?: boolean | null; isTaskReport?: boolean | null; parentReportAction?: ReportAction; - displayNamesWithTooltips?: Array> | null; + displayNamesWithTooltips?: DisplayNameWithTooltips | null; } & Report; + // eslint-disable-next-line rulesdir/no-negated-variables function isNotEmptyObject(arg: T | Record): arg is T { return Object.keys(arg ?? {}).length > 0; @@ -360,6 +362,7 @@ let currentUserPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => { + currentUserPersonalDetails = idOrDefault(value, currentUserAccountID) ?? null; currentUserPersonalDetails = value?.[currentUserAccountID ?? -1] ?? null; allPersonalDetails = value ?? {}; }, @@ -573,7 +576,7 @@ function isAdminRoom(report: OnyxEntry): boolean { * Whether the provided report is an Admin-only posting room */ function isAdminsOnlyPostingRoom(report: OnyxEntry): boolean { - return (report?.writeCapability ?? CONST.REPORT.WRITE_CAPABILITIES.ALL) === CONST.REPORT.WRITE_CAPABILITIES.ADMINS; + return report?.writeCapability === CONST.REPORT.WRITE_CAPABILITIES.ADMINS; } /** @@ -1354,13 +1357,14 @@ function getDisplayNamesWithTooltips( personalDetailsList: PersonalDetails[] | Record, isMultipleParticipantReport: boolean, shouldFallbackToHidden = true, -): Array> { +): DisplayNameWithTooltips { const personalDetailsListArray = Array.isArray(personalDetailsList) ? personalDetailsList : Object.values(personalDetailsList); return personalDetailsListArray ?.map((user) => { const accountID = Number(user.accountID); - const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport, shouldFallbackToHidden) ?? user.login ?? ''; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport, shouldFallbackToHidden) || user.login || ''; const avatar = UserUtils.getDefaultAvatar(accountID); let pronouns = user.pronouns; @@ -1393,7 +1397,7 @@ function getDisplayNamesWithTooltips( * Gets a joined string of display names from the list of display name with tooltip objects. * */ -function getDisplayNamesStringFromTooltips(displayNamesWithTooltips: Array> | undefined) { +function getDisplayNamesStringFromTooltips(displayNamesWithTooltips: DisplayNameWithTooltips | undefined) { return displayNamesWithTooltips ?.map(({displayName}) => displayName) .filter(Boolean) @@ -1624,8 +1628,7 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName, amount: formattedAmount}); } - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (report?.hasOutstandingIOU || moneyRequestTotal === 0) { + if (!!report?.hasOutstandingIOU || moneyRequestTotal === 0) { return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName, amount: formattedAmount}); } @@ -1815,7 +1818,6 @@ function getTransactionReportName(reportAction: OnyxEntry): string * Get money request message for an IOU report * * @param [reportAction] This can be either a report preview action or the IOU action - * @param [shouldConsiderReceiptBeingScanned=false] */ function getReportPreviewMessage( report: OnyxEntry, @@ -1871,8 +1873,7 @@ function getReportPreviewMessage( const originalMessage = reportAction?.originalMessage as IOUMessage; if ( [CONST.IOU.PAYMENT_TYPE.VBBA, CONST.IOU.PAYMENT_TYPE.EXPENSIFY].some((paymentType) => paymentType === originalMessage?.paymentType) || - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - reportActionMessage.match(/ (with Expensify|using Expensify)$/) || + !!reportActionMessage.match(/ (with Expensify|using Expensify)$/) || report.isWaitingOnBankAccount ) { translatePhraseKey = 'iou.paidWithExpensifyWithAmount'; @@ -2194,8 +2195,7 @@ function getChatRoomSubtitle(report: OnyxEntry): string | undefined { // The domainAll rooms are just #domainName, so we ignore the prefix '#' to get the domainName return report?.reportName?.substring(1) ?? ''; } - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if ((isPolicyExpenseChat(report) && report?.isOwnPolicyExpenseChat) || isExpenseReport(report)) { + if ((isPolicyExpenseChat(report) && !!report?.isOwnPolicyExpenseChat) || isExpenseReport(report)) { return Localize.translateLocal('workspace.common.workspace'); } if (isArchivedRoom(report)) { From e672ccd75051a9da608d4aede7712e9334ea363b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 2 Nov 2023 19:22:38 +0100 Subject: [PATCH 38/63] fix: fixed types issues --- src/libs/ReportUtils.ts | 1 - src/libs/SidebarUtils.ts | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 486991afa817..bd368e90479b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -362,7 +362,6 @@ let currentUserPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => { - currentUserPersonalDetails = idOrDefault(value, currentUserAccountID) ?? null; currentUserPersonalDetails = value?.[currentUserAccountID ?? -1] ?? null; allPersonalDetails = value ?? {}; }, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index a6c25d1d46df..3f826b8c8df4 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -119,7 +119,8 @@ function getOrderedReportIDs( ): string[] { // Generate a unique cache key based on the function arguments const cachedReportsKey = JSON.stringify( - [currentReportId, allReports, betas, policies, priorityMode, allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length ?? 1], + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + [currentReportId, allReports, betas, policies, priorityMode, allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], (key, value: unknown) => { /** * Exclude 'participantAccountIDs', 'participants' and 'lastMessageText' not to overwhelm a cached key value with huge data, From 0dc1e13a3c8c48348e9973fe47f8e239e293412b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 3 Nov 2023 11:25:03 +0100 Subject: [PATCH 39/63] fix: unit tests --- src/libs/ReportUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b49aabe2f38d..761b3d543a21 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -476,7 +476,7 @@ function isTaskReport(report: OnyxEntry): boolean { * There's another situation where you don't have access to the parentReportAction (because it was created in a chat you don't have access to) * In this case, we have added the key to the report itself */ -function isCanceledTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry): boolean { +function isCanceledTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry | Record): boolean { if (Object.keys(parentReportAction ?? {}).length > 0 && (parentReportAction?.message?.[0]?.isDeletedParentAction ?? false)) { return true; } @@ -493,7 +493,7 @@ function isCanceledTaskReport(report: OnyxEntry, parentReportAction?: On * * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ -function isOpenTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry): boolean { +function isOpenTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry | Record): boolean { return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; } @@ -1475,7 +1475,7 @@ function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: Rep * @param [parentReportAction] - The parent report action of the report (Used to check if the task has been canceled) */ function isWaitingForAssigneeToCompleteTask(report: OnyxEntry, parentReportAction: OnyxEntry | Record = {}): boolean { - return isTaskReport(report) && isReportManager(report) && isNotEmptyObject(parentReportAction) && isOpenTaskReport(report, parentReportAction); + return isTaskReport(report) && isReportManager(report) && isOpenTaskReport(report, parentReportAction); } function isUnreadWithMention(report: OnyxEntry | OptionData): boolean { From 1de9b9ba07f7f7130fbe3e3c26e7fc43e94c04eb Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 6 Nov 2023 14:22:19 +0100 Subject: [PATCH 40/63] fix: resolve comments --- src/libs/Permissions.ts | 2 +- src/libs/ReportUtils.ts | 174 +++++++++++++++------------------ src/types/onyx/ReportAction.ts | 18 ++++ src/types/utils/EmptyObject.ts | 11 +++ src/types/utils/Falsy.ts | 3 + 5 files changed, 114 insertions(+), 94 deletions(-) create mode 100644 src/types/utils/EmptyObject.ts create mode 100644 src/types/utils/Falsy.ts diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 47df2acec770..71d51a438513 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -3,7 +3,7 @@ import CONST from '@src/CONST'; import Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { - return Boolean(betas?.includes(CONST.BETAS.ALL)); + return !!betas?.includes(CONST.BETAS.ALL); } function canUseChronos(betas: Beta[]): boolean { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7fde02bd3141..78326115be87 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -19,6 +19,7 @@ import {IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMes import {Message, ReportActions} from '@src/types/onyx/ReportAction'; import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; import DeepValueOf from '@src/types/utils/DeepValueOf'; +import isNotEmptyObject, {EmptyObject} from '@src/types/utils/EmptyObject'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import isReportMessageAttachment from './isReportMessageAttachment'; @@ -334,9 +335,8 @@ type OptionData = { displayNamesWithTooltips?: DisplayNameWithTooltips | null; } & Report; -// eslint-disable-next-line rulesdir/no-negated-variables -function isNotEmptyObject(arg: T | Record): arg is T { - return Object.keys(arg ?? {}).length > 0; +function isEmptyObject(obj: T): boolean { + return Object.keys(obj ?? {}).length === 0; } let currentUserEmail: string | undefined; @@ -397,7 +397,7 @@ function getChatType(report: OnyxEntry): ValueOf | Record { +function getPolicy(policyID: string): OnyxEntry | EmptyObject { if (!allPolicies || !policyID) { return {}; } @@ -406,7 +406,7 @@ function getPolicy(policyID: string): OnyxEntry | Record /** * Get the policy type from a given report - * @param policies must have Onyxkey prefix (i.e 'policy_') for keys + * @param policies must have Onyxkey prefix (i.e 'policy_') for keys */ function getPolicyType(report: OnyxEntry, policies: OnyxCollection): string { return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.type ?? ''; @@ -417,7 +417,7 @@ function getPolicyType(report: OnyxEntry, policies: OnyxCollection | undefined, returnEmptyIfNotFound = false, policy: OnyxEntry = null): string { const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); - if (Object.keys(report ?? {}).length === 0) { + if (isEmptyObject(report)) { return noPolicyFound; } @@ -476,7 +476,7 @@ function isTaskReport(report: OnyxEntry): boolean { * There's another situation where you don't have access to the parentReportAction (because it was created in a chat you don't have access to) * In this case, we have added the key to the report itself */ -function isCanceledTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry | Record): boolean { +function isCanceledTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry | EmptyObject): boolean { if (Object.keys(parentReportAction ?? {}).length > 0 && (parentReportAction?.message?.[0]?.isDeletedParentAction ?? false)) { return true; } @@ -493,7 +493,7 @@ function isCanceledTaskReport(report: OnyxEntry, parentReportAction?: On * * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ -function isOpenTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry | Record): boolean { +function isOpenTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry | EmptyObject): boolean { return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; } @@ -540,7 +540,7 @@ function isSettled(reportID: string | undefined): boolean { return false; } const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - if ((typeof report === 'object' && Object.keys(report ?? {}).length === 0) || report?.isWaitingOnBankAccount) { + if ((typeof report === 'object' && isEmptyObject(report)) || report?.isWaitingOnBankAccount) { return false; } @@ -802,7 +802,7 @@ function findLastAccessedReport( /** * Whether the provided report is an archived room */ -function isArchivedRoom(report: OnyxEntry | Record): boolean { +function isArchivedRoom(report: OnyxEntry | EmptyObject): boolean { return report?.statusNum === CONST.REPORT.STATUS.CLOSED && report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED; } @@ -951,7 +951,7 @@ function isOneOnOneChat(report: OnyxEntry): boolean { /** * Get the report given a reportID */ -function getReport(reportID: string | undefined): OnyxEntry | Record { +function getReport(reportID: string | undefined): OnyxEntry | EmptyObject { // Deleted reports are set to null and lodashGet will still return null in that case, so we need to add an extra check return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; } @@ -1198,7 +1198,7 @@ function getIcons( defaultAccountID = -1, policy: OnyxEntry = null, ): Icon[] { - if (Object.keys(report ?? {}).length === 0) { + if (isEmptyObject(report)) { const fallbackIcon: Icon = { source: defaultIcon ?? Expensicons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, @@ -1466,7 +1466,7 @@ function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: Rep * * @param [parentReportAction] - The parent report action of the report (Used to check if the task has been canceled) */ -function isWaitingForAssigneeToCompleteTask(report: OnyxEntry, parentReportAction: OnyxEntry | Record = {}): boolean { +function isWaitingForAssigneeToCompleteTask(report: OnyxEntry, parentReportAction: OnyxEntry | EmptyObject = {}): boolean { return isTaskReport(report) && isReportManager(report) && isOpenTaskReport(report, parentReportAction); } @@ -1489,7 +1489,7 @@ function isUnreadWithMention(report: OnyxEntry | OptionData): boolean { * @param option (report or optionItem) * @param parentReportAction (the report action the current report is a thread of) */ -function requiresAttentionFromCurrentUser(option: OnyxEntry | OptionData, parentReportAction: Record | OnyxEntry = {}) { +function requiresAttentionFromCurrentUser(option: OnyxEntry | OptionData, parentReportAction: EmptyObject | OnyxEntry = {}) { if (!option) { return false; } @@ -1538,7 +1538,7 @@ function getMoneyRequestReimbursableTotal(report: OnyxEntry, allReportsD moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; } if (moneyRequestReport) { - const total = moneyRequestReport.total ?? 0; + const total = moneyRequestReport?.total ?? 0; if (total !== 0) { // There is a possibility that if the Expense report has a negative total. @@ -1716,7 +1716,7 @@ function canEditMoneyRequest(reportAction: OnyxEntry): boolean { */ function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, reportID: string, fieldToEdit: ValueOf): boolean { // A list of fields that cannot be edited by anyone, once a money request has been settled - const nonEditableFieldsWhenSettled = [ + const nonEditableFieldsWhenSettled: string[] = [ CONST.EDIT_REQUEST_FIELD.AMOUNT, CONST.EDIT_REQUEST_FIELD.CURRENCY, CONST.EDIT_REQUEST_FIELD.DATE, @@ -1731,7 +1731,7 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, repor // Checks if the report is settled // Checks if the provided property is a restricted one - return !isSettled(reportID) || !nonEditableFieldsWhenSettled.some((item) => item === fieldToEdit); + return !isSettled(reportID) || !nonEditableFieldsWhenSettled.includes(fieldToEdit); } /** @@ -1784,7 +1784,6 @@ function areAllRequestsBeingSmartScanned(iouReportID: string, reportPreviewActio /** * Check if any of the transactions in the report has required missing fields * - * @param iouReportID */ function hasMissingSmartscanFields(iouReportID: string): boolean { const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); @@ -1826,7 +1825,7 @@ function getTransactionReportName(reportAction: OnyxEntry): string /** * Get money request message for an IOU report * - * @param [reportAction] This can be either a report preview action or the IOU action + * @param [reportAction] This can be either a report preview action or the IOU action */ function getReportPreviewMessage( report: OnyxEntry, @@ -1835,7 +1834,7 @@ function getReportPreviewMessage( isPreviewMessageForParentChatReport = false, ): string { const reportActionMessage = reportAction?.message?.[0].html ?? ''; - if (Object.keys(report ?? {}).length === 0 || !report?.reportID) { + if (isEmptyObject(report) || !report?.reportID) { // The iouReport is not found locally after SignIn because the OpenApp API won't return iouReports if they're settled // As a temporary solution until we know how to solve this the best, we just use the message that returned from BE return reportActionMessage; @@ -2072,7 +2071,7 @@ function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry): OnyxEntry | Record { +function getParentReport(report: OnyxEntry): OnyxEntry | EmptyObject { if (!report?.parentReportID) { return {}; } @@ -2083,7 +2082,7 @@ function getParentReport(report: OnyxEntry): OnyxEntry | Record< * Returns the root parentReport if the given report is nested. * Uses recursion to iterate any depth of nested reports. */ -function getRootParentReport(report: OnyxEntry): OnyxEntry | Record { +function getRootParentReport(report: OnyxEntry): OnyxEntry | EmptyObject { if (!report) { return {}; } @@ -2216,7 +2215,7 @@ function getChatRoomSubtitle(report: OnyxEntry): string | undefined { /** * Gets the parent navigation subtitle for the report */ -function getReportAndWorkspaceName(report: OnyxEntry): ReportAndWorkspaceName | Record { +function getReportAndWorkspaceName(report: OnyxEntry): ReportAndWorkspaceName | EmptyObject { if (isThread(report)) { const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; const {rootReportName, workspaceName} = getRootReportAndWorkspaceName(parentReport); @@ -2232,7 +2231,7 @@ function getReportAndWorkspaceName(report: OnyxEntry): ReportAndWorkspac /** * Gets the parent navigation subtitle for the report */ -function getParentNavigationSubtitle(report: OnyxEntry): ReportAndWorkspaceName | Record { +function getParentNavigationSubtitle(report: OnyxEntry): ReportAndWorkspaceName | EmptyObject { if (isThread(report)) { const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; const {rootReportName, workspaceName} = getRootReportAndWorkspaceName(parentReport); @@ -2328,9 +2327,9 @@ function buildOptimisticAddCommentReportAction(text?: string, file?: File & {sou /** * update optimistic parent reportAction when a comment is added or remove in the child report - * @param parentReportAction - Parent report action of the child report - * @param lastVisibleActionCreated - Last visible action created of the child report - * @param type - The type of action in the child report + * @param parentReportAction - Parent report action of the child report + * @param lastVisibleActionCreated - Last visible action created of the child report + * @param type - The type of action in the child report */ function updateOptimisticParentReportAction(parentReportAction: OnyxEntry, lastVisibleActionCreated: string, type: string): UpdateOptimisticParentReportAction { @@ -2370,19 +2369,13 @@ function updateOptimisticParentReportAction(parentReportAction: OnyxEntry { + * @param reportID The reportID of the report that is updated + * @param lastVisibleActionCreated Last visible action created of the child report + * @param type The type of action in the child report + * @param parentReportID Custom reportID to be updated + * @param parentReportActionID Custom reportActionID to be updated + */ +function getOptimisticDataForParentReportAction(reportID: string, lastVisibleActionCreated: string, type: string, parentReportID = '', parentReportActionID = ''): OnyxUpdate | EmptyObject { const report = getReport(reportID); if (!report || !isNotEmptyObject(report)) { return {}; @@ -2404,11 +2397,11 @@ function getOptimisticDataForParentReportAction( /** * Builds an optimistic reportAction for the parent report when a task is created - * @param taskReportID - Report ID of the task - * @param taskTitle - Title of the task - * @param taskAssigneeAccountID - AccountID of the person assigned to the task - * @param text - Text of the comment - * @param parentReportID - Report ID of the parent report + * @param taskReportID - Report ID of the task + * @param taskTitle - Title of the task + * @param taskAssigneeAccountID - AccountID of the person assigned to the task + * @param text - Text of the comment + * @param parentReportID - Report ID of the parent report */ function buildOptimisticTaskCommentReportAction(taskReportID: string, taskTitle: string, taskAssigneeAccountID: number, text: string, parentReportID: string): OptimisticReportAction { const reportAction = buildOptimisticAddCommentReportAction(text); @@ -2436,12 +2429,12 @@ function buildOptimisticTaskCommentReportAction(taskReportID: string, taskTitle: /** * Builds an optimistic IOU report with a randomly generated reportID * - * @param payeeAccountID - AccountID of the person generating the IOU. - * @param payerAccountID - AccountID of the other person participating in the IOU. - * @param total - IOU amount in the smallest unit of the currency. - * @param chatReportID - Report ID of the chat where the IOU is. - * @param currency - IOU currency. - * @param isSendingMoney - If we send money the IOU should be created as settled + * @param payeeAccountID - AccountID of the person generating the IOU. + * @param payerAccountID - AccountID of the other person participating in the IOU. + * @param total - IOU amount in the smallest unit of the currency. + * @param chatReportID - Report ID of the chat where the IOU is. + * @param currency - IOU currency. + * @param isSendingMoney - If we send money the IOU should be created as settled */ function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number, total: number, chatReportID: string, currency: string, isSendingMoney = false): OptimisticIOUReport { @@ -2474,11 +2467,11 @@ function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number /** * Builds an optimistic Expense report with a randomly generated reportID * - * @param chatReportID - Report ID of the PolicyExpenseChat where the Expense Report is - * @param policyID - The policy ID of the PolicyExpenseChat - * @param payeeAccountID - AccountID of the employee (payee) - * @param total - Amount in cents - * @param currency + * @param chatReportID - Report ID of the PolicyExpenseChat where the Expense Report is + * @param policyID - The policy ID of the PolicyExpenseChat + * @param payeeAccountID - AccountID of the employee (payee) + * @param total - Amount in cents + * @param currency */ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, payeeAccountID: number, total: number, currency: string): OptimisticExpenseReport { @@ -2573,18 +2566,18 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num /** * Builds an optimistic IOU reportAction object * - * @param type - IOUReportAction type. Can be oneOf(create, delete, pay, split). - * @param amount - IOU amount in cents. - * @param currency - * @param comment - User comment for the IOU. - * @param participants - An array with participants details. - * @param [transactionID] - Not required if the IOUReportAction type is 'pay' - * @param [paymentType] - Only required if the IOUReportAction type is 'pay'. Can be oneOf(elsewhere, Expensify). - * @param [iouReportID] - Only required if the IOUReportActions type is oneOf(decline, cancel, pay). Generates a randomID as default. - * @param [isSettlingUp] - Whether we are settling up an IOU. - * @param [isSendMoneyFlow] - Whether this is send money flow - * @param [receipt] - * @param [isOwnPolicyExpenseChat] - Whether this is an expense report create from the current user's policy expense chat + * @param type - IOUReportAction type. Can be oneOf(create, delete, pay, split). + * @param amount - IOU amount in cents. + * @param currency + * @param comment - User comment for the IOU. + * @param participants - An array with participants details. + * @param [transactionID] - Not required if the IOUReportAction type is 'pay' + * @param [paymentType] - Only required if the IOUReportAction type is 'pay'. Can be oneOf(elsewhere, Expensify). + * @param [iouReportID] - Only required if the IOUReportActions type is oneOf(decline, cancel, pay). Generates a randomID as default. + * @param [isSettlingUp] - Whether we are settling up an IOU. + * @param [isSendMoneyFlow] - Whether this is send money flow + * @param [receipt] + * @param [isOwnPolicyExpenseChat] - Whether this is an expense report create from the current user's policy expense chat */ function buildOptimisticIOUReportAction( @@ -2733,10 +2726,10 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string, /** * Builds an optimistic report preview action with a randomly generated reportActionID. * - * @param chatReport - * @param iouReport - * @param [comment] - User comment for the IOU. - * @param [transaction] - optimistic first transaction of preview + * @param chatReport + * @param iouReport + * @param [comment] - User comment for the IOU. + * @param [transaction] - optimistic first transaction of preview */ function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: OnyxEntry, comment = '', transaction: OnyxEntry = null): OptimisticReportPreview { const hasReceipt = TransactionUtils.hasReceipt(transaction); @@ -2813,8 +2806,8 @@ function buildOptimisticModifiedExpenseReportAction( /** * Updates a report preview action that exists for an IOU report. * - * @param [comment] - User comment for the IOU. - * @param [transaction] - optimistic newest transaction of a report preview + * @param [comment] - User comment for the IOU. + * @param [transaction] - optimistic newest transaction of a report preview * */ function updateReportPreview( @@ -2827,15 +2820,12 @@ function updateReportPreview( const hasReceipt = TransactionUtils.hasReceipt(transaction); const recentReceiptTransactions = reportPreviewAction?.childRecentReceiptTransactionIDs ?? {}; const transactionsToKeep = TransactionUtils.getRecentTransactions(recentReceiptTransactions); - const previousTransactionsArray = Object.entries(recentReceiptTransactions ?? {}).map((item) => { - const [key, value] = item; - return transactionsToKeep.includes(key) ? {[key]: value} : null; - }); + const previousTransactionsArray = Object.entries(recentReceiptTransactions ?? {}).map(([key, value]) => (transactionsToKeep.includes(key) ? {[key]: value} : null)); const previousTransactions: Record = {}; for (const obj of previousTransactionsArray) { for (const key in obj) { - if (Object.hasOwn(obj, key)) { + if (obj) { previousTransactions[key] = obj[key]; } } @@ -2952,7 +2942,7 @@ function buildOptimisticChatReport( /** * Returns the necessary reportAction onyx data to indicate that the chat has been created optimistically - * @param [created] - Action created time + * @param [created] - Action created time */ function buildOptimisticCreatedReportAction(emailCreatingAction: string, created = DateUtils.getDBTime()): OptimisticCreatedReportAction { return { @@ -3024,8 +3014,6 @@ function buildOptimisticEditedTaskReportAction(emailEditingTask: string): Optimi /** * Returns the necessary reportAction onyx data to indicate that a chat has been archived * - * @param emailClosingReport - * @param policyName * @param reason - A reason why the chat has been archived */ function buildOptimisticClosedReportAction(emailClosingReport: string, policyName: string, reason: string = CONST.REPORT.ARCHIVE_REASON.DEFAULT): OptimisticClosedReportAction { @@ -3126,12 +3114,12 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string): Op /** * Builds an optimistic Task Report with a randomly generated reportID * - * @param ownerAccountID - Account ID of the person generating the Task. - * @param assigneeAccountID - AccountID of the other person participating in the Task. - * @param parentReportID - Report ID of the chat where the Task is. - * @param title - Task title. - * @param description - Task description. - * @param policyID - PolicyID of the parent report + * @param ownerAccountID - Account ID of the person generating the Task. + * @param assigneeAccountID - AccountID of the other person participating in the Task. + * @param parentReportID - Report ID of the chat where the Task is. + * @param title - Task title. + * @param description - Task description. + * @param policyID - PolicyID of the parent report */ function buildOptimisticTaskReport( @@ -3856,9 +3844,9 @@ function getTaskAssigneeChatOnyxData( assigneeChatReport: OnyxEntry, ): OnyxDataTaskAssigneeChat { // Set if we need to add a comment to the assignee chat notifying them that they have been assigned a task - let optimisticAssigneeAddComment; + let optimisticAssigneeAddComment: OptimisticReportAction | undefined; // Set if this is a new chat that needs to be created for the assignee - let optimisticChatCreatedReportAction; + let optimisticChatCreatedReportAction: OptimisticCreatedReportAction | undefined; const currentTime = DateUtils.getDBTime(); const optimisticData: OnyxUpdate[] = []; const successData: OnyxUpdate[] = []; @@ -3882,7 +3870,7 @@ function getTaskAssigneeChatOnyxData( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, - value: {[optimisticChatCreatedReportAction.reportActionID ?? '']: optimisticChatCreatedReportAction as Partial}, + value: {[optimisticChatCreatedReportAction.reportActionID]: optimisticChatCreatedReportAction as Partial}, }, ); @@ -3906,7 +3894,7 @@ function getTaskAssigneeChatOnyxData( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, - value: {[optimisticChatCreatedReportAction.reportActionID ?? '']: {pendingAction: null}}, + value: {[optimisticChatCreatedReportAction.reportActionID]: {pendingAction: null}}, }, // If we failed, we want to remove the optimistic personal details as it was likely due to an invalid login { @@ -3935,7 +3923,7 @@ function getTaskAssigneeChatOnyxData( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, - value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? -1]: optimisticAssigneeAddComment.reportAction}, + value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '']: optimisticAssigneeAddComment.reportAction}, }, { onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 726243ead6d4..915022adf10d 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -81,11 +81,21 @@ type ReportActionBase = { whisperedToAccountIDs?: number[]; avatar?: string | React.FC; + + /** Whether timezone is automatically set */ automatic?: boolean; + shouldShow?: boolean; + + /** The ID of childReport */ childReportID?: string; + + /** Name of child report */ childReportName?: string; + + /** Type of child report */ childType?: string; + childOldestFourEmails?: string; childOldestFourAccountIDs?: string; childCommenterCount?: number; @@ -93,13 +103,21 @@ type ReportActionBase = { childVisibleActionCount?: number; parentReportID?: string; childManagerAccountID?: number; + + /** The status of the child report */ childStatusNum?: ValueOf; + + /** The state of the child report */ childStateNum?: ValueOf; childLastReceiptTransactionIDs?: string; childLastMoneyRequestComment?: string; childMoneyRequestCount?: number; isFirstItem?: boolean; + + /** Informations about attachments of report action */ attachmentInfo?: (File & {source: string; uri: string}) | Record; + + /** Receipt tied to report action */ receipt?: Receipt; /** ISO-formatted datetime */ diff --git a/src/types/utils/EmptyObject.ts b/src/types/utils/EmptyObject.ts new file mode 100644 index 000000000000..eeffb3e0dc80 --- /dev/null +++ b/src/types/utils/EmptyObject.ts @@ -0,0 +1,11 @@ +import Falsy from './Falsy'; + +type EmptyObject = Record; + +// eslint-disable-next-line rulesdir/no-negated-variables +function isNotEmptyObject | Falsy>(arg: T | EmptyObject): arg is T { + return Object.keys(arg ?? {}).length > 0; +} + +export default isNotEmptyObject; +export type {EmptyObject}; diff --git a/src/types/utils/Falsy.ts b/src/types/utils/Falsy.ts new file mode 100644 index 000000000000..c1bd7527a223 --- /dev/null +++ b/src/types/utils/Falsy.ts @@ -0,0 +1,3 @@ +type Falsy = undefined | null | false; + +export default Falsy; From e4df9cbbb89ef21a9200babae0215f4a37f5b6f0 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 7 Nov 2023 10:00:17 +0100 Subject: [PATCH 41/63] fix: crash when sending money request --- src/libs/IOUUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index ff4f2aafc8a8..543261f7561c 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -1,3 +1,4 @@ +import {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import {Report, Transaction} from '@src/types/onyx'; import * as CurrencyUtils from './CurrencyUtils'; @@ -35,13 +36,13 @@ function calculateAmount(numberOfParticipants: number, total: number, currency: * * @param isDeleting - whether the user is deleting the request */ -function updateIOUOwnerAndTotal(iouReport: Report, actorAccountID: number, amount: number, currency: string, isDeleting = false): Report { - if (currency !== iouReport.currency) { +function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: number, amount: number, currency: string, isDeleting = false): OnyxEntry { + if (currency !== iouReport?.currency) { return iouReport; } // Make a copy so we don't mutate the original object - const iouReportUpdate: Report = {...iouReport}; + const iouReportUpdate: OnyxEntry = {...iouReport}; if (iouReportUpdate.total) { if (actorAccountID === iouReport.ownerAccountID) { From 611dd1aaf691b55eb4fe6aa576a578e7469b5edf Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 7 Nov 2023 10:45:24 +0100 Subject: [PATCH 42/63] fix: resolve comments, fixed type and lint issues --- src/languages/types.ts | 6 ++-- src/libs/ReportUtils.ts | 53 ++++++++++++----------------- src/libs/actions/PersonalDetails.ts | 2 +- src/types/utils/EmptyObject.ts | 6 +++- 4 files changed, 31 insertions(+), 36 deletions(-) diff --git a/src/languages/types.ts b/src/languages/types.ts index 5f6669315041..b4fa0db6b04e 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -111,17 +111,17 @@ type DidSplitAmountMessageParams = {formattedAmount: string; comment: string}; type AmountEachParams = {amount: number}; -type PayerOwesAmountParams = {payer: string; amount: number}; +type PayerOwesAmountParams = {payer: string; amount: number | string}; type PayerOwesParams = {payer: string}; -type PayerPaidAmountParams = {payer: string; amount: number}; +type PayerPaidAmountParams = {payer: string; amount: number | string}; type ManagerApprovedParams = {manager: string}; type PayerPaidParams = {payer: string}; -type PayerSettledParams = {amount: number}; +type PayerSettledParams = {amount: number | string}; type WaitingOnBankAccountParams = {submitterDisplayName: string}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 676513d53b90..4da20269694b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -11,6 +11,7 @@ import {ValueOf} from 'type-fest'; import * as Expensicons from '@components/Icon/Expensicons'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; 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, Report, ReportAction, Transaction} from '@src/types/onyx'; @@ -19,7 +20,7 @@ import {IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMes import {Message, ReportActions} from '@src/types/onyx/ReportAction'; import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; import DeepValueOf from '@src/types/utils/DeepValueOf'; -import isNotEmptyObject, {EmptyObject} from '@src/types/utils/EmptyObject'; +import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import isReportMessageAttachment from './isReportMessageAttachment'; @@ -335,10 +336,6 @@ type OptionData = { displayNamesWithTooltips?: DisplayNameWithTooltips | null; } & Report; -function isEmptyObject(obj: T): boolean { - return Object.keys(obj ?? {}).length === 0; -} - let currentUserEmail: string | undefined; let currentUserAccountID: number | undefined; let isAnonymousUser = false; @@ -1012,7 +1009,7 @@ function getRoomWelcomeMessage(report: OnyxEntry, isUserPolicyAdmin: boo welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartOne'); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartTwo'); } else if (isDomainRoom(report)) { - welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartOne', {domainRoom: report?.reportName}); + welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartOne', {domainRoom: report?.reportName ?? ''}); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartTwo'); } else if (isAdminRoom(report)) { welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartOne', {workspaceName}); @@ -1370,7 +1367,7 @@ function getDisplayNamesWithTooltips( let pronouns = user.pronouns; if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) { const pronounTranslationKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); - pronouns = Localize.translateLocal(`pronouns.${pronounTranslationKey}`); + pronouns = Localize.translateLocal(`pronouns.${pronounTranslationKey}` as TranslationPaths); } return { @@ -1426,9 +1423,9 @@ function getDeletedParentActionMessageForChatReport(reportAction: OnyxEntry, report: OnyxEntry): string { - const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, true); + const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, true) ?? ''; const originalMessage = reportAction?.originalMessage as IOUMessage; - let messageKey; + let messageKey: TranslationPaths; if (originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { messageKey = 'iou.waitingOnEnabledWallet'; } else { @@ -1623,7 +1620,7 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry

, policy: OnyxEntry): string { const moneyRequestTotal = getMoneyRequestReimbursableTotal(report); const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID)); - const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID); + const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? ''; const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', { payer: payerName, amount: formattedAmount, @@ -1817,8 +1814,8 @@ function getTransactionReportName(reportAction: OnyxEntry): string const transactionDetails = getTransactionDetails(transaction); return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) ? 'iou.threadSentMoneyReportName' : 'iou.threadRequestReportName', { - formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? '', TransactionUtils.isDistanceRequest(transaction)), - comment: transactionDetails?.comment, + formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? '', TransactionUtils.isDistanceRequest(transaction)) ?? '', + comment: transactionDetails?.comment ?? '', }); } @@ -1854,12 +1851,12 @@ function getReportPreviewMessage( const transactionDetails = getTransactionDetails(linkedTransaction); const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); - return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment: transactionDetails?.comment}); + return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment: transactionDetails?.comment ?? ''}); } } const totalAmount = getMoneyRequestReimbursableTotal(report); - const payerName = isExpenseReport(report) ? getPolicyName(report) : getDisplayNameForParticipant(report.managerID, true); + const payerName = isExpenseReport(report) ? getPolicyName(report) : getDisplayNameForParticipant(report.managerID, true) ?? ''; const formattedAmount = CurrencyUtils.convertToDisplayString(totalAmount, report.currency); if (isReportApproved(report) && getPolicyType(report, allPolicies) === CONST.POLICY.TYPE.CORPORATE) { @@ -1877,7 +1874,7 @@ function getReportPreviewMessage( // Show Paid preview message if it's settled or if the amount is paid & stuck at receivers end for only chat reports. if (isSettled(report.reportID) || (report.isWaitingOnBankAccount && isPreviewMessageForParentChatReport)) { // A settled report preview message can come in three formats "paid ... elsewhere" or "paid ... with Expensify" - let translatePhraseKey = 'iou.paidElsewhereWithAmount'; + let translatePhraseKey: TranslationPaths = 'iou.paidElsewhereWithAmount'; const originalMessage = reportAction?.originalMessage as IOUMessage; if ( [CONST.IOU.PAYMENT_TYPE.VBBA, CONST.IOU.PAYMENT_TYPE.EXPENSIFY].some((paymentType) => paymentType === originalMessage?.paymentType) || @@ -1890,7 +1887,7 @@ function getReportPreviewMessage( } if (report.isWaitingOnBankAccount) { - const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID ?? -1, true); + const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID ?? -1, true) ?? ''; return Localize.translateLocal('iou.waitingOnBankAccount', {submitterDisplayName}); } @@ -3979,27 +3976,27 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) if (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { const {IOUReportID} = originalMessage; const {amount, currency} = originalMessage.IOUDetails ?? originalMessage; - const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency); + const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency) ?? ''; const iouReport = getReport(IOUReportID); - const payerName = isNotEmptyObject(iouReport) && isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport?.managerID, true); - let translationKey; + const payerName = isNotEmptyObject(iouReport) && isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport?.managerID, true) ?? ''; + let translationKey: TranslationPaths; switch (reportAction?.originalMessage.paymentType) { case CONST.IOU.PAYMENT_TYPE.ELSEWHERE: translationKey = 'iou.paidElsewhereWithAmount'; break; case CONST.IOU.PAYMENT_TYPE.EXPENSIFY: case CONST.IOU.PAYMENT_TYPE.VBBA: - translationKey = 'iou.paidUsingExpensifyWithAmount'; + translationKey = 'iou.paidWithExpensifyWithAmount'; break; default: - translationKey = ''; + translationKey = 'iou.payerPaidAmount'; break; } displayMessage = Localize.translateLocal(translationKey, {amount: formattedAmount, payer: payerName}); } else { const transaction = TransactionUtils.getTransaction(originalMessage.IOUTransactionID ?? ''); const transactionDetails = isNotEmptyObject(transaction) ? getTransactionDetails(transaction) : null; - const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency); + const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency) ?? ''; const isRequestSettled = isSettled(originalMessage.IOUReportID); if (isRequestSettled) { displayMessage = Localize.translateLocal('iou.payerSettled', { @@ -4008,7 +4005,7 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) } else { displayMessage = Localize.translateLocal('iou.requestedAmount', { formattedAmount, - comment: transactionDetails?.comment, + comment: transactionDetails?.comment ?? '', }); } } @@ -4047,14 +4044,8 @@ function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report); } -/** - * - * @param {String} type - * @param {String} policyID - * @returns {Object} - */ -function getRoom(type, policyID) { - const room = _.find(allReports, (report) => report && report.policyID === policyID && report.chatType === type && !isThread(report)); +function getRoom(type: ValueOf, policyID: string) { + const room = Object.values(allReports ?? {}).find((report) => report && report.policyID === policyID && report.chatType === type && !isThread(report)); return room; } diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 01f8c2f4916b..75e3e434c438 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -490,7 +490,7 @@ function updateAvatar(file: FileWithUri | CustomRNImageManipulatorResult) { pendingFields: { avatar: null, }, - }, + } as OnyxEntry>, }, }, ]; diff --git a/src/types/utils/EmptyObject.ts b/src/types/utils/EmptyObject.ts index eeffb3e0dc80..7f72ecb631d2 100644 --- a/src/types/utils/EmptyObject.ts +++ b/src/types/utils/EmptyObject.ts @@ -7,5 +7,9 @@ function isNotEmptyObject | Falsy>(arg: T | Em return Object.keys(arg ?? {}).length > 0; } -export default isNotEmptyObject; +function isEmptyObject(obj: T): boolean { + return Object.keys(obj ?? {}).length === 0; +} + +export {isNotEmptyObject, isEmptyObject}; export type {EmptyObject}; From 24177794282be7bcf92c51c88571b214b2cb260e Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 7 Nov 2023 18:04:24 +0100 Subject: [PATCH 43/63] fix: test and typings --- src/libs/ReportUtils.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 13f9c796312e..d25c35933173 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -955,12 +955,9 @@ function getReport(reportID: string | undefined): OnyxEntry | EmptyObjec /** * Get the notification preference given a report - * - * @param {Object} report - * @returns {String} */ -function getReportNotificationPreference(report) { - return lodashGet(report, 'notificationPreference', ''); +function getReportNotificationPreference(report: OnyxEntry): string | number { + return report?.notificationPreference ?? ''; } /** From 2f20a4642ee5785dd9f54f961506be49a4a9a101 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 9 Nov 2023 10:25:44 +0100 Subject: [PATCH 44/63] fix: types --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 934447cb02b6..8e48d438346c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3636,7 +3636,7 @@ function getMoneyRequestOptions(report: OnyxEntry, reportParticipants: n // unless there are no other participants at all (e.g. #admins room for a policy with only 1 admin) // DM chats will have the Split Bill option only when there are at least 2 other people in the chat. // Your own workspace chats will have the split bill option. - if ((isChatRoom(report) && otherParticipants.length > 0) || (isDM(report) && hasMultipleOtherParticipants) || (isPolicyExpenseChat(report) && report.isOwnPolicyExpenseChat)) { + if ((isChatRoom(report) && otherParticipants.length > 0) || (isDM(report) && hasMultipleOtherParticipants) || (isPolicyExpenseChat(report) && report?.isOwnPolicyExpenseChat)) { options = [CONST.IOU.TYPE.SPLIT]; } From dd95a3d2a63a7480bd62ed3207253e3fa5ea386a Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 13 Nov 2023 14:41:13 +0100 Subject: [PATCH 45/63] fix: resolved comments --- src/libs/ReportUtils.ts | 172 ++++++++++++++++----------------- src/libs/TransactionUtils.ts | 5 +- src/libs/UserUtils.ts | 6 +- src/types/utils/EmptyObject.ts | 2 +- 4 files changed, 89 insertions(+), 96 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 212445f9ce41..770aaf6627a1 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -336,6 +336,14 @@ type OptionData = { displayNamesWithTooltips?: DisplayNameWithTooltips | null; } & Report; +type OnyxDataTaskAssigneeChat = { + optimisticData: OnyxUpdate[]; + successData: OnyxUpdate[]; + failureData: OnyxUpdate[]; + optimisticAssigneeAddComment?: OptimisticReportAction; + optimisticChatCreatedReportAction?: OptimisticCreatedReportAction; +}; + let currentUserEmail: string | undefined; let currentUserAccountID: number | undefined; let isAnonymousUser = false; @@ -368,7 +376,7 @@ let allReports: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, waitForCollectionCallback: true, - callback: (val) => (allReports = val), + callback: (value) => (allReports = value), }); let doesDomainHaveApprovedAccountant = false; @@ -381,13 +389,13 @@ let allPolicies: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, waitForCollectionCallback: true, - callback: (val) => (allPolicies = val), + callback: (value) => (allPolicies = value), }); let loginList: OnyxEntry; Onyx.connect({ key: ONYXKEYS.LOGIN_LIST, - callback: (val) => (loginList = val), + callback: (value) => (loginList = value), }); function getChatType(report: OnyxEntry): ValueOf | undefined { @@ -512,7 +520,7 @@ function isReportManager(report: OnyxEntry): boolean { * Checks if the supplied report has been approved */ function isReportApproved(report: OnyxEntry): boolean { - return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report.statusNum === CONST.REPORT.STATUS.APPROVED; + return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS.APPROVED; } /** @@ -520,7 +528,7 @@ function isReportApproved(report: OnyxEntry): boolean { */ function sortReportsByLastRead(reports: OnyxCollection): Array> { return Object.values(reports ?? {}) - .filter((report) => report?.reportID && report?.lastReadTime) + .filter((report) => !!report?.reportID && !!report?.lastReadTime) .sort((a, b) => { const aTime = new Date(a?.lastReadTime ?? ''); const bTime = new Date(b?.lastReadTime ?? ''); @@ -537,7 +545,7 @@ function isSettled(reportID: string | undefined): boolean { return false; } const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - if ((typeof report === 'object' && isEmptyObject(report)) || report?.isWaitingOnBankAccount) { + if (report?.isWaitingOnBankAccount) { return false; } @@ -655,7 +663,7 @@ function getBankAccountRoute(report: OnyxEntry): string { * Check if personal detail of accountID is empty or optimistic data */ function isOptimisticPersonalDetail(accountID: number): boolean { - return Object.keys(allPersonalDetails?.[accountID] ?? {}).length === 0 || !!allPersonalDetails?.[accountID]?.isOptimisticPersonalDetail; + return isEmptyObject(allPersonalDetails?.[accountID]) || !!allPersonalDetails?.[accountID]?.isOptimisticPersonalDetail; } /** @@ -724,9 +732,8 @@ function isExpensifyOnlyParticipantInReport(report: OnyxEntry): boolean * */ function canCreateTaskInReport(report: OnyxEntry): boolean { - const otherReportParticipants = report?.participantAccountIDs?.filter((accountID) => accountID !== currentUserAccountID); - const areExpensifyAccountsOnlyOtherParticipants = - otherReportParticipants && otherReportParticipants?.length >= 1 && otherReportParticipants?.every((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)); + const otherReportParticipants = report?.participantAccountIDs?.filter((accountID) => accountID !== currentUserAccountID) ?? []; + const areExpensifyAccountsOnlyOtherParticipants = otherReportParticipants?.length >= 1 && otherReportParticipants?.every((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)); if (areExpensifyAccountsOnlyOtherParticipants && isDM(report)) { return false; } @@ -765,7 +772,7 @@ function findLastAccessedReport( // since the Concierge report would be incorrectly selected over the deep-linked report in the logic below. let sortedReports = sortReportsByLastRead(reports); - let adminReport; + let adminReport: OnyxEntry | undefined; if (openOnAdminRoom) { adminReport = sortedReports.find((report) => { const chatType = getChatType(report); @@ -803,9 +810,9 @@ function isArchivedRoom(report: OnyxEntry | EmptyObject): boolean { /** * Checks if the current user is allowed to comment on the given report. */ -function isAllowedToComment(report: OnyxEntry): boolean { +function isAllowedToComment(report: Report): boolean { // Default to allowing all users to post - const capability = (report?.writeCapability ?? CONST.REPORT.WRITE_CAPABILITIES.ALL) || CONST.REPORT.WRITE_CAPABILITIES.ALL; + const capability = report?.writeCapability ?? CONST.REPORT.WRITE_CAPABILITIES.ALL; if (capability === CONST.REPORT.WRITE_CAPABILITIES.ALL) { return true; @@ -864,7 +871,7 @@ function hasOnlyDistanceRequestTransactions(iouReportID: string | undefined): bo * If the report is a thread and has a chat type set, it is a workspace chat. */ function isWorkspaceThread(report: OnyxEntry): boolean { - return Boolean(isThread(report) && !isDM(report)); + return isThread(report) && !isDM(report); } /** @@ -886,7 +893,7 @@ function isChildReport(report: OnyxEntry): boolean { * the parentReportAction is a transaction. */ function isExpenseRequest(report: OnyxEntry): boolean { - if (report && isThread(report)) { + if (isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; return isExpenseReport(parentReport) && isNotEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); @@ -899,7 +906,7 @@ function isExpenseRequest(report: OnyxEntry): boolean { * the parentReportAction is a transaction. */ function isIOURequest(report: OnyxEntry): boolean { - if (report && isThread(report)) { + if (isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; return isIOUReport(parentReport) && isNotEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); @@ -976,7 +983,7 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: if (ReportActionsUtils.isMoneyRequestAction(reportAction) && reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { // For now, users cannot delete split actions - const isSplitAction = reportAction.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; + const isSplitAction = reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; if (isSplitAction || isSettled(String(reportAction?.originalMessage?.IOUReportID)) || (isNotEmptyObject(report) && isReportApproved(report))) { return false; @@ -991,7 +998,7 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || reportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || ReportActionsUtils.isCreatedTaskReportAction(reportAction) || - reportAction.actorAccountID === CONST.ACCOUNT_ID.CONCIERGE + reportAction?.actorAccountID === CONST.ACCOUNT_ID.CONCIERGE ) { return false; } @@ -1362,7 +1369,7 @@ function getDisplayNamesWithTooltips( const personalDetailsListArray = Array.isArray(personalDetailsList) ? personalDetailsList : Object.values(personalDetailsList); return personalDetailsListArray - ?.map((user) => { + .map((user) => { const accountID = Number(user.accountID); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport, shouldFallbackToHidden) || user.login || ''; @@ -1428,9 +1435,9 @@ function getDeletedParentActionMessageForChatReport(reportAction: OnyxEntry, report: OnyxEntry): string { const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, true) ?? ''; - const originalMessage = reportAction?.originalMessage as IOUMessage; + const originalMessage = reportAction?.originalMessage as IOUMessage | undefined; let messageKey: TranslationPaths; - if (originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { + if (originalMessage?.paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { messageKey = 'iou.waitingOnEnabledWallet'; } else { messageKey = 'iou.waitingOnBankAccount'; @@ -1451,7 +1458,7 @@ function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: Rep const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID ?? '', actionsToMerge); // For Chat Report with deleted parent actions, let us fetch the correct message - if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && report && isNotEmptyObject(report) && isChatReport(report)) { + if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && isNotEmptyObject(report) && isChatReport(report)) { const lastMessageText = getDeletedParentActionMessageForChatReport(lastVisibleAction); return { lastMessageText, @@ -1476,8 +1483,8 @@ function isUnreadWithMention(report: OnyxEntry | OptionData): boolean { return false; } // lastMentionedTime and lastReadTime are both datetime strings and can be compared directly - const lastMentionedTime = report?.lastMentionedTime ?? ''; - const lastReadTime = report?.lastReadTime ?? ''; + const lastMentionedTime = report.lastMentionedTime ?? ''; + const lastReadTime = report.lastReadTime ?? ''; return lastReadTime < lastMentionedTime; } @@ -1531,7 +1538,7 @@ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolea function getMoneyRequestReimbursableTotal(report: OnyxEntry, allReportsDict?: OnyxCollection): number { const allAvailableReports = allReportsDict ?? allReports; - let moneyRequestReport; + let moneyRequestReport: OnyxEntry | undefined; if (isMoneyRequestReport(report)) { moneyRequestReport = report; } @@ -1591,7 +1598,7 @@ function getMoneyRequestSpendBreakdown(report: OnyxEntry, allReportsDict /** * Get the title for a policy expense chat which depends on the role of the policy member seeing this report */ -function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry): string | undefined { +function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string | undefined { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const reportOwnerDisplayName = getDisplayNameForParticipant(report?.ownerAccountID) || allPersonalDetails?.[report?.ownerAccountID ?? -1]?.login || report?.reportName; @@ -1621,7 +1628,7 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry

, policy: OnyxEntry): string { +function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string { const moneyRequestTotal = getMoneyRequestReimbursableTotal(report); const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID)); const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? ''; @@ -1844,7 +1851,7 @@ function getReportPreviewMessage( if (!isIOUReport(report) && reportAction && ReportActionsUtils.isSplitBillAction(reportAction)) { // This covers group chats where the last action is a split bill action const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); - if (Object.keys(linkedTransaction ?? {}).length === 0) { + if (isEmptyObject(linkedTransaction)) { return reportActionMessage; } @@ -1879,7 +1886,7 @@ function getReportPreviewMessage( if (isSettled(report.reportID) || (report.isWaitingOnBankAccount && isPreviewMessageForParentChatReport)) { // A settled report preview message can come in three formats "paid ... elsewhere" or "paid ... with Expensify" let translatePhraseKey: TranslationPaths = 'iou.paidElsewhereWithAmount'; - const originalMessage = reportAction?.originalMessage as IOUMessage; + const originalMessage = reportAction?.originalMessage as IOUMessage | undefined; if ( [CONST.IOU.PAYMENT_TYPE.VBBA, CONST.IOU.PAYMENT_TYPE.EXPENSIFY].some((paymentType) => paymentType === originalMessage?.paymentType) || !!reportActionMessage.match(/ (with Expensify|using Expensify)$/) || @@ -1891,7 +1898,7 @@ function getReportPreviewMessage( } if (report.isWaitingOnBankAccount) { - const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID ?? -1, true) ?? ''; + const submitterDisplayName = getDisplayNameForParticipant(report.ownerAccountID ?? -1, true) ?? ''; return Localize.translateLocal('iou.waitingOnBankAccount', {submitterDisplayName}); } @@ -1939,15 +1946,19 @@ function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDista * 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; - if (Object.keys(reportActionOriginalMessage ?? {}).length === 0) { + const reportActionOriginalMessage = reportAction?.originalMessage as ExpenseOriginalMessage | undefined; + if (isEmptyObject(reportActionOriginalMessage)) { return Localize.translateLocal('iou.changedTheRequest'); } const hasModifiedAmount = - 'oldAmount' in reportActionOriginalMessage && 'oldCurrency' in reportActionOriginalMessage && 'amount' in reportActionOriginalMessage && 'currency' in reportActionOriginalMessage; + reportActionOriginalMessage && + 'oldAmount' in reportActionOriginalMessage && + 'oldCurrency' in reportActionOriginalMessage && + 'amount' in reportActionOriginalMessage && + 'currency' in reportActionOriginalMessage; - const hasModifiedMerchant = 'oldMerchant' in reportActionOriginalMessage && 'merchant' 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 ?? ''); @@ -1964,7 +1975,7 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin return getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); } - const hasModifiedComment = 'oldComment' in reportActionOriginalMessage && 'newComment' in reportActionOriginalMessage; + const hasModifiedComment = reportActionOriginalMessage && 'oldComment' in reportActionOriginalMessage && 'newComment' in reportActionOriginalMessage; if (hasModifiedComment) { return getProperSchemaForModifiedExpenseMessage( reportActionOriginalMessage?.newComment ?? '', @@ -1974,10 +1985,10 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin ); } - const hasModifiedCreated = 'oldCreated' in reportActionOriginalMessage && 'created' in reportActionOriginalMessage; + 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 ?? ''); + 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); @@ -1992,7 +2003,7 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin ); } - const hasModifiedCategory = 'oldCategory' in reportActionOriginalMessage && 'category' in reportActionOriginalMessage; + const hasModifiedCategory = reportActionOriginalMessage && 'oldCategory' in reportActionOriginalMessage && 'category' in reportActionOriginalMessage; if (hasModifiedCategory) { return getProperSchemaForModifiedExpenseMessage( reportActionOriginalMessage?.category ?? '', @@ -2002,12 +2013,12 @@ function getModifiedExpenseMessage(reportAction: OnyxEntry): strin ); } - const hasModifiedTag = 'oldTag' in reportActionOriginalMessage && 'tag' in reportActionOriginalMessage; + const hasModifiedTag = reportActionOriginalMessage && 'oldTag' in reportActionOriginalMessage && 'tag' in reportActionOriginalMessage; if (hasModifiedTag) { return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.tag ?? '', reportActionOriginalMessage?.oldTag ?? '', Localize.translateLocal('common.tag'), true); } - const hasModifiedBillable = 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage; + const hasModifiedBillable = reportActionOriginalMessage && 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage; if (hasModifiedBillable) { return getProperSchemaForModifiedExpenseMessage( reportActionOriginalMessage?.billable ?? '', @@ -2045,9 +2056,9 @@ function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry): OnyxEntry | Emp * Get the title for a report. */ function getReportName(report: OnyxEntry, policy: OnyxEntry = null): string { - let formattedName; + let formattedName: string | undefined; const parentReportAction = ReportActionsUtils.getParentReportAction(report); if (isChatThread(report)) { if (isNotEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction)) { @@ -2124,7 +2135,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu return parentReportActionMessage || Localize.translateLocal('parentReportAction.deletedMessage'); } - if (isTaskReport(report) && isNotEmptyObject(parentReportAction) && isCanceledTaskReport(report, parentReportAction)) { + if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { return Localize.translateLocal('parentReportAction.deletedTask'); } @@ -2164,7 +2175,6 @@ function getRootReportAndWorkspaceName(report: OnyxEntry): ReportAndWork if (!report) { return { rootReportName: '', - workspaceName: '', }; } if (isChildReport(report) && !isMoneyRequestReport(report) && !isTaskReport(report)) { @@ -2213,22 +2223,6 @@ function getChatRoomSubtitle(report: OnyxEntry): string | undefined { return getPolicyName(report); } -/** - * Gets the parent navigation subtitle for the report - */ -function getReportAndWorkspaceName(report: OnyxEntry): ReportAndWorkspaceName | EmptyObject { - if (isThread(report)) { - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; - const {rootReportName, workspaceName} = getRootReportAndWorkspaceName(parentReport); - if (!rootReportName) { - return {}; - } - - return {rootReportName, workspaceName}; - } - return {}; -} - /** * Gets the parent navigation subtitle for the report */ @@ -2256,7 +2250,9 @@ function navigateToDetailsPage(report: OnyxEntry) { Navigation.navigate(ROUTES.PROFILE.getRoute(participantAccountIDs[0])); return; } - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '')); + if (report?.reportID) { + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID)); + } } /** @@ -2272,7 +2268,7 @@ function generateReportID(): string { } function hasReportNameError(report: OnyxEntry): boolean { - return Object.keys(report?.errorFields?.reportName ?? {}).length !== 0; + return !isEmptyObject(report?.errorFields?.reportName); } /** @@ -2632,7 +2628,9 @@ function buildOptimisticIOUReportAction( if (isOwnPolicyExpenseChat) { originalMessage.participantAccountIDs = currentUserAccountID ? [currentUserAccountID] : []; } else { - originalMessage.participantAccountIDs = currentUserAccountID ? [currentUserAccountID, ...participants.map((participant) => participant.accountID)] : []; + originalMessage.participantAccountIDs = currentUserAccountID + ? [currentUserAccountID, ...participants.map((participant) => participant.accountID)] + : participants.map((participant) => participant.accountID); } } @@ -2734,7 +2732,7 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string, */ function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: OnyxEntry, comment = '', transaction: OnyxEntry = null): OptimisticReportPreview { const hasReceipt = TransactionUtils.hasReceipt(transaction); - const isReceiptBeingScanned = hasReceipt && transaction && TransactionUtils.isReceiptBeingScanned(transaction); + const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); const message = getReportPreviewMessage(iouReport); const created = DateUtils.getDBTime(); return { @@ -2759,7 +2757,7 @@ function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.managerID ?? 0, childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, - childRecentReceiptTransactionIDs: hasReceipt && transaction ? {[transaction.transactionID]: created} : undefined, + childRecentReceiptTransactionIDs: hasReceipt && isNotEmptyObject(transaction) ? {[transaction?.transactionID ?? '']: created} : undefined, whisperedToAccountIDs: isReceiptBeingScanned ? [currentUserAccountID ?? -1] : [], }; } @@ -2816,7 +2814,7 @@ function updateReportPreview( reportPreviewAction: OnyxEntry, isPayRequest = false, comment = '', - transaction?: OnyxEntry, + transaction: OnyxEntry = null, ): UpdateReportPreview { const hasReceipt = TransactionUtils.hasReceipt(transaction); const recentReceiptTransactions = reportPreviewAction?.childRecentReceiptTransactionIDs ?? {}; @@ -3279,12 +3277,11 @@ function shouldReportBeInOptionList( // This can also happen for anyone accessing a public room or archived room for which they don't have access to the underlying policy. if ( !report?.reportID || - !report.type || - report.reportName === undefined || + !report?.type || + report?.reportName === undefined || // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - report.isHidden || - (report.participantAccountIDs && - report.participantAccountIDs.length === 0 && + report?.isHidden || + (report?.participantAccountIDs?.length === 0 && !isChatThread(report) && !isPublicRoom(report) && !isUserCreatedPolicyRoom(report) && @@ -3400,7 +3397,7 @@ function getAllPolicyReports(policyID: string): Array> { * Returns true if Chronos is one of the chat participants (1:1) */ function chatIncludesChronos(report: OnyxEntry): boolean { - return Boolean(report?.participantAccountIDs && report.participantAccountIDs.includes(CONST.ACCOUNT_ID.CHRONOS)); + return Boolean(report?.participantAccountIDs?.includes(CONST.ACCOUNT_ID.CHRONOS)); } /** @@ -3433,6 +3430,7 @@ function canFlagReportAction(reportAction: OnyxEntry, reportID: st !ReportActionsUtils.isDeletedAction(reportAction) && !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && isNotEmptyObject(report) && + report && isAllowedToComment(report), ); } @@ -3450,6 +3448,9 @@ function shouldShowFlagComment(reportAction: OnyxEntry, report: On ); } +/** + * @param sortedAndFilteredReportActions - reportActions for the report, sorted newest to oldest, and filtered for only those that should be visible + */ function getNewMarkerReportActionID(report: OnyxEntry, sortedAndFilteredReportActions: ReportAction[]): string { if (!isUnread(report)) { return ''; @@ -3683,7 +3684,7 @@ function canLeaveRoom(report: OnyxEntry, isPolicyMember: boolean): boole } function isCurrentUserTheOnlyParticipant(participantAccountIDs?: number[]): boolean { - return Boolean(participantAccountIDs && participantAccountIDs.length === 1 && participantAccountIDs[0] === currentUserAccountID); + return Boolean(participantAccountIDs?.length === 1 && participantAccountIDs?.[0] === currentUserAccountID); } /** @@ -3736,14 +3737,14 @@ function shouldReportShowSubscript(report: OnyxEntry): boolean { * Return true if reports data exists */ function isReportDataReady(): boolean { - return Object.keys(allReports ?? {}).length !== 0 && Object.keys(allReports ?? {}).some((key) => allReports?.[key]?.reportID); + return !isEmptyObject(allReports) && Object.keys(allReports ?? {}).some((key) => allReports?.[key]?.reportID); } /** * Return true if reportID from path is valid */ function isValidReportIDFromPath(reportIDFromPath: string): boolean { - return typeof reportIDFromPath === 'string' && !['', 'null', '0'].includes(reportIDFromPath); + return !['', 'null', '0'].includes(reportIDFromPath); } /** @@ -3757,7 +3758,7 @@ function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry): Recor function canUserPerformWriteAction(report: OnyxEntry) { const reportErrors = getAddWorkspaceRoomOrChatReportErrors(report); - return !isArchivedRoom(report) && Object.keys(reportErrors ?? {}).length === 0 && isAllowedToComment(report) && !isAnonymousUser; + return !isArchivedRoom(report) && isEmptyObject(reportErrors) && report && isAllowedToComment(report) && !isAnonymousUser; } /** @@ -3806,7 +3807,7 @@ function canCreateRequest(report: OnyxEntry, iouType: (typeof CONST.IOU. } function getWorkspaceChats(policyID: string, accountIDs: number[]): Array> { - return Object.values(allReports ?? {})?.filter((report) => isPolicyExpenseChat(report) && (report?.policyID ?? '') === policyID && accountIDs.includes(report?.ownerAccountID ?? -1)); + return Object.values(allReports ?? {}).filter((report) => isPolicyExpenseChat(report) && (report?.policyID ?? '') === policyID && accountIDs.includes(report?.ownerAccountID ?? -1)); } /** @@ -3828,14 +3829,6 @@ function shouldDisableRename(report: OnyxEntry, policy: OnyxEntry) if (reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { return ''; } - const originalMessage = reportAction?.originalMessage; + const originalMessage = reportAction.originalMessage; let translationKey: TranslationPaths; if (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { const {IOUReportID} = originalMessage; @@ -4042,7 +4035,7 @@ function isGroupChat(report: OnyxEntry): boolean { !isMoneyRequestReport(report) && !isArchivedRoom(report) && !Object.values(CONST.REPORT.CHAT_TYPE).some((chatType) => chatType === getChatType(report)) && - (report?.participantAccountIDs?.length ?? 0) > 2, + (report.participantAccountIDs?.length ?? 0) > 2, ); } @@ -4051,7 +4044,7 @@ function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { } function getRoom(type: ValueOf, policyID: string) { - const room = Object.values(allReports ?? {}).find((report) => report && report.policyID === policyID && report.chatType === type && !isThread(report)); + const room = Object.values(allReports ?? {}).find((report) => report?.policyID === policyID && report?.chatType === type && !isThread(report)); return room; } @@ -4074,7 +4067,6 @@ export { isChatRoom, getChatRoomSubtitle, getParentNavigationSubtitle, - getReportAndWorkspaceName, getPolicyName, getPolicyType, isArchivedRoom, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 62602dab712d..4464412284b2 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -4,6 +4,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {RecentWaypoint, ReportAction, Transaction} from '@src/types/onyx'; import {Comment, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; +import {EmptyObject} from '@src/types/utils/EmptyObject'; import {isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; import * as NumberUtils from './NumberUtils'; @@ -192,7 +193,7 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra * * @deprecated Use withOnyx() or Onyx.connect() instead */ -function getTransaction(transactionID: string): OnyxEntry | Record { +function getTransaction(transactionID: string): OnyxEntry | EmptyObject { return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {}; } @@ -376,7 +377,7 @@ function hasRoute(transaction: Transaction): boolean { * * @deprecated Use Onyx.connect() or withOnyx() instead */ -function getLinkedTransaction(reportAction: OnyxEntry): Transaction | Record { +function getLinkedTransaction(reportAction: OnyxEntry): Transaction | EmptyObject { let transactionID = ''; if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 075371d41a8c..3b6ad97f0e43 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -133,7 +133,7 @@ function isDefaultAvatar(avatarURL?: string | React.FC): boolean { * @param avatarURL - the avatar source from user's personalDetails * @param accountID - the accountID of the user */ -function getAvatar(avatarURL: string | React.FC, accountID: number): React.FC | string { +function getAvatar(avatarURL: AvatarSource, accountID: number): AvatarSource { return isDefaultAvatar(avatarURL) ? getDefaultAvatar(accountID) : avatarURL; } @@ -152,7 +152,7 @@ function getAvatarUrl(avatarURL: string, accountID: number): string { * Avatars uploaded by users will have a _128 appended so that the asset server returns a small version. * This removes that part of the URL so the full version of the image can load. */ -function getFullSizeAvatar(avatarURL: string, accountID: number): React.FC | string { +function getFullSizeAvatar(avatarURL: string, accountID: number): AvatarSource { const source = getAvatar(avatarURL, accountID); if (typeof source !== 'string') { return source; @@ -164,7 +164,7 @@ function getFullSizeAvatar(avatarURL: string, accountID: number): React.FC. This adds the _128 at the end of the * source URL (before the file type) if it doesn't exist there already. */ -function getSmallSizeAvatar(avatarURL: string, accountID: number): React.FC | string { +function getSmallSizeAvatar(avatarURL: string, accountID: number): AvatarSource { const source = getAvatar(avatarURL, accountID); if (typeof source !== 'string') { return source; diff --git a/src/types/utils/EmptyObject.ts b/src/types/utils/EmptyObject.ts index 7f72ecb631d2..9b9f3244a5f8 100644 --- a/src/types/utils/EmptyObject.ts +++ b/src/types/utils/EmptyObject.ts @@ -3,7 +3,7 @@ import Falsy from './Falsy'; type EmptyObject = Record; // eslint-disable-next-line rulesdir/no-negated-variables -function isNotEmptyObject | Falsy>(arg: T | EmptyObject): arg is T { +function isNotEmptyObject | Falsy>(arg: T | EmptyObject): arg is NonNullable { return Object.keys(arg ?? {}).length > 0; } From af54ad456aed2375b9d243adb66f0ede56cf2132 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 13 Nov 2023 14:55:52 +0100 Subject: [PATCH 46/63] fix: resolve comments --- src/libs/ReportUtils.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 770aaf6627a1..6918917e46a6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1510,8 +1510,7 @@ function requiresAttentionFromCurrentUser(option: OnyxEntry | OptionData return false; } - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (('isUnreadWithMention' in option && option.isUnreadWithMention) || isUnreadWithMention(option)) { + if (Boolean('isUnreadWithMention' in option && option.isUnreadWithMention) || isUnreadWithMention(option)) { return true; } @@ -1536,7 +1535,7 @@ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolea return allTransactions.filter((transaction) => transaction.reimbursable === false).length > 0; } -function getMoneyRequestReimbursableTotal(report: OnyxEntry, allReportsDict?: OnyxCollection): number { +function getMoneyRequestReimbursableTotal(report: OnyxEntry, allReportsDict: OnyxCollection = null): number { const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport: OnyxEntry | undefined; if (isMoneyRequestReport(report)) { From 23cf6e7ed103ebc38effe9239f9811335e07e1fc Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 14 Nov 2023 10:01:54 +0100 Subject: [PATCH 47/63] fix: resolve comments --- src/libs/ReportUtils.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6918917e46a6..2d0d30b88dd9 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -402,7 +402,7 @@ function getChatType(report: OnyxEntry): ValueOf | EmptyObject { +function getPolicy(policyID: string): Policy | EmptyObject { if (!allPolicies || !policyID) { return {}; } @@ -981,7 +981,7 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: const isActionOwner = reportAction?.actorAccountID === currentUserAccountID; - if (ReportActionsUtils.isMoneyRequestAction(reportAction) && reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { + if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { // For now, users cannot delete split actions const isSplitAction = reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; @@ -3122,7 +3122,7 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string): Op function buildOptimisticTaskReport( ownerAccountID: number, - assigneeAccountID?: number, + assigneeAccountID = 0, parentReportID?: string, title?: string, description?: string, @@ -3906,7 +3906,8 @@ function getTaskAssigneeChatOnyxData( // If you're choosing to share the task in the same DM as the assignee then we don't need to create another reportAction indicating that you've been assigned if (assigneeChatReportID !== parentReportID) { - const displayname = allPersonalDetails?.[assigneeAccountID]?.displayName ?? allPersonalDetails?.[assigneeAccountID]?.login ?? ''; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const displayname = allPersonalDetails?.[assigneeAccountID]?.displayName || allPersonalDetails?.[assigneeAccountID]?.login || ''; optimisticAssigneeAddComment = buildOptimisticTaskCommentReportAction(taskReportID, title, assigneeAccountID, `assigned to ${displayname}`, parentReportID); const lastAssigneeCommentText = formatReportLastMessageText(optimisticAssigneeAddComment.reportAction.message?.[0].text ?? ''); const optimisticAssigneeReport = { From 340061d875e82eab7343e47e8e6f0c2bddba2402 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 14 Nov 2023 10:31:39 +0100 Subject: [PATCH 48/63] fix: resolve comments --- src/libs/ReportUtils.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 918ffbd481d7..86ba1fb735aa 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -544,8 +544,8 @@ function isSettled(reportID: string | undefined): boolean { if (!allReports) { return false; } - const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - if (report?.isWaitingOnBankAccount) { + const report: Report | EmptyObject = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; + if (isEmptyObject(report) || report?.isWaitingOnBankAccount) { return false; } @@ -1756,8 +1756,7 @@ function canEditReportAction(reportAction: OnyxEntry): boolean { reportAction?.actorAccountID === currentUserAccountID && isCommentOrIOU && canEditMoneyRequest(reportAction) && // Returns true for non-IOU actions - reportAction?.message && - !isReportMessageAttachment(reportAction.message[0] ?? {}) && + !isReportMessageAttachment(reportAction?.message?.[0] ?? {type: '', text: ''}) && !ReportActionsUtils.isDeletedAction(reportAction) && !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && reportAction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, @@ -1836,7 +1835,7 @@ function getTransactionReportName(reportAction: OnyxEntry): string */ function getReportPreviewMessage( report: OnyxEntry, - reportAction?: OnyxEntry, + reportAction: OnyxEntry | EmptyObject = {}, shouldConsiderReceiptBeingScanned = false, isPreviewMessageForParentChatReport = false, ): string { @@ -1847,7 +1846,7 @@ function getReportPreviewMessage( return reportActionMessage; } - if (!isIOUReport(report) && reportAction && ReportActionsUtils.isSplitBillAction(reportAction)) { + if (isNotEmptyObject(reportAction) && !isIOUReport(report) && reportAction && ReportActionsUtils.isSplitBillAction(reportAction)) { // This covers group chats where the last action is a split bill action const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); if (isEmptyObject(linkedTransaction)) { @@ -1873,7 +1872,7 @@ function getReportPreviewMessage( return `approved ${formattedAmount}`; } - if (shouldConsiderReceiptBeingScanned && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { + if (isNotEmptyObject(reportAction) && shouldConsiderReceiptBeingScanned && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); if (isNotEmptyObject(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { From 8b3d147098de85c3d417ea132f31da84c6a78b9b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 14 Nov 2023 11:00:35 +0100 Subject: [PATCH 49/63] fix: add ts expect error to file which is using not migrated lib yet --- src/libs/GroupChatUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/GroupChatUtils.ts b/src/libs/GroupChatUtils.ts index dcb2b13f092c..db64f6574824 100644 --- a/src/libs/GroupChatUtils.ts +++ b/src/libs/GroupChatUtils.ts @@ -13,10 +13,11 @@ Onyx.connect({ /** * Returns the report name if the report is a group chat */ -function getGroupChatName(report: Report): string { +function getGroupChatName(report: Report): string | undefined { const participants = report.participantAccountIDs ?? []; const isMultipleParticipantReport = participants.length > 1; const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, allPersonalDetails ?? {}); + // @ts-expect-error Error will gone when OptionsListUtils will be migrated to Typescript const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipantReport); return ReportUtils.getDisplayNamesStringFromTooltips(displayNamesWithTooltips); } From fc89e18262e85577910aa8f3d45274ed043383ee Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 14 Nov 2023 11:03:41 +0100 Subject: [PATCH 50/63] fix: resolved comment --- src/libs/Permissions.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 71d51a438513..947d687d8c3d 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -1,9 +1,8 @@ -import {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import Beta from '@src/types/onyx/Beta'; -function canUseAllBetas(betas: OnyxEntry): boolean { - return !!betas?.includes(CONST.BETAS.ALL); +function canUseAllBetas(betas: Beta[]): boolean { + return betas.includes(CONST.BETAS.ALL); } function canUseChronos(betas: Beta[]): boolean { From 9e51ef0b6f33dcdc624af058073098e96af96da6 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 14 Nov 2023 11:37:58 +0100 Subject: [PATCH 51/63] fix: removed unecessary check --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 86ba1fb735aa..b0c15accc1f0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1707,7 +1707,7 @@ function canEditMoneyRequest(reportAction: OnyxEntry): boolean { const moneyRequestReport = getReport(String(moneyRequestReportID)); const isReportSettled = isSettled(moneyRequestReport?.reportID); - const isAdmin = ((isNotEmptyObject(moneyRequestReport) && isExpenseReport(moneyRequestReport) && getPolicy(moneyRequestReport?.policyID ?? '')?.role) ?? '') === CONST.POLICY.ROLE.ADMIN; + const isAdmin = isExpenseReport(moneyRequestReport) && (getPolicy(moneyRequestReport?.policyID ?? '')?.role ?? '') === CONST.POLICY.ROLE.ADMIN; const isRequestor = currentUserAccountID === reportAction?.actorAccountID; if (isAdmin) { From c83e4d06fe5b08886b6c8ce62a6a0902b92aa3ba Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 14 Nov 2023 11:42:19 +0100 Subject: [PATCH 52/63] fix: correct type in Permissions lib --- src/libs/Permissions.ts | 45 +++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 947d687d8c3d..7d54c2eec289 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -1,28 +1,29 @@ +import {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import Beta from '@src/types/onyx/Beta'; -function canUseAllBetas(betas: Beta[]): boolean { - return betas.includes(CONST.BETAS.ALL); +function canUseAllBetas(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.ALL); } -function canUseChronos(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.CHRONOS_IN_CASH) || canUseAllBetas(betas); +function canUseChronos(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.CHRONOS_IN_CASH) || canUseAllBetas(betas); } -function canUsePayWithExpensify(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.PAY_WITH_EXPENSIFY) || canUseAllBetas(betas); +function canUsePayWithExpensify(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.PAY_WITH_EXPENSIFY) || canUseAllBetas(betas); } -function canUseDefaultRooms(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas(betas); +function canUseDefaultRooms(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas(betas); } -function canUseWallet(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.BETA_EXPENSIFY_WALLET) || canUseAllBetas(betas); +function canUseWallet(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.BETA_EXPENSIFY_WALLET) || canUseAllBetas(betas); } -function canUseCommentLinking(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.BETA_COMMENT_LINKING) || canUseAllBetas(betas); +function canUseCommentLinking(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.BETA_COMMENT_LINKING) || canUseAllBetas(betas); } /** @@ -30,24 +31,24 @@ function canUseCommentLinking(betas: Beta[]): boolean { * since contributors have been reporting a number of false issues related to the feature being under development. * See https://expensify.slack.com/archives/C01GTK53T8Q/p1641921996319400?thread_ts=1641598356.166900&cid=C01GTK53T8Q */ -function canUsePolicyRooms(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.POLICY_ROOMS) || canUseAllBetas(betas); +function canUsePolicyRooms(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.POLICY_ROOMS) || canUseAllBetas(betas); } -function canUseTasks(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.TASKS) || canUseAllBetas(betas); +function canUseTasks(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.TASKS) || canUseAllBetas(betas); } -function canUseCustomStatus(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.CUSTOM_STATUS) || canUseAllBetas(betas); +function canUseCustomStatus(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.CUSTOM_STATUS) || canUseAllBetas(betas); } -function canUseTags(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.NEW_DOT_TAGS) || canUseAllBetas(betas); +function canUseTags(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.NEW_DOT_TAGS) || canUseAllBetas(betas); } -function canUseViolations(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas); +function canUseViolations(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas); } /** From 306a8711d2ba5641ed9fe49a57565e7e07f15d1e Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 15 Nov 2023 12:56:04 +0100 Subject: [PATCH 53/63] fix: adjusted types --- src/libs/PolicyUtils.ts | 5 +++-- src/types/onyx/ReportAction.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 62640a11311a..40d518e3511c 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -2,7 +2,8 @@ import Str from 'expensify-common/lib/str'; 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 {PersonalDetails, Policy, PolicyMembers, PolicyTag, PolicyTags} from '@src/types/onyx'; +import {EmptyObject} from '@src/types/utils/EmptyObject'; type MemberEmailsToAccountIDs = Record; type PersonalDetailsList = Record; @@ -157,7 +158,7 @@ function getIneligibleInvitees(policyMembers: OnyxEntry, personal /** * Gets the tag from policy tags, defaults to the first if no key is provided. */ -function getTag(policyTags: OnyxEntry, tagKey?: keyof typeof policyTags) { +function getTag(policyTags: OnyxEntry, tagKey?: keyof typeof policyTags): PolicyTag | undefined | EmptyObject { if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 25c065522288..891a0ffcb7b8 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -137,6 +137,7 @@ type ReportActionBase = { isAttachment?: boolean; childRecentReceiptTransactionIDs?: Record; + reportID?: string; }; type ReportAction = ReportActionBase & OriginalMessage; From aa7a703eaf19a8507a9650190aa56ac22991db4c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 15 Nov 2023 13:03:02 +0100 Subject: [PATCH 54/63] fix: resolve type issue --- src/types/onyx/Report.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 54633a7889b7..81a92c4bf603 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -41,13 +41,13 @@ type Report = { lastReadSequenceNumber?: number; /** The time of the last mention of the report */ - lastMentionedTime?: string; + lastMentionedTime?: string | null; /** The current user's notification preference for this report */ notificationPreference?: string | number; /** The policy name to use */ - policyName?: string; + policyName?: string | null; /** The policy name to use for an archived report */ oldPolicyName?: string; @@ -67,9 +67,6 @@ type Report = { /** Linked policy's ID */ policyID?: string; - /** Linked policy's name */ - policyName?: string | null; - /** Name of the report */ reportName?: string; @@ -95,7 +92,7 @@ type Report = { type?: string; /** The report visibility */ - visibility?: string; + visibility?: ValueOf; /** Report cached total */ cachedTotal?: string; @@ -115,9 +112,6 @@ type Report = { participantAccountIDs?: number[]; total?: number; currency?: string; - isDeletedParentAction?: boolean; - visibility?: ValueOf; - lastMentionedTime?: string | null; parentReportActionIDs?: number[]; errorFields?: OnyxCommon.ErrorFields; @@ -141,13 +135,9 @@ type Report = { /** If the report contains nonreimbursable expenses, send the nonreimbursable total */ nonReimbursableTotal?: number; - cachedTotal?: string; - chatReportID?: string; - state?: ValueOf; isHidden?: boolean; isChatRoom?: boolean; participantsList?: Array>; - description?: string; text?: string; }; From eba55d11656a25d3c8e0618b69575c190340e7e7 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 15 Nov 2023 17:05:14 +0100 Subject: [PATCH 55/63] fix: resolve comments --- src/libs/IOUUtils.ts | 2 +- src/libs/PolicyUtils.ts | 4 ++-- src/libs/ReportActionsUtils.ts | 5 +++-- src/libs/ReportUtils.ts | 18 +++++++++--------- src/types/onyx/PersonalDetails.ts | 3 ++- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 543261f7561c..afbbcc2684a0 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -42,7 +42,7 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu } // Make a copy so we don't mutate the original object - const iouReportUpdate: OnyxEntry = {...iouReport}; + const iouReportUpdate: Report = {...iouReport}; if (iouReportUpdate.total) { if (actorAccountID === iouReport.ownerAccountID) { diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 40d518e3511c..19129959d016 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -3,7 +3,7 @@ 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'; -import {EmptyObject} from '@src/types/utils/EmptyObject'; +import {EmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; type MemberEmailsToAccountIDs = Record; type PersonalDetailsList = Record; @@ -159,7 +159,7 @@ function getIneligibleInvitees(policyMembers: OnyxEntry, personal * Gets the tag from policy tags, defaults to the first if no key is provided. */ function getTag(policyTags: OnyxEntry, tagKey?: keyof typeof policyTags): PolicyTag | undefined | EmptyObject { - if (Object.keys(policyTags ?? {})?.length === 0) { + if (isEmptyObject(policyTags)) { return {}; } diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8809c91ae20b..d539e8e44e03 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -8,6 +8,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import {ActionName} from '@src/types/onyx/OriginalMessage'; import Report from '@src/types/onyx/Report'; import ReportAction, {ReportActions} from '@src/types/onyx/ReportAction'; +import {EmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CollectionUtils from './CollectionUtils'; import * as Environment from './Environment/Environment'; import isReportMessageAttachment from './isReportMessageAttachment'; @@ -72,8 +73,8 @@ function isReversedTransaction(reportAction: OnyxEntry) { return (reportAction?.message?.[0].isReversedTransaction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0; } -function isPendingRemove(reportAction: OnyxEntry | Record): boolean { - if (!reportAction) { +function isPendingRemove(reportAction: OnyxEntry | EmptyObject): boolean { + if (isEmptyObject(reportAction)) { return false; } return reportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5e9bc4a1f17d..78d8fbca3bfb 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -483,12 +483,12 @@ function isTaskReport(report: OnyxEntry): boolean { * There's another situation where you don't have access to the parentReportAction (because it was created in a chat you don't have access to) * In this case, we have added the key to the report itself */ -function isCanceledTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry | EmptyObject): boolean { - if (Object.keys(parentReportAction ?? {}).length > 0 && (parentReportAction?.message?.[0]?.isDeletedParentAction ?? false)) { +function isCanceledTaskReport(report: OnyxEntry | EmptyObject = {}, parentReportAction: OnyxEntry | EmptyObject = {}): boolean { + if (isNotEmptyObject(parentReportAction) && (parentReportAction?.message?.[0]?.isDeletedParentAction ?? false)) { return true; } - if (Object.keys(report ?? {}).length > 0 && report?.isDeletedParentAction) { + if (isNotEmptyObject(report) && report?.isDeletedParentAction) { return true; } @@ -500,7 +500,7 @@ function isCanceledTaskReport(report: OnyxEntry, parentReportAction?: On * * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ -function isOpenTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry | EmptyObject): boolean { +function isOpenTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry | EmptyObject = {}): boolean { return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; } @@ -515,7 +515,7 @@ function isCompletedTaskReport(report: OnyxEntry): boolean { * Checks if the current user is the manager of the supplied report */ function isReportManager(report: OnyxEntry): boolean { - return report?.managerID === currentUserAccountID; + return Boolean(report && report.managerID === currentUserAccountID); } /** @@ -547,13 +547,13 @@ function isSettled(reportID: string | undefined): boolean { return false; } const report: Report | EmptyObject = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; - if (isEmptyObject(report) || report?.isWaitingOnBankAccount) { + if (isEmptyObject(report) || report.isWaitingOnBankAccount) { return false; } // In case the payment is scheduled and we are waiting for the payee to set up their wallet, // consider the report as paid as well. - if (report?.isWaitingOnBankAccount && report?.statusNum === CONST.REPORT.STATUS.APPROVED) { + if (report.isWaitingOnBankAccount && report.statusNum === CONST.REPORT.STATUS.APPROVED) { return true; } @@ -568,7 +568,7 @@ function isCurrentUserSubmitter(reportID: string): boolean { return false; } const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - return report?.ownerAccountID === currentUserAccountID; + return Boolean(report && report.ownerAccountID === currentUserAccountID); } /** @@ -701,7 +701,7 @@ function isDM(report: OnyxEntry): boolean { * Only returns true if this is our main 1:1 DM report with Concierge */ function isConciergeChatReport(report: OnyxEntry): boolean { - return report?.participantAccountIDs?.length === 1 && Number(report?.participantAccountIDs?.[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); + return report?.participantAccountIDs?.length === 1 && Number(report.participantAccountIDs?.[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); } /** diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 18cd679b7f77..16ce2ad98fee 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -1,4 +1,5 @@ import {SvgProps} from 'react-native-svg'; +import {AvatarSource} from '@libs/UserUtils'; import TIMEZONES from '@src/TIMEZONES'; import * as OnyxCommon from './OnyxCommon'; @@ -32,7 +33,7 @@ type PersonalDetails = { phoneNumber?: string; /** Avatar URL of the current user from their personal details */ - avatar: string | React.FC; + avatar: AvatarSource; /** Avatar thumbnail URL of the current user from their personal details */ avatarThumbnail?: string; From b97854f2d55720cde4b061a0301dc57c5bdbacf9 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 16 Nov 2023 08:59:55 +0100 Subject: [PATCH 56/63] fix: types issues --- src/libs/ReportUtils.ts | 8 +++----- src/types/onyx/OnyxCommon.ts | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0dab9605dc85..ae8992016467 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -500,7 +500,7 @@ function isCanceledTaskReport(report: OnyxEntry | EmptyObject = {}, pare * * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ -function isOpenTaskReport(report: OnyxEntry, parentReportAction?: OnyxEntry | EmptyObject = {}): boolean { +function isOpenTaskReport(report: OnyxEntry, parentReportAction: OnyxEntry | EmptyObject = {}): boolean { return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; } @@ -4062,13 +4062,11 @@ function getRoom(type: ValueOf, policyID: string) const room = Object.values(allReports ?? {}).find((report) => report?.policyID === policyID && report?.chatType === type && !isThread(report)); return room; } + /** * We only want policy owners and admins to be able to modify the welcome message, but not in thread chat. - * @param {Object} report - * @param {Object} policy - * @return {Boolean} */ -function shouldDisableWelcomeMessage(report, policy) { +function shouldDisableWelcomeMessage(report: OnyxEntry, policy: OnyxEntry) { return isMoneyRequestReport(report) || isArchivedRoom(report) || !isChatRoom(report) || isChatThread(report) || !PolicyUtils.isPolicyAdmin(policy); } diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index ac69baed3ef1..49d3428be532 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -21,7 +21,7 @@ type Icon = { name: string; /** Avatar id */ - id: number | string; + id?: number | string; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ fallbackIcon?: AvatarSource; From fc8f676914a1265f78cee8660318d61b65322866 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 16 Nov 2023 14:52:44 +0100 Subject: [PATCH 57/63] fix: lint --- src/types/onyx/PersonalDetails.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 16ce2ad98fee..3b307fcdfbb1 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -1,4 +1,3 @@ -import {SvgProps} from 'react-native-svg'; import {AvatarSource} from '@libs/UserUtils'; import TIMEZONES from '@src/TIMEZONES'; import * as OnyxCommon from './OnyxCommon'; From a43fd7f04c4e770ad278537e900b41265ce04331 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 17 Nov 2023 12:28:39 +0100 Subject: [PATCH 58/63] fix: resolve comment --- src/libs/ReportUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 99c40228adb2..8245ed4b2cb9 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4123,7 +4123,7 @@ function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report); } -function getRoom(type: ValueOf, policyID: string) { +function getRoom(type: ValueOf, policyID: string): OnyxEntry | undefined { const room = Object.values(allReports ?? {}).find((report) => report?.policyID === policyID && report?.chatType === type && !isThread(report)); return room; } @@ -4131,7 +4131,7 @@ function getRoom(type: ValueOf, policyID: string) /** * We only want policy owners and admins to be able to modify the welcome message, but not in thread chat. */ -function shouldDisableWelcomeMessage(report: OnyxEntry, policy: OnyxEntry) { +function shouldDisableWelcomeMessage(report: OnyxEntry, policy: OnyxEntry): boolean { return isMoneyRequestReport(report) || isArchivedRoom(report) || !isChatRoom(report) || isChatThread(report) || !PolicyUtils.isPolicyAdmin(policy); } From e7e84984d3a6f0998640cd4e35a3a2546295b738 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 22 Nov 2023 08:44:14 +0100 Subject: [PATCH 59/63] fix: type issues --- src/libs/ReportUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 98f5f8453a8f..d724bd3a7868 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1633,6 +1633,7 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry

) { */ function goBackToDetailsPage(report: OnyxEntry) { if (isOneOnOneChat(report)) { - Navigation.goBack(ROUTES.PROFILE.getRoute(report.participantAccountIDs[0])); + Navigation.goBack(ROUTES.PROFILE.getRoute(report?.participantAccountIDs?.[0] ?? '')); return; } - Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID)); + Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report?.reportID ?? '')); } /** From f24294e04155b99913a77f9044f8b1416d59a43e Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 23 Nov 2023 14:36:30 +0100 Subject: [PATCH 60/63] fix: avatar and footer problem when archived room --- src/libs/ReportUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 609ce23ce7a6..7278793d5c92 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -441,7 +441,7 @@ function getPolicyType(report: OnyxEntry, policies: OnyxCollection | undefined | EmptyObject, returnEmptyIfNotFound = false, policy: OnyxEntry = null): string { +function getPolicyName(report: OnyxEntry | undefined | EmptyObject, returnEmptyIfNotFound = false, policy: OnyxEntry | undefined = undefined): string { const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); if (isEmptyObject(report)) { return noPolicyFound; @@ -454,7 +454,8 @@ function getPolicyName(report: OnyxEntry | undefined | EmptyObject, retu // Public rooms send back the policy name with the reportSummary, // since they can also be accessed by people who aren't in the workspace - const policyName = finalPolicy?.name ?? report?.policyName ?? report?.oldPolicyName ?? noPolicyFound; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const policyName = finalPolicy?.name || report?.policyName || report?.oldPolicyName || noPolicyFound; return policyName; } From 09a3cb3cad0635891fafc4e2435db1d52e10e5dd Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 23 Nov 2023 15:07:12 +0100 Subject: [PATCH 61/63] fix: problem with displaying report name --- src/libs/ReportUtils.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7278793d5c92..44dfd202b750 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1375,9 +1375,9 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f } const personalDetails = getPersonalDetailsForAccountID(accountID); + // console.log(personalDetails); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetails.login || ''); - // This is to check if account is an invite/optimistically created one // and prevent from falling back to 'Hidden', so a correct value is shown // when searching for a new user @@ -1385,9 +1385,10 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return formattedLogin; } - const longName = personalDetails.displayName ?? formattedLogin; + const longName = personalDetails.displayName ? personalDetails.displayName : formattedLogin; + + const shortName = personalDetails.firstName ? personalDetails.firstName : longName; - const shortName = personalDetails.firstName ?? longName; if (!longName && shouldFallbackToHidden) { return Localize.translateLocal('common.hidden'); } @@ -2220,6 +2221,9 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu const participantAccountIDs = report?.participantAccountIDs ?? []; const participantsWithoutCurrentUser = participantAccountIDs.filter((accountID) => accountID !== currentUserAccountID); const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1; + if (report?.reportID === '7379653634604316') { + console.log(participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport)).join(', ')); + } return participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport)).join(', '); } From 48c8598c5a98624439398aa5a42d0e261e7bdab2 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 23 Nov 2023 20:51:29 +0100 Subject: [PATCH 62/63] fix: resolved comment --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4752ee1aed72..7ad9e0ee17d4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1871,7 +1871,7 @@ function getTransactionReportName(reportAction: OnyxEntry): string const transactionDetails = getTransactionDetails(transaction); return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) ? 'iou.threadSentMoneyReportName' : 'iou.threadRequestReportName', { - formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? '', TransactionUtils.isDistanceRequest(transaction)) ?? '', + formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency, TransactionUtils.isDistanceRequest(transaction)) ?? '', comment: transactionDetails?.comment ?? '', }); } From 7ff0f22c01b02c787db3682fe5e8e59d13fdfe9e Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 27 Nov 2023 09:20:35 +0100 Subject: [PATCH 63/63] fix: resolve comments --- src/libs/ReportUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 72e85184753e..9ebd2ff0a089 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1080,7 +1080,7 @@ function chatIncludesConcierge(report: OnyxEntry): boolean { * Returns true if there is any automated expensify account `in accountIDs */ function hasAutomatedExpensifyAccountIDs(accountIDs: number[]): boolean { - return accountIDs.filter((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)).length > 0; + return accountIDs.some((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)); } function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAccountID: number): number[] { @@ -1701,10 +1701,10 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< */ function getTransactionDetails(transaction: OnyxEntry, createdDateFormat: string = CONST.DATE.FNS_FORMAT_STRING): TransactionDetails { - const report = getReport(transaction?.reportID); if (!transaction) { return; } + const report = getReport(transaction?.reportID); return { created: TransactionUtils.getCreated(transaction, createdDateFormat), amount: TransactionUtils.getAmount(transaction, isNotEmptyObject(report) && isExpenseReport(report)), @@ -4136,7 +4136,7 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) } function isReportDraft(report: OnyxEntry): boolean { - return isExpenseReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report.statusNum === CONST.REPORT.STATUS.OPEN; + return isExpenseReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; } /**