) {
+ const customUnitRateDate = transaction?.comment?.customUnit?.attributes?.dates ?? {start: '', end: ''};
+ const startDate = new Date(customUnitRateDate.start);
+ const endDate = new Date(customUnitRateDate.end);
+ const firstDayDiff = differenceInMinutes(startOfDay(addDays(startDate, 1)), startDate);
+ const tripDaysDiff = differenceInDays(startOfDay(endDate), startOfDay(addDays(startDate, 1)));
+ const lastDayDiff = differenceInMinutes(endDate, startOfDay(endDate));
+ return {
+ firstDay: firstDayDiff === 1440 ? undefined : (firstDayDiff / 60).toFixed(2),
+ tripDays: firstDayDiff === 1440 ? tripDaysDiff + 1 : tripDaysDiff,
+ lastDay: lastDayDiff === 0 ? undefined : (lastDayDiff / 60).toFixed(2),
+ };
+}
+
+export type {Destination};
+
+export {getCustomUnitID, getDestinationListSections, getDestinationForDisplay, getSubratesFields, getSubratesForDisplay, getTimeForDisplay, getTimeDifferenceIntervals};
diff --git a/src/libs/Performance.tsx b/src/libs/Performance.tsx
index ef2b08e47229..df642e9f0681 100644
--- a/src/libs/Performance.tsx
+++ b/src/libs/Performance.tsx
@@ -3,51 +3,13 @@ import isObject from 'lodash/isObject';
import lodashTransform from 'lodash/transform';
import React, {forwardRef, Profiler} from 'react';
import {Alert, InteractionManager} from 'react-native';
-import type {PerformanceEntry, PerformanceMark, PerformanceMeasure, ReactNativePerformance, Performance as RNPerformance} from 'react-native-performance';
-import type {PerformanceObserverEntryList} from 'react-native-performance/lib/typescript/performance-observer';
+import performance, {PerformanceObserver, setResourceLoggingEnabled} from 'react-native-performance';
+import type {PerformanceEntry, PerformanceMark, PerformanceMeasure} from 'react-native-performance';
import CONST from '@src/CONST';
import isE2ETestSession from './E2E/isE2ETestSession';
import getComponentDisplayName from './getComponentDisplayName';
import * as Metrics from './Metrics';
-type WrappedComponentConfig = {id: string};
-
-type PerformanceEntriesCallback = (entry: PerformanceEntry) => void;
-
-type Phase = 'mount' | 'update' | 'nested-update';
-
-type WithRenderTraceHOC = >(WrappedComponent: React.ComponentType
) => React.ComponentType
>;
-
-type BlankHOC =
>(Component: React.ComponentType
) => React.ComponentType
;
-
-type SetupPerformanceObserver = () => void;
-type DiffObject = (object: Record, base: Record) => Record;
-type GetPerformanceMetrics = () => PerformanceEntry[];
-type PrintPerformanceMetrics = () => void;
-type MarkStart = (name: string, detail?: Record) => PerformanceMark | void;
-type MarkEnd = (name: string, detail?: Record) => PerformanceMark | void;
-type MeasureFailSafe = (measureName: string, startOrMeasureOptions: string, endMark?: string) => void;
-type MeasureTTI = (endMark?: string) => void;
-type TraceRender = (id: string, phase: Phase, actualDuration: number, baseDuration: number, startTime: number, commitTime: number, interactions: Set) => PerformanceMeasure | void;
-type WithRenderTrace = ({id}: WrappedComponentConfig) => WithRenderTraceHOC | BlankHOC;
-type SubscribeToMeasurements = (callback: PerformanceEntriesCallback) => void;
-
-type PerformanceModule = {
- diffObject: DiffObject;
- setupPerformanceObserver: SetupPerformanceObserver;
- getPerformanceMetrics: GetPerformanceMetrics;
- printPerformanceMetrics: PrintPerformanceMetrics;
- markStart: MarkStart;
- markEnd: MarkEnd;
- measureFailSafe: MeasureFailSafe;
- measureTTI: MeasureTTI;
- traceRender: TraceRender;
- withRenderTrace: WithRenderTrace;
- subscribeToMeasurements: SubscribeToMeasurements;
-};
-
-let rnPerformance: RNPerformance;
-
/**
* Deep diff between two objects. Useful for figuring out what changed about an object from one render to the next so
* that state and props updates can be optimized.
@@ -66,204 +28,242 @@ function diffObject(object: Record, base: Record {},
- getPerformanceMetrics: () => [],
- printPerformanceMetrics: () => {},
- markStart: () => {},
- markEnd: () => {},
- measureFailSafe: () => {},
- measureTTI: () => {},
- traceRender: () => {},
- withRenderTrace:
- () =>
- // eslint-disable-next-line @typescript-eslint/naming-convention
- >(Component: React.ComponentType
): React.ComponentType
=>
- Component,
- subscribeToMeasurements: () => {},
-};
+function measureFailSafe(measureName: string, startOrMeasureOptions: string, endMark?: string): void {
+ try {
+ performance.measure(measureName, startOrMeasureOptions, endMark);
+ } catch (error) {
+ // Sometimes there might be no start mark recorded and the measure will fail with an error
+ if (error instanceof Error) {
+ console.debug(error.message);
+ }
+ }
+}
+
+/**
+ * Measures the TTI (time to interactive) time starting from the `nativeLaunchStart` event.
+ * To be called when the app is considered to be interactive.
+ */
+function measureTTI(endMark?: string): void {
+ // Make sure TTI is captured when the app is really usable
+ InteractionManager.runAfterInteractions(() => {
+ requestAnimationFrame(() => {
+ measureFailSafe('TTI', 'nativeLaunchStart', endMark);
-if (Metrics.canCapturePerformanceMetrics()) {
- const perfModule = require('react-native-performance');
- perfModule.setResourceLoggingEnabled(true);
- rnPerformance = perfModule.default;
-
- Performance.measureFailSafe = (measureName: string, startOrMeasureOptions: string, endMark?: string) => {
- try {
- rnPerformance.measure(measureName, startOrMeasureOptions, endMark);
- } catch (error) {
- // Sometimes there might be no start mark recorded and the measure will fail with an error
- if (error instanceof Error) {
- console.debug(error.message);
+ // We don't want an alert to show:
+ // - on builds with performance metrics collection disabled by a feature flag
+ // - e2e test sessions
+ if (!Metrics.canCapturePerformanceMetrics() || isE2ETestSession()) {
+ return;
}
- }
- };
- /**
- * Measures the TTI time. To be called when the app is considered to be interactive.
- */
- Performance.measureTTI = (endMark?: string) => {
- // Make sure TTI is captured when the app is really usable
- InteractionManager.runAfterInteractions(() => {
- requestAnimationFrame(() => {
- Performance.measureFailSafe('TTI', 'nativeLaunchStart', endMark);
-
- // we don't want the alert to show on an e2e test session
- if (!isE2ETestSession()) {
- Performance.printPerformanceMetrics();
- }
- });
+ printPerformanceMetrics();
});
- };
+ });
+}
- /**
- * Sets up an observer to capture events recorded in the native layer before the app fully initializes.
- */
- Performance.setupPerformanceObserver = () => {
- // Monitor some native marks that we want to put on the timeline
- new perfModule.PerformanceObserver((list: PerformanceObserverEntryList, observer: PerformanceObserver) => {
- list.getEntries().forEach((entry: PerformanceEntry) => {
- if (entry.name === 'nativeLaunchEnd') {
- Performance.measureFailSafe('nativeLaunch', 'nativeLaunchStart', 'nativeLaunchEnd');
- }
- if (entry.name === 'downloadEnd') {
- Performance.measureFailSafe('jsBundleDownload', 'downloadStart', 'downloadEnd');
- }
- if (entry.name === 'runJsBundleEnd') {
- Performance.measureFailSafe('runJsBundle', 'runJsBundleStart', 'runJsBundleEnd');
- }
- if (entry.name === 'appCreationEnd') {
- Performance.measureFailSafe('appCreation', 'appCreationStart', 'appCreationEnd');
- Performance.measureFailSafe('nativeLaunchEnd_To_appCreationStart', 'nativeLaunchEnd', 'appCreationStart');
- }
- if (entry.name === 'contentAppeared') {
- Performance.measureFailSafe('appCreationEnd_To_contentAppeared', 'appCreationEnd', 'contentAppeared');
- }
-
- // We don't need to keep the observer past this point
- if (entry.name === 'runJsBundleEnd' || entry.name === 'downloadEnd') {
- observer.disconnect();
- }
- });
- }).observe({type: 'react-native-mark', buffered: true});
-
- // Monitor for "_end" marks and capture "_start" to "_end" measures
- new perfModule.PerformanceObserver((list: PerformanceObserverEntryList) => {
- list.getEntriesByType('mark').forEach((mark: PerformanceEntry) => {
- if (mark.name.endsWith('_end')) {
- const end = mark.name;
- const name = end.replace(/_end$/, '');
- const start = `${name}_start`;
- Performance.measureFailSafe(name, start, end);
- }
-
- // Capture any custom measures or metrics below
- if (mark.name === `${CONST.TIMING.SIDEBAR_LOADED}_end`) {
- Performance.measureFailSafe('contentAppeared_To_screenTTI', 'contentAppeared', mark.name);
- Performance.measureTTI(mark.name);
- }
- });
- }).observe({type: 'mark', buffered: true});
- };
+/*
+ * Monitor native marks that we want to put on the timeline
+ */
+const nativeMarksObserver = new PerformanceObserver((list, _observer) => {
+ list.getEntries().forEach((entry) => {
+ if (entry.name === 'nativeLaunchEnd') {
+ measureFailSafe('nativeLaunch', 'nativeLaunchStart', 'nativeLaunchEnd');
+ }
+ if (entry.name === 'downloadEnd') {
+ measureFailSafe('jsBundleDownload', 'downloadStart', 'downloadEnd');
+ }
+ if (entry.name === 'runJsBundleEnd') {
+ measureFailSafe('runJsBundle', 'runJsBundleStart', 'runJsBundleEnd');
+ }
+ if (entry.name === 'appCreationEnd') {
+ measureFailSafe('appCreation', 'appCreationStart', 'appCreationEnd');
+ measureFailSafe('nativeLaunchEnd_To_appCreationStart', 'nativeLaunchEnd', 'appCreationStart');
+ }
+ if (entry.name === 'contentAppeared') {
+ measureFailSafe('appCreationEnd_To_contentAppeared', 'appCreationEnd', 'contentAppeared');
+ }
- Performance.getPerformanceMetrics = (): PerformanceEntry[] =>
- [
- ...rnPerformance.getEntriesByName('nativeLaunch'),
- ...rnPerformance.getEntriesByName('nativeLaunchEnd_To_appCreationStart'),
- ...rnPerformance.getEntriesByName('appCreation'),
- ...rnPerformance.getEntriesByName('appCreationEnd_To_contentAppeared'),
- ...rnPerformance.getEntriesByName('contentAppeared_To_screenTTI'),
- ...rnPerformance.getEntriesByName('runJsBundle'),
- ...rnPerformance.getEntriesByName('jsBundleDownload'),
- ...rnPerformance.getEntriesByName('TTI'),
- ...rnPerformance.getEntriesByName('regularAppStart'),
- ...rnPerformance.getEntriesByName('appStartedToReady'),
- ].filter((entry) => entry.duration > 0);
-
- /**
- * Outputs performance stats. We alert these so that they are easy to access in release builds.
- */
- Performance.printPerformanceMetrics = () => {
- const stats = Performance.getPerformanceMetrics();
- const statsAsText = stats.map((entry) => `\u2022 ${entry.name}: ${entry.duration.toFixed(1)}ms`).join('\n');
-
- if (stats.length > 0) {
- Alert.alert('Performance', statsAsText);
+ // At this point we've captured and processed all the native marks we're interested in
+ // and are not expecting to have more thus we can safely disconnect the observer
+ if (entry.name === 'runJsBundleEnd' || entry.name === 'downloadEnd') {
+ _observer.disconnect();
}
- };
+ });
+});
- Performance.subscribeToMeasurements = (callback: PerformanceEntriesCallback) => {
- new perfModule.PerformanceObserver((list: PerformanceObserverEntryList) => {
- list.getEntriesByType('measure').forEach(callback);
- }).observe({type: 'measure', buffered: true});
+function setNativeMarksObserverEnabled(enabled = false): void {
+ if (!enabled) {
+ nativeMarksObserver.disconnect();
+ return;
+ }
+
+ nativeMarksObserver.disconnect();
+ nativeMarksObserver.observe({type: 'react-native-mark', buffered: true});
+}
+
+/**
+ * Monitor for "_end" marks and capture "_start" to "_end" measures, including events recorded in the native layer before the app fully initializes.
+ */
+const customMarksObserver = new PerformanceObserver((list) => {
+ list.getEntriesByType('mark').forEach((mark) => {
+ if (mark.name.endsWith('_end')) {
+ const end = mark.name;
+ const name = end.replace(/_end$/, '');
+ const start = `${name}_start`;
+ measureFailSafe(name, start, end);
+ }
+
+ // Capture any custom measures or metrics below
+ if (mark.name === `${CONST.TIMING.SIDEBAR_LOADED}_end`) {
+ measureFailSafe('contentAppeared_To_screenTTI', 'contentAppeared', mark.name);
+ measureTTI(mark.name);
+ }
+ });
+});
+
+function setCustomMarksObserverEnabled(enabled = false): void {
+ if (!enabled) {
+ customMarksObserver.disconnect();
+ return;
+ }
+
+ customMarksObserver.disconnect();
+ customMarksObserver.observe({type: 'mark', buffered: true});
+}
+
+function getPerformanceMetrics(): PerformanceEntry[] {
+ return [
+ ...performance.getEntriesByName('nativeLaunch'),
+ ...performance.getEntriesByName('nativeLaunchEnd_To_appCreationStart'),
+ ...performance.getEntriesByName('appCreation'),
+ ...performance.getEntriesByName('appCreationEnd_To_contentAppeared'),
+ ...performance.getEntriesByName('contentAppeared_To_screenTTI'),
+ ...performance.getEntriesByName('runJsBundle'),
+ ...performance.getEntriesByName('jsBundleDownload'),
+ ...performance.getEntriesByName('TTI'),
+ ...performance.getEntriesByName('regularAppStart'),
+ ...performance.getEntriesByName('appStartedToReady'),
+ ].filter((entry) => entry.duration > 0);
+}
+
+function getPerformanceMeasures(): PerformanceEntry[] {
+ return performance.getEntriesByType('measure');
+}
+
+/**
+ * Outputs performance stats. We alert these so that they are easy to access in release builds.
+ */
+function printPerformanceMetrics(): void {
+ const stats = getPerformanceMetrics();
+ const statsAsText = stats.map((entry) => `\u2022 ${entry.name}: ${entry.duration.toFixed(1)}ms`).join('\n');
+
+ if (stats.length > 0) {
+ Alert.alert('Performance', statsAsText);
+ }
+}
+
+function subscribeToMeasurements(callback: (entry: PerformanceEntry) => void): () => void {
+ const observer = new PerformanceObserver((list) => {
+ list.getEntriesByType('measure').forEach(callback);
+ });
+
+ observer.observe({type: 'measure', buffered: true});
+
+ return () => observer.disconnect();
+}
+
+/**
+ * Add a start mark to the performance entries
+ */
+function markStart(name: string, detail?: Record): PerformanceMark {
+ return performance.mark(`${name}_start`, {detail});
+}
+
+/**
+ * Add an end mark to the performance entries
+ * A measure between start and end is captured automatically
+ */
+function markEnd(name: string, detail?: Record): PerformanceMark {
+ return performance.mark(`${name}_end`, {detail});
+}
+
+type Phase = 'mount' | 'update' | 'nested-update';
+
+/**
+ * Put data emitted by Profiler components on the timeline
+ * @param id the "id" prop of the Profiler tree that has just committed
+ * @param phase either "mount" (if the tree just mounted) or "update" (if it re-rendered)
+ * @param actualDuration time spent rendering the committed update
+ * @param baseDuration estimated time to render the entire subtree without memoization
+ * @param startTime when React began rendering this update
+ * @param commitTime when React committed this update
+ * @param interactions the Set of interactions belonging to this update
+ */
+function traceRender(id: string, phase: Phase, actualDuration: number, baseDuration: number, startTime: number, commitTime: number, interactions: Set): PerformanceMeasure {
+ return performance.measure(id, {
+ start: startTime,
+ duration: actualDuration,
+ detail: {
+ phase,
+ baseDuration,
+ commitTime,
+ interactions,
+ },
+ });
+}
+
+type WrappedComponentConfig = {id: string};
+
+/**
+ * A HOC that captures render timings of the Wrapped component
+ */
+function withRenderTrace({id}: WrappedComponentConfig) {
+ if (!Metrics.canCapturePerformanceMetrics()) {
+ return >(WrappedComponent: React.ComponentType
): React.ComponentType
=> WrappedComponent;
+ }
+
+ return
>(WrappedComponent: React.ComponentType
): React.ComponentType
> => {
+ const WithRenderTrace: React.ComponentType
> = forwardRef((props: P, ref) => (
+
+
+
+ ));
+
+ WithRenderTrace.displayName = `withRenderTrace(${getComponentDisplayName(WrappedComponent as React.ComponentType)})`;
+ return WithRenderTrace;
};
+}
- /**
- * Add a start mark to the performance entries
- */
- Performance.markStart = (name: string, detail?: Record): PerformanceMark => rnPerformance.mark(`${name}_start`, {detail});
-
- /**
- * Add an end mark to the performance entries
- * A measure between start and end is captured automatically
- */
- Performance.markEnd = (name: string, detail?: Record): PerformanceMark => rnPerformance.mark(`${name}_end`, {detail});
-
- /**
- * Put data emitted by Profiler components on the timeline
- * @param id the "id" prop of the Profiler tree that has just committed
- * @param phase either "mount" (if the tree just mounted) or "update" (if it re-rendered)
- * @param actualDuration time spent rendering the committed update
- * @param baseDuration estimated time to render the entire subtree without memoization
- * @param startTime when React began rendering this update
- * @param commitTime when React committed this update
- * @param interactions the Set of interactions belonging to this update
- */
- Performance.traceRender = (
- id: string,
- phase: Phase,
- actualDuration: number,
- baseDuration: number,
- startTime: number,
- commitTime: number,
- interactions: Set,
- ): PerformanceMeasure =>
- rnPerformance.measure(id, {
- start: startTime,
- duration: actualDuration,
- detail: {
- phase,
- baseDuration,
- commitTime,
- interactions,
- },
- });
+function enableMonitoring() {
+ setResourceLoggingEnabled(true);
+ setNativeMarksObserverEnabled(true);
+ setCustomMarksObserverEnabled(true);
+}
- /**
- * A HOC that captures render timings of the Wrapped component
- */
- Performance.withRenderTrace =
- ({id}: WrappedComponentConfig) =>
- // eslint-disable-next-line @typescript-eslint/naming-convention
- >(WrappedComponent: React.ComponentType
): React.ComponentType
> => {
- const WithRenderTrace: React.ComponentType
> = forwardRef((props: P, ref) => (
-
-
-
- ));
-
- WithRenderTrace.displayName = `withRenderTrace(${getComponentDisplayName(WrappedComponent as React.ComponentType)})`;
- return WithRenderTrace;
- };
+function disableMonitoring() {
+ setResourceLoggingEnabled(false);
+ setNativeMarksObserverEnabled(false);
+ setCustomMarksObserverEnabled(false);
}
-export default Performance;
+export default {
+ diffObject,
+ measureFailSafe,
+ measureTTI,
+ enableMonitoring,
+ disableMonitoring,
+ getPerformanceMetrics,
+ getPerformanceMeasures,
+ printPerformanceMetrics,
+ subscribeToMeasurements,
+ markStart,
+ markEnd,
+ withRenderTrace,
+};
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index bebd54698288..1ab0807c1900 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -35,11 +35,6 @@ function canUsePerDiem(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.PER_DIEM) || canUseAllBetas(betas);
}
-// TEMPORARY BETA TO HIDE PRODUCT TRAINING TOOLTIP AND MIGRATE USER WELCOME MODAL
-function shouldShowProductTrainingElements(betas: OnyxEntry): boolean {
- return !!betas?.includes(CONST.BETAS.PRODUCT_TRAINING) || canUseAllBetas(betas);
-}
-
/**
* Link previews are temporarily disabled.
*/
@@ -47,12 +42,8 @@ function canUseLinkPreviews(): boolean {
return false;
}
-/**
- * Workspace downgrade is temporarily disabled
- * API is being integrated in this GH issue https://github.com/Expensify/App/issues/51494
- */
-function canUseWorkspaceDowngrade() {
- return false;
+function canUseMergeAccounts(betas: OnyxEntry): boolean {
+ return !!betas?.includes(CONST.BETAS.NEWDOT_MERGE_ACCOUNTS) || canUseAllBetas(betas);
}
export default {
@@ -63,6 +54,5 @@ export default {
canUseCombinedTrackSubmit,
canUseCategoryAndTagApprovers,
canUsePerDiem,
- canUseWorkspaceDowngrade,
- shouldShowProductTrainingElements,
+ canUseMergeAccounts,
};
diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts
index 3d9aed117ca3..395ab930b116 100644
--- a/src/libs/PersonalDetailsUtils.ts
+++ b/src/libs/PersonalDetailsUtils.ts
@@ -52,6 +52,8 @@ const regexMergedAccount = new RegExp(CONST.REGEX.MERGED_ACCOUNT_PREFIX);
function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true, shouldAddCurrentUserPostfix = false): string {
let displayName = passedPersonalDetails?.displayName ?? '';
+ let login = passedPersonalDetails?.login ?? '';
+
// If the displayName starts with the merged account prefix, remove it.
if (regexMergedAccount.test(displayName)) {
// Remove the merged account prefix from the displayName.
@@ -60,8 +62,11 @@ function getDisplayNameOrDefault(passedPersonalDetails?: Partial, rate: Rate | undefined): FormInputErrors {
const errors: FormInputErrors = {};
- if (rate?.rate && Number(values.taxClaimableValue) > rate.rate / 100) {
+ if (rate?.rate && Number(values.taxClaimableValue) >= rate.rate / 100) {
errors.taxClaimableValue = Localize.translateLocal('workspace.taxes.error.updateTaxClaimableFailureMessage');
}
return errors;
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 4982e8660dec..2a4168a24668 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -246,12 +246,18 @@ const isPolicyUser = (policy: OnyxInputOrEntry, currentUserLogin?: strin
const isPolicyAuditor = (policy: OnyxInputOrEntry, currentUserLogin?: string): boolean =>
(policy?.role ?? (currentUserLogin && policy?.employeeList?.[currentUserLogin]?.role)) === CONST.POLICY.ROLE.AUDITOR;
-const isPolicyEmployee = (policyID: string, policies: OnyxCollection): boolean => Object.values(policies ?? {}).some((policy) => policy?.id === policyID);
+const isPolicyEmployee = (policyID: string | undefined, policies: OnyxCollection): boolean => {
+ if (!policyID) {
+ return false;
+ }
+
+ return Object.values(policies ?? {}).some((policy) => policy?.id === policyID);
+};
/**
* Checks if the current user is an owner (creator) of the policy.
*/
-const isPolicyOwner = (policy: OnyxInputOrEntry, currentUserAccountID: number): boolean => policy?.ownerAccountID === currentUserAccountID;
+const isPolicyOwner = (policy: OnyxInputOrEntry, currentUserAccountID: number | undefined): boolean => !!currentUserAccountID && policy?.ownerAccountID === currentUserAccountID;
/**
* Create an object mapping member emails to their accountIDs. Filter for members without errors if includeMemberWithErrors is false, and get the login email from the personalDetail object using the accountID.
@@ -406,11 +412,15 @@ function isControlPolicy(policy: OnyxEntry): boolean {
return policy?.type === CONST.POLICY.TYPE.CORPORATE;
}
+function isCollectPolicy(policy: OnyxEntry): boolean {
+ return policy?.type === CONST.POLICY.TYPE.TEAM;
+}
+
function isTaxTrackingEnabled(isPolicyExpenseChat: boolean, policy: OnyxEntry, isDistanceRequest: boolean): boolean {
const distanceUnit = getDistanceRateCustomUnit(policy);
const customUnitID = distanceUnit?.customUnitID ?? CONST.DEFAULT_NUMBER_ID;
const isPolicyTaxTrackingEnabled = isPolicyExpenseChat && policy?.tax?.trackingEnabled;
- const isTaxEnabledForDistance = isPolicyTaxTrackingEnabled && policy?.customUnits?.[customUnitID]?.attributes?.taxEnabled;
+ const isTaxEnabledForDistance = isPolicyTaxTrackingEnabled && !!customUnitID && policy?.customUnits?.[customUnitID]?.attributes?.taxEnabled;
return !!(isDistanceRequest ? isTaxEnabledForDistance : isPolicyTaxTrackingEnabled);
}
@@ -647,11 +657,11 @@ function getAdminEmployees(policy: OnyxEntry): PolicyEmployee[] {
/**
* Returns the policy of the report
*/
-function getPolicy(policyID: string | undefined): OnyxEntry {
- if (!allPolicies || !policyID) {
+function getPolicy(policyID: string | undefined, policies: OnyxCollection = allPolicies): OnyxEntry {
+ if (!policies || !policyID) {
return undefined;
}
- return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
+ return policies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
}
/** Return active policies where current user is an admin */
@@ -666,6 +676,12 @@ function canSendInvoiceFromWorkspace(policyID: string | undefined): boolean {
return policy?.areInvoicesEnabled ?? false;
}
+/** Whether the user can submit per diem expense from the workspace */
+function canSubmitPerDiemExpenseFromWorkspace(policy: OnyxEntry): boolean {
+ const perDiemCustomUnit = getPerDiemCustomUnit(policy);
+ return !isEmptyObject(perDiemCustomUnit) && !!perDiemCustomUnit?.enabled;
+}
+
/** Whether the user can send invoice */
function canSendInvoice(policies: OnyxCollection | null, currentUserLogin: string | undefined): boolean {
return getActiveAdminWorkspaces(policies, currentUserLogin).some((policy) => canSendInvoiceFromWorkspace(policy.id));
@@ -1093,7 +1109,7 @@ function getCurrentTaxID(policy: OnyxEntry, taxID: string): string | und
return Object.keys(policy?.taxRates?.taxes ?? {}).find((taxIDKey) => policy?.taxRates?.taxes?.[taxIDKey].previousTaxCode === taxID || taxIDKey === taxID);
}
-function getWorkspaceAccountID(policyID: string) {
+function getWorkspaceAccountID(policyID?: string) {
const policy = getPolicy(policyID);
if (!policy) {
@@ -1108,6 +1124,10 @@ function hasVBBA(policyID: string) {
}
function getTagApproverRule(policyOrID: string | SearchPolicy | OnyxEntry, tagName: string) {
+ if (!policyOrID) {
+ return;
+ }
+
const policy = typeof policyOrID === 'string' ? getPolicy(policyOrID) : policyOrID;
const approvalRules = policy?.rules?.approvalRules ?? [];
@@ -1165,6 +1185,11 @@ function areAllGroupPoliciesExpenseChatDisabled(policies = allPolicies) {
return !groupPolicies.some((policy) => !!policy?.isPolicyExpenseChatEnabled);
}
+function hasOtherControlWorkspaces(currentPolicyID: string) {
+ const otherControlWorkspaces = Object.values(allPolicies ?? {}).filter((policy) => policy?.id !== currentPolicyID && isPolicyAdmin(policy) && isControlPolicy(policy));
+ return otherControlWorkspaces.length > 0;
+}
+
export {
canEditTaxRate,
extractPolicyIDFromPath,
@@ -1222,6 +1247,7 @@ export {
getActiveAdminWorkspaces,
getOwnedPaidPolicies,
canSendInvoiceFromWorkspace,
+ canSubmitPerDiemExpenseFromWorkspace,
canSendInvoice,
hasWorkspaceWithInvoices,
hasDependentTags,
@@ -1265,6 +1291,7 @@ export {
getApprovalWorkflow,
getReimburserAccountID,
isControlPolicy,
+ isCollectPolicy,
isNetSuiteCustomSegmentRecord,
getNameFromNetSuiteCustomField,
isNetSuiteCustomFieldPropertyEditable,
@@ -1288,6 +1315,7 @@ export {
getUserFriendlyWorkspaceType,
isPolicyAccessible,
areAllGroupPoliciesExpenseChatDisabled,
+ hasOtherControlWorkspaces,
getManagerAccountEmail,
getRuleApprovers,
};
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 455a125ad0c3..c1f4057199ee 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -33,7 +33,6 @@ import StringUtils from './StringUtils';
import * as TransactionUtils from './TransactionUtils';
type LastVisibleMessage = {
- lastMessageTranslationKey?: string;
lastMessageText: string;
lastMessageHtml?: string;
};
@@ -287,7 +286,7 @@ function isWhisperActionTargetedToOthers(reportAction: OnyxInputOrEntry): reportAction is ReportAction {
@@ -348,7 +347,7 @@ function isInviteOrRemovedAction(
/**
* Returns whether the comment is a thread parent message/the first message in a thread
*/
-function isThreadParentMessage(reportAction: OnyxEntry, reportID: string): boolean {
+function isThreadParentMessage(reportAction: OnyxEntry, reportID: string | undefined): boolean {
const {childType, childVisibleActionCount = 0, childReportID} = reportAction ?? {};
return childType === CONST.REPORT.TYPE.CHAT && (childVisibleActionCount > 0 || String(childReportID) === reportID);
}
@@ -772,7 +771,11 @@ function replaceBaseURLInPolicyChangeLogAction(reportAction: ReportAction): Repo
return updatedReportAction;
}
-function getLastVisibleAction(reportID: string, canUserPerformWriteAction?: boolean, actionsToMerge: Record | null> = {}): OnyxEntry {
+function getLastVisibleAction(
+ reportID: string | undefined,
+ canUserPerformWriteAction?: boolean,
+ actionsToMerge: Record | null> = {},
+): OnyxEntry {
let reportActions: Array = [];
if (!isEmpty(actionsToMerge)) {
reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge ?? {}, true)) as Array<
@@ -802,7 +805,7 @@ function formatLastMessageText(lastMessageText: string) {
}
function getLastVisibleMessage(
- reportID: string,
+ reportID: string | undefined,
canUserPerformWriteAction?: boolean,
actionsToMerge: Record | null> = {},
reportAction: OnyxInputOrEntry | undefined = undefined,
@@ -812,7 +815,6 @@ function getLastVisibleMessage(
if (message && isReportMessageAttachment(message)) {
return {
- lastMessageTranslationKey: CONST.TRANSLATION_KEYS.ATTACHMENT,
lastMessageText: CONST.ATTACHMENT_MESSAGE_TEXT,
lastMessageHtml: CONST.TRANSLATION_KEYS.ATTACHMENT,
};
@@ -895,12 +897,12 @@ function getLastClosedReportAction(reportActions: OnyxEntry): Ony
* 4. We will get the second last action from filtered actions because the last
* action is always the created action
*/
-function getFirstVisibleReportActionID(sortedReportActions: ReportAction[] = [], isOffline = false): string {
+function getFirstVisibleReportActionID(sortedReportActions: ReportAction[] = [], isOffline = false): string | undefined {
if (!Array.isArray(sortedReportActions)) {
return '';
}
const sortedFilterReportActions = sortedReportActions.filter((action) => !isDeletedAction(action) || (action?.childVisibleActionCount ?? 0) > 0 || isOffline);
- return sortedFilterReportActions.length > 1 ? sortedFilterReportActions.at(sortedFilterReportActions.length - 2)?.reportActionID ?? '-1' : '';
+ return sortedFilterReportActions.length > 1 ? sortedFilterReportActions.at(sortedFilterReportActions.length - 2)?.reportActionID : undefined;
}
/**
@@ -929,7 +931,11 @@ function getLinkedTransactionID(reportActionOrID: string | OnyxEntry): string {
- return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) ? getOriginalMessage(reportAction)?.linkedReportID ?? '-1' : '-1';
+function getIOUReportIDFromReportActionPreview(reportAction: OnyxEntry): string | undefined {
+ return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) ? getOriginalMessage(reportAction)?.linkedReportID : undefined;
}
/**
@@ -1055,7 +1061,11 @@ const iouRequestTypes = new Set>([
* Gets the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions.
* Returns a reportID if there is exactly one transaction thread for the report, and null otherwise.
*/
-function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): string | undefined {
+function getOneTransactionThreadReportID(
+ reportID: string | undefined,
+ reportActions: OnyxEntry | ReportAction[],
+ isOffline: boolean | undefined = undefined,
+): string | undefined {
// If the report is not an IOU, Expense report, or Invoice, it shouldn't be treated as one-transaction report.
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE && report?.type !== CONST.REPORT.TYPE.INVOICE) {
@@ -1124,7 +1134,7 @@ function doesReportHaveVisibleActions(reportID: string, canUserPerformWriteActio
return visibleReportActionsWithoutTaskSystemMessage.length > 0;
}
-function getAllReportActions(reportID: string): ReportActions {
+function getAllReportActions(reportID: string | undefined): ReportActions {
return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {};
}
@@ -1498,7 +1508,7 @@ function isReportActionUnread(reportAction: OnyxEntry, lastReadTim
*/
function isCurrentActionUnread(report: OnyxEntry, reportAction: ReportAction): boolean {
const lastReadTime = report?.lastReadTime ?? '';
- const sortedReportActions = getSortedReportActions(Object.values(getAllReportActions(report?.reportID ?? '-1')));
+ const sortedReportActions = getSortedReportActions(Object.values(getAllReportActions(report?.reportID)));
const currentActionIndex = sortedReportActions.findIndex((action) => action.reportActionID === reportAction.reportActionID);
if (currentActionIndex === -1) {
return false;
@@ -1558,7 +1568,8 @@ function getDismissedViolationMessageText(originalMessage: ReportAction) {
@@ -1575,7 +1586,7 @@ function didMessageMentionCurrentUser(reportAction: OnyxInputOrEntry');
+ return accountIDsFromMessage.includes(currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID) || emailsFromMessage.includes(currentEmail) || message.includes('');
}
/**
@@ -1590,9 +1601,9 @@ function wasActionTakenByCurrentUser(reportAction: OnyxInputOrEntry {
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
- const reportActions = getAllReportActions(report?.reportID ?? '');
+ const reportActions = getAllReportActions(report?.reportID);
const action = Object.values(reportActions ?? {})?.find((reportAction) => {
- const IOUTransactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : -1;
+ const IOUTransactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : undefined;
return IOUTransactionID === transactionID;
});
return action;
@@ -1601,7 +1612,11 @@ function getIOUActionForReportID(reportID: string, transactionID: string): OnyxE
/**
* Get the track expense actionable whisper of the corresponding track expense
*/
-function getTrackExpenseActionableWhisper(transactionID: string, chatReportID: string) {
+function getTrackExpenseActionableWhisper(transactionID: string | undefined, chatReportID: string | undefined) {
+ if (!transactionID || !chatReportID) {
+ return undefined;
+ }
+
const chatReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`] ?? {};
return Object.values(chatReportActions).find((action: ReportAction) => isActionableTrackExpense(action) && getOriginalMessage(action)?.transactionID === transactionID);
}
@@ -1635,7 +1650,7 @@ function getExportIntegrationActionFragments(reportAction: OnyxEntry '2022-11-14';
const base62ReportID = getBase62ReportID(Number(reportID));
@@ -1769,7 +1784,7 @@ function getRenamedAction(reportAction: OnyxEntry>) {
const originalMessage = getOriginalMessage(reportAction);
- const submittersNames = PersonalDetailsUtils.getPersonalDetailsByIDs(originalMessage?.submittersAccountIDs ?? [], currentUserAccountID ?? -1).map(
+ const submittersNames = PersonalDetailsUtils.getPersonalDetailsByIDs(originalMessage?.submittersAccountIDs ?? [], currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID).map(
({displayName, login}) => displayName ?? login ?? 'Unknown Submitter',
);
return Localize.translateLocal('workspaceActions.removedFromApprovalWorkflow', {submittersNames, count: submittersNames.length});
@@ -1796,9 +1811,9 @@ function getCardIssuedMessage(reportAction: OnyxEntry, shouldRende
? getOriginalMessage(reportAction)
: undefined;
- const assigneeAccountID = cardIssuedActionOriginalMessage?.assigneeAccountID ?? -1;
- const cardID = cardIssuedActionOriginalMessage?.cardID ?? -1;
- const assigneeDetails = PersonalDetailsUtils.getPersonalDetailsByIDs([assigneeAccountID], currentUserAccountID ?? -1).at(0);
+ const assigneeAccountID = cardIssuedActionOriginalMessage?.assigneeAccountID ?? CONST.DEFAULT_NUMBER_ID;
+ const cardID = cardIssuedActionOriginalMessage?.cardID ?? CONST.DEFAULT_NUMBER_ID;
+ const assigneeDetails = PersonalDetailsUtils.getPersonalDetailsByIDs([assigneeAccountID], currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID).at(0);
const isPolicyAdmin = PolicyUtils.isPolicyAdmin(PolicyUtils.getPolicy(policyID));
const assignee = shouldRenderHTML ? `` : assigneeDetails?.firstName ?? assigneeDetails?.login ?? '';
const navigateRoute = isPolicyAdmin ? ROUTES.EXPENSIFY_CARD_DETAILS.getRoute(policyID, String(cardID)) : ROUTES.SETTINGS_DOMAINCARD_DETAIL.getRoute(String(cardID));
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index a3d1510830ab..5b726e9a536b 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -1,7 +1,6 @@
import {format} from 'date-fns';
import {Str} from 'expensify-common';
import lodashEscape from 'lodash/escape';
-import lodashFindLastIndex from 'lodash/findLastIndex';
import lodashIntersection from 'lodash/intersection';
import isEmpty from 'lodash/isEmpty';
import lodashIsEqual from 'lodash/isEqual';
@@ -52,8 +51,9 @@ import type {ErrorFields, Errors, Icon, PendingAction} from '@src/types/onyx/Ony
import type {OriginalMessageChangeLog, PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type {Status} from '@src/types/onyx/PersonalDetails';
import type {ConnectionName} from '@src/types/onyx/Policy';
-import type {NotificationPreference, Participants, PendingChatMember, Participant as ReportParticipant} from '@src/types/onyx/Report';
+import type {InvoiceReceiverType, NotificationPreference, Participants, Participant as ReportParticipant} from '@src/types/onyx/Report';
import type {Message, OldDotReportAction, ReportActions} from '@src/types/onyx/ReportAction';
+import type {PendingChatMember} from '@src/types/onyx/ReportMetadata';
import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
import type {Comment, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -314,7 +314,6 @@ type OptimisticChatReport = Pick<
| 'isOwnPolicyExpenseChat'
| 'isPinned'
| 'lastActorAccountID'
- | 'lastMessageTranslationKey'
| 'lastMessageHtml'
| 'lastMessageText'
| 'lastReadTime'
@@ -385,6 +384,7 @@ type OptimisticWorkspaceChats = {
expenseChatData: OptimisticChatReport;
expenseReportActionData: Record;
expenseCreatedReportActionID: string;
+ pendingChatMembers: PendingChatMember[];
};
type OptimisticModifiedExpenseReportAction = Pick<
@@ -617,7 +617,9 @@ let currentUserPersonalDetails: OnyxEntry;
Onyx.connect({
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
- currentUserPersonalDetails = value?.[currentUserAccountID ?? -1] ?? undefined;
+ if (currentUserAccountID) {
+ currentUserPersonalDetails = value?.[currentUserAccountID] ?? undefined;
+ }
allPersonalDetails = value ?? {};
allPersonalDetailLogins = Object.values(allPersonalDetails).map((personalDetail) => personalDetail?.login ?? '');
},
@@ -708,6 +710,7 @@ Onyx.connect({
});
let allReportMetadata: OnyxCollection;
+const allReportMetadataKeyValue: Record = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_METADATA,
waitForCollectionCallback: true,
@@ -716,6 +719,15 @@ Onyx.connect({
return;
}
allReportMetadata = value;
+
+ Object.entries(value).forEach(([reportID, reportMetadata]) => {
+ if (!reportMetadata) {
+ return;
+ }
+
+ const [, id] = reportID.split('_');
+ allReportMetadataKeyValue[id] = reportMetadata;
+ });
},
});
@@ -743,7 +755,7 @@ Onyx.connect({
},
});
-let onboarding: OnyxEntry;
+let onboarding: OnyxEntry;
Onyx.connect({
key: ONYXKEYS.NVP_ONBOARDING,
callback: (value) => (onboarding = value),
@@ -782,6 +794,14 @@ function getReportOrDraftReport(reportID: string | undefined): OnyxEntry
return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`];
}
+function getReportTransactions(reportID: string | undefined): Transaction[] {
+ if (!reportID) {
+ return [];
+ }
+
+ return reportsTransactions[reportID] ?? [];
+}
+
/**
* Check if a report is a draft report
*/
@@ -794,7 +814,7 @@ function isDraftReport(reportID: string | undefined): boolean {
/**
* Returns the report
*/
-function getReport(reportID: string): OnyxEntry {
+function getReport(reportID: string | undefined): OnyxEntry {
return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
}
@@ -802,7 +822,7 @@ function getReport(reportID: string): OnyxEntry {
* Returns the report
*/
function getReportNameValuePairs(reportID?: string): OnyxEntry {
- return allReportNameValuePair?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID ?? -1}`];
+ return allReportNameValuePair?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`];
}
/**
@@ -1046,8 +1066,8 @@ function isSettled(reportOrID: OnyxInputOrEntry | SearchReport | string
/**
* Whether the current user is the submitter of the report
*/
-function isCurrentUserSubmitter(reportID: string): boolean {
- if (!allReports) {
+function isCurrentUserSubmitter(reportID: string | undefined): boolean {
+ if (!allReports || !reportID) {
return false;
}
const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
@@ -1303,7 +1323,8 @@ function getDefaultNotificationPreferenceForReport(report: OnyxEntry): V
* Get the notification preference given a report. This should ALWAYS default to 'hidden'. Do not change this!
*/
function getReportNotificationPreference(report: OnyxEntry): ValueOf {
- return report?.participants?.[currentUserAccountID ?? -1]?.notificationPreference ?? CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
+ const participant = currentUserAccountID ? report?.participants?.[currentUserAccountID] : undefined;
+ return participant?.notificationPreference ?? CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
}
const CONCIERGE_ACCOUNT_ID_STRING = CONST.ACCOUNT_ID.CONCIERGE.toString();
@@ -1449,7 +1470,7 @@ function getMostRecentlyVisitedReport(reports: Array>, reportM
const shouldKeep = !isChatThread(report) || !isHiddenForCurrentUser(report);
return shouldKeep && !!report?.reportID && !!(reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report.reportID}`]?.lastVisitTime ?? report?.lastReadTime);
});
- return lodashMaxBy(filteredReports, (a) => new Date(reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${a?.reportID}`]?.lastVisitTime ?? a?.lastReadTime ?? '').valueOf());
+ return lodashMaxBy(filteredReports, (a) => [reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${a?.reportID}`]?.lastVisitTime ?? '', a?.lastReadTime ?? '']);
}
function findLastAccessedReport(ignoreDomainRooms: boolean, openOnAdminRoom = false, policyID?: string, excludeReportID?: string): OnyxEntry {
@@ -1638,7 +1659,11 @@ function isPolicyExpenseChatAdmin(report: OnyxEntry, policies: OnyxColle
/**
* Checks if the current user is the admin of the policy.
*/
-function isPolicyAdmin(policyID: string, policies: OnyxCollection): boolean {
+function isPolicyAdmin(policyID: string | undefined, policies: OnyxCollection): boolean {
+ if (!policyID) {
+ return false;
+ }
+
const policyRole = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.role;
return policyRole === CONST.POLICY.ROLE.ADMIN;
@@ -1648,7 +1673,7 @@ function isPolicyAdmin(policyID: string, policies: OnyxCollection): bool
* Checks whether all the transactions linked to the IOU report are of the Distance Request type with pending routes
*/
function hasOnlyTransactionsWithPendingRoutes(iouReportID: string | undefined): boolean {
- const transactions = reportsTransactions[iouReportID ?? ''] ?? [];
+ const transactions = getReportTransactions(iouReportID);
// Early return false in case not having any transaction
if (!transactions || transactions.length === 0) {
@@ -1732,11 +1757,7 @@ function isMoneyRequestReport(reportOrID: OnyxInputOrEntry | SearchRepor
* Checks if a report contains only Non-Reimbursable transactions
*/
function hasOnlyNonReimbursableTransactions(iouReportID: string | undefined): boolean {
- if (!iouReportID) {
- return false;
- }
-
- const transactions = reportsTransactions[iouReportID ?? ''] ?? [];
+ const transactions = getReportTransactions(iouReportID);
if (!transactions || transactions.length === 0) {
return false;
}
@@ -1747,7 +1768,7 @@ function hasOnlyNonReimbursableTransactions(iouReportID: string | undefined): bo
/**
* Checks if a report has only one transaction associated with it
*/
-function isOneTransactionReport(reportID: string): boolean {
+function isOneTransactionReport(reportID: string | undefined): boolean {
const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? ([] as ReportAction[]);
return ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions) !== null;
}
@@ -1766,7 +1787,11 @@ function isPayAtEndExpenseReport(reportID: string, transactions: Transaction[] |
/**
* Checks if a report is a transaction thread associated with a report that has only one transaction
*/
-function isOneTransactionThread(reportID: string, parentReportID: string, threadParentReportAction: OnyxEntry): boolean {
+function isOneTransactionThread(reportID: string | undefined, parentReportID: string | undefined, threadParentReportAction: OnyxEntry): boolean {
+ if (!reportID || !parentReportID) {
+ return false;
+ }
+
const parentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? ([] as ReportAction[]);
const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(parentReportID, parentReportActions);
return reportID === transactionThreadReportID && !ReportActionsUtils.isSentMoneyReportAction(threadParentReportAction);
@@ -1777,9 +1802,9 @@ function isOneTransactionThread(reportID: string, parentReportID: string, thread
*/
function getDisplayedReportID(reportID: string): string {
const report = getReport(reportID);
- const parentReportID = report?.parentReportID ?? '';
- const parentReportAction = ReportActionsUtils.getReportAction(parentReportID, report?.parentReportActionID ?? '');
- return isOneTransactionThread(reportID, parentReportID, parentReportAction) ? parentReportID : reportID;
+ const parentReportID = report?.parentReportID;
+ const parentReportAction = ReportActionsUtils.getReportAction(parentReportID, report?.parentReportActionID);
+ return parentReportID && isOneTransactionThread(reportID, parentReportID, parentReportAction) ? parentReportID : reportID;
}
/**
@@ -1788,7 +1813,8 @@ function getDisplayedReportID(reportID: string): string {
*/
function isOneOnOneChat(report: OnyxEntry): boolean {
const participants = report?.participants ?? {};
- const isCurrentUserParticipant = participants[currentUserAccountID ?? 0] ? 1 : 0;
+ const participant = currentUserAccountID ? participants[currentUserAccountID] : undefined;
+ const isCurrentUserParticipant = participant ? 1 : 0;
const participantAmount = Object.keys(participants).length - isCurrentUserParticipant;
if (participantAmount !== 1) {
return false;
@@ -2220,6 +2246,7 @@ function getDisplayNameForParticipant(
function getParticipantsAccountIDsForDisplay(report: OnyxEntry, shouldExcludeHidden = false, shouldExcludeDeleted = false, shouldForceExcludeCurrentUser = false): number[] {
const reportParticipants = report?.participants ?? {};
+ const reportMetadata = getReportMetadata(report?.reportID);
let participantsEntries = Object.entries(reportParticipants);
// We should not show participants that have an optimistic entry with the same login in the personal details
@@ -2259,7 +2286,7 @@ function getParticipantsAccountIDsForDisplay(report: OnyxEntry, shouldEx
if (
shouldExcludeDeleted &&
- report?.pendingChatMembers?.findLast((member) => Number(member.accountID) === accountID)?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE
+ reportMetadata?.pendingChatMembers?.findLast((member) => Number(member.accountID) === accountID)?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE
) {
return false;
}
@@ -2320,8 +2347,10 @@ function getGroupChatName(participants?: SelectedParticipant[], shouldApplyLimit
return report.reportName;
}
+ const reportMetadata = getReportMetadata(report?.reportID);
+
const pendingMemberAccountIDs = new Set(
- report?.pendingChatMembers?.filter((member) => member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).map((member) => member.accountID),
+ reportMetadata?.pendingChatMembers?.filter((member) => member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).map((member) => member.accountID),
);
let participantAccountIDs =
participants?.map((participant) => participant.accountID) ??
@@ -2369,6 +2398,8 @@ function getIcons(
policy?: OnyxInputOrEntry,
invoiceReceiverPolicy?: OnyxInputOrEntry,
): Icon[] {
+ const ownerDetails = report?.ownerAccountID ? personalDetails?.[report.ownerAccountID] : undefined;
+
if (isEmptyObject(report)) {
const fallbackIcon: Icon = {
source: defaultIcon ?? FallbackAvatar,
@@ -2381,12 +2412,13 @@ function getIcons(
if (isExpenseRequest(report)) {
const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID];
const workspaceIcon = getWorkspaceIcon(report, policy);
+ const actorDetails = parentReportAction?.actorAccountID ? personalDetails?.[parentReportAction.actorAccountID] : undefined;
const memberIcon = {
- source: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.avatar ?? FallbackAvatar,
+ source: actorDetails?.avatar ?? FallbackAvatar,
id: parentReportAction?.actorAccountID,
type: CONST.ICON_TYPE_AVATAR,
- name: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.displayName ?? '',
- fallbackIcon: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.fallbackIcon,
+ name: actorDetails?.displayName ?? '',
+ fallbackIcon: actorDetails?.fallbackIcon,
};
return [memberIcon, workspaceIcon];
@@ -2395,13 +2427,14 @@ function getIcons(
const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID];
const actorAccountID = getReportActionActorAccountID(parentReportAction, report, report);
- const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[actorAccountID ?? -1], '', false);
+ const actorDetails = actorAccountID ? personalDetails?.[actorAccountID] : undefined;
+ const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(actorDetails, '', false);
const actorIcon = {
id: actorAccountID,
- source: personalDetails?.[actorAccountID ?? -1]?.avatar ?? FallbackAvatar,
+ source: actorDetails?.avatar ?? FallbackAvatar,
name: LocalePhoneNumber.formatPhoneNumber(actorDisplayName),
type: CONST.ICON_TYPE_AVATAR,
- fallbackIcon: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.fallbackIcon,
+ fallbackIcon: actorDetails?.fallbackIcon,
};
if (isWorkspaceThread(report)) {
@@ -2413,10 +2446,10 @@ function getIcons(
if (isTaskReport(report)) {
const ownerIcon = {
id: report?.ownerAccountID,
- source: personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? FallbackAvatar,
+ source: ownerDetails?.avatar ?? FallbackAvatar,
type: CONST.ICON_TYPE_AVATAR,
- name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '',
- fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon,
+ name: ownerDetails?.displayName ?? '',
+ fallbackIcon: ownerDetails?.fallbackIcon,
};
if (isWorkspaceTaskReport(report)) {
@@ -2463,33 +2496,34 @@ function getIcons(
if (isPolicyExpenseChat(report) || isExpenseReport(report)) {
const workspaceIcon = getWorkspaceIcon(report, policy);
const memberIcon = {
- source: personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? FallbackAvatar,
+ source: ownerDetails?.avatar ?? FallbackAvatar,
id: report?.ownerAccountID,
type: CONST.ICON_TYPE_AVATAR,
- name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '',
- fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon,
+ name: ownerDetails?.displayName ?? '',
+ fallbackIcon: ownerDetails?.fallbackIcon,
};
return isExpenseReport(report) ? [memberIcon, workspaceIcon] : [workspaceIcon, memberIcon];
}
if (isIOUReport(report)) {
+ const managerDetails = report?.managerID ? personalDetails?.[report.managerID] : undefined;
const managerIcon = {
- source: personalDetails?.[report?.managerID ?? -1]?.avatar ?? FallbackAvatar,
+ source: managerDetails?.avatar ?? FallbackAvatar,
id: report?.managerID,
type: CONST.ICON_TYPE_AVATAR,
- name: personalDetails?.[report?.managerID ?? -1]?.displayName ?? '',
- fallbackIcon: personalDetails?.[report?.managerID ?? -1]?.fallbackIcon,
+ name: managerDetails?.displayName ?? '',
+ fallbackIcon: managerDetails?.fallbackIcon,
};
const ownerIcon = {
id: report?.ownerAccountID,
- source: personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? FallbackAvatar,
+ source: ownerDetails?.avatar ?? FallbackAvatar,
type: CONST.ICON_TYPE_AVATAR,
- name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '',
- fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon,
+ name: ownerDetails?.displayName ?? '',
+ fallbackIcon: ownerDetails?.fallbackIcon,
};
const isManager = currentUserAccountID === report?.managerID;
// For one transaction IOUs, display a simplified report icon
- if (isOneTransactionReport(report?.reportID ?? '-1')) {
+ if (isOneTransactionReport(report?.reportID)) {
return [ownerIcon];
}
@@ -2497,7 +2531,7 @@ function getIcons(
}
if (isSelfDM(report)) {
- return getIconsForParticipants([currentUserAccountID ?? -1], personalDetails);
+ return getIconsForParticipants(currentUserAccountID ? [currentUserAccountID] : [], personalDetails);
}
if (isSystemChat(report)) {
@@ -2755,7 +2789,7 @@ function buildOptimisticCancelPaymentReportAction(expenseReportID: string, amoun
*/
function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: ReportActions = {}): LastVisibleMessage {
const report = getReportOrDraftReport(reportID);
- const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID ?? '-1', canUserPerformWriteAction(report), actionsToMerge);
+ const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, canUserPerformWriteAction(report), actionsToMerge);
// For Chat Report with deleted parent actions, let us fetch the correct message
if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && !isEmptyObject(report) && isChatReport(report)) {
@@ -2766,7 +2800,7 @@ function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: Rep
}
// Fetch the last visible message for report represented by reportID and based on actions to merge.
- return ReportActionsUtils.getLastVisibleMessage(reportID ?? '-1', canUserPerformWriteAction(report), actionsToMerge);
+ return ReportActionsUtils.getLastVisibleMessage(reportID, canUserPerformWriteAction(report), actionsToMerge);
}
/**
@@ -2894,11 +2928,10 @@ function requiresAttentionFromCurrentUser(optionOrReport: OnyxEntry | Op
}
/**
- * Returns number of transactions that are nonReimbursable
- *
+ * Checks if the report contains at least one Non-Reimbursable transaction
*/
function hasNonReimbursableTransactions(iouReportID: string | undefined): boolean {
- const transactions = reportsTransactions[iouReportID ?? ''] ?? [];
+ const transactions = getReportTransactions(iouReportID);
return transactions.filter((transaction) => transaction.reimbursable === false).length > 0;
}
@@ -2944,7 +2977,7 @@ function getMoneyRequestSpendBreakdown(report: OnyxInputOrEntry, allRepo
*/
function getPolicyExpenseChatName(report: OnyxEntry, policy?: OnyxEntry): string | undefined {
const ownerAccountID = report?.ownerAccountID;
- const personalDetails = allPersonalDetails?.[ownerAccountID ?? -1];
+ const personalDetails = ownerAccountID ? allPersonalDetails?.[ownerAccountID] : undefined;
const login = personalDetails ? personalDetails.login : null;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const reportOwnerDisplayName = getDisplayNameForParticipant(ownerAccountID) || login || report?.reportName;
@@ -2963,7 +2996,7 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy?: OnyxEntry<
// 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, getReportNameValuePairs(report?.reportID))) {
- const lastAction = ReportActionsUtils.getLastVisibleAction(report?.reportID ?? '-1');
+ const lastAction = ReportActionsUtils.getLastVisibleAction(report?.reportID);
const archiveReason = ReportActionsUtils.isClosedAction(lastAction) ? ReportActionsUtils.getOriginalMessage(lastAction)?.reason : CONST.REPORT.ARCHIVE_REASON.DEFAULT;
if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED && policyExpenseChatRole !== CONST.POLICY.ROLE.ADMIN) {
return getPolicyName(report, false, policy);
@@ -2994,7 +3027,7 @@ function isReportFieldOfTypeTitle(reportField: OnyxEntry): bo
/**
* Check if Report has any held expenses
*/
-function isHoldCreator(transaction: OnyxEntry, reportID: string): boolean {
+function isHoldCreator(transaction: OnyxEntry, reportID: string | undefined): boolean {
const holdReportAction = ReportActionsUtils.getReportAction(reportID, `${transaction?.comment?.hold ?? ''}`);
return isActionCreator(holdReportAction);
}
@@ -3010,7 +3043,7 @@ function isReportFieldDisabled(report: OnyxEntry, reportField: OnyxEntry
const isReportSettled = isSettled(report?.reportID);
const isReportClosed = isClosedReport(report);
const isTitleField = isReportFieldOfTypeTitle(reportField);
- const isAdmin = isPolicyAdmin(report?.policyID ?? '-1', {[`${ONYXKEYS.COLLECTION.POLICY}${policy?.id ?? '-1'}`]: policy});
+ const isAdmin = isPolicyAdmin(report?.policyID, {[`${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`]: policy});
return isTitleField ? !reportField?.deletable : !isAdmin && (isReportSettled || isReportClosed);
}
@@ -3024,7 +3057,11 @@ function getTitleReportField(reportFields: Record) {
/**
* Get the key for a report field
*/
-function getReportFieldKey(reportFieldId: string) {
+function getReportFieldKey(reportFieldId: string | undefined) {
+ if (!reportFieldId) {
+ return '';
+ }
+
// We don't need to add `expensify_` prefix to the title field key, because backend stored title under a unique key `text_title`,
// and all the other report field keys are stored under `expensify_FIELD_ID`.
if (reportFieldId === CONST.REPORT_FIELD_TITLE_FIELD_ID) {
@@ -3037,7 +3074,7 @@ function getReportFieldKey(reportFieldId: string) {
/**
* Get the report fields attached to the policy given policyID
*/
-function getReportFieldsByPolicyID(policyID: string): Record {
+function getReportFieldsByPolicyID(policyID: string | undefined): Record {
const policyReportFields = Object.entries(allPolicies ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY, '') === policyID);
const fieldList = policyReportFields?.[1]?.fieldList;
@@ -3089,8 +3126,8 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo
* Get the title for an IOU or expense chat which will be showing the payer and the amount
*/
function getMoneyRequestReportName(report: OnyxEntry, policy?: OnyxEntry, invoiceReceiverPolicy?: OnyxEntry): string {
- const isReportSettled = isSettled(report?.reportID ?? '-1');
- const reportFields = isReportSettled ? report?.fieldList : getReportFieldsByPolicyID(report?.policyID ?? '-1');
+ const isReportSettled = isSettled(report?.reportID);
+ const reportFields = isReportSettled ? report?.fieldList : getReportFieldsByPolicyID(report?.policyID);
const titleReportField = Object.values(reportFields ?? {}).find((reportField) => reportField?.fieldID === CONST.REPORT_FIELD_TITLE_FIELD_ID);
if (titleReportField && report?.reportName && isPaidGroupPolicyExpenseReport(report)) {
@@ -3213,7 +3250,7 @@ function canEditMoneyRequest(reportAction: OnyxInputOrEntry)
return {canHoldRequest: false, canUnholdRequest: false};
}
- const moneyRequestReportID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID ?? 0;
+ const moneyRequestReportID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID;
const moneyRequestReport = getReportOrDraftReport(String(moneyRequestReportID));
if (!moneyRequestReportID || !moneyRequestReport) {
@@ -3359,7 +3396,7 @@ function canHoldUnholdReportAction(reportAction: OnyxInputOrEntry)
const isRequestSettled = isSettled(moneyRequestReport?.reportID);
const isApproved = isReportApproved(moneyRequestReport);
- const transactionID = moneyRequestReport ? ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID : 0;
+ const transactionID = moneyRequestReport ? ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID : undefined;
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? ({} as Transaction);
const parentReportAction = isThread(moneyRequestReport)
@@ -3367,7 +3404,7 @@ function canHoldUnholdReportAction(reportAction: OnyxInputOrEntry)
: undefined;
const isRequestIOU = isIOUReport(moneyRequestReport);
- const isHoldActionCreator = isHoldCreator(transaction, reportAction.childReportID ?? '-1');
+ const isHoldActionCreator = isHoldCreator(transaction, reportAction.childReportID);
const isTrackExpenseMoneyReport = isTrackExpenseReport(moneyRequestReport);
const isActionOwner =
@@ -3375,7 +3412,7 @@ function canHoldUnholdReportAction(reportAction: OnyxInputOrEntry)
typeof currentUserPersonalDetails?.accountID === 'number' &&
parentReportAction.actorAccountID === currentUserPersonalDetails?.accountID;
const isApprover = isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && currentUserPersonalDetails?.accountID === moneyRequestReport?.managerID;
- const isAdmin = isPolicyAdmin(moneyRequestReport.policyID ?? '-1', allPolicies);
+ const isAdmin = isPolicyAdmin(moneyRequestReport.policyID, allPolicies);
const isOnHold = TransactionUtils.isOnHold(transaction);
const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction);
const isClosed = isClosedReport(moneyRequestReport);
@@ -3397,25 +3434,31 @@ const changeMoneyRequestHoldStatus = (reportAction: OnyxEntry, bac
if (!ReportActionsUtils.isMoneyRequestAction(reportAction)) {
return;
}
- const moneyRequestReportID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID ?? 0;
+ const moneyRequestReportID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID;
const moneyRequestReport = getReportOrDraftReport(String(moneyRequestReportID));
if (!moneyRequestReportID || !moneyRequestReport) {
return;
}
- const transactionID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID ?? '';
+ const transactionID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID;
+
+ if (!transactionID || !reportAction.childReportID) {
+ Log.warn('Missing transactionID and reportAction.childReportID during the change of the money request hold status');
+ return;
+ }
+
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? ({} as Transaction);
const isOnHold = TransactionUtils.isOnHold(transaction);
const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${moneyRequestReport.policyID}`] ?? null;
if (isOnHold) {
- IOU.unholdRequest(transactionID, reportAction.childReportID ?? '', searchHash);
+ IOU.unholdRequest(transactionID, reportAction.childReportID, searchHash);
} else {
const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams());
Navigation.navigate(
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, transactionID, reportAction.childReportID ?? '', backTo || activeRoute, searchHash),
+ ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, transactionID, reportAction.childReportID, backTo || activeRoute, searchHash),
);
}
};
@@ -3424,7 +3467,7 @@ const changeMoneyRequestHoldStatus = (reportAction: OnyxEntry, bac
* Gets all transactions on an IOU report with a receipt
*/
function getTransactionsWithReceipts(iouReportID: string | undefined): Transaction[] {
- const transactions = reportsTransactions[iouReportID ?? ''] ?? [];
+ const transactions = getReportTransactions(iouReportID);
return transactions.filter((transaction) => TransactionUtils.hasReceipt(transaction));
}
@@ -3451,10 +3494,10 @@ function areAllRequestsBeingSmartScanned(iouReportID: string, reportPreviewActio
* NOTE: This method is only meant to be used inside this action file. Do not export and use it elsewhere. Use withOnyx or Onyx.connect() instead.
*/
function getLinkedTransaction(reportAction: OnyxEntry): OnyxEntry {
- let transactionID = '';
+ let transactionID;
if (ReportActionsUtils.isMoneyRequestAction(reportAction)) {
- transactionID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID ?? '-1';
+ transactionID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID;
}
return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
@@ -3464,7 +3507,7 @@ function getLinkedTransaction(reportAction: OnyxEntry {
if (!ReportActionsUtils.isMoneyRequestAction(action)) {
@@ -3492,7 +3535,7 @@ function getReportActionWithMissingSmartscanFields(iouReportID: string): ReportA
/**
* Check if iouReportID has required missing fields
*/
-function shouldShowRBRForMissingSmartscanFields(iouReportID: string): boolean {
+function shouldShowRBRForMissingSmartscanFields(iouReportID: string | undefined): boolean {
return !!getReportActionWithMissingSmartscanFields(iouReportID);
}
@@ -3516,7 +3559,7 @@ function getTransactionReportName(reportAction: OnyxEntry {
- const name = getDisplayNameForParticipant(id);
+ const participants = personalDetails.map((personalDetail) => {
+ const name = PersonalDetailsUtils.getEffectiveDisplayName(personalDetail);
if (name && name?.length > 0) {
return name;
}
@@ -3867,7 +3918,7 @@ function getInvoicePayerName(report: OnyxEntry, invoiceReceiverPolicy?:
/**
* Parse html of reportAction into text
*/
-function parseReportActionHtmlToText(reportAction: OnyxEntry, reportID: string, childReportID?: string): string {
+function parseReportActionHtmlToText(reportAction: OnyxEntry, reportID: string | undefined, childReportID?: string): string {
if (!reportAction) {
return '';
}
@@ -3931,7 +3982,7 @@ function getReportActionMessage(reportAction: OnyxEntry, reportID?
return getReimbursementQueuedActionMessage(reportAction, getReportOrDraftReport(reportID), false);
}
- return parseReportActionHtmlToText(reportAction, reportID ?? '', childReportID);
+ return parseReportActionHtmlToText(reportAction, reportID, childReportID);
}
/**
@@ -3940,8 +3991,8 @@ function getReportActionMessage(reportAction: OnyxEntry, reportID?
function getInvoicesChatName(report: OnyxEntry, receiverPolicy: OnyxEntry): string {
const invoiceReceiver = report?.invoiceReceiver;
const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL;
- const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1;
- const invoiceReceiverPolicyID = isIndividual ? '' : invoiceReceiver?.policyID ?? '-1';
+ const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : CONST.DEFAULT_NUMBER_ID;
+ const invoiceReceiverPolicyID = isIndividual ? undefined : invoiceReceiver?.policyID;
const invoiceReceiverPolicy = receiverPolicy ?? getPolicy(invoiceReceiverPolicyID);
const isCurrentUserReceiver = (isIndividual && invoiceReceiverAccountID === currentUserAccountID) || (!isIndividual && PolicyUtils.isPolicyAdmin(invoiceReceiverPolicy));
@@ -4042,7 +4093,7 @@ function getReportName(
}
const isAttachment = ReportActionsUtils.isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : undefined);
- const reportActionMessage = getReportActionMessage(parentReportAction, report?.parentReportID, report?.reportID ?? '').replace(/(\n+|\r\n|\n|\r)/gm, ' ');
+ const reportActionMessage = getReportActionMessage(parentReportAction, report?.parentReportID, report?.reportID).replace(/(\n+|\r\n|\n|\r)/gm, ' ');
if (isAttachment && reportActionMessage) {
return `[${Localize.translateLocal('common.attachment')}]`;
}
@@ -4255,7 +4306,11 @@ function goBackToDetailsPage(report: OnyxEntry, backTo?: string) {
return;
}
- Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report?.reportID ?? '-1', backTo));
+ if (report?.reportID) {
+ Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID, backTo));
+ } else {
+ Log.warn('Missing reportID during navigation back to the details page');
+ }
}
function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP?: boolean) {
@@ -4442,7 +4497,7 @@ function buildOptimisticAddCommentReportAction(
const isAttachmentOnly = file && !text;
const isAttachmentWithText = !!text && file !== undefined;
- const accountID = actorAccountID ?? currentUserAccountID ?? -1;
+ const accountID = actorAccountID ?? currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID;
const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
// Remove HTML from text when applying optimistic offline comment
@@ -4578,6 +4633,31 @@ function buildOptimisticTaskCommentReportAction(
return reportAction;
}
+function buildOptimisticSelfDMReport(created: string): Report {
+ return {
+ reportID: generateReportID(),
+ participants: {
+ [currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID]: {
+ notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE,
+ },
+ },
+ type: CONST.REPORT.TYPE.CHAT,
+ chatType: CONST.REPORT.CHAT_TYPE.SELF_DM,
+ isOwnPolicyExpenseChat: false,
+ isPinned: true,
+ lastActorAccountID: 0,
+ lastMessageHtml: '',
+ lastMessageText: undefined,
+ lastReadTime: created,
+ lastVisibleActionCreated: created,
+ ownerAccountID: currentUserAccountID,
+ reportName: '',
+ stateNum: 0,
+ statusNum: 0,
+ writeCapability: CONST.REPORT.WRITE_CAPABILITIES.ALL,
+ };
+}
+
/**
* Builds an optimistic IOU report with a randomly generated reportID
*
@@ -4602,7 +4682,7 @@ function buildOptimisticIOUReport(
const formattedTotal = CurrencyUtils.convertToDisplayString(total, currency);
const personalDetails = getPersonalDetailsForAccountID(payerAccountID);
const payerEmail = 'login' in personalDetails ? personalDetails.login : '';
- const policyID = getReport(chatReportID)?.policyID ?? '-1';
+ const policyID = getReport(chatReportID)?.policyID;
const policy = getPolicy(policyID);
const participants: Participants = {
@@ -4668,8 +4748,7 @@ function populateOptimisticReportFormula(formula: string, report: OptimisticExpe
/** Builds an optimistic invoice report with a randomly generated reportID */
function buildOptimisticInvoiceReport(chatReportID: string, policyID: string, receiverAccountID: number, receiverName: string, total: number, currency: string): OptimisticExpenseReport {
const formattedTotal = CurrencyUtils.convertToDisplayString(total, currency);
-
- return {
+ const invoiceReport = {
reportID: generateReportID(),
chatReportID,
policyID,
@@ -4683,9 +4762,6 @@ function buildOptimisticInvoiceReport(chatReportID: string, policyID: string, re
statusNum: CONST.REPORT.STATUS_NUM.OPEN,
total,
participants: {
- [currentUserAccountID ?? -1]: {
- notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
- },
[receiverAccountID]: {
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
},
@@ -4693,6 +4769,12 @@ function buildOptimisticInvoiceReport(chatReportID: string, policyID: string, re
parentReportID: chatReportID,
lastVisibleActionCreated: DateUtils.getDBTime(),
};
+
+ if (currentUserAccountID) {
+ invoiceReport.participants[currentUserAccountID] = {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN};
+ }
+
+ return invoiceReport;
}
/**
@@ -5019,8 +5101,8 @@ function buildOptimisticIOUReportAction(
originalMessage.participantAccountIDs = currentUserAccountID ? [currentUserAccountID] : [];
} else {
originalMessage.participantAccountIDs = currentUserAccountID
- ? [currentUserAccountID, ...participants.map((participant) => participant.accountID ?? -1)]
- : participants.map((participant) => participant.accountID ?? -1);
+ ? [currentUserAccountID, ...participants.map((participant) => participant.accountID ?? CONST.DEFAULT_NUMBER_ID)]
+ : participants.map((participant) => participant.accountID ?? CONST.DEFAULT_NUMBER_ID);
}
}
@@ -5231,14 +5313,14 @@ function buildOptimisticReportPreview(
},
],
created,
- accountID: iouReport?.managerID ?? -1,
+ accountID: iouReport?.managerID,
// The preview is initially whispered if created with a receipt, so the actor is the current user as well
actorAccountID: hasReceipt ? currentUserAccountID : reportActorAccountID,
childReportID: childReportID ?? iouReport?.reportID,
childMoneyRequestCount: 1,
childLastActorAccountID: currentUserAccountID,
childLastMoneyRequestComment: comment,
- childRecentReceiptTransactionIDs: hasReceipt && !isEmptyObject(transaction) ? {[transaction?.transactionID ?? '-1']: created} : undefined,
+ childRecentReceiptTransactionIDs: hasReceipt && !isEmptyObject(transaction) && transaction?.transactionID ? {[transaction.transactionID]: created} : undefined,
};
}
@@ -5416,11 +5498,13 @@ function updateReportPreview(
: recentReceiptTransactions,
// As soon as we add a transaction without a receipt to the report, it will have ready expenses,
// so we remove the whisper
- originalMessage: {
- ...(originalMessage ?? {}),
- whisperedTo: hasReceipt ? originalMessage?.whisperedTo : [],
- linkedReportID: originalMessage?.linkedReportID ?? '0',
- },
+ originalMessage: originalMessage
+ ? {
+ ...originalMessage,
+ whisperedTo: hasReceipt ? originalMessage.whisperedTo : [],
+ linkedReportID: originalMessage.linkedReportID,
+ }
+ : undefined,
};
}
@@ -5512,7 +5596,6 @@ function buildOptimisticChatReport(
isOwnPolicyExpenseChat,
isPinned: isNewlyCreatedWorkspaceChat,
lastActorAccountID: 0,
- lastMessageTranslationKey: '',
lastMessageHtml: '',
lastMessageText: undefined,
lastReadTime: currentTime,
@@ -6071,7 +6154,7 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string, exp
const pendingChatMembers = getPendingChatMembers(currentUserAccountID ? [currentUserAccountID] : [], [], CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD);
const adminsChatData = {
...buildOptimisticChatReport(
- [currentUserAccountID ?? -1],
+ currentUserAccountID ? [currentUserAccountID] : [],
CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS,
CONST.REPORT.CHAT_TYPE.POLICY_ADMINS,
policyID,
@@ -6079,7 +6162,6 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string, exp
false,
policyName,
),
- pendingChatMembers,
};
const adminsChatReportID = adminsChatData.reportID;
const adminsCreatedAction = buildOptimisticCreatedReportAction(CONST.POLICY.OWNER_EMAIL_FAKE);
@@ -6088,7 +6170,7 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string, exp
};
const expenseChatData = buildOptimisticChatReport(
- [currentUserAccountID ?? -1],
+ currentUserAccountID ? [currentUserAccountID] : [],
'',
CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
policyID,
@@ -6119,6 +6201,7 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string, exp
expenseChatData,
expenseReportActionData,
expenseCreatedReportActionID: expenseReportCreatedAction.reportActionID,
+ pendingChatMembers,
};
}
@@ -6231,7 +6314,7 @@ function buildTransactionThread(
participantAccountIDs,
getTransactionReportName(reportAction),
undefined,
- moneyRequestReport?.policyID ?? '-1',
+ moneyRequestReport?.policyID,
CONST.POLICY.OWNER_ACCOUNT_ID_FAKE,
false,
'',
@@ -6307,12 +6390,12 @@ function isEmptyReport(report: OnyxEntry): boolean {
return true;
}
- if (report.lastMessageText ?? report.lastMessageTranslationKey) {
+ if (report.lastMessageText) {
return false;
}
const lastVisibleMessage = getLastVisibleMessage(report.reportID);
- return !lastVisibleMessage.lastMessageText && !lastVisibleMessage.lastMessageTranslationKey;
+ return !lastVisibleMessage.lastMessageText;
}
function isUnread(report: OnyxEntry): boolean {
@@ -6412,7 +6495,7 @@ function shouldDisplayViolationsRBRInLHN(report: OnyxEntry, transactionV
}
// We only show the RBR to the submitter
- if (!isCurrentUserSubmitter(report.reportID ?? '')) {
+ if (!isCurrentUserSubmitter(report.reportID)) {
return false;
}
@@ -6432,7 +6515,7 @@ function shouldDisplayViolationsRBRInLHN(report: OnyxEntry, transactionV
* Checks to see if a report contains a violation
*/
function hasViolations(reportID: string, transactionViolations: OnyxCollection, shouldShowInReview?: boolean): boolean {
- const transactions = reportsTransactions[reportID] ?? [];
+ const transactions = getReportTransactions(reportID);
return transactions.some((transaction) => TransactionUtils.hasViolation(transaction.transactionID, transactionViolations, shouldShowInReview));
}
@@ -6440,7 +6523,7 @@ function hasViolations(reportID: string, transactionViolations: OnyxCollection, shouldShowInReview?: boolean): boolean {
- const transactions = reportsTransactions[reportID] ?? [];
+ const transactions = getReportTransactions(reportID);
return transactions.some((transaction) => TransactionUtils.hasWarningTypeViolation(transaction.transactionID, transactionViolations, shouldShowInReview));
}
@@ -6448,7 +6531,7 @@ function hasWarningTypeViolations(reportID: string, transactionViolations: OnyxC
* Checks to see if a report contains a violation of type `notice`
*/
function hasNoticeTypeViolations(reportID: string, transactionViolations: OnyxCollection, shouldShowInReview?: boolean): boolean {
- const transactions = reportsTransactions[reportID] ?? [];
+ const transactions = getReportTransactions(reportID);
return transactions.some((transaction) => TransactionUtils.hasNoticeTypeViolation(transaction.transactionID, transactionViolations, shouldShowInReview));
}
@@ -6464,7 +6547,7 @@ function hasReportViolations(reportID: string) {
function shouldAdminsRoomBeVisible(report: OnyxEntry): boolean {
const accountIDs = Object.entries(report?.participants ?? {}).map(([accountID]) => Number(accountID));
const adminAccounts = PersonalDetailsUtils.getLoginsByAccountIDs(accountIDs).filter((login) => !PolicyUtils.isExpensifyTeam(login));
- const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(report?.reportID ?? '');
+ const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(report?.reportID);
if ((lastVisibleAction ? ReportActionsUtils.isCreatedAction(lastVisibleAction) : report?.lastActionType === CONST.REPORT.ACTIONS.TYPE.CREATED) && adminAccounts.length <= 1) {
return false;
}
@@ -6493,7 +6576,7 @@ function getAllReportActionsErrorsAndReportActionThatRequiresAttention(report: O
const parentReportAction: OnyxEntry =
!report?.parentReportID || !report?.parentReportActionID
? undefined
- : allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? '-1'}`]?.[report.parentReportActionID ?? '-1'];
+ : allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID];
if (!isArchivedRoom(report)) {
if (ReportActionsUtils.wasActionTakenByCurrentUser(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction)) {
@@ -6504,9 +6587,9 @@ function getAllReportActionsErrorsAndReportActionThatRequiresAttention(report: O
reportAction = undefined;
}
} else if ((isIOUReport(report) || isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) {
- if (shouldShowRBRForMissingSmartscanFields(report?.reportID ?? '-1') && !isSettled(report?.reportID)) {
+ if (shouldShowRBRForMissingSmartscanFields(report?.reportID) && !isSettled(report?.reportID)) {
reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage');
- reportAction = getReportActionWithMissingSmartscanFields(report?.reportID ?? '-1');
+ reportAction = getReportActionWithMissingSmartscanFields(report?.reportID);
}
} else if (hasSmartscanError(reportActionsArray)) {
reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage');
@@ -6626,7 +6709,7 @@ function reasonForReportToBeInOptionList({
}
// If this is a transaction thread associated with a report that only has one transaction, omit it
- if (isOneTransactionThread(report.reportID, report.parentReportID ?? '-1', parentReportAction)) {
+ if (isOneTransactionThread(report.reportID, report.parentReportID, parentReportAction)) {
return null;
}
@@ -6709,11 +6792,7 @@ function reasonForReportToBeInOptionList({
}
// Hide chat threads where the parent message is pending removal
- if (
- !isEmptyObject(parentReportAction) &&
- ReportActionsUtils.isPendingRemove(parentReportAction) &&
- ReportActionsUtils.isThreadParentMessage(parentReportAction, report?.reportID ?? '')
- ) {
+ if (!isEmptyObject(parentReportAction) && ReportActionsUtils.isPendingRemove(parentReportAction) && ReportActionsUtils.isThreadParentMessage(parentReportAction, report?.reportID)) {
return null;
}
@@ -6759,7 +6838,7 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec
/**
* Attempts to find an invoice chat report in onyx with the provided policyID and receiverID.
*/
-function getInvoiceChatByParticipants(policyID: string, receiverID: string | number, reports: OnyxCollection = allReports): OnyxEntry {
+function getInvoiceChatByParticipants(receiverID: string | number, receiverType: InvoiceReceiverType, policyID?: string, reports: OnyxCollection = allReports): OnyxEntry {
return Object.values(reports ?? {}).find((report) => {
if (!report || !isInvoiceRoom(report) || isArchivedRoom(report)) {
return false;
@@ -6767,6 +6846,7 @@ function getInvoiceChatByParticipants(policyID: string, receiverID: string | num
const isSameReceiver =
report.invoiceReceiver &&
+ report.invoiceReceiver.type === receiverType &&
(('accountID' in report.invoiceReceiver && report.invoiceReceiver.accountID === receiverID) ||
('policyID' in report.invoiceReceiver && report.invoiceReceiver.policyID === receiverID));
@@ -6777,7 +6857,11 @@ function getInvoiceChatByParticipants(policyID: string, receiverID: string | num
/**
* Attempts to find a policy expense report in onyx that is owned by ownerAccountID in a given policy
*/
-function getPolicyExpenseChat(ownerAccountID: number, policyID: string): OnyxEntry {
+function getPolicyExpenseChat(ownerAccountID: number | undefined, policyID: string | undefined): OnyxEntry {
+ if (!ownerAccountID || !policyID) {
+ return;
+ }
+
return Object.values(allReports ?? {}).find((report: OnyxEntry) => {
// If the report has been deleted, then skip it
if (!report) {
@@ -6859,21 +6943,6 @@ function shouldShowFlagComment(reportAction: OnyxInputOrEntry, rep
);
}
-/**
- * @param sortedAndFilteredReportActions - reportActions for the report, sorted newest to oldest, and filtered for only those that should be visible
- */
-function getNewMarkerReportActionID(report: OnyxEntry, sortedAndFilteredReportActions: ReportAction[]): string {
- if (!isUnread(report)) {
- return '';
- }
-
- const newMarkerIndex = lodashFindLastIndex(sortedAndFilteredReportActions, (reportAction) => (reportAction.created ?? '') > (report?.lastReadTime ?? ''));
-
- return newMarkerIndex !== -1 && 'reportActionID' in (sortedAndFilteredReportActions?.at(newMarkerIndex) ?? {})
- ? sortedAndFilteredReportActions.at(newMarkerIndex)?.reportActionID ?? ''
- : '';
-}
-
/**
* 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
@@ -7057,7 +7126,7 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, policy: OnyxEntry 0;
- const isPolicyOwnedByExpensifyAccounts = report?.policyID ? CONST.EXPENSIFY_ACCOUNT_IDS.includes(getPolicy(report?.policyID ?? '-1')?.ownerAccountID ?? -1) : false;
+ const policyOwnerAccountID = getPolicy(report?.policyID)?.ownerAccountID;
+ const isPolicyOwnedByExpensifyAccounts = policyOwnerAccountID ? CONST.EXPENSIFY_ACCOUNT_IDS.includes(policyOwnerAccountID) : false;
if (doParticipantsIncludeExpensifyAccounts && !isPolicyOwnedByExpensifyAccounts) {
return [];
}
@@ -7179,7 +7249,7 @@ function canLeaveRoom(report: OnyxEntry, isPolicyEmployee: boolean): boo
return false;
}
- const invoiceReport = getReportOrDraftReport(report?.iouReportID ?? '-1');
+ const invoiceReport = getReportOrDraftReport(report?.iouReportID);
if (invoiceReport?.ownerAccountID === currentUserAccountID) {
return false;
@@ -7262,7 +7332,7 @@ function shouldReportShowSubscript(report: OnyxEntry): boolean {
return true;
}
- if (isExpenseReport(report) && isOneTransactionReport(report?.reportID ?? '-1')) {
+ if (isExpenseReport(report) && isOneTransactionReport(report?.reportID)) {
return true;
}
@@ -7313,7 +7383,7 @@ function isMoneyRequestReportPendingDeletion(reportOrID: OnyxEntry | str
return false;
}
- const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1');
+ const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID, report?.parentReportActionID);
return parentReportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
}
@@ -7334,7 +7404,7 @@ function canUserPerformWriteAction(report: OnyxEntry) {
*/
function getOriginalReportID(reportID: string, reportAction: OnyxInputOrEntry): string | undefined {
const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`];
- const currentReportAction = reportActions?.[reportAction?.reportActionID ?? '-1'] ?? null;
+ const currentReportAction = reportAction?.reportActionID ? reportActions?.[reportAction.reportActionID] : undefined;
const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions ?? ([] as ReportAction[]));
const isThreadReportParentAction = reportAction?.childReportID?.toString() === reportID;
if (Object.keys(currentReportAction ?? {}).length === 0) {
@@ -7379,7 +7449,9 @@ function canCreateRequest(report: OnyxEntry, policy: OnyxEntry,
}
function getWorkspaceChats(policyID: string, accountIDs: number[], reports: OnyxCollection = allReports): Array> {
- return Object.values(reports ?? {}).filter((report) => isPolicyExpenseChat(report) && (report?.policyID ?? '-1') === policyID && accountIDs.includes(report?.ownerAccountID ?? -1));
+ return Object.values(reports ?? {}).filter(
+ (report) => isPolicyExpenseChat(report) && report?.policyID === policyID && report?.ownerAccountID && accountIDs.includes(report?.ownerAccountID),
+ );
}
/**
@@ -7388,7 +7460,7 @@ function getWorkspaceChats(policyID: string, accountIDs: number[], reports: Onyx
* @param policyID - the workspace ID to get all associated reports
*/
function getAllWorkspaceReports(policyID: string): Array> {
- return Object.values(allReports ?? {}).filter((report) => (report?.policyID ?? '-1') === policyID);
+ return Object.values(allReports ?? {}).filter((report) => report?.policyID === policyID);
}
/**
@@ -7538,7 +7610,7 @@ function getTaskAssigneeChatOnyxData(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`,
- value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '-1']: optimisticAssigneeAddComment.reportAction as ReportAction},
+ value: {[optimisticAssigneeAddComment.reportAction.reportActionID]: optimisticAssigneeAddComment.reportAction as ReportAction},
},
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -7549,12 +7621,12 @@ function getTaskAssigneeChatOnyxData(
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`,
- value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '-1']: {isOptimisticAction: null}},
+ value: {[optimisticAssigneeAddComment.reportAction.reportActionID]: {isOptimisticAction: null}},
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`,
- value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '-1']: {pendingAction: null}},
+ value: {[optimisticAssigneeAddComment.reportAction.reportActionID]: {pendingAction: null}},
});
}
@@ -7587,7 +7659,7 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry,
switch (originalMessage.paymentType) {
case CONST.IOU.PAYMENT_TYPE.ELSEWHERE:
- translationKey = hasMissingInvoiceBankAccount(IOUReportID ?? '-1') ? 'iou.payerSettledWithMissingBankAccount' : 'iou.paidElsewhereWithAmount';
+ translationKey = hasMissingInvoiceBankAccount(IOUReportID) ? 'iou.payerSettledWithMissingBankAccount' : 'iou.paidElsewhereWithAmount';
break;
case CONST.IOU.PAYMENT_TYPE.EXPENSIFY:
case CONST.IOU.PAYMENT_TYPE.VBBA:
@@ -7674,7 +7746,7 @@ function isValidReport(report?: OnyxEntry): boolean {
/**
* Check to see if we are a participant of this report.
*/
-function isReportParticipant(accountID: number, report: OnyxEntry): boolean {
+function isReportParticipant(accountID: number | undefined, report: OnyxEntry): boolean {
if (!accountID) {
return false;
}
@@ -7693,7 +7765,7 @@ function isReportParticipant(accountID: number, report: OnyxEntry): bool
* Check to see if the current user has access to view the report.
*/
function canCurrentUserOpenReport(report: OnyxEntry): boolean {
- return (isReportParticipant(currentUserAccountID ?? 0, report) || isPublicRoom(report)) && canAccessReport(report, allPolicies, allBetas);
+ return (isReportParticipant(currentUserAccountID, report) || isPublicRoom(report)) && canAccessReport(report, allPolicies, allBetas);
}
function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean {
@@ -7717,7 +7789,7 @@ function canEditReportDescription(report: OnyxEntry, policy: OnyxEntry, session: OnyxEntry TransactionUtils.isOnHold(transaction));
}
@@ -7798,15 +7870,17 @@ function getAllHeldTransactions(iouReportID?: string): Transaction[] {
* Check if Report has any held expenses
*/
function hasHeldExpenses(iouReportID?: string, allReportTransactions?: SearchTransaction[]): boolean {
- const transactions = allReportTransactions ?? reportsTransactions[iouReportID ?? ''] ?? [];
+ const iouReportTransactions = getReportTransactions(iouReportID);
+ const transactions = allReportTransactions ?? iouReportTransactions;
return transactions.some((transaction) => TransactionUtils.isOnHold(transaction));
}
/**
* Check if all expenses in the Report are on hold
*/
-function hasOnlyHeldExpenses(iouReportID: string, allReportTransactions?: SearchTransaction[]): boolean {
- const reportTransactions = allReportTransactions ?? reportsTransactions[iouReportID ?? ''] ?? [];
+function hasOnlyHeldExpenses(iouReportID?: string, allReportTransactions?: SearchTransaction[]): boolean {
+ const transactionsByIouReportID = getReportTransactions(iouReportID);
+ const reportTransactions = allReportTransactions ?? transactionsByIouReportID;
return reportTransactions.length > 0 && !reportTransactions.some((transaction) => !TransactionUtils.isOnHold(transaction));
}
@@ -7826,7 +7900,7 @@ function hasUpdatedTotal(report: OnyxInputOrEntry, policy: OnyxInputOrEn
return true;
}
- const allReportTransactions = reportsTransactions[report.reportID] ?? [];
+ const allReportTransactions = getReportTransactions(report.reportID);
const hasPendingTransaction = allReportTransactions.some((transaction) => !!transaction.pendingAction);
const hasTransactionWithDifferentCurrency = allReportTransactions.some((transaction) => transaction.currency !== report.currency);
@@ -7896,7 +7970,7 @@ function getAllAncestorReportActions(report: Report | null | undefined, currentU
while (parentReportID) {
const parentReport = currentUpdatedReport && currentUpdatedReport.reportID === parentReportID ? currentUpdatedReport : getReportOrDraftReport(parentReportID);
- const parentReportAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? '-1');
+ const parentReportAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID);
if (
!parentReport ||
@@ -7938,7 +8012,7 @@ function getAllAncestorReportActionIDs(report: Report | null | undefined, includ
while (parentReportID) {
const parentReport = getReportOrDraftReport(parentReportID);
- const parentReportAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? '-1');
+ const parentReportAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID);
if (
!parentReportAction ||
@@ -7949,8 +8023,10 @@ function getAllAncestorReportActionIDs(report: Report | null | undefined, includ
break;
}
- allAncestorIDs.reportIDs.push(parentReportID ?? '-1');
- allAncestorIDs.reportActionsIDs.push(parentReportActionID ?? '-1');
+ allAncestorIDs.reportIDs.push(parentReportID);
+ if (parentReportActionID) {
+ allAncestorIDs.reportActionsIDs.push(parentReportActionID);
+ }
if (!parentReport) {
break;
@@ -7988,7 +8064,7 @@ function getOptimisticDataForParentReportAction(reportID: string, lastVisibleAct
const ancestorReportAction = ReportActionsUtils.getReportAction(ancestorReport.reportID, ancestors.reportActionsIDs.at(index) ?? '');
- if (!ancestorReportAction || isEmptyObject(ancestorReportAction)) {
+ if (!ancestorReportAction?.reportActionID || isEmptyObject(ancestorReportAction)) {
return null;
}
@@ -7996,7 +8072,7 @@ function getOptimisticDataForParentReportAction(reportID: string, lastVisibleAct
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`,
value: {
- [ancestorReportAction?.reportActionID ?? '-1']: updateOptimisticParentReportAction(ancestorReportAction, lastVisibleActionCreated, type),
+ [ancestorReportAction.reportActionID]: updateOptimisticParentReportAction(ancestorReportAction, lastVisibleActionCreated, type),
},
};
});
@@ -8101,7 +8177,7 @@ function getTripTransactions(tripRoomReportID: string | undefined, reportFieldTo
const tripTransactionReportIDs = Object.values(allReports ?? {})
.filter((report) => report && report?.[reportFieldToCompare] === tripRoomReportID)
.map((report) => report?.reportID);
- return tripTransactionReportIDs.flatMap((reportID) => reportsTransactions[reportID ?? ''] ?? []);
+ return tripTransactionReportIDs.flatMap((reportID) => getReportTransactions(reportID));
}
function getTripIDFromTransactionParentReportID(transactionParentReportID: string | undefined): string | undefined {
@@ -8119,13 +8195,13 @@ function hasActionsWithErrors(reportID: string): boolean {
}
function isNonAdminOrOwnerOfPolicyExpenseChat(report: OnyxInputOrEntry, policy: OnyxInputOrEntry): boolean {
- return isPolicyExpenseChat(report) && !(PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPolicyOwner(policy, currentUserAccountID ?? -1) || isReportOwner(report));
+ return isPolicyExpenseChat(report) && !(PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPolicyOwner(policy, currentUserAccountID) || isReportOwner(report));
}
function isAdminOwnerApproverOrReportOwner(report: OnyxEntry, policy: OnyxEntry): boolean {
const isApprover = isMoneyRequestReport(report) && report?.managerID !== null && currentUserPersonalDetails?.accountID === report?.managerID;
- return PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPolicyOwner(policy, currentUserAccountID ?? -1) || isReportOwner(report) || isApprover;
+ return PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPolicyOwner(policy, currentUserAccountID) || isReportOwner(report) || isApprover;
}
/**
@@ -8233,7 +8309,16 @@ function createDraftWorkspaceAndNavigateToConfirmationScreen(transactionID: stri
}
}
-function createDraftTransactionAndNavigateToParticipantSelector(transactionID: string, reportID: string, actionName: IOUAction, reportActionID: string): void {
+function createDraftTransactionAndNavigateToParticipantSelector(
+ transactionID: string | undefined,
+ reportID: string | undefined,
+ actionName: IOUAction,
+ reportActionID: string | undefined,
+): void {
+ if (!transactionID || !reportID || !reportActionID) {
+ return;
+ }
+
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? ({} as Transaction);
const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? ([] as ReportAction[]);
@@ -8272,18 +8357,22 @@ function createDraftTransactionAndNavigateToParticipantSelector(transactionID: s
if (actionName === CONST.IOU.ACTION.CATEGORIZE) {
const activePolicy = getPolicy(activePolicyID);
if (activePolicy && activePolicy?.type !== CONST.POLICY.TYPE.PERSONAL && activePolicy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
- const policyExpenseReportID = getPolicyExpenseChat(currentUserAccountID ?? -1, activePolicyID ?? '-1')?.reportID ?? '-1';
+ const policyExpenseReportID = getPolicyExpenseChat(currentUserAccountID, activePolicyID)?.reportID;
IOU.setMoneyRequestParticipants(transactionID, [
{
selected: true,
accountID: 0,
isPolicyExpenseChat: true,
reportID: policyExpenseReportID,
- policyID: activePolicyID ?? '-1',
+ policyID: activePolicyID,
searchText: activePolicy?.name,
},
]);
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(actionName, CONST.IOU.TYPE.SUBMIT, transactionID, policyExpenseReportID));
+ if (policyExpenseReportID) {
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(actionName, CONST.IOU.TYPE.SUBMIT, transactionID, policyExpenseReportID));
+ } else {
+ Log.warn('policyExpenseReportID is not valid during expense categorizing');
+ }
return;
}
if (filteredPolicies.length === 0 || filteredPolicies.length > 1) {
@@ -8292,18 +8381,22 @@ function createDraftTransactionAndNavigateToParticipantSelector(transactionID: s
}
const policyID = filteredPolicies.at(0)?.id;
- const policyExpenseReportID = getPolicyExpenseChat(currentUserAccountID ?? -1, policyID ?? '-1')?.reportID ?? '-1';
+ const policyExpenseReportID = getPolicyExpenseChat(currentUserAccountID, policyID)?.reportID;
IOU.setMoneyRequestParticipants(transactionID, [
{
selected: true,
accountID: 0,
isPolicyExpenseChat: true,
reportID: policyExpenseReportID,
- policyID: policyID ?? '-1',
+ policyID,
searchText: activePolicy?.name,
},
]);
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(actionName, CONST.IOU.TYPE.SUBMIT, transactionID, policyExpenseReportID));
+ if (policyExpenseReportID) {
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(actionName, CONST.IOU.TYPE.SUBMIT, transactionID, policyExpenseReportID));
+ } else {
+ Log.warn('policyExpenseReportID is not valid during expense categorizing');
+ }
return;
}
@@ -8342,8 +8435,8 @@ function getOutstandingChildRequest(iouReport: OnyxInputOrEntry): Outsta
return {};
}
-function canReportBeMentionedWithinPolicy(report: OnyxEntry, policyID: string): boolean {
- if (report?.policyID !== policyID) {
+function canReportBeMentionedWithinPolicy(report: OnyxEntry, policyID: string | undefined): boolean {
+ if (!policyID || report?.policyID !== policyID) {
return false;
}
@@ -8360,8 +8453,8 @@ function shouldShowMerchantColumn(transactions: Transaction[]) {
* only use the Concierge chat.
*/
function isChatUsedForOnboarding(optionOrReport: OnyxEntry | OptionData): boolean {
- // onboarding can be an array or an empty object for old accounts and accounts created from olddot
- if (onboarding && !Array.isArray(onboarding) && !isEmptyObject(onboarding) && onboarding.chatReportID) {
+ // onboarding can be an empty object for old accounts and accounts created from olddot
+ if (onboarding && !isEmptyObject(onboarding) && onboarding.chatReportID) {
return onboarding.chatReportID === optionOrReport?.reportID;
}
@@ -8427,20 +8520,18 @@ function findPolicyExpenseChatByPolicyID(policyID: string): OnyxEntry {
*/
function getReportLastMessage(reportID: string, actionsToMerge?: ReportActions) {
let result: Partial = {
- lastMessageTranslationKey: '',
lastMessageText: '',
lastVisibleActionCreated: '',
};
- const {lastMessageText = '', lastMessageTranslationKey = ''} = getLastVisibleMessage(reportID, actionsToMerge);
+ const {lastMessageText = ''} = getLastVisibleMessage(reportID, actionsToMerge);
- if (lastMessageText || lastMessageTranslationKey) {
+ if (lastMessageText) {
const report = getReport(reportID);
const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, canUserPerformWriteAction(report), actionsToMerge);
const lastVisibleActionCreated = lastVisibleAction?.created;
const lastActorAccountID = lastVisibleAction?.actorAccountID;
result = {
- lastMessageTranslationKey,
lastMessageText,
lastVisibleActionCreated,
lastActorAccountID,
@@ -8494,7 +8585,7 @@ function getApprovalChain(policy: OnyxEntry, expenseReport: OnyxEntry, expenseReport: OnyxEntry isInvoiceReport(report));
}
-function getReportMetadata(reportID?: string) {
- return allReportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`];
+function getReportMetadata(reportID: string | undefined) {
+ return reportID ? allReportMetadataKeyValue[reportID] : undefined;
}
export {
@@ -8661,7 +8752,6 @@ export {
getLastVisibleMessage,
getMoneyRequestOptions,
getMoneyRequestSpendBreakdown,
- getNewMarkerReportActionID,
getNonHeldAndFullAmount,
getOptimisticDataForParentReportAction,
getOriginalReportID,
@@ -8875,6 +8965,7 @@ export {
getAllReportActionsErrorsAndReportActionThatRequiresAttention,
hasInvoiceReports,
getReportMetadata,
+ buildOptimisticSelfDMReport,
isHiddenForCurrentUser,
};
diff --git a/src/libs/SearchInputOnKeyPress/index.native.ts b/src/libs/SearchInputOnKeyPress/index.native.ts
new file mode 100644
index 000000000000..3621eba63d8e
--- /dev/null
+++ b/src/libs/SearchInputOnKeyPress/index.native.ts
@@ -0,0 +1,5 @@
+import type {NativeSyntheticEvent, TextInputKeyPressEventData} from 'react-native';
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const handleKeyPress = (onSubmit: () => void) => (event: NativeSyntheticEvent) => {};
+export default handleKeyPress;
diff --git a/src/libs/SearchInputOnKeyPress/index.ts b/src/libs/SearchInputOnKeyPress/index.ts
new file mode 100644
index 000000000000..f5283719eab3
--- /dev/null
+++ b/src/libs/SearchInputOnKeyPress/index.ts
@@ -0,0 +1,16 @@
+import type {NativeSyntheticEvent, TextInputKeyPressEventData} from 'react-native';
+import CONST from '@src/CONST';
+
+function handleKeyPress(onSubmit: () => void) {
+ return (event: NativeSyntheticEvent) => {
+ const isEnterKey = event.nativeEvent.key.toLowerCase() === CONST.PLATFORM_SPECIFIC_KEYS.ENTER.DEFAULT;
+
+ if (!isEnterKey) {
+ return;
+ }
+
+ onSubmit();
+ };
+}
+
+export default handleKeyPress;
diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js
index 47b534d32cad..941ac7f59797 100644
--- a/src/libs/SearchParser/searchParser.js
+++ b/src/libs/SearchParser/searchParser.js
@@ -298,7 +298,7 @@ function peg$parse(input, options) {
const keywordFilter = buildFilter(
"eq",
"keyword",
- keywords.map((filter) => filter.right).flat()
+ keywords.map((filter) => filter.right.replace(/^(['"])(.*)\1$/, '$2')).flat()
);
if (keywordFilter.right.length > 0) {
nonKeywords.push(keywordFilter);
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index 68b3ce60963a..b48846f95a0a 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -66,7 +66,7 @@ function getTransactionItemCommonFormattedProperties(
const formattedTotal = TransactionUtils.getAmount(transactionItem, isExpenseReport);
const date = transactionItem?.modifiedCreated ? transactionItem.modifiedCreated : transactionItem?.created;
const merchant = TransactionUtils.getMerchant(transactionItem);
- const formattedMerchant = merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || merchant === CONST.TRANSACTION.DEFAULT_MERCHANT ? '' : merchant;
+ const formattedMerchant = merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT ? '' : merchant;
return {
formattedFrom,
@@ -106,7 +106,7 @@ function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean {
if (isTransactionEntry(key)) {
const item = data[key];
const merchant = item.modifiedMerchant ? item.modifiedMerchant : item.merchant ?? '';
- return merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && merchant !== CONST.TRANSACTION.DEFAULT_MERCHANT;
+ return merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
}
return false;
});
@@ -264,7 +264,7 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr
// We need to check both options for a falsy value since the transaction might not have an error but the report associated with it might
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- if (transaction?.hasError || report.hasError) {
+ if (transaction?.errors || report?.errors) {
return CONST.SEARCH.ACTION_TYPES.REVIEW;
}
@@ -338,6 +338,10 @@ function getReportActionsSections(data: OnyxTypes.SearchResults['data']): Report
// eslint-disable-next-line no-continue
continue;
}
+ if (!ReportActionsUtils.isAddCommentAction(reportAction)) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
reportActionItems.push({
...reportAction,
from,
@@ -374,7 +378,7 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx
...reportItem,
action: getAction(data, key),
keyForList: reportItem.reportID,
- from: data.personalDetailsList?.[reportItem.accountID ?? -1],
+ from: data.personalDetailsList?.[reportItem.accountID ?? CONST.DEFAULT_NUMBER_ID],
to: reportItem.managerID ? data.personalDetailsList?.[reportItem.managerID] : emptyPersonalDetails,
transactions,
reportName: isIOUReport ? getIOUReportName(data, reportItem) : reportItem.reportName,
diff --git a/src/libs/SuffixUkkonenTree/index.ts b/src/libs/SuffixUkkonenTree/index.ts
new file mode 100644
index 000000000000..bcefd1008493
--- /dev/null
+++ b/src/libs/SuffixUkkonenTree/index.ts
@@ -0,0 +1,211 @@
+/* eslint-disable rulesdir/prefer-at */
+// .at() has a performance overhead we explicitly want to avoid here
+
+/* eslint-disable no-continue */
+import {ALPHABET_SIZE, DELIMITER_CHAR_CODE, END_CHAR_CODE, SPECIAL_CHAR_CODE, stringToNumeric} from './utils';
+
+/**
+ * This implements a suffix tree using Ukkonen's algorithm.
+ * A good visualization to learn about the algorithm can be found here: https://brenden.github.io/ukkonen-animation/
+ * A good video explaining Ukkonen's algorithm can be found here: https://www.youtube.com/watch?v=ALEV0Hc5dDk
+ * Note: This implementation is optimized for performance, not necessarily for readability.
+ *
+ * You probably don't want to use this directly, but rather use @libs/FastSearch.ts as a easy to use wrapper around this.
+ */
+
+/**
+ * Creates a new tree instance that can be used to build a suffix tree and search in it.
+ * The input is a numeric representation of the search string, which can be created using {@link stringToNumeric}.
+ * Separate search values must be separated by the {@link DELIMITER_CHAR_CODE}. The search string must end with the {@link END_CHAR_CODE}.
+ *
+ * The tree will be built using the Ukkonen's algorithm: https://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf
+ */
+function makeTree(numericSearchValues: Uint8Array) {
+ // Every leaf represents a suffix. There can't be more than n suffixes.
+ // Every internal node has to have at least 2 children. So the total size of ukkonen tree is not bigger than 2n - 1.
+ // + 1 is because an extra character at the beginning to offset the 1-based indexing.
+ const maxNodes = 2 * numericSearchValues.length + 1;
+ /*
+ This array represents all internal nodes in the suffix tree.
+ When building this tree, we'll be given a character in the string, and we need to be able to lookup in constant time
+ if there's any edge connected to a node starting with that character. For example, given a tree like this:
+
+ root
+ / | \
+ a b c
+
+ and the next character in our string is 'd', we need to be able do check if any of the edges from the root node
+ start with the letter 'd', without looping through all the edges.
+
+ To accomplish this, each node gets an array matching the alphabet size.
+ So you can imagine if our alphabet was just [a,b,c,d], then each node would get an array like [0,0,0,0].
+ If we add an edge starting with 'a', then the root node would be [1,0,0,0]
+ So given an arbitrary letter such as 'd', then we can take the position of that letter in its alphabet (position 3 in our example)
+ and check whether that index in the array is 0 or 1. If it's a 1, then there's an edge starting with the letter 'd'.
+
+ Note that for efficiency, all nodes are stored in a single flat array. That's how we end up with (maxNodes * alphabet_size).
+ In the example of a 4-character alphabet, we'd have an array like this:
+
+ root root.left root.right last possible node
+ / \ / \ / \ / \
+ [0,0,0,0, 0,0,0,0, 0,0,0,0, ................. 0,0,0,0]
+ */
+ const transitionNodes = new Uint32Array(maxNodes * ALPHABET_SIZE);
+
+ // Storing the range of the original string that each node represents:
+ const rangeStart = new Uint32Array(maxNodes);
+ const rangeEnd = new Uint32Array(maxNodes);
+
+ const parent = new Uint32Array(maxNodes);
+ const suffixLink = new Uint32Array(maxNodes);
+
+ let currentNode = 1;
+ let currentPosition = 1;
+ let nodeCounter = 3;
+ let currentIndex = 1;
+
+ function initializeTree() {
+ rangeEnd.fill(numericSearchValues.length);
+ rangeEnd[1] = 0;
+ rangeEnd[2] = 0;
+ suffixLink[1] = 2;
+ for (let i = 0; i < ALPHABET_SIZE; ++i) {
+ transitionNodes[ALPHABET_SIZE * 2 + i] = 1;
+ }
+ }
+
+ function processCharacter(char: number) {
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ if (rangeEnd[currentNode] < currentPosition) {
+ if (transitionNodes[currentNode * ALPHABET_SIZE + char] === 0) {
+ createNewLeaf(char);
+ continue;
+ }
+ currentNode = transitionNodes[currentNode * ALPHABET_SIZE + char];
+ currentPosition = rangeStart[currentNode];
+ }
+ if (currentPosition === 0 || char === numericSearchValues[currentPosition]) {
+ currentPosition++;
+ } else {
+ splitEdge(char);
+ continue;
+ }
+ break;
+ }
+ }
+
+ function createNewLeaf(c: number) {
+ transitionNodes[currentNode * ALPHABET_SIZE + c] = nodeCounter;
+ rangeStart[nodeCounter] = currentIndex;
+ parent[nodeCounter++] = currentNode;
+ currentNode = suffixLink[currentNode];
+
+ currentPosition = rangeEnd[currentNode] + 1;
+ }
+
+ function splitEdge(c: number) {
+ rangeStart[nodeCounter] = rangeStart[currentNode];
+ rangeEnd[nodeCounter] = currentPosition - 1;
+ parent[nodeCounter] = parent[currentNode];
+
+ transitionNodes[nodeCounter * ALPHABET_SIZE + numericSearchValues[currentPosition]] = currentNode;
+ transitionNodes[nodeCounter * ALPHABET_SIZE + c] = nodeCounter + 1;
+ rangeStart[nodeCounter + 1] = currentIndex;
+ parent[nodeCounter + 1] = nodeCounter;
+ rangeStart[currentNode] = currentPosition;
+ parent[currentNode] = nodeCounter;
+
+ transitionNodes[parent[nodeCounter] * ALPHABET_SIZE + numericSearchValues[rangeStart[nodeCounter]]] = nodeCounter;
+ nodeCounter += 2;
+ handleDescent(nodeCounter);
+ }
+
+ function handleDescent(latestNodeIndex: number) {
+ currentNode = suffixLink[parent[latestNodeIndex - 2]];
+ currentPosition = rangeStart[latestNodeIndex - 2];
+ while (currentPosition <= rangeEnd[latestNodeIndex - 2]) {
+ currentNode = transitionNodes[currentNode * ALPHABET_SIZE + numericSearchValues[currentPosition]];
+ currentPosition += rangeEnd[currentNode] - rangeStart[currentNode] + 1;
+ }
+ if (currentPosition === rangeEnd[latestNodeIndex - 2] + 1) {
+ suffixLink[latestNodeIndex - 2] = currentNode;
+ } else {
+ suffixLink[latestNodeIndex - 2] = latestNodeIndex;
+ }
+ currentPosition = rangeEnd[currentNode] - (currentPosition - rangeEnd[latestNodeIndex - 2]) + 2;
+ }
+
+ function build() {
+ initializeTree();
+ for (currentIndex = 1; currentIndex < numericSearchValues.length; ++currentIndex) {
+ const c = numericSearchValues[currentIndex];
+ processCharacter(c);
+ }
+ }
+
+ /**
+ * Returns all occurrences of the given (sub)string in the input string.
+ *
+ * You can think of the tree that we create as a big string that looks like this:
+ *
+ * "banana$pancake$apple|"
+ * The example delimiter character '$' is used to separate the different strings.
+ * The end character '|' is used to indicate the end of our search string.
+ *
+ * This function will return the index(es) of found occurrences within this big string.
+ * So, when searching for "an", it would return [1, 3, 8].
+ */
+ function findSubstring(searchValue: number[]) {
+ const occurrences: number[] = [];
+
+ function dfs(node: number, depth: number) {
+ const leftRange = rangeStart[node];
+ const rightRange = rangeEnd[node];
+ const rangeLen = node === 1 ? 0 : rightRange - leftRange + 1;
+
+ for (let i = 0; i < rangeLen && depth + i < searchValue.length && leftRange + i < numericSearchValues.length; i++) {
+ if (searchValue[depth + i] !== numericSearchValues[leftRange + i]) {
+ return;
+ }
+ }
+
+ let isLeaf = true;
+ for (let i = 0; i < ALPHABET_SIZE; ++i) {
+ const tNode = transitionNodes[node * ALPHABET_SIZE + i];
+
+ // Search speed optimization: don't go through the edge if it's different than the next char:
+ const correctChar = depth + rangeLen >= searchValue.length || i === searchValue[depth + rangeLen];
+
+ if (tNode !== 0 && tNode !== 1 && correctChar) {
+ isLeaf = false;
+ dfs(tNode, depth + rangeLen);
+ }
+ }
+
+ if (isLeaf && depth + rangeLen >= searchValue.length) {
+ occurrences.push(numericSearchValues.length - (depth + rangeLen) + 1);
+ }
+ }
+
+ dfs(1, 0);
+ return occurrences;
+ }
+
+ return {
+ build,
+ findSubstring,
+ };
+}
+
+const SuffixUkkonenTree = {
+ makeTree,
+
+ // Re-exported from utils:
+ DELIMITER_CHAR_CODE,
+ SPECIAL_CHAR_CODE,
+ END_CHAR_CODE,
+ stringToNumeric,
+};
+
+export default SuffixUkkonenTree;
diff --git a/src/libs/SuffixUkkonenTree/utils.ts b/src/libs/SuffixUkkonenTree/utils.ts
new file mode 100644
index 000000000000..96ee35b15796
--- /dev/null
+++ b/src/libs/SuffixUkkonenTree/utils.ts
@@ -0,0 +1,115 @@
+/* eslint-disable rulesdir/prefer-at */ // .at() has a performance overhead we explicitly want to avoid here
+/* eslint-disable no-continue */
+
+const CHAR_CODE_A = 'a'.charCodeAt(0);
+const ALPHABET = 'abcdefghijklmnopqrstuvwxyz';
+const LETTER_ALPHABET_SIZE = ALPHABET.length;
+const ALPHABET_SIZE = LETTER_ALPHABET_SIZE + 3; // +3: special char, delimiter char, end char
+const SPECIAL_CHAR_CODE = ALPHABET_SIZE - 3;
+const DELIMITER_CHAR_CODE = ALPHABET_SIZE - 2;
+const END_CHAR_CODE = ALPHABET_SIZE - 1;
+
+// Store the results for a char code in a lookup table to avoid recalculating the same values (performance optimization)
+const base26LookupTable = new Array();
+
+/**
+ * Converts a number to a base26 representation.
+ */
+function convertToBase26(num: number): number[] {
+ if (base26LookupTable[num]) {
+ return base26LookupTable[num];
+ }
+ if (num < 0) {
+ throw new Error('convertToBase26: Input must be a non-negative integer');
+ }
+
+ const result: number[] = [];
+
+ do {
+ // eslint-disable-next-line no-param-reassign
+ num--;
+ result.unshift(num % 26);
+ // eslint-disable-next-line no-bitwise, no-param-reassign
+ num >>= 5; // Equivalent to Math.floor(num / 26), but faster
+ } while (num > 0);
+
+ base26LookupTable[num] = result;
+ return result;
+}
+
+/**
+ * Converts a string to an array of numbers representing the characters of the string.
+ * Every number in the array is in the range [0, ALPHABET_SIZE-1] (0-28).
+ *
+ * The numbers are offset by the character code of 'a' (97).
+ * - This is so that the numbers from a-z are in the range 0-28.
+ * - 26 is for encoding special characters. Character numbers that are not within the range of a-z will be encoded as "specialCharacter + base26(charCode)"
+ * - 27 is for the delimiter character
+ * - 28 is for the end character
+ *
+ * Note: The string should be converted to lowercase first (otherwise uppercase letters get base26'ed taking more space than necessary).
+ */
+function stringToNumeric(
+ // The string we want to convert to a numeric representation
+ input: string,
+ options?: {
+ // A set of characters that should be skipped and not included in the numeric representation
+ charSetToSkip?: Set;
+ // When out is provided, the function will write the result to the provided arrays instead of creating new ones (performance)
+ out?: {
+ outArray: Uint8Array;
+ // As outArray is a ArrayBuffer we need to keep track of the current offset
+ offset: {value: number};
+ // A map of to map the found occurrences to the correct data set
+ // As the search string can be very long for high traffic accounts (500k+), this has to be big enough, thus its a Uint32Array
+ outOccurrenceToIndex?: Uint32Array;
+ // The index that will be used in the outOccurrenceToIndex array (this is the index of your original data position)
+ index?: number;
+ };
+ // By default false. By default the outArray may be larger than necessary. If clamp is set to true the outArray will be clamped to the actual size.
+ clamp?: boolean;
+ },
+): {
+ numeric: Uint8Array;
+ occurrenceToIndex: Uint32Array;
+ offset: {value: number};
+} {
+ // The out array might be longer than our input string length, because we encode special characters as multiple numbers using the base26 encoding.
+ // * 6 is because the upper limit of encoding any char in UTF-8 to base26 is at max 6 numbers.
+ const outArray = options?.out?.outArray ?? new Uint8Array(input.length * 6);
+ const offset = options?.out?.offset ?? {value: 0};
+ const occurrenceToIndex = options?.out?.outOccurrenceToIndex ?? new Uint32Array(input.length * 16 * 4);
+ const index = options?.out?.index ?? 0;
+
+ for (let i = 0; i < input.length; i++) {
+ const char = input[i];
+
+ if (options?.charSetToSkip?.has(char)) {
+ continue;
+ }
+
+ if (char >= 'a' && char <= 'z') {
+ // char is an alphabet character
+ occurrenceToIndex[offset.value] = index;
+ outArray[offset.value++] = char.charCodeAt(0) - CHAR_CODE_A;
+ } else {
+ const charCode = input.charCodeAt(i);
+ occurrenceToIndex[offset.value] = index;
+ outArray[offset.value++] = SPECIAL_CHAR_CODE;
+ const asBase26Numeric = convertToBase26(charCode);
+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
+ for (let j = 0; j < asBase26Numeric.length; j++) {
+ occurrenceToIndex[offset.value] = index;
+ outArray[offset.value++] = asBase26Numeric[j];
+ }
+ }
+ }
+
+ return {
+ numeric: options?.clamp ? outArray.slice(0, offset.value) : outArray,
+ occurrenceToIndex,
+ offset,
+ };
+}
+
+export {stringToNumeric, ALPHABET, ALPHABET_SIZE, SPECIAL_CHAR_CODE, DELIMITER_CHAR_CODE, END_CHAR_CODE};
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index 6643cd721d45..528481dae237 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -33,6 +33,34 @@ import type DeepValueOf from '@src/types/utils/DeepValueOf';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import getDistanceInMeters from './getDistanceInMeters';
+type TransactionParams = {
+ amount: number;
+ currency: string;
+ reportID: string;
+ comment?: string;
+ attendees?: Attendee[];
+ created?: string;
+ merchant?: string;
+ receipt?: OnyxEntry;
+ category?: string;
+ tag?: string;
+ taxCode?: string;
+ taxAmount?: number;
+ billable?: boolean;
+ pendingFields?: Partial<{[K in TransactionPendingFieldsKey]: ValueOf}>;
+ reimbursable?: boolean;
+ source?: string;
+ filename?: string;
+};
+
+type BuildOptimisticTransactionParams = {
+ originalTransactionID?: string;
+ existingTransactionID?: string;
+ existingTransaction?: OnyxEntry;
+ policy?: OnyxEntry;
+ transactionParams: TransactionParams;
+};
+
let allTransactions: OnyxCollection = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.TRANSACTION,
@@ -103,6 +131,18 @@ function isScanRequest(transaction: OnyxEntry): boolean {
return !!transaction?.receipt?.source && transaction?.amount === 0;
}
+function isPerDiemRequest(transaction: OnyxEntry): boolean {
+ // This is used during the expense creation flow before the transaction has been saved to the server
+ if (lodashHas(transaction, 'iouRequestType')) {
+ return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.PER_DIEM;
+ }
+
+ // This is the case for transaction objects once they have been saved to the server
+ const type = transaction?.comment?.type;
+ const customUnitName = transaction?.comment?.customUnit?.name;
+ return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_PER_DIEM_INTERNATIONAL;
+}
+
function getRequestType(transaction: OnyxEntry): IOURequestType {
if (isDistanceRequest(transaction)) {
return CONST.IOU.REQUEST_TYPE.DISTANCE;
@@ -111,6 +151,10 @@ function getRequestType(transaction: OnyxEntry): IOURequestType {
return CONST.IOU.REQUEST_TYPE.SCAN;
}
+ if (isPerDiemRequest(transaction)) {
+ return CONST.IOU.REQUEST_TYPE.PER_DIEM;
+ }
+
return CONST.IOU.REQUEST_TYPE.MANUAL;
}
@@ -130,29 +174,27 @@ function isManualRequest(transaction: Transaction): boolean {
* @param [existingTransactionID] When creating a distance expense, an empty transaction has already been created with a transactionID. In that case, the transaction here needs to have
* it's transactionID match what was already generated.
*/
-function buildOptimisticTransaction(
- amount: number,
- currency: string,
- reportID: string,
- comment = '',
- attendees: Attendee[] = [],
- created = '',
- source = '',
- originalTransactionID = '',
- merchant = '',
- receipt?: OnyxEntry,
- filename = '',
- existingTransactionID: string | null = null,
- category = '',
- tag = '',
- taxCode = '',
- taxAmount = 0,
- billable = false,
- pendingFields: Partial<{[K in TransactionPendingFieldsKey]: ValueOf}> | undefined = undefined,
- reimbursable = true,
- existingTransaction: OnyxEntry | undefined = undefined,
- policy: OnyxEntry = undefined,
-): Transaction {
+function buildOptimisticTransaction(params: BuildOptimisticTransactionParams): Transaction {
+ const {originalTransactionID = '', existingTransactionID, existingTransaction, policy, transactionParams} = params;
+ const {
+ amount,
+ currency,
+ reportID,
+ comment = '',
+ attendees = [],
+ created = '',
+ merchant = '',
+ receipt,
+ category = '',
+ tag = '',
+ taxCode = '',
+ taxAmount = 0,
+ billable = false,
+ pendingFields,
+ reimbursable = true,
+ source = '',
+ filename = '',
+ } = transactionParams;
// transactionIDs are random, positive, 64-bit numeric strings.
// Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID)
const transactionID = existingTransactionID ?? NumberUtils.rand64();
@@ -376,6 +418,11 @@ function getUpdatedTransaction({
if (Object.hasOwn(transactionChanges, 'category') && typeof transactionChanges.category === 'string') {
updatedTransaction.category = transactionChanges.category;
+ const {categoryTaxCode, categoryTaxAmount} = getCategoryTaxCodeAndAmount(transactionChanges.category, transaction, policy);
+ if (categoryTaxCode && categoryTaxAmount !== undefined) {
+ updatedTransaction.taxCode = categoryTaxCode;
+ updatedTransaction.taxAmount = categoryTaxAmount;
+ }
}
if (Object.hasOwn(transactionChanges, 'tag') && typeof transactionChanges.tag === 'string') {
@@ -703,7 +750,7 @@ function hasMissingSmartscanFields(transaction: OnyxInputOrEntry):
/**
* Get all transaction violations of the transaction with given tranactionID.
*/
-function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection | null): TransactionViolations | null {
+function getTransactionViolations(transactionID: string | undefined, transactionViolations: OnyxCollection | null): TransactionViolations | null {
return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null;
}
@@ -723,7 +770,7 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations |
/**
* Check if there is broken connection violation.
*/
-function hasBrokenConnectionViolation(transactionID: string): boolean {
+function hasBrokenConnectionViolation(transactionID?: string): boolean {
const violations = getTransactionViolations(transactionID, allTransactionViolations);
return !!violations?.find(
(violation) =>
@@ -735,7 +782,7 @@ function hasBrokenConnectionViolation(transactionID: string): boolean {
/**
* Check if user should see broken connection violation warning.
*/
-function shouldShowBrokenConnectionViolation(transactionID: string, report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy): boolean {
+function shouldShowBrokenConnectionViolation(transactionID: string | undefined, report: OnyxEntry | SearchReport, policy: OnyxEntry