From 4187fbb6e6bc3abc041a45aa7e9102e9af85e31c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 3 Oct 2023 13:44:48 +0200 Subject: [PATCH 001/329] 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 002/329] 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 003/329] 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 004/329] 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 005/329] 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 006/329] 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 007/329] 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 008/329] 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 15e82588b5e7085fcaa8ad1072c6179fdc4319ca Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 20 Oct 2023 16:05:37 +0200 Subject: [PATCH 009/329] WIP TS migration --- .../{RadioButton.js => RadioButton.tsx} | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) rename src/components/{RadioButton.js => RadioButton.tsx} (61%) diff --git a/src/components/RadioButton.js b/src/components/RadioButton.tsx similarity index 61% rename from src/components/RadioButton.js rename to src/components/RadioButton.tsx index 726afd609588..1819975bb1cf 100644 --- a/src/components/RadioButton.js +++ b/src/components/RadioButton.tsx @@ -29,17 +29,34 @@ const defaultProps = { disabled: false, }; -function RadioButton(props) { +type RadioButtonProps = { + /** Whether radioButton is checked */ + isChecked: boolean; + + /** A function that is called when the box/label is pressed */ + onPress: () => void; + + /** Specifies the accessibility label for the radio button */ + accessibilityLabel: string; + + /** Should the input be styled for errors */ + hasError: boolean; + + /** Should the input be disabled */ + disabled: boolean; +} + +function RadioButton({accessibilityLabel, disabled = false, hasError = false, isChecked, onPress}: RadioButtonProps) { return ( - + Date: Mon, 23 Oct 2023 09:55:34 +0700 Subject: [PATCH 010/329] fix: 30045 --- src/components/AttachmentModal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 61b138747950..abede4e5cea1 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -394,7 +394,7 @@ function AttachmentModal(props) { { From 57b42e844fbad216690bf2ed497479e8e8be492a Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 23 Oct 2023 12:12:49 +0200 Subject: [PATCH 011/329] 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 012/329] 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 013/329] 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 014/329] 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 015/329] 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 016/329] 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 017/329] 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 018/329] 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 595cd5d302a5b601e50b8ed72f5aa0e86d3ccf82 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 26 Oct 2023 15:06:35 +0200 Subject: [PATCH 019/329] draft button implementation --- src/pages/ShareCodePage.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/pages/ShareCodePage.js b/src/pages/ShareCodePage.js index d6a6b79d3273..b7c1f59a1803 100644 --- a/src/pages/ShareCodePage.js +++ b/src/pages/ShareCodePage.js @@ -97,6 +97,15 @@ class ShareCodePage extends React.Component { + Navigation.navigate()} + /> + Date: Thu, 26 Oct 2023 17:26:23 +0200 Subject: [PATCH 020/329] 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 021/329] 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 022/329] 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 023/329] 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 024/329] 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 f911a33342bf5adae3581db049bed6c2970fd50a Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 27 Oct 2023 14:38:44 +0200 Subject: [PATCH 025/329] Auto map panning when location rights granted --- src/CONST.ts | 4 + src/ONYXKEYS.ts | 4 + src/components/MapView/MapView.web.tsx | 144 ++++++++++++++++++------- src/types/onyx/UserLocation.ts | 3 + src/types/onyx/index.ts | 2 + 5 files changed, 119 insertions(+), 38 deletions(-) create mode 100644 src/types/onyx/UserLocation.ts diff --git a/src/CONST.ts b/src/CONST.ts index 9d912a4df20e..08da9f601468 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -875,6 +875,10 @@ const CONST = { YOUR_LOCATION_TEXT: 'Your Location', + // Value indicating the maximum age in milliseconds + // of a possible cached position that is acceptable to return + MAXIMUM_AGE_OF_CACHED_USER_LOCATION: 10 * 60 * 1000, + ATTACHMENT_MESSAGE_TEXT: '[Attachment]', // This is a placeholder for attachment which is uploading ATTACHMENT_UPLOADING_MESSAGE_HTML: 'Uploading attachment...', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 646e23c8af1e..8378f62b6bba 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -84,6 +84,9 @@ const ONYXKEYS = { /** Contains all the users settings for the Settings page and sub pages */ USER: 'user', + /** Contains latitude and longitude of user's last known location */ + USER_LOCATION: 'userLocation', + /** Contains metadata (partner, login, validation date) for all of the user's logins */ LOGIN_LIST: 'loginList', @@ -333,6 +336,7 @@ type OnyxValues = { [ONYXKEYS.COUNTRY_CODE]: number; [ONYXKEYS.COUNTRY]: string; [ONYXKEYS.USER]: OnyxTypes.User; + [ONYXKEYS.USER_LOCATION]: OnyxTypes.UserLocation; [ONYXKEYS.LOGIN_LIST]: OnyxTypes.Login; [ONYXKEYS.SESSION]: OnyxTypes.Session; [ONYXKEYS.BETAS]: OnyxTypes.Beta[]; diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index 78c5a9175594..b433099ca6e4 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -5,25 +5,81 @@ import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useState} from 'react'; import {View} from 'react-native'; +import {useFocusEffect} from '@react-navigation/native'; import Map, {MapRef, Marker} from 'react-map-gl'; import mapboxgl from 'mapbox-gl'; - +import Onyx, { OnyxEntry, withOnyx } from 'react-native-onyx'; import responder from './responder'; import utils from './utils'; - import CONST from '../../CONST'; +import ONYXKEYS from '../../ONYXKEYS'; +import styles from '../../styles/styles'; +import * as OnyxTypes from '../../types/onyx'; import * as StyleUtils from '../../styles/StyleUtils'; import themeColors from '../../styles/themes/default'; import Direction from './Direction'; import {MapViewHandle, MapViewProps} from './MapViewTypes'; - +import getCurrentPosition from '../../libs/getCurrentPosition'; +import Text from '../../components/Text' import 'mapbox-gl/dist/mapbox-gl.css'; -const MapView = forwardRef( - ({style, styleURL, waypoints, mapPadding, accessToken, directionCoordinates, initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}}, ref) => { +type MapViewOnyxProps = { + userLocation: OnyxEntry; +} + +type ComponentProps = MapViewProps & MapViewOnyxProps + +const MapView = forwardRef( + ({style, styleURL, waypoints, mapPadding, accessToken, userLocation: cachedUserLocation, directionCoordinates, initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}}, ref) => { const [mapRef, setMapRef] = useState(null); + const [userLocation, setUserLocation] = useState(cachedUserLocation); + const [isCurrentPositionFetching, setIsCurrentPositionFetching] = useState(true); + const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); const setRef = useCallback((newRef: MapRef | null) => setMapRef(newRef), []); + useFocusEffect( + useCallback(() => { + console.log('Start location search') + getCurrentPosition((params) => { + setUserLocation({latitude: params.coords.latitude, longitude: params.coords.longitude}) + Onyx.merge(ONYXKEYS.USER_LOCATION, { latitude: params.coords.latitude, longitude: params.coords.longitude}) + setIsCurrentPositionFetching(false); + console.log('Location search success') + }, + () => { + // On error do nothing - an already cached location + // or the default location will be presented to the user + console.log('Location search error') + setIsCurrentPositionFetching(false); + }) + }, []) + ) + + useEffect(() => { + if (!userLocation || !mapRef) { + return; + } + + console.log('Map loaded') + + if (waypoints && waypoints.length > 0) { + console.log('Waypoints existing. Dont jump') + return; + } + + if (userInteractedWithMap) { + console.log('User already started clicking or dragging through the map. Dont jump') + return; + } + + console.log('No waypoints added. JumpTo') + + mapRef.jumpTo({ + center: [userLocation.longitude, userLocation.latitude], + zoom: CONST.MAPBOX.DEFAULT_ZOOM, + }); + }, [userLocation, userInteractedWithMap, mapRef]) + useEffect(() => { if (!waypoints || waypoints.length === 0) { return; @@ -36,7 +92,7 @@ const MapView = forwardRef( if (waypoints.length === 1) { mapRef.flyTo({ center: waypoints[0].coordinate, - zoom: 15, + zoom: CONST.MAPBOX.DEFAULT_ZOOM, }); return; } @@ -80,40 +136,52 @@ const MapView = forwardRef( ); return ( - - + + setUserInteractedWithMap(true)} + ref={setRef} + mapLib={mapboxgl} + mapboxAccessToken={accessToken} + initialViewState={{ + longitude: initialState.location[0], + latitude: initialState.location[1], + zoom: initialState.zoom, + }} + style={StyleUtils.getTextColorStyle(themeColors.mapAttributionText) as React.CSSProperties} + mapStyle={styleURL} + > + {waypoints?.map(({coordinate, markerComponent, id}) => { + const MarkerComponent = markerComponent; + return ( + + + + ); + })} + {directionCoordinates && } + + + - {waypoints?.map(({coordinate, markerComponent, id}) => { - const MarkerComponent = markerComponent; - return ( - - - - ); - })} - {directionCoordinates && } - - + {isCurrentPositionFetching ? 'Finding your location...' : ' '} + + ); }, ); -export default MapView; +export default withOnyx({ + userLocation: { + key: ONYXKEYS.USER_LOCATION, + } +})(MapView) \ No newline at end of file diff --git a/src/types/onyx/UserLocation.ts b/src/types/onyx/UserLocation.ts new file mode 100644 index 000000000000..b22802bfefb1 --- /dev/null +++ b/src/types/onyx/UserLocation.ts @@ -0,0 +1,3 @@ +type UserLocation = Pick; + +export default UserLocation; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index cbe1074f630d..ca679da8c9a9 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -11,6 +11,7 @@ import Task from './Task'; import Currency from './Currency'; import ScreenShareRequest from './ScreenShareRequest'; import User from './User'; +import UserLocation from './UserLocation'; import Login from './Login'; import Session from './Session'; import Beta from './Beta'; @@ -63,6 +64,7 @@ export type { Currency, ScreenShareRequest, User, + UserLocation, Login, Session, Beta, From 0c1af91dc3a492e8ebd3924399fc3bcd8d032b5b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 27 Oct 2023 14:42:11 +0200 Subject: [PATCH 026/329] 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 6b2527666b7cfbc7cd6db3a07865bb7ffe8031c2 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 27 Oct 2023 16:42:42 +0200 Subject: [PATCH 027/329] Code cleanup - helper method; drop console logs --- src/components/MapView/MapView.web.tsx | 40 +++++++++----------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index b433099ca6e4..f39fab19bc7e 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -32,53 +32,42 @@ type ComponentProps = MapViewProps & MapViewOnyxProps const MapView = forwardRef( ({style, styleURL, waypoints, mapPadding, accessToken, userLocation: cachedUserLocation, directionCoordinates, initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}}, ref) => { const [mapRef, setMapRef] = useState(null); - const [userLocation, setUserLocation] = useState(cachedUserLocation); + const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); const [isCurrentPositionFetching, setIsCurrentPositionFetching] = useState(true); const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); const setRef = useCallback((newRef: MapRef | null) => setMapRef(newRef), []); useFocusEffect( useCallback(() => { - console.log('Start location search') getCurrentPosition((params) => { - setUserLocation({latitude: params.coords.latitude, longitude: params.coords.longitude}) + setCurrentPosition({latitude: params.coords.latitude, longitude: params.coords.longitude}) Onyx.merge(ONYXKEYS.USER_LOCATION, { latitude: params.coords.latitude, longitude: params.coords.longitude}) setIsCurrentPositionFetching(false); - console.log('Location search success') }, - () => { - // On error do nothing - an already cached location - // or the default location will be presented to the user - console.log('Location search error') - setIsCurrentPositionFetching(false); - }) + () => { setIsCurrentPositionFetching(false); }) }, []) ) useEffect(() => { - if (!userLocation || !mapRef) { + if (!currentPosition || !mapRef) { return; } - console.log('Map loaded') - - if (waypoints && waypoints.length > 0) { - console.log('Waypoints existing. Dont jump') + if (!shouldPanMapToCurrentPosition()) { return; } - if (userInteractedWithMap) { - console.log('User already started clicking or dragging through the map. Dont jump') - return; - } - - console.log('No waypoints added. JumpTo') - mapRef.jumpTo({ - center: [userLocation.longitude, userLocation.latitude], + center: [currentPosition.longitude, currentPosition.latitude], zoom: CONST.MAPBOX.DEFAULT_ZOOM, }); - }, [userLocation, userInteractedWithMap, mapRef]) + }, [currentPosition, userInteractedWithMap, mapRef]) + + // Determines if map can be panned to user's detected + // location without bothering the user. It will return + // false if user has already started dragging the map or + // if there are one or more waypoints present. + const shouldPanMapToCurrentPosition = () => !userInteractedWithMap && (!waypoints || waypoints.length === 0) useEffect(() => { if (!waypoints || waypoints.length === 0) { @@ -139,7 +128,6 @@ const MapView = forwardRef( <> ( - {isCurrentPositionFetching ? 'Finding your location...' : ' '} + {isCurrentPositionFetching && shouldPanMapToCurrentPosition() ? 'Finding your location...' : ' '} ); From e45f4012afd9db7dd329812dd770a416394cc8d5 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 30 Oct 2023 09:02:55 +0100 Subject: [PATCH 028/329] 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 111921a3ac3ad3dcaa46d5ce4f0c54bdc318449b Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 09:45:29 +0100 Subject: [PATCH 029/329] Drop Location loading indicator --- src/components/MapView/MapView.web.tsx | 71 +++++++++++--------------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index f39fab19bc7e..dd30aab22558 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -13,14 +13,12 @@ import responder from './responder'; import utils from './utils'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; -import styles from '../../styles/styles'; import * as OnyxTypes from '../../types/onyx'; import * as StyleUtils from '../../styles/StyleUtils'; import themeColors from '../../styles/themes/default'; import Direction from './Direction'; import {MapViewHandle, MapViewProps} from './MapViewTypes'; import getCurrentPosition from '../../libs/getCurrentPosition'; -import Text from '../../components/Text' import 'mapbox-gl/dist/mapbox-gl.css'; type MapViewOnyxProps = { @@ -125,45 +123,38 @@ const MapView = forwardRef( ); return ( - <> - + setUserInteractedWithMap(true)} + ref={setRef} + mapLib={mapboxgl} + mapboxAccessToken={accessToken} + initialViewState={{ + longitude: initialState.location[0], + latitude: initialState.location[1], + zoom: initialState.zoom, + }} + style={StyleUtils.getTextColorStyle(themeColors.mapAttributionText) as React.CSSProperties} + mapStyle={styleURL} > - setUserInteractedWithMap(true)} - ref={setRef} - mapLib={mapboxgl} - mapboxAccessToken={accessToken} - initialViewState={{ - longitude: initialState.location[0], - latitude: initialState.location[1], - zoom: initialState.zoom, - }} - style={StyleUtils.getTextColorStyle(themeColors.mapAttributionText) as React.CSSProperties} - mapStyle={styleURL} - > - {waypoints?.map(({coordinate, markerComponent, id}) => { - const MarkerComponent = markerComponent; - return ( - - - - ); - })} - {directionCoordinates && } - - - - {isCurrentPositionFetching && shouldPanMapToCurrentPosition() ? 'Finding your location...' : ' '} - - + {waypoints?.map(({coordinate, markerComponent, id}) => { + const MarkerComponent = markerComponent; + return ( + + + + ); + })} + {directionCoordinates && } + + ); }, ); From c68dc77e017abdd04451b1ea7cd1ce1fca6163bf Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 30 Oct 2023 10:54:10 +0100 Subject: [PATCH 030/329] 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 031/329] 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 4f1563682703656fec5e064f425e34eae1d18ec7 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 11:27:12 +0100 Subject: [PATCH 032/329] Move PendingMapView directly to MapView --- .../distanceMapViewPropTypes.js | 2 +- .../DistanceRequest/DistanceRequestFooter.js | 39 +++----- src/components/MapView/MapView.web.tsx | 93 +++++++++++-------- 3 files changed, 70 insertions(+), 64 deletions(-) diff --git a/src/components/DistanceMapView/distanceMapViewPropTypes.js b/src/components/DistanceMapView/distanceMapViewPropTypes.js index 05068cbc9b34..89aca61510e1 100644 --- a/src/components/DistanceMapView/distanceMapViewPropTypes.js +++ b/src/components/DistanceMapView/distanceMapViewPropTypes.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; const propTypes = { // Public access token to be used to fetch map data from Mapbox. - accessToken: PropTypes.string.isRequired, + accessToken: PropTypes.string, // Style applied to MapView component. Note some of the View Style props are not available on ViewMap style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), diff --git a/src/components/DistanceRequest/DistanceRequestFooter.js b/src/components/DistanceRequest/DistanceRequestFooter.js index eaa02968c388..f7216ae199c2 100644 --- a/src/components/DistanceRequest/DistanceRequestFooter.js +++ b/src/components/DistanceRequest/DistanceRequestFooter.js @@ -8,14 +8,12 @@ import _ from 'underscore'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; import styles from '../../styles/styles'; -import useNetwork from '../../hooks/useNetwork'; import useLocalize from '../../hooks/useLocalize'; import theme from '../../styles/themes/default'; import * as TransactionUtils from '../../libs/TransactionUtils'; import Button from '../Button'; import DistanceMapView from '../DistanceMapView'; import * as Expensicons from '../Icon/Expensicons'; -import PendingMapView from '../MapView/PendingMapView'; import transactionPropTypes from '../transactionPropTypes'; const MAX_WAYPOINTS = 25; @@ -55,7 +53,6 @@ const defaultProps = { transaction: {}, }; function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navigateToWaypointEditPage}) { - const {isOffline} = useNetwork(); const {translate} = useLocalize(); const numberOfWaypoints = _.size(waypoints); @@ -109,28 +106,20 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig /> - {!isOffline && Boolean(mapboxAccessToken.token) ? ( - - ) : ( - - )} + ); diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index dd30aab22558..a689adbe5723 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -8,6 +8,7 @@ import {View} from 'react-native'; import {useFocusEffect} from '@react-navigation/native'; import Map, {MapRef, Marker} from 'react-map-gl'; import mapboxgl from 'mapbox-gl'; +import PendingMapView from '../MapView/PendingMapView'; import Onyx, { OnyxEntry, withOnyx } from 'react-native-onyx'; import responder from './responder'; import utils from './utils'; @@ -19,6 +20,9 @@ import themeColors from '../../styles/themes/default'; import Direction from './Direction'; import {MapViewHandle, MapViewProps} from './MapViewTypes'; import getCurrentPosition from '../../libs/getCurrentPosition'; +import useNetwork from '../../hooks/useNetwork'; +import useLocalize from '../../hooks/useLocalize'; +import styles from '../../styles/styles'; import 'mapbox-gl/dist/mapbox-gl.css'; type MapViewOnyxProps = { @@ -29,20 +33,23 @@ type ComponentProps = MapViewProps & MapViewOnyxProps const MapView = forwardRef( ({style, styleURL, waypoints, mapPadding, accessToken, userLocation: cachedUserLocation, directionCoordinates, initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}}, ref) => { + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + const [mapRef, setMapRef] = useState(null); const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); - const [isCurrentPositionFetching, setIsCurrentPositionFetching] = useState(true); const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); const setRef = useCallback((newRef: MapRef | null) => setMapRef(newRef), []); useFocusEffect( useCallback(() => { getCurrentPosition((params) => { - setCurrentPosition({latitude: params.coords.latitude, longitude: params.coords.longitude}) - Onyx.merge(ONYXKEYS.USER_LOCATION, { latitude: params.coords.latitude, longitude: params.coords.longitude}) - setIsCurrentPositionFetching(false); + setCurrentPosition({longitude: params.coords.longitude, latitude: params.coords.latitude}) + Onyx.merge(ONYXKEYS.USER_LOCATION, { longitude: params.coords.longitude, latitude: params.coords.latitude}) }, - () => { setIsCurrentPositionFetching(false); }) + () => { + setCurrentPosition({ longitude: initialState.location[0], latitude: initialState.location[1] }) + }) }, []) ) @@ -55,7 +62,7 @@ const MapView = forwardRef( return; } - mapRef.jumpTo({ + mapRef.flyTo({ center: [currentPosition.longitude, currentPosition.latitude], zoom: CONST.MAPBOX.DEFAULT_ZOOM, }); @@ -123,38 +130,48 @@ const MapView = forwardRef( ); return ( - - setUserInteractedWithMap(true)} - ref={setRef} - mapLib={mapboxgl} - mapboxAccessToken={accessToken} - initialViewState={{ - longitude: initialState.location[0], - latitude: initialState.location[1], - zoom: initialState.zoom, - }} - style={StyleUtils.getTextColorStyle(themeColors.mapAttributionText) as React.CSSProperties} - mapStyle={styleURL} - > - {waypoints?.map(({coordinate, markerComponent, id}) => { - const MarkerComponent = markerComponent; - return ( - - - - ); - })} - {directionCoordinates && } - - + <> + {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( + + setUserInteractedWithMap(true)} + ref={setRef} + mapLib={mapboxgl} + mapboxAccessToken={accessToken} + initialViewState={{ + longitude: currentPosition?.longitude, + latitude: currentPosition?.latitude, + zoom: initialState.zoom, + }} + style={StyleUtils.getTextColorStyle(themeColors.mapAttributionText) as React.CSSProperties} + mapStyle={styleURL} + > + {waypoints?.map(({coordinate, markerComponent, id}) => { + const MarkerComponent = markerComponent; + return ( + + + + ); + })} + {directionCoordinates && } + + + ) : ( + + )} + ); }, ); From e61b8da071c2486027a55e9ab400b16e61b4c40c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 30 Oct 2023 11:55:16 +0100 Subject: [PATCH 033/329] 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 88dc1a74bab22205067fa533684cdcd4e5aae888 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 12:01:16 +0100 Subject: [PATCH 034/329] Drop duplicate import --- src/types/onyx/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 794f69f943a9..832e1009f9d0 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -1,5 +1,4 @@ import Account from './Account'; -import Task from './Task'; import UserLocation from './UserLocation'; import AccountData from './AccountData'; import BankAccount from './BankAccount'; From b6362d4b33d263e98dced059a9d0f66192fadffa Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 12:05:15 +0100 Subject: [PATCH 035/329] Blank lines --- src/components/MapView/MapView.web.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index 1f43ddc63f58..709ae8a0bfe7 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -2,7 +2,6 @@ // This is why we have separate components for web and native to handle the specific implementations. // For the web version, we use the Mapbox Web library called react-map-gl, while for the native mobile version, // we utilize a different Mapbox library @rnmapbox/maps tailored for mobile development. - import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useState} from 'react'; import {View} from 'react-native'; import {useFocusEffect} from '@react-navigation/native'; @@ -180,4 +179,4 @@ export default withOnyx({ userLocation: { key: ONYXKEYS.USER_LOCATION, } -})(MapView) \ No newline at end of file +})(MapView) From a953f81dea1c738f13d3f2fa93976f13355a6094 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 30 Oct 2023 13:54:27 +0100 Subject: [PATCH 036/329] 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 103eed2e57539f8efba865ecc94a394daf37f974 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 14:10:34 +0100 Subject: [PATCH 037/329] Linter and typecheck --- .../DistanceRequest/DistanceRequestFooter.js | 2 - src/components/MapView/MapView.web.tsx | 90 +++++++++++-------- 2 files changed, 54 insertions(+), 38 deletions(-) diff --git a/src/components/DistanceRequest/DistanceRequestFooter.js b/src/components/DistanceRequest/DistanceRequestFooter.js index 138fcc003401..039cbb807e94 100644 --- a/src/components/DistanceRequest/DistanceRequestFooter.js +++ b/src/components/DistanceRequest/DistanceRequestFooter.js @@ -8,10 +8,8 @@ import _ from 'underscore'; import Button from '@components/Button'; import DistanceMapView from '@components/DistanceMapView'; import * as Expensicons from '@components/Icon/Expensicons'; -import PendingMapView from '@components/MapView/PendingMapView'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import * as TransactionUtils from '@libs/TransactionUtils'; import styles from '@styles/styles'; import theme from '@styles/themes/default'; diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index 709ae8a0bfe7..5ee06b682b02 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -2,37 +2,51 @@ // This is why we have separate components for web and native to handle the specific implementations. // For the web version, we use the Mapbox Web library called react-map-gl, while for the native mobile version, // we utilize a different Mapbox library @rnmapbox/maps tailored for mobile development. -import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useState} from 'react'; -import {View} from 'react-native'; import {useFocusEffect} from '@react-navigation/native'; -import Map, {MapRef, Marker} from 'react-map-gl'; import mapboxgl from 'mapbox-gl'; -import PendingMapView from '../MapView/PendingMapView'; -import Onyx, { OnyxEntry, withOnyx } from 'react-native-onyx'; -import responder from './responder'; -import utils from './utils'; +import 'mapbox-gl/dist/mapbox-gl.css'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useState} from 'react'; +import Map, {MapRef, Marker} from 'react-map-gl'; +import {View} from 'react-native'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import * as StyleUtils from '@styles/StyleUtils'; +import themeColors from '@styles/themes/default'; +import setUserLocation from '@userActions/UserLocation'; import CONST from '@src/CONST'; +import useLocalize from '@src/hooks/useLocalize'; +import useNetwork from '@src/hooks/useNetwork'; +import getCurrentPosition from '@src/libs/getCurrentPosition'; import ONYXKEYS from '@src/ONYXKEYS'; +import styles from '@src/styles/styles'; import * as OnyxTypes from '@src/types/onyx'; -import * as StyleUtils from '@styles/StyleUtils'; -import themeColors from '@styles/themes/default'; import Direction from './Direction'; import {MapViewHandle, MapViewProps} from './MapViewTypes'; -import getCurrentPosition from '@src/libs/getCurrentPosition'; -import useNetwork from '@src/hooks/useNetwork'; -import useLocalize from '@src/hooks/useLocalize'; -import styles from '@src/styles/styles'; -import 'mapbox-gl/dist/mapbox-gl.css'; +import PendingMapView from './PendingMapView'; +import responder from './responder'; +import utils from './utils'; type MapViewOnyxProps = { userLocation: OnyxEntry; -} +}; -type ComponentProps = MapViewProps & MapViewOnyxProps +type ComponentProps = MapViewProps & MapViewOnyxProps; const MapView = forwardRef( - ({style, styleURL, waypoints, mapPadding, accessToken, userLocation: cachedUserLocation, directionCoordinates, initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}}, ref) => { + ( + { + style, + styleURL, + waypoints, + mapPadding, + accessToken, + userLocation: cachedUserLocation, + directionCoordinates, + initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}, + }, + ref, + ) => { const {isOffline} = useNetwork(); + // @ts-ignore - useLocalize not migrated to TypeScript yet const {translate} = useLocalize(); const [mapRef, setMapRef] = useState(null); @@ -42,15 +56,24 @@ const MapView = forwardRef( useFocusEffect( useCallback(() => { - getCurrentPosition((params) => { - setCurrentPosition({longitude: params.coords.longitude, latitude: params.coords.latitude}) - Onyx.merge(ONYXKEYS.USER_LOCATION, { longitude: params.coords.longitude, latitude: params.coords.latitude}) - }, - () => { - setCurrentPosition({ longitude: initialState.location[0], latitude: initialState.location[1] }) - }) - }, []) - ) + getCurrentPosition( + (params) => { + const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; + setCurrentPosition(currentCoords); + setUserLocation(currentCoords); + }, + () => { + setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); + }, + ); + }, [initialState.location]), + ); + + // Determines if map can be panned to user's detected + // location without bothering the user. It will return + // false if user has already started dragging the map or + // if there are one or more waypoints present. + const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]); useEffect(() => { if (!currentPosition || !mapRef) { @@ -65,13 +88,7 @@ const MapView = forwardRef( center: [currentPosition.longitude, currentPosition.latitude], zoom: CONST.MAPBOX.DEFAULT_ZOOM, }); - }, [currentPosition, userInteractedWithMap, mapRef]) - - // Determines if map can be panned to user's detected - // location without bothering the user. It will return - // false if user has already started dragging the map or - // if there are one or more waypoints present. - const shouldPanMapToCurrentPosition = () => !userInteractedWithMap && (!waypoints || waypoints.length === 0) + }, [currentPosition, userInteractedWithMap, mapRef, shouldPanMapToCurrentPosition]); useEffect(() => { if (!waypoints || waypoints.length === 0) { @@ -133,6 +150,7 @@ const MapView = forwardRef( {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( ( }, ); -export default withOnyx({ +export default withOnyx({ userLocation: { key: ONYXKEYS.USER_LOCATION, - } -})(MapView) + }, +})(MapView); From c6e25015156e5959df3c1ebee80db893cb91aed2 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 14:10:54 +0100 Subject: [PATCH 038/329] Set user location action --- src/libs/actions/UserLocation.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/libs/actions/UserLocation.ts diff --git a/src/libs/actions/UserLocation.ts b/src/libs/actions/UserLocation.ts new file mode 100644 index 000000000000..4c58f7a83284 --- /dev/null +++ b/src/libs/actions/UserLocation.ts @@ -0,0 +1,12 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {UserLocation} from '@src/types/onyx'; + +/** + * Sets the longitude and latitude of user's current location + */ +function setUserLocation({longitude, latitude}: UserLocation) { + Onyx.set(ONYXKEYS.USER_LOCATION, {longitude, latitude}); +} + +export default setUserLocation; From 86d4468c990785f5922bd47db132b62cdfa7f62f Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 15:28:31 +0100 Subject: [PATCH 039/329] Move import down --- src/types/onyx/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 832e1009f9d0..382ba0bbd9bf 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -1,5 +1,4 @@ import Account from './Account'; -import UserLocation from './UserLocation'; import AccountData from './AccountData'; import BankAccount from './BankAccount'; import Beta from './Beta'; @@ -43,6 +42,7 @@ import Session from './Session'; import Task from './Task'; import Transaction from './Transaction'; import User from './User'; +import UserLocation from './UserLocation'; import UserWallet from './UserWallet'; import WalletAdditionalDetails from './WalletAdditionalDetails'; import WalletOnfido from './WalletOnfido'; From 85cc9515e41936810c608a831a0396fdaa0fcfb1 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 15:28:59 +0100 Subject: [PATCH 040/329] Handle offline mode and cached locations --- src/components/MapView/MapView.web.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index 5ee06b682b02..567e613fe9f1 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -46,8 +46,7 @@ const MapView = forwardRef( ref, ) => { const {isOffline} = useNetwork(); - // @ts-ignore - useLocalize not migrated to TypeScript yet - const {translate} = useLocalize(); + const {translate} = useLocalize() const [mapRef, setMapRef] = useState(null); const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); @@ -56,6 +55,10 @@ const MapView = forwardRef( useFocusEffect( useCallback(() => { + if (isOffline) { + return; + } + getCurrentPosition( (params) => { const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; @@ -63,10 +66,14 @@ const MapView = forwardRef( setUserLocation(currentCoords); }, () => { + if (cachedUserLocation) { + return; + } + setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); }, ); - }, [initialState.location]), + }, [cachedUserLocation, isOffline, initialState.location]), ); // Determines if map can be panned to user's detected From 8b081b2f798ede1865e3400dc61ed7cd76ff5f41 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 31 Oct 2023 08:32:15 +0100 Subject: [PATCH 041/329] 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 042/329] 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 043/329] 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 044/329] 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 a66411034b1e1a1cb9d9b4e85c8316bf4593bbdc Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Tue, 31 Oct 2023 18:16:26 +0100 Subject: [PATCH 045/329] Auto map panning for mobile --- src/components/MapView/MapView.tsx | 193 ++++++++++++++++++------- src/components/MapView/MapView.web.tsx | 14 +- src/components/MapView/types.ts | 14 ++ 3 files changed, 157 insertions(+), 64 deletions(-) create mode 100644 src/components/MapView/types.ts diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index c91dc63a3bd1..1facc5dae242 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -4,15 +4,76 @@ import {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, u import {View} from 'react-native'; import styles from '@styles/styles'; import CONST from '@src/CONST'; +import useLocalize from '@src/hooks/useLocalize'; +import useNetwork from '@src/hooks/useNetwork'; +import { withOnyx } from 'react-native-onyx'; +import compose from '@libs/compose'; +import ONYXKEYS from '@src/ONYXKEYS'; +import PendingMapView from './PendingMapView'; import Direction from './Direction'; -import {MapViewHandle, MapViewProps} from './MapViewTypes'; +import {MapViewHandle} from './MapViewTypes'; import responder from './responder'; import utils from './utils'; +import { ComponentProps, MapViewOnyxProps } from './types'; +import getCurrentPosition from '@libs/getCurrentPosition'; +import setUserLocation from '@libs/actions/UserLocation'; +import Text from '@components/Text'; -const MapView = forwardRef(({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady}, ref) => { +const MapView = forwardRef(({accessToken, style, mapPadding, userLocation: cachedUserLocation, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady}, ref) => { + const navigation = useNavigation(); + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + + const [logger, setLogger] = useState(''); const cameraRef = useRef(null); const [isIdle, setIsIdle] = useState(false); - const navigation = useNavigation(); + const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); + const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); + + useFocusEffect( + useCallback(() => { + if (isOffline) { + return; + } + + getCurrentPosition( + (params) => { + const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; + setCurrentPosition(currentCoords); + setUserLocation(currentCoords); + }, + () => { + if (cachedUserLocation || !initialState) { + return; + } + + setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); + } + ) + }, []) + ) + + // Determines if map can be panned to user's detected + // location without bothering the user. It will return + // false if user has already started dragging the map or + // if there are one or more waypoints present. + const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]); + + useEffect(() => { + if (!currentPosition || !cameraRef.current) { + return; + } + + if (!shouldPanMapToCurrentPosition()) { + return; + } + + cameraRef.current.setCamera({ + zoomLevel: CONST.MAPBOX.DEFAULT_ZOOM, + animationDuration: 1500, + centerCoordinate: [currentPosition.longitude, currentPosition.latitude], + }); + }, [currentPosition, cameraRef.current, shouldPanMapToCurrentPosition]); useImperativeHandle( ref, @@ -29,22 +90,23 @@ const MapView = forwardRef(({accessToken, style, ma // When the page regains focus, the onIdled method of the map will set the actual "idled" state, // which in turn triggers the callback. useFocusEffect( - // eslint-disable-next-line rulesdir/prefer-early-return useCallback(() => { - if (waypoints?.length && isIdle) { - if (waypoints.length === 1) { - cameraRef.current?.setCamera({ - zoomLevel: 15, - animationDuration: 1500, - centerCoordinate: waypoints[0].coordinate, - }); - } else { - const {southWest, northEast} = utils.getBounds( - waypoints.map((waypoint) => waypoint.coordinate), - directionCoordinates, - ); - cameraRef.current?.fitBounds(northEast, southWest, mapPadding, 1000); - } + if (!waypoints || waypoints.length === 0 || !isIdle) { + return; + } + + if (waypoints.length === 1) { + cameraRef.current?.setCamera({ + zoomLevel: 15, + animationDuration: 1500, + centerCoordinate: waypoints[0].coordinate, + }); + } else { + const {southWest, northEast} = utils.getBounds( + waypoints.map((waypoint) => waypoint.coordinate), + directionCoordinates, + ); + cameraRef.current?.fitBounds(northEast, southWest, mapPadding, 1000); } }, [mapPadding, waypoints, isIdle, directionCoordinates]), ); @@ -71,43 +133,66 @@ const MapView = forwardRef(({accessToken, style, ma }; return ( - - - - - {waypoints?.map(({coordinate, markerComponent, id}) => { - const MarkerComponent = markerComponent; - return ( - + {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( + <> + + setUserInteractedWithMap(true)} + pitchEnabled={pitchEnabled} + attributionPosition={{...styles.r2, ...styles.b2}} + scaleBarEnabled={false} + logoPosition={{...styles.l2, ...styles.b2}} + // eslint-disable-next-line react/jsx-props-no-spreading + {...responder.panHandlers} > - - - ); - })} - - {directionCoordinates && } - - + + + {waypoints?.map(({coordinate, markerComponent, id}) => { + const MarkerComponent = markerComponent; + return ( + + + + ); + })} + + {directionCoordinates && } + + + + {logger} + + + ): ( + + )} + ); }); -export default memo(MapView); +export default compose( + withOnyx({ + userLocation: { + key: ONYXKEYS.USER_LOCATION, + }, + }), + memo, +)(MapView); diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index 567e613fe9f1..b7cb84b16dbc 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -8,7 +8,7 @@ import 'mapbox-gl/dist/mapbox-gl.css'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useState} from 'react'; import Map, {MapRef, Marker} from 'react-map-gl'; import {View} from 'react-native'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; import setUserLocation from '@userActions/UserLocation'; @@ -18,18 +18,12 @@ import useNetwork from '@src/hooks/useNetwork'; import getCurrentPosition from '@src/libs/getCurrentPosition'; import ONYXKEYS from '@src/ONYXKEYS'; import styles from '@src/styles/styles'; -import * as OnyxTypes from '@src/types/onyx'; import Direction from './Direction'; -import {MapViewHandle, MapViewProps} from './MapViewTypes'; +import {MapViewHandle} from './MapViewTypes'; import PendingMapView from './PendingMapView'; import responder from './responder'; import utils from './utils'; - -type MapViewOnyxProps = { - userLocation: OnyxEntry; -}; - -type ComponentProps = MapViewProps & MapViewOnyxProps; +import { ComponentProps, MapViewOnyxProps } from './types'; const MapView = forwardRef( ( @@ -46,7 +40,7 @@ const MapView = forwardRef( ref, ) => { const {isOffline} = useNetwork(); - const {translate} = useLocalize() + const {translate} = useLocalize(); const [mapRef, setMapRef] = useState(null); const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); diff --git a/src/components/MapView/types.ts b/src/components/MapView/types.ts new file mode 100644 index 000000000000..2253e9018be1 --- /dev/null +++ b/src/components/MapView/types.ts @@ -0,0 +1,14 @@ +import {OnyxEntry} from 'react-native-onyx'; +import * as OnyxTypes from '@src/types/onyx'; +import {MapViewProps} from './MapViewTypes'; + +type MapViewOnyxProps = { + userLocation: OnyxEntry; +}; + +type ComponentProps = MapViewProps & MapViewOnyxProps; + +export type { + MapViewOnyxProps, + ComponentProps +} From 29cf406dcb55b0bb4adfb5388c620669da927ca3 Mon Sep 17 00:00:00 2001 From: Stephanie Elliott <31225194+stephanieelliott@users.noreply.github.com> Date: Tue, 31 Oct 2023 11:06:14 -1000 Subject: [PATCH 046/329] Update Pay-Bills.md Added new Pay Bills helpdot article to navigation --- .../send-payments/Pay-Bills.md | 109 +++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/docs/articles/expensify-classic/send-payments/Pay-Bills.md b/docs/articles/expensify-classic/send-payments/Pay-Bills.md index 41c0146126ba..8a5c7c5c7f88 100644 --- a/docs/articles/expensify-classic/send-payments/Pay-Bills.md +++ b/docs/articles/expensify-classic/send-payments/Pay-Bills.md @@ -1,5 +1,110 @@ --- title: Pay Bills -description: Pay Bills +description: How to receive and pay company bills in Expensify --- -## Resource Coming Soon! + + +# Overview +Simplify your back office by receiving bills from vendors and suppliers in Expensify. Anyone with or without an Expensify account can send you a bill, and Expensify will file it as a Bill and help you issue the payment. + +# How to Receive Vendor or Supplier Bills in Expensify + +There are three ways to get a vendor or supplier bill into Expensify: + +**Option 1:** Have vendors send bills to Expensify directly: Ask your vendors to email all bills to your Expensify billing intake email. + +**Option 2:** Forward bills to Expensify: If your bills are emailed to you, you can forward those bills to your Expensify billing intake email yourself. + +**Option 3:** Manually upload bills to Expensify: If you receive physical bills, you can manually create a Bill in Expensify on the web from the Reports page: +1. Click **New Report** and choose **Bill** +2. Add the expense details and vendor's email address to the pop-up window +3. Upload a pdf/image of the bill +4. Click **Submit** + +# How to Pay Bills + +There are multiple ways to pay Bills in Expensify. Let’s go over each method below: + +## ACH bank-to-bank transfer + +To use this payment method, you must have a business bank account connected to your Expensify account. + +To pay with an ACH bank-to-bank transfer: + +1. Sign in to your Expensify account on the web at www.expensify.com. +2. Go to the Inbox or Reports page and locate the Bill that needs to be paid. +3. Click the **Pay** button to be redirected to the Bill. +4. Choose the ACH option from the drop-down list. +5. Follow the prompts to connect your business bank account to Expensify. + +**Fees:** None + +## Pay using a credit or debit card + +This option is available to all US and International customers receiving an bill from a US vendor with a US business bank account. + +To pay with a credit or debit card: +1. Sign-in to your Expensify account on the web app at www.expensify.com. +2, Click on the Bill you’d like to pay to see the details. +3, Click the **Pay** button. +4. You’ll be prompted to enter your credit card or debit card details. + +**Fees:** Includes 2.9% credit card payment fee + +## Venmo + +If both you and the vendor have Venmo setup in their Expensify account, you can opt to pay the bill through Venmo. + +**Fees:** Venmo charges a 3% sender’s fee + +## Pay Outside of Expensify + +If you are not able to pay using one of the above methods, then you can mark the Bill as paid manually in Expensify to update its status and indicate that you have made payment outside Expensify. + +To mark a Bill as paid outside of Expensify: + +1. Sign-in to your Expensify account on the web app at www.expensify.com. +2. Click on the Bill you’d like to pay to see the details. +3. Click on the **Reimburse** button. +4. Choose **I’ll do it manually** + +**Fees:** None + +# FAQ + +## What is my company's billing intake email? +Your billing intake email is [yourdomain.com]@expensify.cash. Example, if your domain is `company.io` your billing email is `company.io@expensify.cash`. + +## When a vendor or supplier bill is sent to Expensify, who receives it? + +Bills are received by the Primary Contact for the domain. This is the email address listed at **Settings > Domains > Domain Admins**. + +## Who can view a Bill in Expensify? + +Only the primary contact of the domain can view a Bill. + +## Who can pay a Bill? + +Only the primary domain contact (owner of the bill) will be able to pay the Mill. + +## How can you share access to Bills? + +To give others the ability to view a Bill, the primary contact can manually “share” the Bill under the Details section of the report via the Sharing Options button. +To give someone else the ability to pay Bills, the primary domain contact will need to grant those individuals Copilot access to the primary domain contact's account. + +## Is Bill Pay supported internationally? + +Payments are currently only supported for users paying in United States Dollars (USD). + +## What’s the difference between a Bill and an Invoice in Expensify? + +A Bill is a payable which represents an amount owed to a payee (usually a vendor or supplier), and is usually created from a vendor invoice. An Invoice is a receivable, and indicates an amount owed to you by someone else. + +# Deep Dive: How company bills and vendor invoices are processed in Expensify + +Here is how a vendor or supplier bill goes from received to paid in Expensify: + +1. When a vendor or supplier bill is received in Expensify via, the document is SmartScanned automatically and a Bill is created. The Bill is owned by the primary domain contact, who will see the Bill on the Reports page on their default group policy. +2. When the Bill is ready for processing, it is submitted and follows the primary domain contact’s approval workflow. Each time the Bill is approved, it is visible in the next approver's Inbox. +3. The final approver pays the Bill from their Expensify account on the web via one of the methods. +4. The Bill is coded with the relevant imported GL codes from a connected accounting software. After it has finished going through the approval workflow the Bill can be exported back to the accounting package connected to the policy. From 7d308eba006a250b7eff5f96f9b56cba78f2f5a2 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 2 Nov 2023 13:35:02 +0100 Subject: [PATCH 047/329] 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 048/329] 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 049/329] 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 050/329] 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 051/329] 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 1c4f7277d3df08265ea0a84ce93546b7130e9068 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 2 Nov 2023 17:03:39 +0100 Subject: [PATCH 052/329] Drop unused const --- src/CONST.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 5be25e0ff220..db8a9cc49dc0 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -877,10 +877,6 @@ const CONST = { YOUR_LOCATION_TEXT: 'Your Location', - // Value indicating the maximum age in milliseconds - // of a possible cached position that is acceptable to return - MAXIMUM_AGE_OF_CACHED_USER_LOCATION: 10 * 60 * 1000, - ATTACHMENT_MESSAGE_TEXT: '[Attachment]', // This is a placeholder for attachment which is uploading ATTACHMENT_UPLOADING_MESSAGE_HTML: 'Uploading attachment...', From 478967988f1b6054870707aa27a2eb9b6d19eb4c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 2 Nov 2023 19:21:26 +0100 Subject: [PATCH 053/329] 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 054/329] 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 8c008e338a30992f4e8fc9cd60c7b18d846f75c7 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 3 Nov 2023 09:23:20 +0100 Subject: [PATCH 055/329] start migrating PlaidLink to TypeScript --- .../{index.native.js => index.native.tsx} | 17 ++++++++-------- .../PlaidLink/{index.js => index.tsx} | 20 ++++++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) rename src/components/PlaidLink/{index.native.js => index.native.tsx} (66%) rename src/components/PlaidLink/{index.js => index.tsx} (70%) diff --git a/src/components/PlaidLink/index.native.js b/src/components/PlaidLink/index.native.tsx similarity index 66% rename from src/components/PlaidLink/index.native.js rename to src/components/PlaidLink/index.native.tsx index 7d995d03926b..e1e9e7756620 100644 --- a/src/components/PlaidLink/index.native.js +++ b/src/components/PlaidLink/index.native.tsx @@ -1,27 +1,28 @@ import {useEffect} from 'react'; -import {dismissLink, openLink, useDeepLinkRedirector, usePlaidEmitter} from 'react-native-plaid-link-sdk'; +import {dismissLink, LinkEvent, openLink, useDeepLinkRedirector, usePlaidEmitter} from 'react-native-plaid-link-sdk'; import Log from '@libs/Log'; import CONST from '@src/CONST'; import {plaidLinkDefaultProps, plaidLinkPropTypes} from './plaidLinkPropTypes'; +import PlaidLinkProps from './types'; -function PlaidLink(props) { +function PlaidLink({token, onSuccess = () => {}, onExit = () => {}, onEvent}: PlaidLinkProps) { useDeepLinkRedirector(); - usePlaidEmitter((event) => { + usePlaidEmitter((event: LinkEvent) => { Log.info('[PlaidLink] Handled Plaid Event: ', false, event); - props.onEvent(event.eventName, event.metadata); + onEvent?.(event.eventName, event.metadata); }); useEffect(() => { - props.onEvent(CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.OPEN, {}); + onEvent?.(CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.OPEN, {}); openLink({ tokenConfig: { - token: props.token, + token, }, onSuccess: ({publicToken, metadata}) => { - props.onSuccess({publicToken, metadata}); + onSuccess({publicToken, metadata}); }, onExit: (exitError, metadata) => { Log.info('[PlaidLink] Exit: ', false, {exitError, metadata}); - props.onExit(); + onExit(); }, }); diff --git a/src/components/PlaidLink/index.js b/src/components/PlaidLink/index.tsx similarity index 70% rename from src/components/PlaidLink/index.js rename to src/components/PlaidLink/index.tsx index 790206f34ce7..39b9ffda54b2 100644 --- a/src/components/PlaidLink/index.js +++ b/src/components/PlaidLink/index.tsx @@ -1,35 +1,33 @@ import {useCallback, useEffect, useState} from 'react'; -import {usePlaidLink} from 'react-plaid-link'; +import {PlaidLinkOnSuccessMetadata, usePlaidLink} from 'react-plaid-link'; import Log from '@libs/Log'; -import {plaidLinkDefaultProps, plaidLinkPropTypes} from './plaidLinkPropTypes'; +import PlaidLinkProps from './types'; -function PlaidLink(props) { +function PlaidLink({token, onSuccess = () => {}, onError = () => {}, onExit = () => {}, onEvent, receivedRedirectURI}: PlaidLinkProps) { const [isPlaidLoaded, setIsPlaidLoaded] = useState(false); - const onSuccess = props.onSuccess; - const onError = props.onError; const successCallback = useCallback( - (publicToken, metadata) => { + (publicToken: string, metadata: PlaidLinkOnSuccessMetadata) => { onSuccess({publicToken, metadata}); }, [onSuccess], ); const {open, ready, error} = usePlaidLink({ - token: props.token, + token, onSuccess: successCallback, onExit: (exitError, metadata) => { Log.info('[PlaidLink] Exit: ', false, {exitError, metadata}); - props.onExit(); + onExit(); }, onEvent: (event, metadata) => { Log.info('[PlaidLink] Event: ', false, {event, metadata}); - props.onEvent(event, metadata); + onEvent?.(event, metadata); }, onLoad: () => setIsPlaidLoaded(true), // The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the // user to their respective bank platform - receivedRedirectUri: props.receivedRedirectURI, + receivedRedirectUri: receivedRedirectURI, }); useEffect(() => { @@ -52,7 +50,5 @@ function PlaidLink(props) { return null; } -PlaidLink.propTypes = plaidLinkPropTypes; -PlaidLink.defaultProps = plaidLinkDefaultProps; PlaidLink.displayName = 'PlaidLink'; export default PlaidLink; From 0205ca59215bbbbb36e85fbedff479aab95f3780 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 3 Nov 2023 09:23:54 +0100 Subject: [PATCH 056/329] migrate PlaidLinks native module to TypeScript, create a file for types --- .../{index.android.js => index.android.ts} | 0 .../{index.ios.js => index.ios.ts} | 0 src/components/PlaidLink/types.ts | 24 +++++++++++++++++++ 3 files changed, 24 insertions(+) rename src/components/PlaidLink/nativeModule/{index.android.js => index.android.ts} (100%) rename src/components/PlaidLink/nativeModule/{index.ios.js => index.ios.ts} (100%) create mode 100644 src/components/PlaidLink/types.ts diff --git a/src/components/PlaidLink/nativeModule/index.android.js b/src/components/PlaidLink/nativeModule/index.android.ts similarity index 100% rename from src/components/PlaidLink/nativeModule/index.android.js rename to src/components/PlaidLink/nativeModule/index.android.ts diff --git a/src/components/PlaidLink/nativeModule/index.ios.js b/src/components/PlaidLink/nativeModule/index.ios.ts similarity index 100% rename from src/components/PlaidLink/nativeModule/index.ios.js rename to src/components/PlaidLink/nativeModule/index.ios.ts diff --git a/src/components/PlaidLink/types.ts b/src/components/PlaidLink/types.ts new file mode 100644 index 000000000000..06b81d06b5c9 --- /dev/null +++ b/src/components/PlaidLink/types.ts @@ -0,0 +1,24 @@ +import {PlaidLinkOnEvent, PlaidLinkOnSuccessMetadata} from 'react-plaid-link'; + +type PlaidLinkProps = { + // Plaid Link SDK public token used to initialize the Plaid SDK + token: string; + + // Callback to execute once the user taps continue after successfully entering their account information + onSuccess?: (args: {publicToken?: string; metadata: PlaidLinkOnSuccessMetadata}) => void; + + // Callback to execute when there is an error event emitted by the Plaid SDK + onError?: (error: ErrorEvent | null) => void; + + // Callback to execute when the user leaves the Plaid widget flow without entering any information + onExit?: () => void; + + // Callback to execute whenever a Plaid event occurs + onEvent?: PlaidLinkOnEvent; + + // The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the + // user to their respective bank platform + receivedRedirectURI?: string; +}; + +export default PlaidLinkProps; From 0dc1e13a3c8c48348e9973fe47f8e239e293412b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 3 Nov 2023 11:25:03 +0100 Subject: [PATCH 057/329] 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 5754b001b0916aa6992164ac08ed883d14090ea3 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 6 Nov 2023 09:35:37 +0100 Subject: [PATCH 058/329] Prettier --- src/components/MapView/MapView.web.tsx | 2 +- src/components/MapView/types.ts | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index b7cb84b16dbc..14d105e265bd 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -22,8 +22,8 @@ import Direction from './Direction'; import {MapViewHandle} from './MapViewTypes'; import PendingMapView from './PendingMapView'; import responder from './responder'; +import {ComponentProps, MapViewOnyxProps} from './types'; import utils from './utils'; -import { ComponentProps, MapViewOnyxProps } from './types'; const MapView = forwardRef( ( diff --git a/src/components/MapView/types.ts b/src/components/MapView/types.ts index 2253e9018be1..2c8b9240c445 100644 --- a/src/components/MapView/types.ts +++ b/src/components/MapView/types.ts @@ -3,12 +3,9 @@ import * as OnyxTypes from '@src/types/onyx'; import {MapViewProps} from './MapViewTypes'; type MapViewOnyxProps = { - userLocation: OnyxEntry; + userLocation: OnyxEntry; }; type ComponentProps = MapViewProps & MapViewOnyxProps; -export type { - MapViewOnyxProps, - ComponentProps -} +export type {MapViewOnyxProps, ComponentProps}; From a86094559f697d01e06b3424e2a3aa1f9a2aa09d Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 6 Nov 2023 09:35:56 +0100 Subject: [PATCH 059/329] Auto map panning for native --- src/components/MapView/MapView.tsx | 260 ++++++++++++++--------------- 1 file changed, 128 insertions(+), 132 deletions(-) diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 1facc5dae242..db3e076eacca 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -2,140 +2,139 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'; import Mapbox, {MapState, MarkerView, setAccessToken} from '@rnmapbox/maps'; import {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import setUserLocation from '@libs/actions/UserLocation'; +import compose from '@libs/compose'; +import getCurrentPosition from '@libs/getCurrentPosition'; import styles from '@styles/styles'; import CONST from '@src/CONST'; import useLocalize from '@src/hooks/useLocalize'; import useNetwork from '@src/hooks/useNetwork'; -import { withOnyx } from 'react-native-onyx'; -import compose from '@libs/compose'; import ONYXKEYS from '@src/ONYXKEYS'; -import PendingMapView from './PendingMapView'; import Direction from './Direction'; import {MapViewHandle} from './MapViewTypes'; +import PendingMapView from './PendingMapView'; import responder from './responder'; +import {ComponentProps, MapViewOnyxProps} from './types'; import utils from './utils'; -import { ComponentProps, MapViewOnyxProps } from './types'; -import getCurrentPosition from '@libs/getCurrentPosition'; -import setUserLocation from '@libs/actions/UserLocation'; -import Text from '@components/Text'; - -const MapView = forwardRef(({accessToken, style, mapPadding, userLocation: cachedUserLocation, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady}, ref) => { - const navigation = useNavigation(); - const {isOffline} = useNetwork(); - const {translate} = useLocalize(); - - const [logger, setLogger] = useState(''); - const cameraRef = useRef(null); - const [isIdle, setIsIdle] = useState(false); - const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); - const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); - - useFocusEffect( - useCallback(() => { - if (isOffline) { + +const MapView = forwardRef( + ({accessToken, style, mapPadding, userLocation: cachedUserLocation, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady}, ref) => { + const navigation = useNavigation(); + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + + const cameraRef = useRef(null); + const [isIdle, setIsIdle] = useState(false); + const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); + const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); + + useFocusEffect( + useCallback(() => { + if (isOffline) { + return; + } + + getCurrentPosition( + (params) => { + const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; + setCurrentPosition(currentCoords); + setUserLocation(currentCoords); + }, + () => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (cachedUserLocation || !initialState) { + return; + } + + setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); + }, + ); + }, [cachedUserLocation, initialState, isOffline]), + ); + + // Determines if map can be panned to user's detected + // location without bothering the user. It will return + // false if user has already started dragging the map or + // if there are one or more waypoints present. + const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]); + + useEffect(() => { + if (!currentPosition || !cameraRef.current) { return; } - getCurrentPosition( - (params) => { - const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; - setCurrentPosition(currentCoords); - setUserLocation(currentCoords); - }, - () => { - if (cachedUserLocation || !initialState) { - return; - } - - setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); - } - ) - }, []) - ) - - // Determines if map can be panned to user's detected - // location without bothering the user. It will return - // false if user has already started dragging the map or - // if there are one or more waypoints present. - const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]); - - useEffect(() => { - if (!currentPosition || !cameraRef.current) { - return; - } - - if (!shouldPanMapToCurrentPosition()) { - return; - } - - cameraRef.current.setCamera({ - zoomLevel: CONST.MAPBOX.DEFAULT_ZOOM, - animationDuration: 1500, - centerCoordinate: [currentPosition.longitude, currentPosition.latitude], - }); - }, [currentPosition, cameraRef.current, shouldPanMapToCurrentPosition]); - - useImperativeHandle( - ref, - () => ({ - flyTo: (location: [number, number], zoomLevel: number = CONST.MAPBOX.DEFAULT_ZOOM, animationDuration?: number) => - cameraRef.current?.setCamera({zoomLevel, centerCoordinate: location, animationDuration}), - fitBounds: (northEast: [number, number], southWest: [number, number], paddingConfig?: number | number[] | undefined, animationDuration?: number | undefined) => - cameraRef.current?.fitBounds(northEast, southWest, paddingConfig, animationDuration), - }), - [], - ); - - // When the page loses focus, we temporarily set the "idled" state to false. - // When the page regains focus, the onIdled method of the map will set the actual "idled" state, - // which in turn triggers the callback. - useFocusEffect( - useCallback(() => { - if (!waypoints || waypoints.length === 0 || !isIdle) { + if (!shouldPanMapToCurrentPosition()) { return; } - if (waypoints.length === 1) { - cameraRef.current?.setCamera({ - zoomLevel: 15, - animationDuration: 1500, - centerCoordinate: waypoints[0].coordinate, - }); - } else { - const {southWest, northEast} = utils.getBounds( - waypoints.map((waypoint) => waypoint.coordinate), - directionCoordinates, - ); - cameraRef.current?.fitBounds(northEast, southWest, mapPadding, 1000); + cameraRef.current.setCamera({ + zoomLevel: CONST.MAPBOX.DEFAULT_ZOOM, + animationDuration: 1500, + centerCoordinate: [currentPosition.longitude, currentPosition.latitude], + }); + }, [currentPosition, shouldPanMapToCurrentPosition]); + + useImperativeHandle( + ref, + () => ({ + flyTo: (location: [number, number], zoomLevel: number = CONST.MAPBOX.DEFAULT_ZOOM, animationDuration?: number) => + cameraRef.current?.setCamera({zoomLevel, centerCoordinate: location, animationDuration}), + fitBounds: (northEast: [number, number], southWest: [number, number], paddingConfig?: number | number[] | undefined, animationDuration?: number | undefined) => + cameraRef.current?.fitBounds(northEast, southWest, paddingConfig, animationDuration), + }), + [], + ); + + // When the page loses focus, we temporarily set the "idled" state to false. + // When the page regains focus, the onIdled method of the map will set the actual "idled" state, + // which in turn triggers the callback. + useFocusEffect( + useCallback(() => { + if (!waypoints || waypoints.length === 0 || !isIdle) { + return; + } + + if (waypoints.length === 1) { + cameraRef.current?.setCamera({ + zoomLevel: 15, + animationDuration: 1500, + centerCoordinate: waypoints[0].coordinate, + }); + } else { + const {southWest, northEast} = utils.getBounds( + waypoints.map((waypoint) => waypoint.coordinate), + directionCoordinates, + ); + cameraRef.current?.fitBounds(northEast, southWest, mapPadding, 1000); + } + }, [mapPadding, waypoints, isIdle, directionCoordinates]), + ); + + useEffect(() => { + const unsubscribe = navigation.addListener('blur', () => { + setIsIdle(false); + }); + return unsubscribe; + }, [navigation]); + + useEffect(() => { + setAccessToken(accessToken); + }, [accessToken]); + + const setMapIdle = (e: MapState) => { + if (e.gestures.isGestureActive) { + return; } - }, [mapPadding, waypoints, isIdle, directionCoordinates]), - ); - - useEffect(() => { - const unsubscribe = navigation.addListener('blur', () => { - setIsIdle(false); - }); - return unsubscribe; - }, [navigation]); - - useEffect(() => { - setAccessToken(accessToken); - }, [accessToken]); - - const setMapIdle = (e: MapState) => { - if (e.gestures.isGestureActive) { - return; - } - setIsIdle(true); - if (onMapReady) { - onMapReady(); - } - }; - - return ( - <> - {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( - <> + setIsIdle(true); + if (onMapReady) { + onMapReady(); + } + }; + + return ( + <> + {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( (({accessToken, style, {directionCoordinates && } - - {logger} - - - ): ( - - )} - - ); -}); + ) : ( + + )} + + ); + }, +); export default compose( withOnyx({ From 59ba483433ac4a190f12e668831b5402ccba3638 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 6 Nov 2023 10:00:42 +0100 Subject: [PATCH 060/329] Add type definitions to useLocalize that is pending TS migration --- src/hooks/useLocalize.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/hooks/useLocalize.js b/src/hooks/useLocalize.js index 71968cdb6e61..7b810eae63d9 100644 --- a/src/hooks/useLocalize.js +++ b/src/hooks/useLocalize.js @@ -1,6 +1,24 @@ import {useContext} from 'react'; import {LocaleContext} from '@components/LocaleContextProvider'; +/** + * @typedef {Object} LocalizationContext + * @property {(phrase: string, variables?: object) => string} translate Translates a provided phrase using the preferred locale and optional variables. + * @property {(number: number, options: Intl.NumberFormatOptions) => string} numberFormat Formats a provided number using the preferred locale and optional format options. + * @property {(datetime: string) => string} datetimeToRelative Converts an ISO-formatted datetime to a relative time string in the preferred locale. + * @property {(datetime: string, includeTimezone? boolean, isLowercase: boolean) => string} datetimeToCalendarTime Converts an ISO-formatted datetime to a calendar time string in the preferred locale. Optional includeTimezone and isLowercase. + * @property {() => void} updateLocale Updates internal date-fns locale to the user's preferred locale. + * @property {(phoneNumber: string) => string} formatPhoneNumber Formats given phoneNumber. + * @property {(digit: string) => string} toLocaleDigit Converts the provided digit to the locale digit. + * @property {(localeDigit: string) => string} fromLocaleDigit Reverses the operation of `toLocaleDigit`, taking a locale-specific digit and returning the equivalent in the standard number system. + * @property {string} preferredLocale The preferred locale value. + */ + +/** + * Hook to access the localization context which provides multiple utility functions and locale. + * + * @returns {LocalizationContext} The localization context + */ export default function useLocalize() { return useContext(LocaleContext); } From 1de9b9ba07f7f7130fbe3e3c26e7fc43e94c04eb Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 6 Nov 2023 14:22:19 +0100 Subject: [PATCH 061/329] 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 e55fe141ecdce19e1b39becf1e29f7b7a1c98de4 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 6 Nov 2023 15:59:55 +0100 Subject: [PATCH 062/329] migrate native PlaidLink to TypeScript --- src/components/PlaidLink/index.native.tsx | 12 +++---- .../PlaidLink/plaidLinkPropTypes.js | 31 ------------------- src/components/PlaidLink/types.ts | 7 +++-- 3 files changed, 9 insertions(+), 41 deletions(-) delete mode 100644 src/components/PlaidLink/plaidLinkPropTypes.js diff --git a/src/components/PlaidLink/index.native.tsx b/src/components/PlaidLink/index.native.tsx index e1e9e7756620..874d7c77414c 100644 --- a/src/components/PlaidLink/index.native.tsx +++ b/src/components/PlaidLink/index.native.tsx @@ -2,26 +2,26 @@ import {useEffect} from 'react'; import {dismissLink, LinkEvent, openLink, useDeepLinkRedirector, usePlaidEmitter} from 'react-native-plaid-link-sdk'; import Log from '@libs/Log'; import CONST from '@src/CONST'; -import {plaidLinkDefaultProps, plaidLinkPropTypes} from './plaidLinkPropTypes'; import PlaidLinkProps from './types'; function PlaidLink({token, onSuccess = () => {}, onExit = () => {}, onEvent}: PlaidLinkProps) { useDeepLinkRedirector(); usePlaidEmitter((event: LinkEvent) => { - Log.info('[PlaidLink] Handled Plaid Event: ', false, event); + Log.info('[PlaidLink] Handled Plaid Event: ', false, event.eventName); onEvent?.(event.eventName, event.metadata); }); useEffect(() => { - onEvent?.(CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.OPEN, {}); + onEvent?.(CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.OPEN); openLink({ tokenConfig: { token, + noLoadingState: false, }, onSuccess: ({publicToken, metadata}) => { onSuccess({publicToken, metadata}); }, - onExit: (exitError, metadata) => { - Log.info('[PlaidLink] Exit: ', false, {exitError, metadata}); + onExit: ({error, metadata}) => { + Log.info('[PlaidLink] Exit: ', false, {error, metadata}); onExit(); }, }); @@ -36,8 +36,6 @@ function PlaidLink({token, onSuccess = () => {}, onExit = () => {}, onEvent}: Pl return null; } -PlaidLink.propTypes = plaidLinkPropTypes; -PlaidLink.defaultProps = plaidLinkDefaultProps; PlaidLink.displayName = 'PlaidLink'; export default PlaidLink; diff --git a/src/components/PlaidLink/plaidLinkPropTypes.js b/src/components/PlaidLink/plaidLinkPropTypes.js deleted file mode 100644 index 6d647d26f17e..000000000000 --- a/src/components/PlaidLink/plaidLinkPropTypes.js +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; - -const plaidLinkPropTypes = { - // Plaid Link SDK public token used to initialize the Plaid SDK - token: PropTypes.string.isRequired, - - // Callback to execute once the user taps continue after successfully entering their account information - onSuccess: PropTypes.func, - - // Callback to execute when there is an error event emitted by the Plaid SDK - onError: PropTypes.func, - - // Callback to execute when the user leaves the Plaid widget flow without entering any information - onExit: PropTypes.func, - - // Callback to execute whenever a Plaid event occurs - onEvent: PropTypes.func, - - // The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the - // user to their respective bank platform - receivedRedirectURI: PropTypes.string, -}; - -const plaidLinkDefaultProps = { - onSuccess: () => {}, - onError: () => {}, - onExit: () => {}, - receivedRedirectURI: null, -}; - -export {plaidLinkPropTypes, plaidLinkDefaultProps}; diff --git a/src/components/PlaidLink/types.ts b/src/components/PlaidLink/types.ts index 06b81d06b5c9..4fc44cbf9b9c 100644 --- a/src/components/PlaidLink/types.ts +++ b/src/components/PlaidLink/types.ts @@ -1,11 +1,12 @@ -import {PlaidLinkOnEvent, PlaidLinkOnSuccessMetadata} from 'react-plaid-link'; +import {LinkEventMetadata, LinkSuccessMetadata} from 'react-native-plaid-link-sdk'; +import {PlaidLinkOnEventMetadata, PlaidLinkOnSuccessMetadata, PlaidLinkStableEvent} from 'react-plaid-link'; type PlaidLinkProps = { // Plaid Link SDK public token used to initialize the Plaid SDK token: string; // Callback to execute once the user taps continue after successfully entering their account information - onSuccess?: (args: {publicToken?: string; metadata: PlaidLinkOnSuccessMetadata}) => void; + onSuccess?: (args: {publicToken?: string; metadata: PlaidLinkOnSuccessMetadata | LinkSuccessMetadata}) => void; // Callback to execute when there is an error event emitted by the Plaid SDK onError?: (error: ErrorEvent | null) => void; @@ -14,7 +15,7 @@ type PlaidLinkProps = { onExit?: () => void; // Callback to execute whenever a Plaid event occurs - onEvent?: PlaidLinkOnEvent; + onEvent?: (eventName: PlaidLinkStableEvent | string, metadata?: PlaidLinkOnEventMetadata | LinkEventMetadata) => void; // The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the // user to their respective bank platform From e4df9cbbb89ef21a9200babae0215f4a37f5b6f0 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 7 Nov 2023 10:00:17 +0100 Subject: [PATCH 063/329] 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 064/329] 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 cb75361ceefbeaa98ad45a782c0ba7a0a5b0d777 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Tue, 7 Nov 2023 11:51:24 +0100 Subject: [PATCH 065/329] Add workflow Added the first version of GHA workflow running workflow tests on opening and syncing of PRs See: https://github.com/Expensify/App/issues/13604 --- .../workflows/testGithubActionsWorkflows.yml | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/testGithubActionsWorkflows.yml diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml new file mode 100644 index 000000000000..a7f86cee3b77 --- /dev/null +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -0,0 +1,27 @@ +name: Test GitHub Actions workflows + +on: + workflow_call: + pull_request: + types: [opened, reopened, synchronize] + branches-ignore: [staging, production] + paths: ['.github'] + +jobs: + testGHWorkflows: + if: ${{ github.actor != 'OSBotify' && github.actor != 'imgbot[bot]' || github.event_name == 'workflow_call' }} + runs-on: ubuntu-latest + env: + CI: true + strategy: + fail-fast: false + name: test GitHub Workflows + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main + + - name: Run tests + runs: npm run workflow-test From 62256f70092b557e8b82ae2094c4394bb184cade Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Tue, 7 Nov 2023 11:52:51 +0100 Subject: [PATCH 066/329] Fix runs -> run See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index a7f86cee3b77..2a65b4fb4ffe 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -24,4 +24,4 @@ jobs: uses: Expensify/App/.github/actions/composite/setupNode@main - name: Run tests - runs: npm run workflow-test + run: npm run workflow-test From 6df5949c47b45f6b585368c241f0f94aa1ad29c1 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Tue, 7 Nov 2023 11:55:12 +0100 Subject: [PATCH 067/329] Add trigger Added workflow_dispatch trigger See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index 2a65b4fb4ffe..b871663b06b6 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -1,6 +1,7 @@ name: Test GitHub Actions workflows on: + workflow_dispatch: workflow_call: pull_request: types: [opened, reopened, synchronize] From 9e15bc8409ee4d66943e3dfa0638364305170058 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 7 Nov 2023 12:00:22 +0100 Subject: [PATCH 068/329] make onEvent a required prop to avoid optional chaining --- src/components/PlaidLink/index.native.tsx | 4 ++-- src/components/PlaidLink/index.tsx | 2 +- src/components/PlaidLink/types.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/PlaidLink/index.native.tsx b/src/components/PlaidLink/index.native.tsx index 874d7c77414c..b9accb0c0ad7 100644 --- a/src/components/PlaidLink/index.native.tsx +++ b/src/components/PlaidLink/index.native.tsx @@ -8,10 +8,10 @@ function PlaidLink({token, onSuccess = () => {}, onExit = () => {}, onEvent}: Pl useDeepLinkRedirector(); usePlaidEmitter((event: LinkEvent) => { Log.info('[PlaidLink] Handled Plaid Event: ', false, event.eventName); - onEvent?.(event.eventName, event.metadata); + onEvent(event.eventName, event.metadata); }); useEffect(() => { - onEvent?.(CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.OPEN); + onEvent(CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.OPEN); openLink({ tokenConfig: { token, diff --git a/src/components/PlaidLink/index.tsx b/src/components/PlaidLink/index.tsx index 39b9ffda54b2..2109771473aa 100644 --- a/src/components/PlaidLink/index.tsx +++ b/src/components/PlaidLink/index.tsx @@ -21,7 +21,7 @@ function PlaidLink({token, onSuccess = () => {}, onError = () => {}, onExit = () }, onEvent: (event, metadata) => { Log.info('[PlaidLink] Event: ', false, {event, metadata}); - onEvent?.(event, metadata); + onEvent(event, metadata); }, onLoad: () => setIsPlaidLoaded(true), diff --git a/src/components/PlaidLink/types.ts b/src/components/PlaidLink/types.ts index 4fc44cbf9b9c..fe23e09151ca 100644 --- a/src/components/PlaidLink/types.ts +++ b/src/components/PlaidLink/types.ts @@ -15,7 +15,7 @@ type PlaidLinkProps = { onExit?: () => void; // Callback to execute whenever a Plaid event occurs - onEvent?: (eventName: PlaidLinkStableEvent | string, metadata?: PlaidLinkOnEventMetadata | LinkEventMetadata) => void; + onEvent: (eventName: PlaidLinkStableEvent | string, metadata?: PlaidLinkOnEventMetadata | LinkEventMetadata) => void; // The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the // user to their respective bank platform From 38f51dd8a7b24413b2592e8b240499443d12cd19 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Tue, 7 Nov 2023 12:05:54 +0100 Subject: [PATCH 069/329] Update trigger Added PR event type of edited See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index b871663b06b6..17d1af433a89 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: workflow_call: pull_request: - types: [opened, reopened, synchronize] + types: [opened, reopened, edited, synchronize] branches-ignore: [staging, production] paths: ['.github'] From 9bb710e1f218ffb611286471c9f9973b3d047950 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Tue, 7 Nov 2023 12:16:31 +0100 Subject: [PATCH 070/329] Add step Added step for Act installation See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index 17d1af433a89..2e8046e47b1b 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -24,5 +24,8 @@ jobs: - name: Setup Node uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Install Act + run: brew install act + - name: Run tests run: npm run workflow-test From 4010b801fe7455c1de08085eef7c7db5b646fabd Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Tue, 7 Nov 2023 12:21:26 +0100 Subject: [PATCH 071/329] Add step Added step for Homebrew installation See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index 2e8046e47b1b..d8decc2c14da 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -24,6 +24,9 @@ jobs: - name: Setup Node uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Install Homebrew + run: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + - name: Install Act run: brew install act From 28cb3bff429e921cf03536f9d7b3440f26a8828a Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Tue, 7 Nov 2023 12:27:24 +0100 Subject: [PATCH 072/329] Update step Updated step for Homebrew installation with post install commands See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index d8decc2c14da..2611a15d3c81 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -25,7 +25,11 @@ jobs: uses: Expensify/App/.github/actions/composite/setupNode@main - name: Install Homebrew - run: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + run: | + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + (echo; echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"') >> /home/runner/.bashrc + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" + sudo apt-get install build-essential - name: Install Act run: brew install act From e40c556913003c06ee24457daaf3a2100eaa8262 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Tue, 7 Nov 2023 13:40:54 +0100 Subject: [PATCH 073/329] Update step Added some debug echoes See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index 2611a15d3c81..9b7909c6e7c4 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -27,9 +27,11 @@ jobs: - name: Install Homebrew run: | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + echo "Test" (echo; echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"') >> /home/runner/.bashrc eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" sudo apt-get install build-essential + echo "Homebrew installed" - name: Install Act run: brew install act From 0cca425c9721870855ee95589572843e86af5a59 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Tue, 7 Nov 2023 13:48:54 +0100 Subject: [PATCH 074/329] Update step Found existing Homebrew setup step See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index 9b7909c6e7c4..429ed874bb76 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -24,14 +24,8 @@ jobs: - name: Setup Node uses: Expensify/App/.github/actions/composite/setupNode@main - - name: Install Homebrew - run: | - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - echo "Test" - (echo; echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"') >> /home/runner/.bashrc - eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" - sudo apt-get install build-essential - echo "Homebrew installed" + - name: Setup Homebrew + uses: Homebrew/actions/setup-homebrew@master - name: Install Act run: brew install act From 33d29b6c66996dc42de0c94ae90aef3dfef463b5 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Tue, 7 Nov 2023 13:54:32 +0100 Subject: [PATCH 075/329] Add step Added step for setting the ACT_BINARY env variable See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index 429ed874bb76..387be65d1afc 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -30,5 +30,8 @@ jobs: - name: Install Act run: brew install act + - name: Set ACT_BINARY + run: export ACT_BINARY="$(which act)" + - name: Run tests run: npm run workflow-test From 6cd203e20e2441114150ed0ef0cb39b0cb251b82 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Tue, 7 Nov 2023 14:00:29 +0100 Subject: [PATCH 076/329] Update step Updated step for setting the ACT_BINARY to use the way proposed by GHA See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index 387be65d1afc..9b69364c2cb0 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -31,7 +31,7 @@ jobs: run: brew install act - name: Set ACT_BINARY - run: export ACT_BINARY="$(which act)" + run: echo "ACT_BINARY=$(which act)" >> $GITHUB_ENV - name: Run tests run: npm run workflow-test From d45567959342b33329311097ce7194a02eda09f0 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Tue, 7 Nov 2023 14:07:19 +0100 Subject: [PATCH 077/329] Update step Updated step for running tests with debug echoes to get into what is the working dir See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index 9b69364c2cb0..ab7eb6b5a4a0 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -34,4 +34,7 @@ jobs: run: echo "ACT_BINARY=$(which act)" >> $GITHUB_ENV - name: Run tests - run: npm run workflow-test + run: | + echo "$(pwd)" + echo "$(ls)" + npm run workflow-test From ac5632f7d1ffb40753c9021ae5db303db20c1d70 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Tue, 7 Nov 2023 14:36:28 +0100 Subject: [PATCH 078/329] Update step Updated step for running tests by removing debug echoes See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index ab7eb6b5a4a0..9b69364c2cb0 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -34,7 +34,4 @@ jobs: run: echo "ACT_BINARY=$(which act)" >> $GITHUB_ENV - name: Run tests - run: | - echo "$(pwd)" - echo "$(ls)" - npm run workflow-test + run: npm run workflow-test From 24177794282be7bcf92c51c88571b214b2cb260e Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 7 Nov 2023 18:04:24 +0100 Subject: [PATCH 079/329] 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 d9e6d23b7546e795c01c196eb164541169b93ed5 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Wed, 8 Nov 2023 09:03:33 +0100 Subject: [PATCH 080/329] Optional props --- src/components/RadioButton.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/RadioButton.tsx b/src/components/RadioButton.tsx index 1819975bb1cf..ee6c478408ec 100644 --- a/src/components/RadioButton.tsx +++ b/src/components/RadioButton.tsx @@ -40,13 +40,13 @@ type RadioButtonProps = { accessibilityLabel: string; /** Should the input be styled for errors */ - hasError: boolean; + hasError?: boolean; /** Should the input be disabled */ - disabled: boolean; + disabled?: boolean; } -function RadioButton({accessibilityLabel, disabled = false, hasError = false, isChecked, onPress}: RadioButtonProps) { +function RadioButton({isChecked, onPress = () => undefined, accessibilityLabel, disabled = false, hasError = false}: RadioButtonProps) { return ( Date: Wed, 8 Nov 2023 09:03:56 +0100 Subject: [PATCH 081/329] Drop propTypes and defaultProps --- src/components/RadioButton.tsx | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/components/RadioButton.tsx b/src/components/RadioButton.tsx index ee6c478408ec..b2a37488fd72 100644 --- a/src/components/RadioButton.tsx +++ b/src/components/RadioButton.tsx @@ -1,34 +1,11 @@ import React from 'react'; import {View} from 'react-native'; -import PropTypes from 'prop-types'; import styles from '../styles/styles'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import CONST from '../CONST'; -const propTypes = { - /** Whether radioButton is checked */ - isChecked: PropTypes.bool.isRequired, - - /** A function that is called when the box/label is pressed */ - onPress: PropTypes.func.isRequired, - - /** Specifies the accessibility label for the radio button */ - accessibilityLabel: PropTypes.string.isRequired, - - /** Should the input be styled for errors */ - hasError: PropTypes.bool, - - /** Should the input be disabled */ - disabled: PropTypes.bool, -}; - -const defaultProps = { - hasError: false, - disabled: false, -}; - type RadioButtonProps = { /** Whether radioButton is checked */ isChecked: boolean; From a08af4fc629a27fecdeabe7ea3101bc6248ffb2e Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Wed, 8 Nov 2023 11:21:58 +0100 Subject: [PATCH 082/329] Fix tests Fixed tests for preDeploy after two of the jobs have been removed See: https://github.com/Expensify/App/issues/13604 --- .../assertions/preDeployAssertions.js | 87 ------ workflow_tests/mocks/preDeployMocks.js | 98 ------ workflow_tests/preDeploy.test.js | 292 ------------------ 3 files changed, 477 deletions(-) diff --git a/workflow_tests/assertions/preDeployAssertions.js b/workflow_tests/assertions/preDeployAssertions.js index 90d6f9febb75..1ed7d52bd53f 100644 --- a/workflow_tests/assertions/preDeployAssertions.js +++ b/workflow_tests/assertions/preDeployAssertions.js @@ -36,91 +36,6 @@ const assertTestJobExecuted = (workflowResult, didExecute = true) => { }); }; -const assertIsExpensifyEmployeeJobExecuted = (workflowResult, didExecute = true) => { - const steps = [ - utils.createStepAssertion('Get merged pull request', true, null, 'IS_EXPENSIFY_EMPLOYEE', 'Getting merged pull request', [{key: 'github_token', value: '***'}]), - utils.createStepAssertion( - 'Check whether the PR author is member of Expensify/expensify team', - true, - null, - 'IS_EXPENSIFY_EMPLOYEE', - 'Checking actors Expensify membership', - [], - [{key: 'GITHUB_TOKEN', value: '***'}], - ), - ]; - - steps.forEach((expectedStep) => { - if (didExecute) { - expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); - } else { - expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); - } - }); -}; - -const assertNewContributorWelcomeMessageJobExecuted = (workflowResult, didExecute = true, isOsBotify = false, isFirstPr = false) => { - const steps = [ - utils.createStepAssertion('Checkout', true, null, 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', 'Checking out', [{key: 'token', value: '***'}]), - utils.createStepAssertion('Get merged pull request', true, null, 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', 'Getting merged pull request', [{key: 'github_token', value: '***'}]), - utils.createStepAssertion(isOsBotify ? 'Get PR count for OSBotify' : 'Get PR count for Dummy Author', true, null, 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', 'Getting PR count', [ - {key: 'GITHUB_TOKEN', value: '***'}, - ]), - ]; - const osBotifyBody = - '@OSBotify, Great job getting your first Expensify/App pull request over the finish line! ' + - ":tada:\n\nI know there's a lot of information in our " + - '[contributing guidelines](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md), ' + - 'so here are some points to take note of :memo::\n\n1. Now that your first PR has been merged, you can be ' + - "hired for another issue. Once you've completed a few issues, you may be eligible to work on more than one " + - 'job at a time.\n2. Once your PR is deployed to our staging servers, it will undergo quality assurance (QA) ' + - "testing. If we find that it doesn't work as expected or causes a regression, you'll be responsible for " + - 'fixing it. Typically, we would revert this PR and give you another chance to create a similar PR without ' + - 'causing a regression.\n3. Once your PR is deployed to _production_, we start a 7-day timer :alarm_clock:. ' + - 'After it has been on production for 7 days without causing any regressions, then we pay out the Upwork job. ' + - ":moneybag:\n\nSo it might take a while before you're paid for your work, but we typically post multiple " + - "new jobs every day, so there's plenty of opportunity. I hope you've had a positive experience " + - 'contributing to this repo! :blush:'; - const userBody = - '@Dummy Author, Great job getting your first Expensify/App pull request over the finish ' + - "line! :tada:\n\nI know there's a lot of information in our " + - '[contributing guidelines](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md), ' + - 'so here are some points to take note of :memo::\n\n1. Now that your first PR has been merged, you can be ' + - "hired for another issue. Once you've completed a few issues, you may be eligible to work on more than one " + - 'job at a time.\n2. Once your PR is deployed to our staging servers, it will undergo quality assurance (QA) ' + - "testing. If we find that it doesn't work as expected or causes a regression, you'll be responsible for " + - 'fixing it. Typically, we would revert this PR and give you another chance to create a similar PR without ' + - 'causing a regression.\n3. Once your PR is deployed to _production_, we start a 7-day timer :alarm_clock:. ' + - 'After it has been on production for 7 days without causing any regressions, then we pay out the Upwork ' + - "job. :moneybag:\n\nSo it might take a while before you're paid for your work, but we typically post " + - "multiple new jobs every day, so there's plenty of opportunity. I hope you've had a positive experience " + - 'contributing to this repo! :blush:'; - if (isFirstPr) { - steps.push( - utils.createStepAssertion( - isOsBotify ? "Comment on OSBotify\\'s first pull request!" : "Comment on Dummy Author\\'s first pull request!", - true, - null, - 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', - 'Creating comment', - [ - {key: 'github_token', value: '***'}, - {key: 'number', value: '12345'}, - {key: 'body', value: isOsBotify ? osBotifyBody : userBody}, - ], - ), - ); - } - - steps.forEach((expectedStep) => { - if (didExecute) { - expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); - } else { - expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); - } - }); -}; - const assertChooseDeployActionsJobExecuted = (workflowResult, didExecute = true) => { const steps = [ utils.createStepAssertion('Get merged pull request', true, null, 'CHOOSE_DEPLOY_ACTIONS', 'Getting merged pull request', [{key: 'github_token', value: '***'}]), @@ -209,8 +124,6 @@ module.exports = { assertTypecheckJobExecuted, assertLintJobExecuted, assertTestJobExecuted, - assertIsExpensifyEmployeeJobExecuted, - assertNewContributorWelcomeMessageJobExecuted, assertChooseDeployActionsJobExecuted, assertSkipDeployJobExecuted, assertCreateNewVersionJobExecuted, diff --git a/workflow_tests/mocks/preDeployMocks.js b/workflow_tests/mocks/preDeployMocks.js index 3dc67a904cf9..daadf3d0c743 100644 --- a/workflow_tests/mocks/preDeployMocks.js +++ b/workflow_tests/mocks/preDeployMocks.js @@ -86,99 +86,6 @@ const UPDATE_STAGING_JOB_MOCK_STEPS = [ ANNOUNCE_FAILED_WORKFLOW_IN_SLACK_MOCK_STEP, ]; -// is_expensify_employee -const GET_MERGED_PULL_REQUEST_MOCK_STEP__IS_EXPENSIFY_EMPLOYEE = utils.createMockStep( - 'Get merged pull request', - 'Getting merged pull request', - 'IS_EXPENSIFY_EMPLOYEE', - ['github_token'], - null, - {author: 'Dummy Author'}, -); -const CHECK_TEAM_MEMBERSHIP_MOCK_STEP__TRUE = utils.createMockStep( - 'Check whether the PR author is member of Expensify/expensify team', - 'Checking actors Expensify membership', - 'IS_EXPENSIFY_EMPLOYEE', - [], - ['GITHUB_TOKEN'], - {IS_EXPENSIFY_EMPLOYEE: true}, -); -const IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE = [GET_MERGED_PULL_REQUEST_MOCK_STEP__IS_EXPENSIFY_EMPLOYEE, CHECK_TEAM_MEMBERSHIP_MOCK_STEP__TRUE]; -const CHECK_TEAM_MEMBERSHIP_MOCK_STEP__FALSE = utils.createMockStep( - 'Check whether the PR author is member of Expensify/expensify team', - 'Checking actors Expensify membership', - 'IS_EXPENSIFY_EMPLOYEE', - [], - ['GITHUB_TOKEN'], - {IS_EXPENSIFY_EMPLOYEE: false}, -); -const IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__FALSE = [GET_MERGED_PULL_REQUEST_MOCK_STEP__IS_EXPENSIFY_EMPLOYEE, CHECK_TEAM_MEMBERSHIP_MOCK_STEP__FALSE]; - -// new_contributor_welcome_message -const CHECKOUT_MOCK_STEP = utils.createMockStep('Checkout', 'Checking out', 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', ['token'], null, {author: 'Dummy Author'}); -const CHECKOUT_MOCK_STEP__OSBOTIFY = utils.createMockStep('Checkout', 'Checking out', 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', ['token'], null, {author: 'OSBotify'}); -const GET_MERGED_PULL_REQUEST_MOCK_STEP__WELCOME_MESSAGE = utils.createMockStep( - 'Get merged pull request', - 'Getting merged pull request', - 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', - ['github_token'], - null, - {number: '12345', author: 'Dummy Author'}, -); -const GET_MERGED_PULL_REQUEST_MOCK_STEP__WELCOME_MESSAGE__OSBOTIFY = utils.createMockStep( - 'Get merged pull request', - 'Getting merged pull request', - 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', - ['github_token'], - null, - {number: '12345', author: 'OSBotify'}, -); -const GET_PR_COUNT_MOCK_STEP__1 = utils.createMockStep( - // eslint-disable-next-line no-template-curly-in-string - 'Get PR count for ${{ steps.getMergedPullRequest.outputs.author }}', - 'Getting PR count', - 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', - null, - ['GITHUB_TOKEN'], - null, - {PR_COUNT: '1'}, -); -const GET_PR_COUNT_MOCK_STEP__10 = utils.createMockStep( - // eslint-disable-next-line no-template-curly-in-string - 'Get PR count for ${{ steps.getMergedPullRequest.outputs.author }}', - 'Getting PR count', - 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', - null, - ['GITHUB_TOKEN'], - null, - {PR_COUNT: '10'}, -); -const COMMENT_ON_FIRST_PULL_REQUEST_MOCK_STEP = utils.createMockStep( - // eslint-disable-next-line no-template-curly-in-string - "Comment on ${{ steps.getMergedPullRequest.outputs.author }}\\'s first pull request!", - 'Creating comment', - 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', - ['github_token', 'number', 'body'], -); -const NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS = [ - CHECKOUT_MOCK_STEP, - GET_MERGED_PULL_REQUEST_MOCK_STEP__WELCOME_MESSAGE, - GET_PR_COUNT_MOCK_STEP__10, - COMMENT_ON_FIRST_PULL_REQUEST_MOCK_STEP, -]; -const NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__ONE_PR = [ - CHECKOUT_MOCK_STEP, - GET_MERGED_PULL_REQUEST_MOCK_STEP__WELCOME_MESSAGE, - GET_PR_COUNT_MOCK_STEP__1, - COMMENT_ON_FIRST_PULL_REQUEST_MOCK_STEP, -]; -const NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__OSBOTIFY = [ - CHECKOUT_MOCK_STEP__OSBOTIFY, - GET_MERGED_PULL_REQUEST_MOCK_STEP__WELCOME_MESSAGE__OSBOTIFY, - GET_PR_COUNT_MOCK_STEP__10, - COMMENT_ON_FIRST_PULL_REQUEST_MOCK_STEP, -]; - const PREDEPLOY__E2EPERFORMANCETESTS__PERFORM_E2E_TESTS__MOCK_STEP = utils.createMockStep('Perform E2E tests', 'Perform E2E tests', 'E2EPERFORMANCETESTS'); const PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS = [PREDEPLOY__E2EPERFORMANCETESTS__PERFORM_E2E_TESTS__MOCK_STEP]; @@ -192,10 +99,5 @@ module.exports = { SKIP_DEPLOY_JOB_MOCK_STEPS, CREATE_NEW_VERSION_JOB_MOCK_STEPS, UPDATE_STAGING_JOB_MOCK_STEPS, - IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, - IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__FALSE, - NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, - NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__ONE_PR, - NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__OSBOTIFY, PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, }; diff --git a/workflow_tests/preDeploy.test.js b/workflow_tests/preDeploy.test.js index 4a4d9dcc82bb..0d5b126b4430 100644 --- a/workflow_tests/preDeploy.test.js +++ b/workflow_tests/preDeploy.test.js @@ -68,8 +68,6 @@ describe('test workflow preDeploy', () => { chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, }; const testMockJobs = { typecheck: { @@ -112,7 +110,6 @@ describe('test workflow preDeploy', () => { assertions.assertTypecheckJobExecuted(result); assertions.assertLintJobExecuted(result); assertions.assertTestJobExecuted(result); - assertions.assertIsExpensifyEmployeeJobExecuted(result); assertions.assertChooseDeployActionsJobExecuted(result); assertions.assertSkipDeployJobExecuted(result, false); assertions.assertCreateNewVersionJobExecuted(result); @@ -128,8 +125,6 @@ describe('test workflow preDeploy', () => { chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, }; const testMockJobs = { typecheck: { @@ -180,7 +175,6 @@ describe('test workflow preDeploy', () => { assertions.assertTypecheckJobExecuted(result, false); assertions.assertLintJobExecuted(result, false); assertions.assertTestJobExecuted(result, false); - assertions.assertIsExpensifyEmployeeJobExecuted(result, false); assertions.assertChooseDeployActionsJobExecuted(result, false); assertions.assertSkipDeployJobExecuted(result, false); assertions.assertCreateNewVersionJobExecuted(result, false); @@ -208,7 +202,6 @@ describe('test workflow preDeploy', () => { assertions.assertTypecheckJobExecuted(result, false); assertions.assertLintJobExecuted(result, false); assertions.assertTestJobExecuted(result, false); - assertions.assertIsExpensifyEmployeeJobExecuted(result, false); assertions.assertChooseDeployActionsJobExecuted(result, false); assertions.assertSkipDeployJobExecuted(result, false); assertions.assertCreateNewVersionJobExecuted(result, false); @@ -236,8 +229,6 @@ describe('test workflow preDeploy', () => { chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, }; const testMockJobs = { typecheck: { @@ -277,7 +268,6 @@ describe('test workflow preDeploy', () => { ); assertions.assertLintJobExecuted(result); assertions.assertTestJobExecuted(result); - assertions.assertIsExpensifyEmployeeJobExecuted(result); expect(result).toEqual( expect.arrayContaining([ utils.createStepAssertion('Announce failed workflow in Slack', true, null, 'CONFIRM_PASSING_BUILD', 'Announcing failed workflow in slack', [ @@ -312,8 +302,6 @@ describe('test workflow preDeploy', () => { chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, }; const testMockJobs = { typecheck: { @@ -351,7 +339,6 @@ describe('test workflow preDeploy', () => { assertions.assertTypecheckJobExecuted(result); expect(result).toEqual(expect.arrayContaining([utils.createStepAssertion('Run lint workflow', false, null, 'LINT', 'Running lint workflow - Lint workflow failed')])); assertions.assertTestJobExecuted(result); - assertions.assertIsExpensifyEmployeeJobExecuted(result); expect(result).toEqual( expect.arrayContaining([ utils.createStepAssertion('Announce failed workflow in Slack', true, null, 'CONFIRM_PASSING_BUILD', 'Announcing failed workflow in slack', [ @@ -386,8 +373,6 @@ describe('test workflow preDeploy', () => { chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, }; const testMockJobs = { typecheck: { @@ -425,7 +410,6 @@ describe('test workflow preDeploy', () => { assertions.assertTypecheckJobExecuted(result); assertions.assertLintJobExecuted(result); expect(result).toEqual(expect.arrayContaining([utils.createStepAssertion('Run test workflow', false, null, 'TEST', 'Running test workflow - Test workflow failed')])); - assertions.assertIsExpensifyEmployeeJobExecuted(result); expect(result).toEqual( expect.arrayContaining([ utils.createStepAssertion('Announce failed workflow in Slack', true, null, 'CONFIRM_PASSING_BUILD', 'Announcing failed workflow in slack', [ @@ -460,8 +444,6 @@ describe('test workflow preDeploy', () => { chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, }; const testMockJobs = { typecheck: { @@ -499,7 +481,6 @@ describe('test workflow preDeploy', () => { assertions.assertTypecheckJobExecuted(result); assertions.assertLintJobExecuted(result); assertions.assertTestJobExecuted(result); - assertions.assertIsExpensifyEmployeeJobExecuted(result); assertions.assertChooseDeployActionsJobExecuted(result); assertions.assertSkipDeployJobExecuted(result, false); assertions.assertCreateNewVersionJobExecuted(result); @@ -507,264 +488,6 @@ describe('test workflow preDeploy', () => { }); }); - describe('new contributor welcome message', () => { - it('actor is OSBotify - no comment left', async () => { - const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; - const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); - let act = new eAct.ExtendedAct(repoPath, workflowPath); - act = utils.setUpActParams( - act, - 'push', - {ref: 'refs/heads/main'}, - { - OS_BOTIFY_TOKEN: 'dummy_token', - SLACK_WEBHOOK: 'dummy_slack_webhook', - LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - }, - 'dummy_github_token', - ); - const testMockSteps = { - confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, - chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, - skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, - updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__FALSE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__OSBOTIFY, - }; - const testMockJobs = { - typecheck: { - steps: mocks.TYPECHECK_JOB_MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - lint: { - steps: mocks.LINT_JOB_MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - test: { - steps: mocks.TEST_JOB_MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - createNewVersion: { - steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, - outputs: { - // eslint-disable-next-line no-template-curly-in-string - NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', - }, - runsOn: 'ubuntu-latest', - }, - e2ePerformanceTests: { - steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - }; - const result = await act.runEvent('push', { - workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), - mockSteps: testMockSteps, - actor: 'OSBotify', - logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), - mockJobs: testMockJobs, - }); - assertions.assertTypecheckJobExecuted(result); - assertions.assertLintJobExecuted(result); - assertions.assertTestJobExecuted(result); - assertions.assertIsExpensifyEmployeeJobExecuted(result); - assertions.assertChooseDeployActionsJobExecuted(result); - assertions.assertNewContributorWelcomeMessageJobExecuted(result, false); - }); - - it('actor is Expensify employee - no comment left', async () => { - const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; - const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); - let act = new eAct.ExtendedAct(repoPath, workflowPath); - act = utils.setUpActParams( - act, - 'push', - {ref: 'refs/heads/main'}, - { - OS_BOTIFY_TOKEN: 'dummy_token', - SLACK_WEBHOOK: 'dummy_slack_webhook', - LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - }, - 'dummy_github_token', - ); - const testMockSteps = { - confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, - chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, - skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, - updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, - }; - const testMockJobs = { - typecheck: { - steps: mocks.TYPECHECK_JOB_MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - lint: { - steps: mocks.LINT_JOB_MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - test: { - steps: mocks.TEST_JOB_MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - createNewVersion: { - steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, - outputs: { - // eslint-disable-next-line no-template-curly-in-string - NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', - }, - runsOn: 'ubuntu-latest', - }, - e2ePerformanceTests: { - steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - }; - const result = await act.runEvent('push', { - workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), - mockSteps: testMockSteps, - actor: 'Dummy Tester', - logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), - mockJobs: testMockJobs, - }); - assertions.assertTypecheckJobExecuted(result); - assertions.assertLintJobExecuted(result); - assertions.assertTestJobExecuted(result); - assertions.assertIsExpensifyEmployeeJobExecuted(result); - assertions.assertChooseDeployActionsJobExecuted(result); - assertions.assertNewContributorWelcomeMessageJobExecuted(result, false); - }); - - it('actor is not Expensify employee, its not their first PR - job triggers, but no comment left', async () => { - const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; - const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); - let act = new eAct.ExtendedAct(repoPath, workflowPath); - act = utils.setUpActParams( - act, - 'push', - {ref: 'refs/heads/main'}, - { - OS_BOTIFY_TOKEN: 'dummy_token', - SLACK_WEBHOOK: 'dummy_slack_webhook', - LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - }, - 'dummy_github_token', - ); - const testMockSteps = { - confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, - chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, - skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, - updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__FALSE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, - }; - const testMockJobs = { - typecheck: { - steps: mocks.TYPECHECK_JOB_MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - lint: { - steps: mocks.LINT_JOB_MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - test: { - steps: mocks.TEST_JOB_MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - createNewVersion: { - steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, - outputs: { - // eslint-disable-next-line no-template-curly-in-string - NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', - }, - runsOn: 'ubuntu-latest', - }, - e2ePerformanceTests: { - steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - }; - const result = await act.runEvent('push', { - workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), - mockSteps: testMockSteps, - actor: 'Dummy Tester', - logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), - mockJobs: testMockJobs, - }); - assertions.assertTypecheckJobExecuted(result); - assertions.assertLintJobExecuted(result); - assertions.assertTestJobExecuted(result); - assertions.assertIsExpensifyEmployeeJobExecuted(result); - assertions.assertChooseDeployActionsJobExecuted(result); - assertions.assertNewContributorWelcomeMessageJobExecuted(result, true, false, false); - }); - - it('actor is not Expensify employee, and its their first PR - job triggers and comment left', async () => { - const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; - const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); - let act = new eAct.ExtendedAct(repoPath, workflowPath); - act = utils.setUpActParams( - act, - 'push', - {ref: 'refs/heads/main'}, - { - OS_BOTIFY_TOKEN: 'dummy_token', - SLACK_WEBHOOK: 'dummy_slack_webhook', - LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - }, - 'dummy_github_token', - ); - const testMockSteps = { - confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, - chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, - skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, - updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__FALSE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__ONE_PR, - }; - const testMockJobs = { - typecheck: { - steps: mocks.TYPECHECK_JOB_MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - lint: { - steps: mocks.LINT_JOB_MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - test: { - steps: mocks.TEST_JOB_MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - createNewVersion: { - steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, - outputs: { - // eslint-disable-next-line no-template-curly-in-string - NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', - }, - runsOn: 'ubuntu-latest', - }, - e2ePerformanceTests: { - steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, - runsOn: 'ubuntu-latest', - }, - }; - const result = await act.runEvent('push', { - workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), - mockSteps: testMockSteps, - actor: 'Dummy Tester', - logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), - mockJobs: testMockJobs, - }); - assertions.assertTypecheckJobExecuted(result); - assertions.assertLintJobExecuted(result); - assertions.assertTestJobExecuted(result); - assertions.assertIsExpensifyEmployeeJobExecuted(result); - assertions.assertChooseDeployActionsJobExecuted(result); - assertions.assertNewContributorWelcomeMessageJobExecuted(result, true, false, true); - }); - }); - describe('choose deploy actions', () => { describe('staging locked', () => { it('not automated PR - deploy skipped and comment left', async () => { @@ -777,8 +500,6 @@ describe('test workflow preDeploy', () => { chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_LOCKED, skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, }; const testMockJobs = { typecheck: { @@ -816,7 +537,6 @@ describe('test workflow preDeploy', () => { assertions.assertTypecheckJobExecuted(result); assertions.assertLintJobExecuted(result); assertions.assertTestJobExecuted(result); - assertions.assertIsExpensifyEmployeeJobExecuted(result); assertions.assertChooseDeployActionsJobExecuted(result); assertions.assertSkipDeployJobExecuted(result); assertions.assertCreateNewVersionJobExecuted(result, false); @@ -834,8 +554,6 @@ describe('test workflow preDeploy', () => { chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_LOCKED, skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__OSBOTIFY, }; const testMockJobs = { typecheck: { @@ -873,7 +591,6 @@ describe('test workflow preDeploy', () => { assertions.assertTypecheckJobExecuted(result); assertions.assertLintJobExecuted(result); assertions.assertTestJobExecuted(result); - assertions.assertIsExpensifyEmployeeJobExecuted(result); assertions.assertChooseDeployActionsJobExecuted(result); assertions.assertSkipDeployJobExecuted(result, false); assertions.assertCreateNewVersionJobExecuted(result, false); @@ -903,8 +620,6 @@ describe('test workflow preDeploy', () => { chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, }; const testMockJobs = { typecheck: { @@ -942,7 +657,6 @@ describe('test workflow preDeploy', () => { assertions.assertTypecheckJobExecuted(result); assertions.assertLintJobExecuted(result); assertions.assertTestJobExecuted(result); - assertions.assertIsExpensifyEmployeeJobExecuted(result); assertions.assertChooseDeployActionsJobExecuted(result); assertions.assertSkipDeployJobExecuted(result, false); assertions.assertCreateNewVersionJobExecuted(result); @@ -969,8 +683,6 @@ describe('test workflow preDeploy', () => { chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__OSBOTIFY, }; const testMockJobs = { typecheck: { @@ -1008,7 +720,6 @@ describe('test workflow preDeploy', () => { assertions.assertTypecheckJobExecuted(result); assertions.assertLintJobExecuted(result); assertions.assertTestJobExecuted(result); - assertions.assertIsExpensifyEmployeeJobExecuted(result); assertions.assertChooseDeployActionsJobExecuted(result); assertions.assertSkipDeployJobExecuted(result, false); assertions.assertCreateNewVersionJobExecuted(result, false); @@ -1037,8 +748,6 @@ describe('test workflow preDeploy', () => { chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, - isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, - newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, }; testMockSteps.updateStaging[3].mockWith = 'exit 1'; const testMockJobs = { @@ -1077,7 +786,6 @@ describe('test workflow preDeploy', () => { assertions.assertTypecheckJobExecuted(result); assertions.assertLintJobExecuted(result); assertions.assertTestJobExecuted(result); - assertions.assertIsExpensifyEmployeeJobExecuted(result); assertions.assertChooseDeployActionsJobExecuted(result); assertions.assertSkipDeployJobExecuted(result, false); assertions.assertCreateNewVersionJobExecuted(result); From a6dafa666291ac47fd9c13753da827e7fe43ac70 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Wed, 8 Nov 2023 11:35:24 +0100 Subject: [PATCH 083/329] Fix tests Fixed tests for testBuild after one of the messages have been updated See: https://github.com/Expensify/App/issues/13604 --- workflow_tests/assertions/testBuildAssertions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow_tests/assertions/testBuildAssertions.js b/workflow_tests/assertions/testBuildAssertions.js index a4f7a46259ad..f65349593faa 100644 --- a/workflow_tests/assertions/testBuildAssertions.js +++ b/workflow_tests/assertions/testBuildAssertions.js @@ -365,7 +365,7 @@ const assertPostGithubCommentJobExecuted = ( 'maintain-comment', [ {key: 'token', value: '***'}, - {key: 'body-include', value: 'Use the links below to test this build in android and iOS. Happy testing!'}, + {key: 'body-include', value: 'Use the links below to test this adhoc build in Android, iOS, Desktop, and Web. Happy testing!'}, {key: 'number', value: pullRequestNumber}, {key: 'delete', value: true}, ], From a68fb46c268c2e14a7f62432e70f20586c249963 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Wed, 8 Nov 2023 11:42:31 +0100 Subject: [PATCH 084/329] Fix tests Fixed tests for cherryPick after the way github token is passed to a step has changed See: https://github.com/Expensify/App/issues/13604 --- workflow_tests/assertions/cherryPickAssertions.js | 2 +- workflow_tests/mocks/cherryPickMocks.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/workflow_tests/assertions/cherryPickAssertions.js b/workflow_tests/assertions/cherryPickAssertions.js index 42ecc3d64262..1fe1194cf7b6 100644 --- a/workflow_tests/assertions/cherryPickAssertions.js +++ b/workflow_tests/assertions/cherryPickAssertions.js @@ -76,7 +76,7 @@ const assertCherryPickJobExecuted = (workflowResult, user = 'Dummy Author', pull 'CHERRYPICK', 'Creating Pull Request to manually finish CP', [], - [{key: 'GITHUB_TOKEN', value: '***'}], + [{key: 'GITHUB_TOKEN', value: 'os_botify_api_token'}], ), ]; diff --git a/workflow_tests/mocks/cherryPickMocks.js b/workflow_tests/mocks/cherryPickMocks.js index 778e6fd48ded..8531d9783172 100644 --- a/workflow_tests/mocks/cherryPickMocks.js +++ b/workflow_tests/mocks/cherryPickMocks.js @@ -36,7 +36,7 @@ const CHERRYPICK__CREATENEWVERSION__STEP_MOCKS = [CHERRYPICK__CREATENEWVERSION__ // cherrypick const CHERRYPICK__CHERRYPICK__CHECKOUT_STAGING_BRANCH__STEP_MOCK = utils.createMockStep('Checkout staging branch', 'Checking out staging branch', 'CHERRYPICK', ['ref', 'token'], []); -const CHERRYPICK__CHERRYPICK__SET_UP_GIT_FOR_OSBOTIFY__STEP_MOCK = utils.createMockStep('Set up git for OSBotify', 'Setting up git for OSBotify', 'CHERRYPICK', ['GPG_PASSPHRASE'], []); +const CHERRYPICK__CHERRYPICK__SET_UP_GIT_FOR_OSBOTIFY__STEP_MOCK = utils.createMockStep('Set up git for OSBotify', 'Setting up git for OSBotify', 'CHERRYPICK', ['GPG_PASSPHRASE'], [], {OS_BOTIFY_API_TOKEN: 'os_botify_api_token'}); const CHERRYPICK__CHERRYPICK__GET_PREVIOUS_APP_VERSION__STEP_MOCK = utils.createMockStep('Get previous app version', 'Get previous app version', 'CHERRYPICK', ['SEMVER_LEVEL']); const CHERRYPICK__CHERRYPICK__FETCH_HISTORY_OF_RELEVANT_REFS__STEP_MOCK = utils.createMockStep('Fetch history of relevant refs', 'Fetch history of relevant refs', 'CHERRYPICK'); const CHERRYPICK__CHERRYPICK__GET_VERSION_BUMP_COMMIT__STEP_MOCK = utils.createMockStep('Get version bump commit', 'Get version bump commit', 'CHERRYPICK', [], [], { From 8ae79877f3c2298374c9851beb47731f8b3cc097 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Wed, 8 Nov 2023 11:51:52 +0100 Subject: [PATCH 085/329] Fix tests Fixed tests for platformDeploy after 2 new steps have been added See: https://github.com/Expensify/App/issues/13604 --- workflow_tests/assertions/platformDeployAssertions.js | 10 +++++++++- workflow_tests/mocks/platformDeployMocks.js | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/workflow_tests/assertions/platformDeployAssertions.js b/workflow_tests/assertions/platformDeployAssertions.js index 35242cd24d31..63b5b11ba1ab 100644 --- a/workflow_tests/assertions/platformDeployAssertions.js +++ b/workflow_tests/assertions/platformDeployAssertions.js @@ -63,6 +63,10 @@ const assertAndroidJobExecuted = (workflowResult, didExecute = true, isProductio ); if (!isProduction) { steps.push( + utils.createStepAssertion('Upload Android version to GitHub artifacts', true, null, 'ANDROID', 'Upload Android version to GitHub artifacts', [ + {key: 'name', value: 'app-production-release.aab'}, + {key: 'path', value: 'android/app/build/outputs/bundle/productionRelease/app-production-release.aab'}, + ]), utils.createStepAssertion('Upload Android version to Browser Stack', true, null, 'ANDROID', 'Uploading Android version to Browser Stack', null, [ {key: 'BROWSERSTACK', value: '***'}, ]), @@ -183,7 +187,11 @@ const assertIOSJobExecuted = (workflowResult, didExecute = true, isProduction = ]), ); if (!isProduction) { - steps.push(utils.createStepAssertion('Upload iOS version to Browser Stack', true, null, 'IOS', 'Uploading version to Browser Stack', null, [{key: 'BROWSERSTACK', value: '***'}])); + steps.push( + utils.createStepAssertion('Upload iOS version to GitHub artifacts', true, null, 'IOS', 'Upload iOS version to GitHub artifacts', [ + {key: 'name', value: 'New Expensify.ipa'}, + {key: 'path', value: '/Users/runner/work/App/App/New Expensify.ipa'}, + ]), utils.createStepAssertion('Upload iOS version to Browser Stack', true, null, 'IOS', 'Uploading version to Browser Stack', null, [{key: 'BROWSERSTACK', value: '***'}])); } else { steps.push( utils.createStepAssertion('Set iOS version in ENV', true, null, 'IOS', 'Setting iOS version'), diff --git a/workflow_tests/mocks/platformDeployMocks.js b/workflow_tests/mocks/platformDeployMocks.js index 9e0b91b29156..d660d057259d 100644 --- a/workflow_tests/mocks/platformDeployMocks.js +++ b/workflow_tests/mocks/platformDeployMocks.js @@ -51,6 +51,7 @@ const PLATFORM_DEPLOY__ANDROID__FASTLANE_BETA__STEP_MOCK = utils.createMockStep( ]); const PLATFORM_DEPLOY__ANDROID__FASTLANE_PRODUCTION__STEP_MOCK = utils.createMockStep('Run Fastlane production', 'Running Fastlane production', 'ANDROID', null, ['VERSION']); const PLATFORM_DEPLOY__ANDROID__ARCHIVE_SOURCEMAPS__STEP_MOCK = utils.createMockStep('Archive Android sourcemaps', 'Archiving Android sourcemaps', 'ANDROID', ['name', 'path']); +const PLATFORM_DEPLOY__ANDROID__UPLOAD_ANDROID_VERSION_TO_GITHUB_ARTIFACTS__STEP_MOCK = utils.createMockStep('Upload Android version to GitHub artifacts', 'Upload Android version to GitHub artifacts', 'ANDROID', ['name', 'path']); const PLATFORM_DEPLOY__ANDROID__UPLOAD_TO_BROWSER_STACK__STEP_MOCK = utils.createMockStep( 'Upload Android version to Browser Stack', 'Uploading Android version to Browser Stack', @@ -76,6 +77,7 @@ const PLATFORM_DEPLOY__ANDROID__STEP_MOCKS = [ PLATFORM_DEPLOY__ANDROID__FASTLANE_BETA__STEP_MOCK, PLATFORM_DEPLOY__ANDROID__FASTLANE_PRODUCTION__STEP_MOCK, PLATFORM_DEPLOY__ANDROID__ARCHIVE_SOURCEMAPS__STEP_MOCK, + PLATFORM_DEPLOY__ANDROID__UPLOAD_ANDROID_VERSION_TO_GITHUB_ARTIFACTS__STEP_MOCK, PLATFORM_DEPLOY__ANDROID__UPLOAD_TO_BROWSER_STACK__STEP_MOCK, PLATFORM_DEPLOY__ANDROID__WARN_DEPLOYERS__STEP_MOCK, ]; @@ -139,6 +141,7 @@ const PLATFORM_DEPLOY__IOS__FASTLANE__STEP_MOCK = utils.createMockStep('Run Fast 'APPLE_DEMO_PASSWORD', ]); const PLATFORM_DEPLOY__IOS__ARCHIVE_SOURCEMAPS__STEP_MOCK = utils.createMockStep('Archive iOS sourcemaps', 'Archiving sourcemaps', 'IOS', ['name', 'path']); +const PLATFORM_DEPLOY__IOS__UPLOAD_IOS_VERSION_TO_GITHUB_ARTIFACTS__STEP_MOCK = utils.createMockStep('Upload iOS version to GitHub artifacts', 'Upload iOS version to GitHub artifacts', 'IOS', ['name', 'path']); const PLATFORM_DEPLOY__IOS__UPLOAD_BROWSERSTACK__STEP_MOCK = utils.createMockStep('Upload iOS version to Browser Stack', 'Uploading version to Browser Stack', 'IOS', null, ['BROWSERSTACK']); const PLATFORM_DEPLOY__IOS__SET_VERSION__STEP_MOCK = utils.createMockStep('Set iOS version in ENV', 'Setting iOS version', 'IOS', null, null, null, {IOS_VERSION: '1.2.3'}); const PLATFORM_DEPLOY__IOS__RELEASE_FASTLANE__STEP_MOCK = utils.createMockStep('Run Fastlane for App Store release', 'Running Fastlane for release', 'IOS', null, ['VERSION']); @@ -162,6 +165,7 @@ const PLATFORM_DEPLOY__IOS__STEP_MOCKS = [ PLATFORM_DEPLOY__IOS__DECRYPT_APP_STORE_API_KEY__STEP_MOCK, PLATFORM_DEPLOY__IOS__FASTLANE__STEP_MOCK, PLATFORM_DEPLOY__IOS__ARCHIVE_SOURCEMAPS__STEP_MOCK, + PLATFORM_DEPLOY__IOS__UPLOAD_IOS_VERSION_TO_GITHUB_ARTIFACTS__STEP_MOCK, PLATFORM_DEPLOY__IOS__UPLOAD_BROWSERSTACK__STEP_MOCK, PLATFORM_DEPLOY__IOS__SET_VERSION__STEP_MOCK, PLATFORM_DEPLOY__IOS__RELEASE_FASTLANE__STEP_MOCK, From 498cffc25d10d0a01d221cda37ec3879f9ff5933 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Wed, 8 Nov 2023 12:21:49 +0100 Subject: [PATCH 086/329] Fix tests Fixed tests for finishReleaseCycle after 2 new steps have been added and the way the token is passed has changed See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/finishReleaseCycle.yml | 3 +- .../finishReleaseCycleAssertions.js | 11 ++++--- workflow_tests/finishReleaseCycle.test.js | 31 +++++++------------ .../mocks/finishReleaseCycleMocks.js | 22 +++++++++++++ 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/.github/workflows/finishReleaseCycle.yml b/.github/workflows/finishReleaseCycle.yml index f8b68786aaab..8391204fac54 100644 --- a/.github/workflows/finishReleaseCycle.yml +++ b/.github/workflows/finishReleaseCycle.yml @@ -18,7 +18,8 @@ jobs: ref: main token: ${{ secrets.OS_BOTIFY_TOKEN }} - - uses: Expensify/App/.github/actions/composite/setupGitForOSBotifyApp@8c19d6da4a3d7ce3b15c9cd89a802187d208ecab + - name: Setup Git for OSBotify + uses: Expensify/App/.github/actions/composite/setupGitForOSBotifyApp@8c19d6da4a3d7ce3b15c9cd89a802187d208ecab id: setupGitForOSBotify with: GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} diff --git a/workflow_tests/assertions/finishReleaseCycleAssertions.js b/workflow_tests/assertions/finishReleaseCycleAssertions.js index 12a8b9f2b05d..96c473fca917 100644 --- a/workflow_tests/assertions/finishReleaseCycleAssertions.js +++ b/workflow_tests/assertions/finishReleaseCycleAssertions.js @@ -1,7 +1,10 @@ const utils = require('../utils/utils'); const assertValidateJobExecuted = (workflowResult, issueNumber = '', didExecute = true, isTeamMember = true, hasBlockers = false, isSuccessful = true) => { - const steps = [utils.createStepAssertion('Validate actor is deployer', true, null, 'VALIDATE', 'Validating if actor is deployer', [], [{key: 'GITHUB_TOKEN', value: '***'}])]; + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'VALIDATE', 'Checkout', [{key: 'ref', value: 'main'}, {key: 'token', value: '***'}]), + utils.createStepAssertion('Setup Git for OSBotify', true, null, 'VALIDATE', 'Setup Git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}, {key: 'OS_BOTIFY_APP_ID', value: '***'}, {key: 'OS_BOTIFY_PRIVATE_KEY', value: '***'}]), + utils.createStepAssertion('Validate actor is deployer', true, null, 'VALIDATE', 'Validating if actor is deployer', [], [{key: 'GITHUB_TOKEN', value: 'os_botify_api_token'}])]; if (isTeamMember) { steps.push( utils.createStepAssertion( @@ -11,7 +14,7 @@ const assertValidateJobExecuted = (workflowResult, issueNumber = '', didExecute 'VALIDATE', 'Checking for deploy blockers', [ - {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'GITHUB_TOKEN', value: 'os_botify_api_token'}, {key: 'ISSUE_NUMBER', value: issueNumber}, ], [], @@ -36,7 +39,7 @@ const assertValidateJobExecuted = (workflowResult, issueNumber = '', didExecute 'VALIDATE', 'Reopening issue - not a team member', [ - {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'GITHUB_TOKEN', value: 'os_botify_api_token'}, {key: 'ISSUE_NUMBER', value: issueNumber}, {key: 'COMMENT', value: 'Sorry, only members of @Expensify/Mobile-Deployers can close deploy checklists.\nReopening!'}, ], @@ -60,7 +63,7 @@ const assertValidateJobExecuted = (workflowResult, issueNumber = '', didExecute 'VALIDATE', 'Reopening issue - blockers', [ - {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'GITHUB_TOKEN', value: 'os_botify_api_token'}, {key: 'ISSUE_NUMBER', value: issueNumber}, ], [], diff --git a/workflow_tests/finishReleaseCycle.test.js b/workflow_tests/finishReleaseCycle.test.js index 7c17ca8d4122..26b4d2f60afc 100644 --- a/workflow_tests/finishReleaseCycle.test.js +++ b/workflow_tests/finishReleaseCycle.test.js @@ -38,6 +38,13 @@ describe('test workflow finishReleaseCycle', () => { afterEach(async () => { await mockGithub.teardown(); }); + const secrets = { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + SLACK_WEBHOOK: 'dummy_slack_webhook', + OS_BOTIFY_APP_ID: 'os_botify_app_id', + OS_BOTIFY_PRIVATE_KEY: 'os_botify_private_key', + }; describe('issue closed', () => { describe('issue has StagingDeployCash', () => { describe('actor is a team member', () => { @@ -57,11 +64,7 @@ describe('test workflow finishReleaseCycle', () => { number: '1234', }, }, - { - OS_BOTIFY_TOKEN: 'dummy_token', - LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - SLACK_WEBHOOK: 'dummy_slack_webhook', - }, + secrets, ); const testMockSteps = { validate: mocks.FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS, @@ -107,11 +110,7 @@ describe('test workflow finishReleaseCycle', () => { number: '1234', }, }, - { - OS_BOTIFY_TOKEN: 'dummy_token', - LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - SLACK_WEBHOOK: 'dummy_slack_webhook', - }, + secrets, ); const testMockSteps = { validate: mocks.FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_BLOCKERS__STEP_MOCKS, @@ -158,11 +157,7 @@ describe('test workflow finishReleaseCycle', () => { number: '1234', }, }, - { - OS_BOTIFY_TOKEN: 'dummy_token', - LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - SLACK_WEBHOOK: 'dummy_slack_webhook', - }, + secrets, ); const testMockSteps = { validate: mocks.FINISHRELEASECYCLE__VALIDATE__NOT_TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS, @@ -209,11 +204,7 @@ describe('test workflow finishReleaseCycle', () => { number: '1234', }, }, - { - OS_BOTIFY_TOKEN: 'dummy_token', - LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - SLACK_WEBHOOK: 'dummy_slack_webhook', - }, + secrets, ); const testMockSteps = { validate: mocks.FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS, diff --git a/workflow_tests/mocks/finishReleaseCycleMocks.js b/workflow_tests/mocks/finishReleaseCycleMocks.js index e1bb0d112429..551105705992 100644 --- a/workflow_tests/mocks/finishReleaseCycleMocks.js +++ b/workflow_tests/mocks/finishReleaseCycleMocks.js @@ -1,6 +1,20 @@ const utils = require('../utils/utils'); // validate +const FINISHRELEASECYCLE__VALIDATE__CHECKOUT__STEP_MOCK = utils.createMockStep( + 'Checkout', + 'Checkout', + 'VALIDATE', + ['ref', 'token'], +); +const FINISHRELEASECYCLE__VALIDATE__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK = utils.createMockStep( + 'Setup Git for OSBotify', + 'Setup Git for OSBotify', + 'VALIDATE', + ['GPG_PASSPHRASE', 'OS_BOTIFY_APP_ID', 'OS_BOTIFY_PRIVATE_KEY'], + [], + {OS_BOTIFY_API_TOKEN: 'os_botify_api_token'}, +); const FINISHRELEASECYCLE__VALIDATE__VALIDATE_ACTOR_IS_DEPLOYER_TRUE__STEP_MOCK = utils.createMockStep( 'Validate actor is deployer', 'Validating if actor is deployer', @@ -56,6 +70,8 @@ const FINISHRELEASECYCLE__VALIDATE__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK [], ); const FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS = [ + FINISHRELEASECYCLE__VALIDATE__CHECKOUT__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK, FINISHRELEASECYCLE__VALIDATE__VALIDATE_ACTOR_IS_DEPLOYER_TRUE__STEP_MOCK, FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_NOT_A_TEAM_MEMBER__STEP_MOCK, FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_FALSE__STEP_MOCK, @@ -63,6 +79,8 @@ const FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS = [ FINISHRELEASECYCLE__VALIDATE__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK, ]; const FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_BLOCKERS__STEP_MOCKS = [ + FINISHRELEASECYCLE__VALIDATE__CHECKOUT__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK, FINISHRELEASECYCLE__VALIDATE__VALIDATE_ACTOR_IS_DEPLOYER_TRUE__STEP_MOCK, FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_NOT_A_TEAM_MEMBER__STEP_MOCK, FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_TRUE__STEP_MOCK, @@ -71,6 +89,8 @@ const FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_BLOCKERS__STEP_MOCKS = [ ]; // eslint-disable-next-line rulesdir/no-negated-variables const FINISHRELEASECYCLE__VALIDATE__NOT_TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS = [ + FINISHRELEASECYCLE__VALIDATE__CHECKOUT__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK, FINISHRELEASECYCLE__VALIDATE__VALIDATE_ACTOR_IS_DEPLOYER_FALSE__STEP_MOCK, FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_NOT_A_TEAM_MEMBER__STEP_MOCK, FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_FALSE__STEP_MOCK, @@ -79,6 +99,8 @@ const FINISHRELEASECYCLE__VALIDATE__NOT_TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS = [ ]; // eslint-disable-next-line rulesdir/no-negated-variables const FINISHRELEASECYCLE__VALIDATE__NOT_TEAM_MEMBER_BLOCKERS__STEP_MOCKS = [ + FINISHRELEASECYCLE__VALIDATE__CHECKOUT__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK, FINISHRELEASECYCLE__VALIDATE__VALIDATE_ACTOR_IS_DEPLOYER_FALSE__STEP_MOCK, FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_NOT_A_TEAM_MEMBER__STEP_MOCK, FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_TRUE__STEP_MOCK, From 214a772209441ad9022ab2463547e746a14c8529 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Wed, 8 Nov 2023 12:30:02 +0100 Subject: [PATCH 087/329] Fix tests Fixed tests for createNewVersion after new secrets have been added and the way the token is passed has changed See: https://github.com/Expensify/App/issues/13604 --- workflow_tests/assertions/createNewVersionAssertions.js | 4 ++-- workflow_tests/createNewVersion.test.js | 4 +++- workflow_tests/mocks/createNewVersionMocks.js | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/workflow_tests/assertions/createNewVersionAssertions.js b/workflow_tests/assertions/createNewVersionAssertions.js index e4526ae59be2..3356cb0eef4c 100644 --- a/workflow_tests/assertions/createNewVersionAssertions.js +++ b/workflow_tests/assertions/createNewVersionAssertions.js @@ -26,7 +26,7 @@ const assertCreateNewVersionJobExecuted = (workflowResult, semverLevel = 'BUILD' ], [], ), - utils.createStepAssertion('Setup git for OSBotify', true, null, 'CREATENEWVERSION', 'Setup git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}], []), + utils.createStepAssertion('Setup git for OSBotify', true, null, 'CREATENEWVERSION', 'Setup git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}, {key: 'OS_BOTIFY_APP_ID', value: '***'}, {key: 'OS_BOTIFY_PRIVATE_KEY', value: '***'}], []), utils.createStepAssertion( 'Generate version', true, @@ -34,7 +34,7 @@ const assertCreateNewVersionJobExecuted = (workflowResult, semverLevel = 'BUILD' 'CREATENEWVERSION', 'Generate version', [ - {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'GITHUB_TOKEN', value: 'os_botify_api_token'}, {key: 'SEMVER_LEVEL', value: semverLevel}, ], [], diff --git a/workflow_tests/createNewVersion.test.js b/workflow_tests/createNewVersion.test.js index 259e06450325..dca1afbd9a41 100644 --- a/workflow_tests/createNewVersion.test.js +++ b/workflow_tests/createNewVersion.test.js @@ -46,8 +46,10 @@ describe('test workflow createNewVersion', () => { }; const secrets = { LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - OS_BOTIFY_TOKEN: 'dummy_osbotify_token', + OS_BOTIFY_COMMIT_TOKEN: 'dummy_osbotify_commit_token', SLACK_WEBHOOK: 'dummy_webhook', + OS_BOTIFY_APP_ID: 'os_botify_app_id', + OS_BOTIFY_PRIVATE_KEY: 'os_botify_private_key', }; const githubToken = 'dummy_github_token'; diff --git a/workflow_tests/mocks/createNewVersionMocks.js b/workflow_tests/mocks/createNewVersionMocks.js index a1f601aef47f..5e82e2102ef0 100644 --- a/workflow_tests/mocks/createNewVersionMocks.js +++ b/workflow_tests/mocks/createNewVersionMocks.js @@ -21,8 +21,9 @@ const CREATENEWVERSION__CREATENEWVERSION__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK = ut 'Setup git for OSBotify', 'Setup git for OSBotify', 'CREATENEWVERSION', - ['GPG_PASSPHRASE'], + ['GPG_PASSPHRASE', 'OS_BOTIFY_APP_ID', 'OS_BOTIFY_PRIVATE_KEY'], [], + {OS_BOTIFY_API_TOKEN: 'os_botify_api_token'}, ); const CREATENEWVERSION__CREATENEWVERSION__GENERATE_VERSION__STEP_MOCK = utils.createMockStep( 'Generate version', From 6324a610e6f7b6397375dca76846c7ea26b6fd33 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Wed, 8 Nov 2023 12:36:46 +0100 Subject: [PATCH 088/329] Missing semicolon --- src/components/RadioButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RadioButton.tsx b/src/components/RadioButton.tsx index d5c3830366f8..1d9bd2907936 100644 --- a/src/components/RadioButton.tsx +++ b/src/components/RadioButton.tsx @@ -21,7 +21,7 @@ type RadioButtonProps = { /** Should the input be disabled */ disabled?: boolean; -} +}; function RadioButton({isChecked, onPress = () => undefined, accessibilityLabel, disabled = false, hasError = false}: RadioButtonProps) { return ( From 6721a95de657c38ed5eb463687c32e3a8ce63b83 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Wed, 8 Nov 2023 14:00:00 +0100 Subject: [PATCH 089/329] Fix tests Fixed tests for deploy after new secrets have been added and the way the token is passed has changed See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/deploy.yml | 6 ++-- workflow_tests/assertions/deployAssertions.js | 8 ++--- workflow_tests/deploy.test.js | 32 +++++++------------ workflow_tests/mocks/deployMocks.js | 4 +-- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 78040f237689..6b1bb42e8ace 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,7 +15,8 @@ jobs: ref: staging token: ${{ secrets.OS_BOTIFY_TOKEN }} - - uses: Expensify/App/.github/actions/composite/setupGitForOSBotifyApp@8c19d6da4a3d7ce3b15c9cd89a802187d208ecab + - name: Setup git for OSBotify + uses: Expensify/App/.github/actions/composite/setupGitForOSBotifyApp@8c19d6da4a3d7ce3b15c9cd89a802187d208ecab id: setupGitForOSBotify with: GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} @@ -38,7 +39,8 @@ jobs: ref: production token: ${{ secrets.OS_BOTIFY_TOKEN }} - - uses: Expensify/App/.github/actions/composite/setupGitForOSBotifyApp@8c19d6da4a3d7ce3b15c9cd89a802187d208ecab + - name: Setup git for OSBotify + uses: Expensify/App/.github/actions/composite/setupGitForOSBotifyApp@8c19d6da4a3d7ce3b15c9cd89a802187d208ecab id: setupGitForOSBotify with: GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} diff --git a/workflow_tests/assertions/deployAssertions.js b/workflow_tests/assertions/deployAssertions.js index bff99298bde5..47f8b06d66b4 100644 --- a/workflow_tests/assertions/deployAssertions.js +++ b/workflow_tests/assertions/deployAssertions.js @@ -6,7 +6,7 @@ const assertDeployStagingJobExecuted = (workflowResult, didExecute = true) => { {key: 'ref', value: 'staging'}, {key: 'token', value: '***'}, ]), - utils.createStepAssertion('Setup git for OSBotify', true, null, 'DEPLOY_STAGING', 'Setting up git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}]), + utils.createStepAssertion('Setup git for OSBotify', true, null, 'DEPLOY_STAGING', 'Setting up git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}, {key: 'OS_BOTIFY_APP_ID', value: '***'}, {key: 'OS_BOTIFY_PRIVATE_KEY', value: '***'}]), utils.createStepAssertion('Tag version', true, null, 'DEPLOY_STAGING', 'Tagging new version'), utils.createStepAssertion('🚀 Push tags to trigger staging deploy 🚀', true, null, 'DEPLOY_STAGING', 'Pushing tag to trigger staging deploy'), ]; @@ -26,11 +26,11 @@ const assertDeployProductionJobExecuted = (workflowResult, didExecute = true) => {key: 'ref', value: 'production'}, {key: 'token', value: '***'}, ]), - utils.createStepAssertion('Setup git for OSBotify', true, null, 'DEPLOY_PRODUCTION', 'Setting up git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}]), + utils.createStepAssertion('Setup git for OSBotify', true, null, 'DEPLOY_PRODUCTION', 'Setting up git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}, {key: 'OS_BOTIFY_APP_ID', value: '***'}, {key: 'OS_BOTIFY_PRIVATE_KEY', value: '***'}]), utils.createStepAssertion('Get current app version', true, null, 'DEPLOY_PRODUCTION', 'Getting current app version'), utils.createStepAssertion('Get Release Pull Request List', true, null, 'DEPLOY_PRODUCTION', 'Getting release PR list', [ {key: 'TAG', value: '1.2.3'}, - {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'GITHUB_TOKEN', value: 'os_botify_api_token'}, {key: 'IS_PRODUCTION_DEPLOY', value: 'true'}, ]), utils.createStepAssertion('Generate Release Body', true, null, 'DEPLOY_PRODUCTION', 'Generating release body', [{key: 'PR_LIST', value: '[1.2.1, 1.2.2]'}]), @@ -44,7 +44,7 @@ const assertDeployProductionJobExecuted = (workflowResult, didExecute = true) => {key: 'tag_name', value: '1.2.3'}, {key: 'body', value: 'Release body'}, ], - [{key: 'GITHUB_TOKEN', value: '***'}], + [{key: 'GITHUB_TOKEN', value: 'os_botify_api_token'}], ), ]; diff --git a/workflow_tests/deploy.test.js b/workflow_tests/deploy.test.js index a2ccdebc0b31..4c730050a203 100644 --- a/workflow_tests/deploy.test.js +++ b/workflow_tests/deploy.test.js @@ -39,6 +39,13 @@ describe('test workflow deploy', () => { afterEach(async () => { await mockGithub.teardown(); }); + + const secrets = { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + OS_BOTIFY_APP_ID: 'os_botify_app_id', + OS_BOTIFY_PRIVATE_KEY: 'os_botify_private_key', + }; describe('push', () => { it('to main - nothing triggered', async () => { const repoPath = mockGithub.repo.getPath('testDeployWorkflowRepo') || ''; @@ -50,10 +57,7 @@ describe('test workflow deploy', () => { { ref: 'refs/heads/main', }, - { - OS_BOTIFY_TOKEN: 'dummy_token', - LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - }, + secrets, 'dummy_github_token', ); const testMockSteps = { @@ -80,10 +84,7 @@ describe('test workflow deploy', () => { { ref: 'refs/heads/staging', }, - { - OS_BOTIFY_TOKEN: 'dummy_token', - LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - }, + secrets, 'dummy_github_token', ); const testMockSteps = { @@ -110,10 +111,7 @@ describe('test workflow deploy', () => { { ref: 'refs/heads/production', }, - { - OS_BOTIFY_TOKEN: 'dummy_token', - LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - }, + secrets, 'dummy_github_token', ); const testMockSteps = { @@ -145,10 +143,7 @@ describe('test workflow deploy', () => { act, 'pull_request', {head: {ref: 'main'}}, - { - OS_BOTIFY_TOKEN: 'dummy_token', - LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - }, + secrets, 'dummy_github_token', ); let result = await act.runEvent('pull_request', { @@ -165,10 +160,7 @@ describe('test workflow deploy', () => { act, 'workflow_dispatch', {}, - { - OS_BOTIFY_TOKEN: 'dummy_token', - LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', - }, + secrets, 'dummy_github_token', ); result = await act.runEvent('workflow_dispatch', { diff --git a/workflow_tests/mocks/deployMocks.js b/workflow_tests/mocks/deployMocks.js index dfec48ca7dc3..5f8f00828e9b 100644 --- a/workflow_tests/mocks/deployMocks.js +++ b/workflow_tests/mocks/deployMocks.js @@ -1,13 +1,13 @@ const utils = require('../utils/utils'); const DEPLOY_STAGING__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout staging branch', 'Checking out staging branch', 'DEPLOY_STAGING', ['ref', 'token']); -const DEPLOY_STAGING__SETUP_GIT__STEP_MOCK = utils.createMockStep('Setup git for OSBotify', 'Setting up git for OSBotify', 'DEPLOY_STAGING', ['GPG_PASSPHRASE']); +const DEPLOY_STAGING__SETUP_GIT__STEP_MOCK = utils.createMockStep('Setup git for OSBotify', 'Setting up git for OSBotify', 'DEPLOY_STAGING', ['GPG_PASSPHRASE', 'OS_BOTIFY_APP_ID', 'OS_BOTIFY_PRIVATE_KEY']); const DEPLOY_STAGING__TAG_VERSION__STEP_MOCK = utils.createMockStep('Tag version', 'Tagging new version', 'DEPLOY_STAGING'); const DEPLOY_STAGING__PUSH_TAG__STEP_MOCK = utils.createMockStep('🚀 Push tags to trigger staging deploy 🚀', 'Pushing tag to trigger staging deploy', 'DEPLOY_STAGING'); const DEPLOY_STAGING_STEP_MOCKS = [DEPLOY_STAGING__CHECKOUT__STEP_MOCK, DEPLOY_STAGING__SETUP_GIT__STEP_MOCK, DEPLOY_STAGING__TAG_VERSION__STEP_MOCK, DEPLOY_STAGING__PUSH_TAG__STEP_MOCK]; const DEPLOY_PRODUCTION__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checking out', 'DEPLOY_PRODUCTION', ['ref', 'token']); -const DEPLOY_PRODUCTION__SETUP_GIT__STEP_MOCK = utils.createMockStep('Setup git for OSBotify', 'Setting up git for OSBotify', 'DEPLOY_PRODUCTION', ['GPG_PASSPHRASE']); +const DEPLOY_PRODUCTION__SETUP_GIT__STEP_MOCK = utils.createMockStep('Setup git for OSBotify', 'Setting up git for OSBotify', 'DEPLOY_PRODUCTION', ['GPG_PASSPHRASE', 'OS_BOTIFY_APP_ID', 'OS_BOTIFY_PRIVATE_KEY'], null, {OS_BOTIFY_API_TOKEN: 'os_botify_api_token'}); const DEPLOY_PRODUCTION__CURRENT_APP_VERSION__STEP_MOCK = utils.createMockStep('Get current app version', 'Getting current app version', 'DEPLOY_PRODUCTION', null, null, null, { PRODUCTION_VERSION: '1.2.3', }); From 9737ba437c9ad1f9e1bb96b42542f695c6482c69 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Wed, 8 Nov 2023 14:08:29 +0100 Subject: [PATCH 090/329] Fix tests Fixed tests for lint after new steps have been added See: https://github.com/Expensify/App/issues/13604 --- workflow_tests/assertions/lintAssertions.js | 3 ++- workflow_tests/mocks/lintMocks.js | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/workflow_tests/assertions/lintAssertions.js b/workflow_tests/assertions/lintAssertions.js index 938f9b383464..ddebdd54c70e 100644 --- a/workflow_tests/assertions/lintAssertions.js +++ b/workflow_tests/assertions/lintAssertions.js @@ -5,7 +5,8 @@ const assertLintJobExecuted = (workflowResult, didExecute = true) => { utils.createStepAssertion('Checkout', true, null, 'LINT', 'Checkout', [], []), utils.createStepAssertion('Setup Node', true, null, 'LINT', 'Setup Node', [], []), utils.createStepAssertion('Lint JavaScript and Typescript with ESLint', true, null, 'LINT', 'Lint JavaScript with ESLint', [], [{key: 'CI', value: 'true'}]), - utils.createStepAssertion('Lint shell scripts with ShellCheck', true, null, 'LINT', 'Lint shell scripts with ShellCheck', [], []), + utils.createStepAssertion("Verify there's no Prettier diff", true, null, 'LINT', "Verify theres no Prettier diff", [], []), + utils.createStepAssertion('Run unused style searcher', true, null, 'LINT', 'Run unused style searcher', [], []), ]; steps.forEach((expectedStep) => { diff --git a/workflow_tests/mocks/lintMocks.js b/workflow_tests/mocks/lintMocks.js index ecf11074e20f..ee149d454cfe 100644 --- a/workflow_tests/mocks/lintMocks.js +++ b/workflow_tests/mocks/lintMocks.js @@ -4,12 +4,14 @@ const utils = require('../utils/utils'); const LINT__LINT__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'LINT', [], []); const LINT__LINT__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setup Node', 'LINT', [], []); const LINT__LINT__LINT_JAVASCRIPT_WITH_ESLINT__STEP_MOCK = utils.createMockStep('Lint JavaScript and Typescript with ESLint', 'Lint JavaScript with ESLint', 'LINT', [], ['CI']); -const LINT__LINT__LINT_SHELL_SCRIPTS_WITH_SHELLCHECK__STEP_MOCK = utils.createMockStep('Lint shell scripts with ShellCheck', 'Lint shell scripts with ShellCheck', 'LINT', [], []); +const LINT__LINT__VERIFY_NO_PRETTIER__STEP_MOCK = utils.createMockStep("Verify there's no Prettier diff", "Verify theres no Prettier diff", 'LINT'); +const LINT__LINT__RUN_UNUSED_SEARCHER__STEP_MOCK = utils.createMockStep('Run unused style searcher', 'Run unused style searcher', 'LINT'); const LINT__LINT__STEP_MOCKS = [ LINT__LINT__CHECKOUT__STEP_MOCK, LINT__LINT__SETUP_NODE__STEP_MOCK, LINT__LINT__LINT_JAVASCRIPT_WITH_ESLINT__STEP_MOCK, - LINT__LINT__LINT_SHELL_SCRIPTS_WITH_SHELLCHECK__STEP_MOCK, + LINT__LINT__VERIFY_NO_PRETTIER__STEP_MOCK, + LINT__LINT__RUN_UNUSED_SEARCHER__STEP_MOCK, ]; module.exports = { From eb18ea0532bf21fe3b634e680a3cf267b0833922 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Wed, 8 Nov 2023 14:12:11 +0100 Subject: [PATCH 091/329] Run prettier Run prettier See: https://github.com/Expensify/App/issues/13604 --- .../assertions/createNewVersionAssertions.js | 14 +++++++++++++- workflow_tests/assertions/deployAssertions.js | 12 ++++++++++-- .../assertions/finishReleaseCycleAssertions.js | 14 +++++++++++--- workflow_tests/assertions/lintAssertions.js | 2 +- .../assertions/platformDeployAssertions.js | 10 ++++++---- workflow_tests/deploy.test.js | 18 +++--------------- workflow_tests/mocks/cherryPickMocks.js | 4 +++- workflow_tests/mocks/deployMocks.js | 15 +++++++++++++-- .../mocks/finishReleaseCycleMocks.js | 7 +------ workflow_tests/mocks/lintMocks.js | 2 +- workflow_tests/mocks/platformDeployMocks.js | 14 ++++++++++++-- 11 files changed, 74 insertions(+), 38 deletions(-) diff --git a/workflow_tests/assertions/createNewVersionAssertions.js b/workflow_tests/assertions/createNewVersionAssertions.js index 3356cb0eef4c..27c54e924975 100644 --- a/workflow_tests/assertions/createNewVersionAssertions.js +++ b/workflow_tests/assertions/createNewVersionAssertions.js @@ -26,7 +26,19 @@ const assertCreateNewVersionJobExecuted = (workflowResult, semverLevel = 'BUILD' ], [], ), - utils.createStepAssertion('Setup git for OSBotify', true, null, 'CREATENEWVERSION', 'Setup git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}, {key: 'OS_BOTIFY_APP_ID', value: '***'}, {key: 'OS_BOTIFY_PRIVATE_KEY', value: '***'}], []), + utils.createStepAssertion( + 'Setup git for OSBotify', + true, + null, + 'CREATENEWVERSION', + 'Setup git for OSBotify', + [ + {key: 'GPG_PASSPHRASE', value: '***'}, + {key: 'OS_BOTIFY_APP_ID', value: '***'}, + {key: 'OS_BOTIFY_PRIVATE_KEY', value: '***'}, + ], + [], + ), utils.createStepAssertion( 'Generate version', true, diff --git a/workflow_tests/assertions/deployAssertions.js b/workflow_tests/assertions/deployAssertions.js index 47f8b06d66b4..ce0907e84be8 100644 --- a/workflow_tests/assertions/deployAssertions.js +++ b/workflow_tests/assertions/deployAssertions.js @@ -6,7 +6,11 @@ const assertDeployStagingJobExecuted = (workflowResult, didExecute = true) => { {key: 'ref', value: 'staging'}, {key: 'token', value: '***'}, ]), - utils.createStepAssertion('Setup git for OSBotify', true, null, 'DEPLOY_STAGING', 'Setting up git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}, {key: 'OS_BOTIFY_APP_ID', value: '***'}, {key: 'OS_BOTIFY_PRIVATE_KEY', value: '***'}]), + utils.createStepAssertion('Setup git for OSBotify', true, null, 'DEPLOY_STAGING', 'Setting up git for OSBotify', [ + {key: 'GPG_PASSPHRASE', value: '***'}, + {key: 'OS_BOTIFY_APP_ID', value: '***'}, + {key: 'OS_BOTIFY_PRIVATE_KEY', value: '***'}, + ]), utils.createStepAssertion('Tag version', true, null, 'DEPLOY_STAGING', 'Tagging new version'), utils.createStepAssertion('🚀 Push tags to trigger staging deploy 🚀', true, null, 'DEPLOY_STAGING', 'Pushing tag to trigger staging deploy'), ]; @@ -26,7 +30,11 @@ const assertDeployProductionJobExecuted = (workflowResult, didExecute = true) => {key: 'ref', value: 'production'}, {key: 'token', value: '***'}, ]), - utils.createStepAssertion('Setup git for OSBotify', true, null, 'DEPLOY_PRODUCTION', 'Setting up git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}, {key: 'OS_BOTIFY_APP_ID', value: '***'}, {key: 'OS_BOTIFY_PRIVATE_KEY', value: '***'}]), + utils.createStepAssertion('Setup git for OSBotify', true, null, 'DEPLOY_PRODUCTION', 'Setting up git for OSBotify', [ + {key: 'GPG_PASSPHRASE', value: '***'}, + {key: 'OS_BOTIFY_APP_ID', value: '***'}, + {key: 'OS_BOTIFY_PRIVATE_KEY', value: '***'}, + ]), utils.createStepAssertion('Get current app version', true, null, 'DEPLOY_PRODUCTION', 'Getting current app version'), utils.createStepAssertion('Get Release Pull Request List', true, null, 'DEPLOY_PRODUCTION', 'Getting release PR list', [ {key: 'TAG', value: '1.2.3'}, diff --git a/workflow_tests/assertions/finishReleaseCycleAssertions.js b/workflow_tests/assertions/finishReleaseCycleAssertions.js index 96c473fca917..cd90422bf995 100644 --- a/workflow_tests/assertions/finishReleaseCycleAssertions.js +++ b/workflow_tests/assertions/finishReleaseCycleAssertions.js @@ -2,9 +2,17 @@ const utils = require('../utils/utils'); const assertValidateJobExecuted = (workflowResult, issueNumber = '', didExecute = true, isTeamMember = true, hasBlockers = false, isSuccessful = true) => { const steps = [ - utils.createStepAssertion('Checkout', true, null, 'VALIDATE', 'Checkout', [{key: 'ref', value: 'main'}, {key: 'token', value: '***'}]), - utils.createStepAssertion('Setup Git for OSBotify', true, null, 'VALIDATE', 'Setup Git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}, {key: 'OS_BOTIFY_APP_ID', value: '***'}, {key: 'OS_BOTIFY_PRIVATE_KEY', value: '***'}]), - utils.createStepAssertion('Validate actor is deployer', true, null, 'VALIDATE', 'Validating if actor is deployer', [], [{key: 'GITHUB_TOKEN', value: 'os_botify_api_token'}])]; + utils.createStepAssertion('Checkout', true, null, 'VALIDATE', 'Checkout', [ + {key: 'ref', value: 'main'}, + {key: 'token', value: '***'}, + ]), + utils.createStepAssertion('Setup Git for OSBotify', true, null, 'VALIDATE', 'Setup Git for OSBotify', [ + {key: 'GPG_PASSPHRASE', value: '***'}, + {key: 'OS_BOTIFY_APP_ID', value: '***'}, + {key: 'OS_BOTIFY_PRIVATE_KEY', value: '***'}, + ]), + utils.createStepAssertion('Validate actor is deployer', true, null, 'VALIDATE', 'Validating if actor is deployer', [], [{key: 'GITHUB_TOKEN', value: 'os_botify_api_token'}]), + ]; if (isTeamMember) { steps.push( utils.createStepAssertion( diff --git a/workflow_tests/assertions/lintAssertions.js b/workflow_tests/assertions/lintAssertions.js index ddebdd54c70e..1253bae2e258 100644 --- a/workflow_tests/assertions/lintAssertions.js +++ b/workflow_tests/assertions/lintAssertions.js @@ -5,7 +5,7 @@ const assertLintJobExecuted = (workflowResult, didExecute = true) => { utils.createStepAssertion('Checkout', true, null, 'LINT', 'Checkout', [], []), utils.createStepAssertion('Setup Node', true, null, 'LINT', 'Setup Node', [], []), utils.createStepAssertion('Lint JavaScript and Typescript with ESLint', true, null, 'LINT', 'Lint JavaScript with ESLint', [], [{key: 'CI', value: 'true'}]), - utils.createStepAssertion("Verify there's no Prettier diff", true, null, 'LINT', "Verify theres no Prettier diff", [], []), + utils.createStepAssertion("Verify there's no Prettier diff", true, null, 'LINT', 'Verify theres no Prettier diff', [], []), utils.createStepAssertion('Run unused style searcher', true, null, 'LINT', 'Run unused style searcher', [], []), ]; diff --git a/workflow_tests/assertions/platformDeployAssertions.js b/workflow_tests/assertions/platformDeployAssertions.js index 63b5b11ba1ab..d59223f14b40 100644 --- a/workflow_tests/assertions/platformDeployAssertions.js +++ b/workflow_tests/assertions/platformDeployAssertions.js @@ -188,10 +188,12 @@ const assertIOSJobExecuted = (workflowResult, didExecute = true, isProduction = ); if (!isProduction) { steps.push( - utils.createStepAssertion('Upload iOS version to GitHub artifacts', true, null, 'IOS', 'Upload iOS version to GitHub artifacts', [ - {key: 'name', value: 'New Expensify.ipa'}, - {key: 'path', value: '/Users/runner/work/App/App/New Expensify.ipa'}, - ]), utils.createStepAssertion('Upload iOS version to Browser Stack', true, null, 'IOS', 'Uploading version to Browser Stack', null, [{key: 'BROWSERSTACK', value: '***'}])); + utils.createStepAssertion('Upload iOS version to GitHub artifacts', true, null, 'IOS', 'Upload iOS version to GitHub artifacts', [ + {key: 'name', value: 'New Expensify.ipa'}, + {key: 'path', value: '/Users/runner/work/App/App/New Expensify.ipa'}, + ]), + utils.createStepAssertion('Upload iOS version to Browser Stack', true, null, 'IOS', 'Uploading version to Browser Stack', null, [{key: 'BROWSERSTACK', value: '***'}]), + ); } else { steps.push( utils.createStepAssertion('Set iOS version in ENV', true, null, 'IOS', 'Setting iOS version'), diff --git a/workflow_tests/deploy.test.js b/workflow_tests/deploy.test.js index 4c730050a203..b15a167d4702 100644 --- a/workflow_tests/deploy.test.js +++ b/workflow_tests/deploy.test.js @@ -39,7 +39,7 @@ describe('test workflow deploy', () => { afterEach(async () => { await mockGithub.teardown(); }); - + const secrets = { OS_BOTIFY_TOKEN: 'dummy_token', LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', @@ -139,13 +139,7 @@ describe('test workflow deploy', () => { }; // pull_request - act = utils.setUpActParams( - act, - 'pull_request', - {head: {ref: 'main'}}, - secrets, - 'dummy_github_token', - ); + act = utils.setUpActParams(act, 'pull_request', {head: {ref: 'main'}}, secrets, 'dummy_github_token'); let result = await act.runEvent('pull_request', { workflowFile: path.join(repoPath, '.github', 'workflows', 'deploy.yml'), mockSteps: testMockSteps, @@ -156,13 +150,7 @@ describe('test workflow deploy', () => { assertions.assertDeployProductionJobExecuted(result, false); // workflow_dispatch - act = utils.setUpActParams( - act, - 'workflow_dispatch', - {}, - secrets, - 'dummy_github_token', - ); + act = utils.setUpActParams(act, 'workflow_dispatch', {}, secrets, 'dummy_github_token'); result = await act.runEvent('workflow_dispatch', { workflowFile: path.join(repoPath, '.github', 'workflows', 'deploy.yml'), mockSteps: testMockSteps, diff --git a/workflow_tests/mocks/cherryPickMocks.js b/workflow_tests/mocks/cherryPickMocks.js index 8531d9783172..5ce9b2ecccfb 100644 --- a/workflow_tests/mocks/cherryPickMocks.js +++ b/workflow_tests/mocks/cherryPickMocks.js @@ -36,7 +36,9 @@ const CHERRYPICK__CREATENEWVERSION__STEP_MOCKS = [CHERRYPICK__CREATENEWVERSION__ // cherrypick const CHERRYPICK__CHERRYPICK__CHECKOUT_STAGING_BRANCH__STEP_MOCK = utils.createMockStep('Checkout staging branch', 'Checking out staging branch', 'CHERRYPICK', ['ref', 'token'], []); -const CHERRYPICK__CHERRYPICK__SET_UP_GIT_FOR_OSBOTIFY__STEP_MOCK = utils.createMockStep('Set up git for OSBotify', 'Setting up git for OSBotify', 'CHERRYPICK', ['GPG_PASSPHRASE'], [], {OS_BOTIFY_API_TOKEN: 'os_botify_api_token'}); +const CHERRYPICK__CHERRYPICK__SET_UP_GIT_FOR_OSBOTIFY__STEP_MOCK = utils.createMockStep('Set up git for OSBotify', 'Setting up git for OSBotify', 'CHERRYPICK', ['GPG_PASSPHRASE'], [], { + OS_BOTIFY_API_TOKEN: 'os_botify_api_token', +}); const CHERRYPICK__CHERRYPICK__GET_PREVIOUS_APP_VERSION__STEP_MOCK = utils.createMockStep('Get previous app version', 'Get previous app version', 'CHERRYPICK', ['SEMVER_LEVEL']); const CHERRYPICK__CHERRYPICK__FETCH_HISTORY_OF_RELEVANT_REFS__STEP_MOCK = utils.createMockStep('Fetch history of relevant refs', 'Fetch history of relevant refs', 'CHERRYPICK'); const CHERRYPICK__CHERRYPICK__GET_VERSION_BUMP_COMMIT__STEP_MOCK = utils.createMockStep('Get version bump commit', 'Get version bump commit', 'CHERRYPICK', [], [], { diff --git a/workflow_tests/mocks/deployMocks.js b/workflow_tests/mocks/deployMocks.js index 5f8f00828e9b..9e8b978f05ac 100644 --- a/workflow_tests/mocks/deployMocks.js +++ b/workflow_tests/mocks/deployMocks.js @@ -1,13 +1,24 @@ const utils = require('../utils/utils'); const DEPLOY_STAGING__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout staging branch', 'Checking out staging branch', 'DEPLOY_STAGING', ['ref', 'token']); -const DEPLOY_STAGING__SETUP_GIT__STEP_MOCK = utils.createMockStep('Setup git for OSBotify', 'Setting up git for OSBotify', 'DEPLOY_STAGING', ['GPG_PASSPHRASE', 'OS_BOTIFY_APP_ID', 'OS_BOTIFY_PRIVATE_KEY']); +const DEPLOY_STAGING__SETUP_GIT__STEP_MOCK = utils.createMockStep('Setup git for OSBotify', 'Setting up git for OSBotify', 'DEPLOY_STAGING', [ + 'GPG_PASSPHRASE', + 'OS_BOTIFY_APP_ID', + 'OS_BOTIFY_PRIVATE_KEY', +]); const DEPLOY_STAGING__TAG_VERSION__STEP_MOCK = utils.createMockStep('Tag version', 'Tagging new version', 'DEPLOY_STAGING'); const DEPLOY_STAGING__PUSH_TAG__STEP_MOCK = utils.createMockStep('🚀 Push tags to trigger staging deploy 🚀', 'Pushing tag to trigger staging deploy', 'DEPLOY_STAGING'); const DEPLOY_STAGING_STEP_MOCKS = [DEPLOY_STAGING__CHECKOUT__STEP_MOCK, DEPLOY_STAGING__SETUP_GIT__STEP_MOCK, DEPLOY_STAGING__TAG_VERSION__STEP_MOCK, DEPLOY_STAGING__PUSH_TAG__STEP_MOCK]; const DEPLOY_PRODUCTION__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checking out', 'DEPLOY_PRODUCTION', ['ref', 'token']); -const DEPLOY_PRODUCTION__SETUP_GIT__STEP_MOCK = utils.createMockStep('Setup git for OSBotify', 'Setting up git for OSBotify', 'DEPLOY_PRODUCTION', ['GPG_PASSPHRASE', 'OS_BOTIFY_APP_ID', 'OS_BOTIFY_PRIVATE_KEY'], null, {OS_BOTIFY_API_TOKEN: 'os_botify_api_token'}); +const DEPLOY_PRODUCTION__SETUP_GIT__STEP_MOCK = utils.createMockStep( + 'Setup git for OSBotify', + 'Setting up git for OSBotify', + 'DEPLOY_PRODUCTION', + ['GPG_PASSPHRASE', 'OS_BOTIFY_APP_ID', 'OS_BOTIFY_PRIVATE_KEY'], + null, + {OS_BOTIFY_API_TOKEN: 'os_botify_api_token'}, +); const DEPLOY_PRODUCTION__CURRENT_APP_VERSION__STEP_MOCK = utils.createMockStep('Get current app version', 'Getting current app version', 'DEPLOY_PRODUCTION', null, null, null, { PRODUCTION_VERSION: '1.2.3', }); diff --git a/workflow_tests/mocks/finishReleaseCycleMocks.js b/workflow_tests/mocks/finishReleaseCycleMocks.js index 551105705992..cf5b805ff479 100644 --- a/workflow_tests/mocks/finishReleaseCycleMocks.js +++ b/workflow_tests/mocks/finishReleaseCycleMocks.js @@ -1,12 +1,7 @@ const utils = require('../utils/utils'); // validate -const FINISHRELEASECYCLE__VALIDATE__CHECKOUT__STEP_MOCK = utils.createMockStep( - 'Checkout', - 'Checkout', - 'VALIDATE', - ['ref', 'token'], -); +const FINISHRELEASECYCLE__VALIDATE__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'VALIDATE', ['ref', 'token']); const FINISHRELEASECYCLE__VALIDATE__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK = utils.createMockStep( 'Setup Git for OSBotify', 'Setup Git for OSBotify', diff --git a/workflow_tests/mocks/lintMocks.js b/workflow_tests/mocks/lintMocks.js index ee149d454cfe..1fc5dbfd526f 100644 --- a/workflow_tests/mocks/lintMocks.js +++ b/workflow_tests/mocks/lintMocks.js @@ -4,7 +4,7 @@ const utils = require('../utils/utils'); const LINT__LINT__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'LINT', [], []); const LINT__LINT__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setup Node', 'LINT', [], []); const LINT__LINT__LINT_JAVASCRIPT_WITH_ESLINT__STEP_MOCK = utils.createMockStep('Lint JavaScript and Typescript with ESLint', 'Lint JavaScript with ESLint', 'LINT', [], ['CI']); -const LINT__LINT__VERIFY_NO_PRETTIER__STEP_MOCK = utils.createMockStep("Verify there's no Prettier diff", "Verify theres no Prettier diff", 'LINT'); +const LINT__LINT__VERIFY_NO_PRETTIER__STEP_MOCK = utils.createMockStep("Verify there's no Prettier diff", 'Verify theres no Prettier diff', 'LINT'); const LINT__LINT__RUN_UNUSED_SEARCHER__STEP_MOCK = utils.createMockStep('Run unused style searcher', 'Run unused style searcher', 'LINT'); const LINT__LINT__STEP_MOCKS = [ LINT__LINT__CHECKOUT__STEP_MOCK, diff --git a/workflow_tests/mocks/platformDeployMocks.js b/workflow_tests/mocks/platformDeployMocks.js index d660d057259d..e1f016142c65 100644 --- a/workflow_tests/mocks/platformDeployMocks.js +++ b/workflow_tests/mocks/platformDeployMocks.js @@ -51,7 +51,12 @@ const PLATFORM_DEPLOY__ANDROID__FASTLANE_BETA__STEP_MOCK = utils.createMockStep( ]); const PLATFORM_DEPLOY__ANDROID__FASTLANE_PRODUCTION__STEP_MOCK = utils.createMockStep('Run Fastlane production', 'Running Fastlane production', 'ANDROID', null, ['VERSION']); const PLATFORM_DEPLOY__ANDROID__ARCHIVE_SOURCEMAPS__STEP_MOCK = utils.createMockStep('Archive Android sourcemaps', 'Archiving Android sourcemaps', 'ANDROID', ['name', 'path']); -const PLATFORM_DEPLOY__ANDROID__UPLOAD_ANDROID_VERSION_TO_GITHUB_ARTIFACTS__STEP_MOCK = utils.createMockStep('Upload Android version to GitHub artifacts', 'Upload Android version to GitHub artifacts', 'ANDROID', ['name', 'path']); +const PLATFORM_DEPLOY__ANDROID__UPLOAD_ANDROID_VERSION_TO_GITHUB_ARTIFACTS__STEP_MOCK = utils.createMockStep( + 'Upload Android version to GitHub artifacts', + 'Upload Android version to GitHub artifacts', + 'ANDROID', + ['name', 'path'], +); const PLATFORM_DEPLOY__ANDROID__UPLOAD_TO_BROWSER_STACK__STEP_MOCK = utils.createMockStep( 'Upload Android version to Browser Stack', 'Uploading Android version to Browser Stack', @@ -141,7 +146,12 @@ const PLATFORM_DEPLOY__IOS__FASTLANE__STEP_MOCK = utils.createMockStep('Run Fast 'APPLE_DEMO_PASSWORD', ]); const PLATFORM_DEPLOY__IOS__ARCHIVE_SOURCEMAPS__STEP_MOCK = utils.createMockStep('Archive iOS sourcemaps', 'Archiving sourcemaps', 'IOS', ['name', 'path']); -const PLATFORM_DEPLOY__IOS__UPLOAD_IOS_VERSION_TO_GITHUB_ARTIFACTS__STEP_MOCK = utils.createMockStep('Upload iOS version to GitHub artifacts', 'Upload iOS version to GitHub artifacts', 'IOS', ['name', 'path']); +const PLATFORM_DEPLOY__IOS__UPLOAD_IOS_VERSION_TO_GITHUB_ARTIFACTS__STEP_MOCK = utils.createMockStep( + 'Upload iOS version to GitHub artifacts', + 'Upload iOS version to GitHub artifacts', + 'IOS', + ['name', 'path'], +); const PLATFORM_DEPLOY__IOS__UPLOAD_BROWSERSTACK__STEP_MOCK = utils.createMockStep('Upload iOS version to Browser Stack', 'Uploading version to Browser Stack', 'IOS', null, ['BROWSERSTACK']); const PLATFORM_DEPLOY__IOS__SET_VERSION__STEP_MOCK = utils.createMockStep('Set iOS version in ENV', 'Setting iOS version', 'IOS', null, null, null, {IOS_VERSION: '1.2.3'}); const PLATFORM_DEPLOY__IOS__RELEASE_FASTLANE__STEP_MOCK = utils.createMockStep('Run Fastlane for App Store release', 'Running Fastlane for release', 'IOS', null, ['VERSION']); From cc52b3104af7822c9ca389e41072617caf152da6 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Wed, 8 Nov 2023 14:15:47 +0100 Subject: [PATCH 092/329] Add quotes Add quotes to satisfy linter See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index 9b69364c2cb0..404f964881fc 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -31,7 +31,7 @@ jobs: run: brew install act - name: Set ACT_BINARY - run: echo "ACT_BINARY=$(which act)" >> $GITHUB_ENV + run: echo "ACT_BINARY=$(which act)" >> "$GITHUB_ENV" - name: Run tests run: npm run workflow-test From 96893ad5430bfc5afe97a0f5ae9f18961c6a5170 Mon Sep 17 00:00:00 2001 From: Gray Lewis Date: Wed, 8 Nov 2023 14:18:24 +0100 Subject: [PATCH 093/329] WIP --- .../ReportActionCompose/SuggestionMention.js | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index 2ea2dd334528..2e5b717511df 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -198,13 +198,25 @@ function SuggestionMention({ const leftString = value.substring(0, suggestionEndIndex); const words = leftString.split(CONST.REGEX.SPACE_OR_EMOJI); const lastWord = _.last(words); + const secondToLastWord = words[words.length - 3]; let atSignIndex; + let suggestionWord + let prefix; + if (lastWord.startsWith('@')) { atSignIndex = leftString.lastIndexOf(lastWord); - } + suggestionWord = lastWord; - const prefix = lastWord.substring(1); + prefix = suggestionWord.substring(1); + } else if (secondToLastWord && secondToLastWord.startsWith('@')) { + atSignIndex = leftString.lastIndexOf(secondToLastWord); + suggestionWord = secondToLastWord + ' ' + lastWord; + + prefix = suggestionWord.substring(1); + } else { + prefix = lastWord.substring(1); + } const nextState = { suggestedMentions: [], @@ -212,10 +224,11 @@ function SuggestionMention({ mentionPrefix: prefix, }; - const isCursorBeforeTheMention = valueAfterTheCursor.startsWith(lastWord); + const isCursorBeforeTheMention = valueAfterTheCursor.startsWith(suggestionWord); - if (!isCursorBeforeTheMention && isMentionCode(lastWord)) { + if (!isCursorBeforeTheMention && isMentionCode(suggestionWord)) { const suggestions = getMentionOptions(personalDetails, prefix); + nextState.suggestedMentions = suggestions; nextState.shouldShowSuggestionMenu = !_.isEmpty(suggestions); } @@ -229,6 +242,7 @@ function SuggestionMention({ [getMentionOptions, personalDetails, resetSuggestions, setHighlightedMentionIndex, value, isComposerFocused], ); + useEffect(() => { if (value.length < previousValue.length) { // A workaround to not show the suggestions list when the user deletes a character before the mention. From 46f1132e29a3e97dc9615b1b2fd2bb514ef6db4e Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Wed, 8 Nov 2023 14:45:18 +0100 Subject: [PATCH 094/329] onPress should be optional --- src/components/RadioButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RadioButton.tsx b/src/components/RadioButton.tsx index 1d9bd2907936..452544b858a1 100644 --- a/src/components/RadioButton.tsx +++ b/src/components/RadioButton.tsx @@ -11,7 +11,7 @@ type RadioButtonProps = { isChecked: boolean; /** A function that is called when the box/label is pressed */ - onPress: () => void; + onPress?: () => void; /** Specifies the accessibility label for the radio button */ accessibilityLabel: string; From 398210d898435d1bcfd27e0c975a65821580843c Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Wed, 8 Nov 2023 14:56:53 +0100 Subject: [PATCH 095/329] Remove strategy Removed strategy field from gha workflow See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/testGithubActionsWorkflows.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index 404f964881fc..a5a9e1b1be17 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -14,8 +14,6 @@ jobs: runs-on: ubuntu-latest env: CI: true - strategy: - fail-fast: false name: test GitHub Workflows steps: - name: Checkout From e9815870f2a4ced4b21e85441e135ba5d1c4dbba Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Wed, 8 Nov 2023 16:12:02 +0100 Subject: [PATCH 096/329] Reorder import --- src/components/MapView/MapView.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index 4cb9db36dc46..1aed425ec6ea 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -19,12 +19,12 @@ import getCurrentPosition from '@src/libs/getCurrentPosition'; import ONYXKEYS from '@src/ONYXKEYS'; import styles from '@src/styles/styles'; import Direction from './Direction'; +import './mapbox.css'; import {MapViewHandle} from './MapViewTypes'; import PendingMapView from './PendingMapView'; import responder from './responder'; import {ComponentProps, MapViewOnyxProps} from './types'; import utils from './utils'; -import './mapbox.css'; const MapView = forwardRef( ( From 4835faf35a477f768e58070ce15a8a652a305ce7 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 8 Nov 2023 16:17:55 +0100 Subject: [PATCH 097/329] [TS migration] Migrate 'AutoCompleteSuggestions' component --- ...ons.js => BaseAutoCompleteSuggestions.tsx} | 79 ++++++++----------- .../autoCompleteSuggestionsPropTypes.js | 36 --------- .../{index.native.js => index.native.tsx} | 5 +- .../{index.js => index.tsx} | 14 ++-- .../AutoCompleteSuggestions/types.ts | 55 +++++++++++++ 5 files changed, 99 insertions(+), 90 deletions(-) rename src/components/AutoCompleteSuggestions/{BaseAutoCompleteSuggestions.js => BaseAutoCompleteSuggestions.tsx} (64%) delete mode 100644 src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js rename src/components/AutoCompleteSuggestions/{index.native.js => index.native.tsx} (81%) rename src/components/AutoCompleteSuggestions/{index.js => index.tsx} (79%) create mode 100644 src/components/AutoCompleteSuggestions/types.ts diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx similarity index 64% rename from src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js rename to src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index c024b025c80e..097a495eb212 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -1,4 +1,5 @@ -import React, {useEffect, useRef} from 'react'; +import React, {ForwardedRef, forwardRef, ReactElement, useEffect, useRef} from 'react'; +import {FlatListProps} from 'react-native'; // We take FlatList from this package to properly handle the scrolling of AutoCompleteSuggestions in chats since one scroll is nested inside another import {FlatList} from 'react-native-gesture-handler'; import Animated, {Easing, FadeOutDown, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; @@ -6,14 +7,16 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import styles from '@styles/styles'; import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; -import {propTypes} from './autoCompleteSuggestionsPropTypes'; +import type {AutoCompleteSuggestionsProps, Suggestion} from './types'; -/** - * @param {Number} numRows - * @param {Boolean} isSuggestionPickerLarge - * @returns {Number} - */ -const measureHeightOfSuggestionRows = (numRows, isSuggestionPickerLarge) => { +type RenderSuggestionMenuItemProps = { + item: Suggestion; + index: number; +}; + +type GetItemLayout = FlatListProps['getItemLayout']; + +const measureHeightOfSuggestionRows = (numRows: number, isSuggestionPickerLarge: boolean): number => { if (isSuggestionPickerLarge) { if (numRows > CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER) { // On large screens, if there are more than 5 suggestions, we display a scrollable window with a height of 5 items, indicating that there are more items available @@ -28,26 +31,25 @@ const measureHeightOfSuggestionRows = (numRows, isSuggestionPickerLarge) => { return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; }; -function BaseAutoCompleteSuggestions(props) { +function BaseAutoCompleteSuggestions( + {highlightedSuggestionIndex, onSelect, accessibilityLabelExtractor, renderSuggestionMenuItem, suggestions, isSuggestionPickerLarge, keyExtractor}: AutoCompleteSuggestionsProps, + ref: ForwardedRef, +) { const rowHeight = useSharedValue(0); - const scrollRef = useRef(null); + const scrollRef = useRef(null); /** * Render a suggestion menu item component. - * @param {Object} params - * @param {Object} params.item - * @param {Number} params.index - * @returns {JSX.Element} */ - const renderSuggestionMenuItem = ({item, index}) => ( + const renderItem = ({item, index}: RenderSuggestionMenuItemProps): ReactElement => ( StyleUtils.getAutoCompleteSuggestionItemStyle(props.highlightedSuggestionIndex, CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, hovered, index)} + style={({hovered}) => StyleUtils.getAutoCompleteSuggestionItemStyle(highlightedSuggestionIndex, CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, hovered, index)} hoverDimmingValue={1} onMouseDown={(e) => e.preventDefault()} - onPress={() => props.onSelect(index)} + onPress={() => onSelect(index)} onLongPress={() => {}} - accessibilityLabel={props.accessibilityLabelExtractor(item, index)} + accessibilityLabel={accessibilityLabelExtractor(item, index)} > - {props.renderSuggestionMenuItem(item, index)} + {renderSuggestionMenuItem(item, index)} ); @@ -58,46 +60,44 @@ function BaseAutoCompleteSuggestions(props) { * * Also, `scrollToIndex` should be used in conjunction with `getItemLayout`, otherwise there is no way to know the location of offscreen indices or handle failures. * - * @param {Array} data - This is the same as the data we pass into the component - * @param {Number} index the current item's index in the set of data - * - * @returns {Object} + * @param data - This is the same as the data we pass into the component + * @param index the current item's index in the set of data */ - const getItemLayout = (data, index) => ({ + const getItemLayout: GetItemLayout = (data, index) => ({ length: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, offset: index * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, index, }); - const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * props.suggestions.length; + const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value)); useEffect(() => { - rowHeight.value = withTiming(measureHeightOfSuggestionRows(props.suggestions.length, props.isSuggestionPickerLarge), { + rowHeight.value = withTiming(measureHeightOfSuggestionRows(suggestions.length, isSuggestionPickerLarge), { duration: 100, easing: Easing.inOut(Easing.ease), }); - }, [props.suggestions.length, props.isSuggestionPickerLarge, rowHeight]); + }, [suggestions.length, isSuggestionPickerLarge, rowHeight]); useEffect(() => { if (!scrollRef.current) { return; } - scrollRef.current.scrollToIndex({index: props.highlightedSuggestionIndex, animated: true}); - }, [props.highlightedSuggestionIndex]); + scrollRef.current.scrollToIndex({index: highlightedSuggestionIndex, animated: true}); + }, [highlightedSuggestionIndex]); return ( } style={[styles.autoCompleteSuggestionsContainer, animatedStyles]} exiting={FadeOutDown.duration(100).easing(Easing.inOut(Easing.ease))} > rowHeight.value} style={{flex: 1}} @@ -107,17 +107,6 @@ function BaseAutoCompleteSuggestions(props) { ); } -BaseAutoCompleteSuggestions.propTypes = propTypes; BaseAutoCompleteSuggestions.displayName = 'BaseAutoCompleteSuggestions'; -const BaseAutoCompleteSuggestionsWithRef = React.forwardRef((props, ref) => ( - -)); - -BaseAutoCompleteSuggestionsWithRef.displayName = 'BaseAutoCompleteSuggestionsWithRef'; - -export default BaseAutoCompleteSuggestionsWithRef; +export default forwardRef(BaseAutoCompleteSuggestions); diff --git a/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js b/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js deleted file mode 100644 index 8c6dca1902c5..000000000000 --- a/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js +++ /dev/null @@ -1,36 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - /** Array of suggestions */ - // eslint-disable-next-line react/forbid-prop-types - suggestions: PropTypes.arrayOf(PropTypes.object).isRequired, - - /** Function used to render each suggestion, returned JSX will be enclosed inside a Pressable component */ - renderSuggestionMenuItem: PropTypes.func.isRequired, - - /** Create unique keys for each suggestion item */ - keyExtractor: PropTypes.func.isRequired, - - /** The index of the highlighted suggestion */ - highlightedSuggestionIndex: PropTypes.number.isRequired, - - /** Fired when the user selects a suggestion */ - onSelect: PropTypes.func.isRequired, - - /** Show that we can use large auto-complete suggestion picker. - * Depending on available space and whether the input is expanded, we can have a small or large mention suggester. - * When this value is false, the suggester will have a height of 2.5 items. When this value is true, the height can be up to 5 items. */ - isSuggestionPickerLarge: PropTypes.bool.isRequired, - - /** create accessibility label for each item */ - accessibilityLabelExtractor: PropTypes.func.isRequired, - - /** Meaures the parent container's position and dimensions. */ - measureParentContainer: PropTypes.func, -}; - -const defaultProps = { - measureParentContainer: () => {}, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/AutoCompleteSuggestions/index.native.js b/src/components/AutoCompleteSuggestions/index.native.tsx similarity index 81% rename from src/components/AutoCompleteSuggestions/index.native.js rename to src/components/AutoCompleteSuggestions/index.native.tsx index 439fa45eae78..bb7dd3e46558 100644 --- a/src/components/AutoCompleteSuggestions/index.native.js +++ b/src/components/AutoCompleteSuggestions/index.native.tsx @@ -1,9 +1,9 @@ import {Portal} from '@gorhom/portal'; import React from 'react'; -import {propTypes} from './autoCompleteSuggestionsPropTypes'; import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; +import type {AutoCompleteSuggestionsProps} from './types'; -function AutoCompleteSuggestions({measureParentContainer, ...props}) { +function AutoCompleteSuggestions({measureParentContainer, ...props}: AutoCompleteSuggestionsProps) { return ( {/* eslint-disable-next-line react/jsx-props-no-spreading */} @@ -12,7 +12,6 @@ function AutoCompleteSuggestions({measureParentContainer, ...props}) { ); } -AutoCompleteSuggestions.propTypes = propTypes; AutoCompleteSuggestions.displayName = 'AutoCompleteSuggestions'; export default AutoCompleteSuggestions; diff --git a/src/components/AutoCompleteSuggestions/index.js b/src/components/AutoCompleteSuggestions/index.tsx similarity index 79% rename from src/components/AutoCompleteSuggestions/index.js rename to src/components/AutoCompleteSuggestions/index.tsx index 30654caf5708..2a8e37e17d2e 100644 --- a/src/components/AutoCompleteSuggestions/index.js +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -4,8 +4,8 @@ import {View} from 'react-native'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as StyleUtils from '@styles/StyleUtils'; -import {propTypes} from './autoCompleteSuggestionsPropTypes'; import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; +import type {AutoCompleteSuggestionsProps} from './types'; /** * On the mobile-web platform, when long-pressing on auto-complete suggestions, @@ -14,8 +14,8 @@ import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; * On the native platform, tapping on auto-complete suggestions will not blur the main input. */ -function AutoCompleteSuggestions({measureParentContainer, ...props}) { - const containerRef = React.useRef(null); +function AutoCompleteSuggestions({measureParentContainer = () => {}, ...props}: AutoCompleteSuggestionsProps) { + const containerRef = React.useRef(null); const {windowHeight, windowWidth} = useWindowDimensions(); const [{width, left, bottom}, setContainerState] = React.useState({ width: 0, @@ -25,7 +25,7 @@ function AutoCompleteSuggestions({measureParentContainer, ...props}) { React.useEffect(() => { const container = containerRef.current; if (!container) { - return; + return () => {}; } container.onpointerdown = (e) => { if (DeviceCapabilities.hasHoverSupport()) { @@ -53,11 +53,13 @@ function AutoCompleteSuggestions({measureParentContainer, ...props}) { return ( Boolean(width) && - ReactDOM.createPortal({componentToRender}, document.querySelector('body')) + ReactDOM.createPortal( + {componentToRender}, + document.querySelector('body') as unknown as HTMLBodyElement, + ) ); } -AutoCompleteSuggestions.propTypes = propTypes; AutoCompleteSuggestions.displayName = 'AutoCompleteSuggestions'; export default AutoCompleteSuggestions; diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts new file mode 100644 index 000000000000..c9b38880752f --- /dev/null +++ b/src/components/AutoCompleteSuggestions/types.ts @@ -0,0 +1,55 @@ +import {ReactElement} from 'react'; +import type {Icon} from '@src/types/onyx/OnyxCommon'; + +// TODO: remove when MentionSuggestions will be merged +type Mention = { + /** Display name of the user */ + text: string; + + /** Email/phone number of the user */ + alternateText: string; + + /** Array of icons of the user. We use the first element of this array */ + icons: Icon[]; +}; + +// TODO: remove when EmojiSuggestions will be merged +type SimpleEmoji = { + code: string; + name: string; + types?: string[]; +}; + +type Suggestion = Mention | SimpleEmoji; + +type MeasureParentContainerCallback = (x: number, y: number, width: number) => void; + +type AutoCompleteSuggestionsProps = { + /** Array of suggestions */ + suggestions: Suggestion[]; + + /** Function used to render each suggestion, returned JSX will be enclosed inside a Pressable component */ + renderSuggestionMenuItem: (item: Suggestion, index: number) => ReactElement; + + /** Create unique keys for each suggestion item */ + keyExtractor: () => string; + + /** The index of the highlighted suggestion */ + highlightedSuggestionIndex: number; + + /** Fired when the user selects a suggestion */ + onSelect: (index: number) => void; + + /** Show that we can use large auto-complete suggestion picker. + * Depending on available space and whether the input is expanded, we can have a small or large mention suggester. + * When this value is false, the suggester will have a height of 2.5 items. When this value is true, the height can be up to 5 items. */ + isSuggestionPickerLarge: boolean; + + /** create accessibility label for each item */ + accessibilityLabelExtractor: (item: Suggestion, index: number) => string; + + /** Meaures the parent container's position and dimensions. */ + measureParentContainer?: (callback: MeasureParentContainerCallback) => void; +}; + +export type {AutoCompleteSuggestionsProps, Suggestion}; From 90e599b054cf86bb4f47c3d9e06eb1abbab183d7 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 8 Nov 2023 17:33:28 +0100 Subject: [PATCH 098/329] Revert "Revert "[Form Provider Refactor] AddDebitCardPage"" This reverts commit d7e14618 --- src/components/AddressSearch/index.js | 148 ++++++++++-------- src/components/CheckboxWithLabel.js | 3 +- src/components/Form/FormProvider.js | 21 ++- src/components/Form/InputWrapper.js | 3 +- src/pages/settings/Wallet/AddDebitCardPage.js | 36 +++-- 5 files changed, 131 insertions(+), 80 deletions(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 3e122e029969..e1cd582ef627 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, Keyboard, LogBox, ScrollView, Text, View} from 'react-native'; import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete'; import _ from 'underscore'; @@ -140,27 +140,46 @@ const defaultProps = { resultTypes: 'address', }; -// Do not convert to class component! It's been tried before and presents more challenges than it's worth. -// Relevant thread: https://expensify.slack.com/archives/C03TQ48KC/p1634088400387400 -// Reference: https://github.com/FaridSafi/react-native-google-places-autocomplete/issues/609#issuecomment-886133839 -function AddressSearch(props) { +function AddressSearch({ + canUseCurrentLocation, + containerStyles, + defaultValue, + errorText, + hint, + innerRef, + inputID, + isLimitedToUSA, + label, + maxInputLength, + network, + onBlur, + onInputChange, + onPress, + predefinedPlaces, + preferredLocale, + renamedInputKeys, + resultTypes, + shouldSaveDraft, + translate, + value, +}) { const [displayListViewBorder, setDisplayListViewBorder] = useState(false); const [isTyping, setIsTyping] = useState(false); const [isFocused, setIsFocused] = useState(false); - const [searchValue, setSearchValue] = useState(props.value || props.defaultValue || ''); + const [searchValue, setSearchValue] = useState(value || defaultValue || ''); const [locationErrorCode, setLocationErrorCode] = useState(null); const [isFetchingCurrentLocation, setIsFetchingCurrentLocation] = useState(false); const shouldTriggerGeolocationCallbacks = useRef(true); const containerRef = useRef(); const query = useMemo( () => ({ - language: props.preferredLocale, - types: props.resultTypes, - components: props.isLimitedToUSA ? 'country:us' : undefined, + language: preferredLocale, + types: resultTypes, + components: isLimitedToUSA ? 'country:us' : undefined, }), - [props.preferredLocale, props.resultTypes, props.isLimitedToUSA], + [preferredLocale, resultTypes, isLimitedToUSA], ); - const shouldShowCurrentLocationButton = props.canUseCurrentLocation && searchValue.trim().length === 0 && isFocused; + const shouldShowCurrentLocationButton = canUseCurrentLocation && searchValue.trim().length === 0 && isFocused; const saveLocationDetails = (autocompleteData, details) => { const addressComponents = details.address_components; @@ -169,7 +188,7 @@ function AddressSearch(props) { // to this component which don't match the usual properties coming from auto-complete. In that case, only a limited // amount of data massaging needs to happen for what the parent expects to get from this function. if (_.size(details)) { - props.onPress({ + onPress({ address: lodashGet(details, 'description'), lat: lodashGet(details, 'geometry.location.lat', 0), lng: lodashGet(details, 'geometry.location.lng', 0), @@ -256,7 +275,7 @@ function AddressSearch(props) { // Not all pages define the Address Line 2 field, so in that case we append any additional address details // (e.g. Apt #) to Address Line 1 - if (subpremise && typeof props.renamedInputKeys.street2 === 'undefined') { + if (subpremise && typeof renamedInputKeys.street2 === 'undefined') { values.street += `, ${subpremise}`; } @@ -265,19 +284,19 @@ function AddressSearch(props) { values.country = country; } - if (props.inputID) { - _.each(values, (value, key) => { - const inputKey = lodashGet(props.renamedInputKeys, key, key); + if (inputID) { + _.each(values, (inputValue, key) => { + const inputKey = lodashGet(renamedInputKeys, key, key); if (!inputKey) { return; } - props.onInputChange(value, inputKey); + onInputChange(inputValue, inputKey); }); } else { - props.onInputChange(values); + onInputChange(values); } - props.onPress(values); + onPress(values); }; /** Gets the user's current location and registers success/error callbacks */ @@ -325,16 +344,16 @@ function AddressSearch(props) { }; const renderHeaderComponent = () => - props.predefinedPlaces.length > 0 && ( + predefinedPlaces.length > 0 && ( <> {/* This will show current location button in list if there are some recent destinations */} {shouldShowCurrentLocationButton && ( )} - {!props.value && {props.translate('common.recentDestinations')}} + {!value && {translate('common.recentDestinations')}} ); @@ -346,6 +365,26 @@ function AddressSearch(props) { }; }, []); + const listEmptyComponent = useCallback( + () => + network.isOffline || !isTyping ? null : ( + {translate('common.noResultsFound')} + ), + [isTyping, translate, network.isOffline], + ); + + const listLoader = useCallback( + () => ( + + + + ), + [], + ); + return ( /* * The GooglePlacesAutocomplete component uses a VirtualizedList internally, @@ -372,20 +411,10 @@ function AddressSearch(props) { fetchDetails suppressDefaultStyles enablePoweredByContainer={false} - predefinedPlaces={props.predefinedPlaces} - listEmptyComponent={ - props.network.isOffline || !isTyping ? null : ( - {props.translate('common.noResultsFound')} - ) - } - listLoaderComponent={ - - - - } + predefinedPlaces={predefinedPlaces} + listEmptyComponent={listEmptyComponent} + listLoaderComponent={listLoader} + renderHeaderComponent={renderHeaderComponent} renderRow={(data) => { const title = data.isPredefinedPlace ? data.name : data.structured_formatting.main_text; const subtitle = data.isPredefinedPlace ? data.description : data.structured_formatting.secondary_text; @@ -396,7 +425,6 @@ function AddressSearch(props) { ); }} - renderHeaderComponent={renderHeaderComponent} onPress={(data, details) => { saveLocationDetails(data, details); setIsTyping(false); @@ -411,34 +439,31 @@ function AddressSearch(props) { query={query} requestUrl={{ useOnPlatform: 'all', - url: props.network.isOffline ? null : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}), + url: network.isOffline ? null : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}), }} textInputProps={{ InputComp: TextInput, ref: (node) => { - if (!props.innerRef) { + if (!innerRef) { return; } - if (_.isFunction(props.innerRef)) { - props.innerRef(node); + if (_.isFunction(innerRef)) { + innerRef(node); return; } // eslint-disable-next-line no-param-reassign - props.innerRef.current = node; + innerRef.current = node; }, - label: props.label, - containerStyles: props.containerStyles, - errorText: props.errorText, - hint: - displayListViewBorder || (props.predefinedPlaces.length === 0 && shouldShowCurrentLocationButton) || (props.canUseCurrentLocation && isTyping) - ? undefined - : props.hint, - value: props.value, - defaultValue: props.defaultValue, - inputID: props.inputID, - shouldSaveDraft: props.shouldSaveDraft, + label, + containerStyles, + errorText, + hint: displayListViewBorder || (predefinedPlaces.length === 0 && shouldShowCurrentLocationButton) || (canUseCurrentLocation && isTyping) ? undefined : hint, + value, + defaultValue, + inputID, + shouldSaveDraft, onFocus: () => { setIsFocused(true); }, @@ -448,24 +473,24 @@ function AddressSearch(props) { setIsFocused(false); setIsTyping(false); } - props.onBlur(); + onBlur(); }, autoComplete: 'off', onInputChange: (text) => { setSearchValue(text); setIsTyping(true); - if (props.inputID) { - props.onInputChange(text); + if (inputID) { + onInputChange(text); } else { - props.onInputChange({street: text}); + onInputChange({street: text}); } // If the text is empty and we have no predefined places, we set displayListViewBorder to false to prevent UI flickering - if (_.isEmpty(text) && _.isEmpty(props.predefinedPlaces)) { + if (_.isEmpty(text) && _.isEmpty(predefinedPlaces)) { setDisplayListViewBorder(false); } }, - maxLength: props.maxInputLength, + maxLength: maxInputLength, spellCheck: false, }} styles={{ @@ -486,17 +511,18 @@ function AddressSearch(props) { }} inbetweenCompo={ // We want to show the current location button even if there are no recent destinations - props.predefinedPlaces.length === 0 && shouldShowCurrentLocationButton ? ( + predefinedPlaces.length === 0 && shouldShowCurrentLocationButton ? ( ) : ( <> ) } + placeholder="" /> setLocationErrorCode(null)} diff --git a/src/components/CheckboxWithLabel.js b/src/components/CheckboxWithLabel.js index 86dba1d2a932..3d467b6372d2 100644 --- a/src/components/CheckboxWithLabel.js +++ b/src/components/CheckboxWithLabel.js @@ -7,6 +7,7 @@ import variables from '@styles/variables'; import Checkbox from './Checkbox'; import FormHelpMessage from './FormHelpMessage'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; +import refPropTypes from './refPropTypes'; import Text from './Text'; /** @@ -54,7 +55,7 @@ const propTypes = { defaultValue: PropTypes.bool, /** React ref being forwarded to the Checkbox input */ - forwardedRef: PropTypes.func, + forwardedRef: refPropTypes, /** The ID used to uniquely identify the input in a Form */ /* eslint-disable-next-line react/no-unused-prop-types */ diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 92baa9727832..85408323c9f2 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -71,6 +71,8 @@ const propTypes = { shouldValidateOnChange: PropTypes.bool, }; +const VALIDATE_DELAY = 200; + const defaultProps = { isSubmitButtonVisible: true, formState: { @@ -246,19 +248,28 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC // as this is already happening by the value prop. defaultValue: undefined, onTouched: (event) => { - setTouchedInput(inputID); + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); if (_.isFunction(propsToParse.onTouched)) { propsToParse.onTouched(event); } }, onPress: (event) => { - setTouchedInput(inputID); + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); if (_.isFunction(propsToParse.onPress)) { propsToParse.onPress(event); } }, - onPressIn: (event) => { - setTouchedInput(inputID); + onPressOut: (event) => { + // To prevent validating just pressed inputs, we need to set the touched input right after + // onValidate and to do so, we need to delays setTouchedInput of the same amount of time + // as the onValidate is delayed + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); if (_.isFunction(propsToParse.onPressIn)) { propsToParse.onPressIn(event); } @@ -274,7 +285,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC if (shouldValidateOnBlur) { onValidate(inputValues, !hasServerError); } - }, 200); + }, VALIDATE_DELAY); } if (_.isFunction(propsToParse.onBlur)) { diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index 99237fd8db43..b2e6f4477e89 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -1,12 +1,13 @@ import PropTypes from 'prop-types'; import React, {forwardRef, useContext} from 'react'; +import refPropTypes from '@components/refPropTypes'; import FormContext from './FormContext'; const propTypes = { InputComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.elementType]).isRequired, inputID: PropTypes.string.isRequired, valueType: PropTypes.string, - forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]), + forwardedRef: refPropTypes, }; const defaultProps = { diff --git a/src/pages/settings/Wallet/AddDebitCardPage.js b/src/pages/settings/Wallet/AddDebitCardPage.js index 09d43afc4bf4..d1abb342b7ed 100644 --- a/src/pages/settings/Wallet/AddDebitCardPage.js +++ b/src/pages/settings/Wallet/AddDebitCardPage.js @@ -4,7 +4,8 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import AddressSearch from '@components/AddressSearch'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; -import Form from '@components/Form'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import StatePicker from '@components/StatePicker'; @@ -117,7 +118,7 @@ function DebitCardPage(props) { title={translate('addDebitCardPage.addADebitCard')} onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET)} /> -

- (nameOnCardRef.current = ref)} + ref={nameOnCardRef} spellCheck={false} /> - - - - - - + - ( {`${translate('common.iAcceptThe')}`} @@ -198,7 +210,7 @@ function DebitCardPage(props) { )} style={[styles.mt4]} /> - + ); } From db66206d282d910f719f5d794e50cc353a8a2248 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 9 Nov 2023 09:11:09 +0100 Subject: [PATCH 099/329] ref: adjusted types and finish ts migration --- src/components/Button/{index.js => index.tsx} | 234 ++++++++---------- .../Button/validateSubmitShortcut/index.js | 19 -- .../validateSubmitShortcut/index.native.js | 17 -- .../validateSubmitShortcut/index.native.ts | 20 ++ .../Button/validateSubmitShortcut/index.ts | 22 ++ .../Button/validateSubmitShortcut/types.ts | 3 + 6 files changed, 143 insertions(+), 172 deletions(-) rename src/components/Button/{index.js => index.tsx} (68%) delete mode 100644 src/components/Button/validateSubmitShortcut/index.js delete mode 100644 src/components/Button/validateSubmitShortcut/index.native.js create mode 100644 src/components/Button/validateSubmitShortcut/index.native.ts create mode 100644 src/components/Button/validateSubmitShortcut/index.ts create mode 100644 src/components/Button/validateSubmitShortcut/types.ts diff --git a/src/components/Button/index.js b/src/components/Button/index.tsx similarity index 68% rename from src/components/Button/index.js rename to src/components/Button/index.tsx index 5fe7dd1fe812..9e0e35b914e6 100644 --- a/src/components/Button/index.js +++ b/src/components/Button/index.tsx @@ -1,11 +1,10 @@ import {useIsFocused} from '@react-navigation/native'; -import PropTypes from 'prop-types'; -import React, {useCallback} from 'react'; -import {ActivityIndicator, View} from 'react-native'; +import React, {ForwardedRef, useCallback} from 'react'; +import {ActivityIndicator, GestureResponderEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import {SvgProps} from 'react-native-svg'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import refPropTypes from '@components/refPropTypes'; import Text from '@components/Text'; import withNavigationFallback from '@components/withNavigationFallback'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; @@ -16,195 +15,155 @@ import themeColors from '@styles/themes/default'; import CONST from '@src/CONST'; import validateSubmitShortcut from './validateSubmitShortcut'; -const propTypes = { +type ButtonProps = { /** Should the press event bubble across multiple instances when Enter key triggers it. */ - allowBubble: PropTypes.bool, + allowBubble?: boolean; /** The text for the button label */ - text: PropTypes.string, + text?: string; /** Boolean whether to display the right icon */ - shouldShowRightIcon: PropTypes.bool, + shouldShowRightIcon?: boolean; /** The icon asset to display to the left of the text */ - icon: PropTypes.func, + icon?: React.FC | null; /** The icon asset to display to the right of the text */ - iconRight: PropTypes.func, + iconRight?: React.FC; /** The fill color to pass into the icon. */ - iconFill: PropTypes.string, + iconFill?: string; /** Any additional styles to pass to the left icon container. */ - // eslint-disable-next-line react/forbid-prop-types - iconStyles: PropTypes.arrayOf(PropTypes.object), + iconStyles?: Array>; /** Any additional styles to pass to the right icon container. */ - // eslint-disable-next-line react/forbid-prop-types - iconRightStyles: PropTypes.arrayOf(PropTypes.object), + iconRightStyles?: Array>; /** Small sized button */ - small: PropTypes.bool, + small?: boolean; /** Large sized button */ - large: PropTypes.bool, + large?: boolean; - /** medium sized button */ - medium: PropTypes.bool, + /** Medium sized button */ + medium?: boolean; /** Indicates whether the button should be disabled and in the loading state */ - isLoading: PropTypes.bool, + isLoading?: boolean; /** Indicates whether the button should be disabled */ - isDisabled: PropTypes.bool, + isDisabled?: boolean; /** A function that is called when the button is clicked on */ - onPress: PropTypes.func, + onPress?: (e?: GestureResponderEvent | KeyboardEvent) => void; /** A function that is called when the button is long pressed */ - onLongPress: PropTypes.func, + onLongPress?: (e?: GestureResponderEvent) => void; /** A function that is called when the button is pressed */ - onPressIn: PropTypes.func, + onPressIn?: () => void; /** A function that is called when the button is released */ - onPressOut: PropTypes.func, + onPressOut?: () => void; /** Callback that is called when mousedown is triggered. */ - onMouseDown: PropTypes.func, + onMouseDown?: () => void; /** Call the onPress function when Enter key is pressed */ - pressOnEnter: PropTypes.bool, + pressOnEnter?: boolean; /** The priority to assign the enter key event listener. 0 is the highest priority. */ - enterKeyEventListenerPriority: PropTypes.number, + enterKeyEventListenerPriority?: number; /** Additional styles to add after local styles. Applied to Pressable portion of button */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + style?: ViewStyle | ViewStyle[]; - /** Additional button styles. Specific to the OpacityView of button */ - // eslint-disable-next-line react/forbid-prop-types - innerStyles: PropTypes.arrayOf(PropTypes.object), + /** Additional button styles. Specific to the OpacityView of the button */ + innerStyles?: Array>; /** Additional text styles */ - // eslint-disable-next-line react/forbid-prop-types - textStyles: PropTypes.arrayOf(PropTypes.object), + textStyles?: Array>; /** Whether we should use the default hover style */ - shouldUseDefaultHover: PropTypes.bool, + shouldUseDefaultHover?: boolean; /** Whether we should use the success theme color */ - success: PropTypes.bool, + success?: boolean; /** Whether we should use the danger theme color */ - danger: PropTypes.bool, + danger?: boolean; - /** Children to replace all inner contents of button */ - children: PropTypes.node, + /** Children to replace all inner contents of the button */ + children?: React.ReactNode; /** Should we remove the right border radius top + bottom? */ - shouldRemoveRightBorderRadius: PropTypes.bool, + shouldRemoveRightBorderRadius?: boolean; /** Should we remove the left border radius top + bottom? */ - shouldRemoveLeftBorderRadius: PropTypes.bool, + shouldRemoveLeftBorderRadius?: boolean; /** Should enable the haptic feedback? */ - shouldEnableHapticFeedback: PropTypes.bool, + shouldEnableHapticFeedback?: boolean; /** Id to use for this button */ - id: PropTypes.string, + id?: string; /** Accessibility label for the component */ - accessibilityLabel: PropTypes.string, + accessibilityLabel?: string; /** A ref to forward the button */ - forwardedRef: refPropTypes, -}; - -const defaultProps = { - allowBubble: false, - text: '', - shouldShowRightIcon: false, - icon: null, - iconRight: Expensicons.ArrowRight, - iconFill: themeColors.textLight, - iconStyles: [], - iconRightStyles: [], - isLoading: false, - isDisabled: false, - small: false, - large: false, - medium: false, - onPress: () => {}, - onLongPress: () => {}, - onPressIn: () => {}, - onPressOut: () => {}, - onMouseDown: undefined, - pressOnEnter: false, - enterKeyEventListenerPriority: 0, - style: [], - innerStyles: [], - textStyles: [], - shouldUseDefaultHover: true, - success: false, - danger: false, - children: null, - shouldRemoveRightBorderRadius: false, - shouldRemoveLeftBorderRadius: false, - shouldEnableHapticFeedback: false, - id: '', - accessibilityLabel: '', - forwardedRef: undefined, + forwardedRef?: React.ForwardedRef; }; function Button({ - allowBubble, - text, - shouldShowRightIcon, - - icon, - iconRight, - iconFill, - iconStyles, - iconRightStyles, - - small, - large, - medium, - - isLoading, - isDisabled, - - onPress, - onLongPress, - onPressIn, - onPressOut, - onMouseDown, - - pressOnEnter, - enterKeyEventListenerPriority, - - style, - innerStyles, - textStyles, - - shouldUseDefaultHover, - success, - danger, - children, - - shouldRemoveRightBorderRadius, - shouldRemoveLeftBorderRadius, - shouldEnableHapticFeedback, - - id, - accessibilityLabel, - forwardedRef, -}) { + allowBubble = false, + text = '', + shouldShowRightIcon = false, + + icon = null, + iconRight = Expensicons.ArrowRight, + iconFill = themeColors.textLight, + iconStyles = [], + iconRightStyles = [], + + small = false, + large = false, + medium = false, + + isLoading = false, + isDisabled = false, + + onPress = () => {}, + onLongPress = () => {}, + onPressIn = () => {}, + onPressOut = () => {}, + onMouseDown = undefined, + + pressOnEnter = false, + enterKeyEventListenerPriority = 0, + + style = [], + innerStyles = [], + textStyles = [], + + shouldUseDefaultHover = true, + success = false, + danger = false, + children = null, + + shouldRemoveRightBorderRadius = false, + shouldRemoveLeftBorderRadius = false, + shouldEnableHapticFeedback = false, + + id = '', + accessibilityLabel = '', + forwardedRef = undefined, +}: ButtonProps) { const isFocused = useIsFocused(); const keyboardShortcutCallback = useCallback( - (event) => { + (event: GestureResponderEvent | KeyboardEvent) => { if (!validateSubmitShortcut(isFocused, isDisabled, isLoading, event)) { return; } @@ -246,6 +205,7 @@ function Button({ ); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (icon || shouldShowRightIcon) { return ( @@ -282,7 +242,8 @@ function Button({ ref={forwardedRef} onPress={(event) => { if (event && event.type === 'click') { - event.currentTarget.blur(); + const currentTarget = event?.currentTarget as HTMLElement; + currentTarget?.blur(); } if (shouldEnableHapticFeedback) { @@ -318,6 +279,7 @@ function Button({ isDisabled && !danger && !success ? styles.buttonDisabled : undefined, shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined, shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing icon || shouldShowRightIcon ? styles.alignItemsStretch : undefined, ...innerStyles, ]} @@ -342,18 +304,18 @@ function Button({ ); } -Button.propTypes = propTypes; -Button.defaultProps = defaultProps; Button.displayName = 'Button'; -const ButtonWithRef = React.forwardRef((props, ref) => ( -