Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/libs/Formula.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import type {Policy, Report, Transaction} from '@src/types/onyx';
import {getCurrencySymbol} from './CurrencyUtils';
import type {WorkingUpdates} from './OptimisticReportNamesCache';
import type {UpdateContext} from './OptimisticReportNamesConnectionManager';
import {getAllReportActions} from './ReportActionsUtils';
import {getReportTransactions} from './ReportUtils';
import {getCreated, isPartialTransaction} from './TransactionUtils';
Expand All @@ -25,6 +27,8 @@ type FormulaContext = {
report: Report;
policy: OnyxEntry<Policy>;
transaction?: Transaction;
workingUpdates: WorkingUpdates;
updateContext: UpdateContext;
};

const FORMULA_PART_TYPES = {
Expand Down Expand Up @@ -471,7 +475,7 @@ function formatType(type: string | undefined): string {
/**
* Get the date of the oldest transaction for a given report
*/
function getOldestTransactionDate(reportID: string, context?: FormulaContext): string | undefined {
function getOldestTransactionDate(reportID: string, context: FormulaContext): string | undefined {
if (!reportID) {
return undefined;
}
Expand All @@ -485,6 +489,8 @@ function getOldestTransactionDate(reportID: string, context?: FormulaContext): s

transactions.forEach((transaction) => {
// Use updated transaction data if available and matches this transaction

// FormulaContext transaction is the most current, so it takes priority

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to ensure, that transaction that we're trying checking is most up to date. If there were any previous updates to transaction the cached transaction is the most up to date.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so why can't we always get the transaction from the cache, instead of starting with the FormulaContext value, then falling back to the cache, then the transaction currently stored in Onyx?

Isn't the formula context transaction always the same as the one in the cache?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, context's transaction is from cache ☝️.

const currentTransaction = context?.transaction && transaction.transactionID === context.transaction.transactionID ? context.transaction : transaction;

const created = getCreated(currentTransaction);
Expand Down
180 changes: 88 additions & 92 deletions src/libs/OptimisticReportNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,54 @@ import type Report from '@src/types/onyx/Report';
import type {FormulaContext} from './Formula';
import {compute, FORMULA_PART_TYPES, parse} from './Formula';
import Log from './Log';
import {applyUpdateToCache, getCachedPolicyByID, getCachedReportByID, getCachedReportNameValuePairsByID, getCachedTransactionByID} from './OptimisticReportNamesCache';
import type {WorkingUpdates} from './OptimisticReportNamesCache';
import type {UpdateContext} from './OptimisticReportNamesConnectionManager';
import Permissions from './Permissions';
import {isArchivedReport} from './ReportUtils';

const UPDATE_TYPES = {
REPORT: 'report',
TRANSACTION: 'transaction',
POLICY: 'policy',
REPORT_NAME_VALUE_PAIRS: 'reportNameValuePairs',
} as const;

type UpdateType = (typeof UPDATE_TYPES)[keyof typeof UPDATE_TYPES];

/**
* Get the title field from report name value pairs
*/
function getTitleFieldFromRNVP(reportID: string, context: UpdateContext) {
const reportNameValuePairs = context.allReportNameValuePairs[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`];
function getTitleFieldFromRNVP(reportID: string, context: UpdateContext, workingUpdates: WorkingUpdates) {
const reportNameValuePairs = getCachedReportNameValuePairsByID(reportID, context, workingUpdates);
return reportNameValuePairs?.expensify_text_title;
}

/**
* Get the object type from an Onyx key
*/
function determineObjectTypeByKey(key: string): 'report' | 'policy' | 'transaction' | 'unknown' {
function determineObjectTypeByKey(key: string): UpdateType | 'unknown' {
if (key.startsWith(ONYXKEYS.COLLECTION.REPORT)) {
return 'report';
return UPDATE_TYPES.REPORT;
}
if (key.startsWith(ONYXKEYS.COLLECTION.POLICY)) {
return 'policy';
return UPDATE_TYPES.POLICY;
}
if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) {
return 'transaction';
return UPDATE_TYPES.TRANSACTION;
}
if (key.startsWith(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS)) {
return UPDATE_TYPES.REPORT_NAME_VALUE_PAIRS;
}

return 'unknown';
}

/**
* Extract report ID from an Onyx key
*/
function getReportIDFromKey(key: string): string {
return key.replace(ONYXKEYS.COLLECTION.REPORT, '');
return key.replace(ONYXKEYS.COLLECTION.REPORT, '').replace(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, '');
}

/**
Expand All @@ -59,28 +74,21 @@ function getTransactionIDFromKey(key: string): string {
return key.replace(ONYXKEYS.COLLECTION.TRANSACTION, '');
}

/**
* Get report by ID from the reports collection
*/
function getReportByID(reportID: string, allReports: Record<string, Report>): Report | undefined {
return allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
}

/**
* Get policy by ID from the policies collection
*/
function getPolicyByID(policyID: string | undefined, allPolicies: Record<string, Policy>): Policy | undefined {
function getPolicyByID(policyID: string | undefined, context: UpdateContext, workingUpdates: WorkingUpdates): Policy | undefined {
if (!policyID) {
return;
}
return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
return getCachedPolicyByID(policyID, context, workingUpdates);
}

/**
* Get transaction by ID from the transactions collection
*/
function getTransactionByID(transactionID: string, allTransactions: Record<string, Transaction>): Transaction | undefined {
return allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
function getTransactionByID(transactionID: string, context: UpdateContext, workingUpdates: WorkingUpdates): Transaction | undefined {
return getCachedTransactionByID(transactionID, context, workingUpdates);
}

/**
Expand Down Expand Up @@ -119,19 +127,18 @@ function getReportsForNameComputation(policyID: string, allReports: Record<strin
/**
* Get the report associated with a transaction ID
*/
function getReportByTransactionID(transactionID: string, context: UpdateContext): Report | undefined {
function getReportIDByTransactionID(transactionID: string, context: UpdateContext, workingUpdates: WorkingUpdates): string | undefined {
if (!transactionID) {
return undefined;
}

const transaction = getTransactionByID(transactionID, context.allTransactions);
const transaction = getTransactionByID(transactionID, context, workingUpdates);

if (!transaction?.reportID) {
return undefined;
}

// Get the report using the transaction's reportID from context
return getReportByID(transaction.reportID, context.allReports);
return transaction.reportID;
}

/**
Expand All @@ -144,7 +151,7 @@ function getReportKey(reportID: string): OnyxKey {
/**
* Check if a report should have its name automatically computed
*/
function shouldComputeReportName(report: Report, context: UpdateContext): boolean {
function shouldComputeReportName(report: Report, context: UpdateContext, workingUpdates: WorkingUpdates): boolean {
if (!report) {
return false;
}
Expand All @@ -155,7 +162,7 @@ function shouldComputeReportName(report: Report, context: UpdateContext): boolea

// Only compute names for expense reports with policies that have title fields
// Check if the report has a title field with a formula in rNVP
const reportTitleField = getTitleFieldFromRNVP(report.reportID, context);
const reportTitleField = getTitleFieldFromRNVP(report.reportID, context, workingUpdates);
if (!reportTitleField?.defaultValue) {
return false;
}
Expand All @@ -178,32 +185,29 @@ function isValidReportType(reportType?: string): boolean {
/**
* Compute a new report name if needed based on an optimistic update
*/
function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: OnyxUpdate, context: UpdateContext): string | null {
const {allPolicies} = context;

// If no report is provided, extract it from the update (for new reports)
const targetReport = report ?? (incomingUpdate.value as Report);
function computeReportNameIfNeeded(reportID: string, context: UpdateContext, incomingUpdate: OnyxUpdate, workingUpdates: WorkingUpdates): string | null {
const targetReport = getCachedReportByID(reportID, context, workingUpdates);
const updateType = determineObjectTypeByKey(incomingUpdate.key);

if (!targetReport) {
return null;
}

const policy = getPolicyByID(targetReport.policyID, allPolicies);
const policy = getPolicyByID(targetReport.policyID, context, workingUpdates);

if (!shouldComputeReportName(targetReport, context)) {
if (!shouldComputeReportName(targetReport, context, workingUpdates)) {
return null;
}

const titleField = getTitleFieldFromRNVP(targetReport.reportID, context);
const titleField = getTitleFieldFromRNVP(targetReport.reportID, context, workingUpdates);

// Quick check: see if the update might affect the report name
const updateType = determineObjectTypeByKey(incomingUpdate.key);
const formula = titleField?.defaultValue;
const formulaParts = parse(formula);

let transaction: Transaction | undefined;
if (updateType === 'transaction') {
transaction = getTransactionByID((incomingUpdate.value as Transaction).transactionID, context.allTransactions);
if (updateType === UPDATE_TYPES.TRANSACTION) {
transaction = getTransactionByID(getTransactionIDFromKey(incomingUpdate.key), context, workingUpdates);
}

// Check if any formula part might be affected by this update
Expand All @@ -212,10 +216,10 @@ function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: O
// Checking if the formula part is affected in this manner works, but it could certainly be more precise.
// For example, a policy update only affects the part if the formula in the policy changed, or if the report part references a field on the policy.
// However, if we run into performance problems, this would be a good place to optimize.
return updateType === 'report' || updateType === 'transaction' || updateType === 'policy';
return updateType === UPDATE_TYPES.REPORT || updateType === UPDATE_TYPES.TRANSACTION || updateType === UPDATE_TYPES.POLICY || updateType === UPDATE_TYPES.REPORT_NAME_VALUE_PAIRS;
}
if (part.type === FORMULA_PART_TYPES.FIELD) {
return updateType === 'report';
return updateType === UPDATE_TYPES.REPORT || updateType === UPDATE_TYPES.REPORT_NAME_VALUE_PAIRS;
}
return false;
});
Expand All @@ -224,19 +228,13 @@ function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: O
return null;
}

// Build context with the updated data
const updatedReport =
updateType === 'report' && targetReport.reportID === getReportIDFromKey(incomingUpdate.key) ? {...targetReport, ...(incomingUpdate.value as Partial<Report>)} : targetReport;

const updatedPolicy = updateType === 'policy' && targetReport.policyID === getPolicyIDFromKey(incomingUpdate.key) ? {...(policy ?? {}), ...(incomingUpdate.value as Policy)} : policy;

const updatedTransaction = updateType === 'transaction' ? {...(transaction ?? {}), ...(incomingUpdate.value as Transaction)} : undefined;

// Compute the new name
const formulaContext: FormulaContext = {
report: updatedReport,
policy: updatedPolicy,
transaction: updatedTransaction,
report: targetReport,
policy,
transaction,
workingUpdates,
updateContext: context,
};

const newName = compute(formula, formulaContext);
Expand All @@ -245,7 +243,6 @@ function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: O
if (newName && newName !== targetReport.reportName) {
Log.info('[OptimisticReportNames] Report name computed', false, {
updateType,
isNewReport: !report,
});

return newName;
Expand All @@ -271,70 +268,55 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context:
});

const additionalUpdates: OnyxUpdate[] = [];
let workingUpdates: WorkingUpdates = {};
const affectedReports = new Map<
string,
{
update: OnyxUpdate;
}
>();

for (const update of updates) {
const objectType = determineObjectTypeByKey(update.key);
workingUpdates = applyUpdateToCache(workingUpdates, update, context);

switch (objectType) {
case 'report': {
const reportID = getReportIDFromKey(update.key);
const report = getReportByID(reportID, allReports);

// Handle both existing and new reports with the same function
const reportNameUpdate = computeReportNameIfNeeded(report, update, context);

if (reportNameUpdate) {
additionalUpdates.push({
key: getReportKey(reportID),
onyxMethod: Onyx.METHOD.MERGE,
value: {
reportName: reportNameUpdate,
},
});
}
affectedReports.set(reportID, {
update,
});
break;
}

case 'policy': {
const policyID = getPolicyIDFromKey(update.key);
const affectedReports = getReportsForNameComputation(policyID, allReports, context);
for (const report of affectedReports) {
const reportNameUpdate = computeReportNameIfNeeded(report, update, context);

if (reportNameUpdate) {
additionalUpdates.push({
key: getReportKey(report.reportID),
onyxMethod: Onyx.METHOD.MERGE,
value: {
reportName: reportNameUpdate,
},
});
}
const reports = getReportsForNameComputation(policyID, allReports, context);
for (const report of reports) {
affectedReports.set(report.reportID, {update});
}
break;
}

case 'transaction': {
let report: Report | undefined;
let reportID: string | undefined;
const transactionUpdate = update.value as Partial<Transaction>;

if (transactionUpdate.reportID) {
report = getReportByID(transactionUpdate.reportID, allReports);
reportID = transactionUpdate.reportID;
} else {
report = getReportByTransactionID(getTransactionIDFromKey(update.key), context);
reportID = getReportIDByTransactionID(getTransactionIDFromKey(update.key), context, workingUpdates);
}
if (reportID) {
affectedReports.set(reportID, {update});
}
break;
}
case 'reportNameValuePairs': {
const reportID = getReportIDFromKey(update.key);

if (report) {
const reportNameUpdate = computeReportNameIfNeeded(report, update, context);

if (reportNameUpdate) {
additionalUpdates.push({
key: getReportKey(report.reportID),
onyxMethod: Onyx.METHOD.MERGE,
value: {
reportName: reportNameUpdate,
},
});
}
if (reportID) {
affectedReports.set(reportID, {update});
}
break;
}
Expand All @@ -344,12 +326,26 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context:
}
}

for (const [reportID, {update}] of affectedReports) {
const reportNameUpdate = computeReportNameIfNeeded(reportID, context, update, workingUpdates);

if (reportNameUpdate) {
additionalUpdates.push({
key: getReportKey(reportID),
onyxMethod: Onyx.METHOD.MERGE,
value: {
reportName: reportNameUpdate,
},
});
}
}

Log.info('[OptimisticReportNames] Processing completed', false, {
additionalUpdatesCount: additionalUpdates.length,
});

return updates.concat(additionalUpdates);
}

export {computeReportNameIfNeeded, getReportByTransactionID, shouldComputeReportName, updateOptimisticReportNamesFromUpdates};
export {computeReportNameIfNeeded, shouldComputeReportName, updateOptimisticReportNamesFromUpdates};
export type {UpdateContext};
Loading
Loading