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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear
const yearsList = searchText === '' ? years : years.filter((year) => year.text?.includes(searchText));
return {
headerMessage: !yearsList.length ? translate('common.noResultsFound') : '',
sections: [{data: yearsList.sort((a, b) => b.value - a.value), indexOffset: 0}],
sections: [{data: yearsList, indexOffset: 0}],
};
}, [years, searchText, translate]);

Expand Down
2 changes: 1 addition & 1 deletion src/components/DatePicker/CalendarPicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function CalendarPicker({
const maxYear = getYear(new Date(maxDate));

const [years, setYears] = useState<CalendarPickerListItem[]>(() =>
Array.from({length: maxYear - minYear + 1}, (v, i) => i + minYear).map((year) => ({
Array.from({length: maxYear - minYear + 1}, (v, i) => maxYear - i).map((year) => ({
text: year.toString(),
value: year,
keyForList: year.toString(),
Expand Down
8 changes: 3 additions & 5 deletions src/components/Form/FormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import type {Errors} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import arrayLastElement from '@src/utils/arrayLastElement';
import KeyboardUtils from '@src/utils/keyboard';
import type {RegisterInput} from './FormContext';
import FormContext from './FormContext';
Expand Down Expand Up @@ -314,7 +315,7 @@

const registerInput = useCallback<RegisterInput>(
(inputID, shouldSubmitForm, inputProps) => {
const newRef: MutableRefObject<InputComponentBaseProps> = inputRefs.current[inputID] ?? inputProps.ref ?? createRef();

Check failure on line 318 in src/components/Form/FormProvider.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'MutableRefObject' is deprecated. Use `RefObject` instead
if (inputRefs.current[inputID] !== newRef) {
inputRefs.current[inputID] = newRef;
}
Expand All @@ -332,11 +333,8 @@
}

const errorFields = formState?.errorFields?.[inputID] ?? {};
const fieldErrorMessage =
Object.keys(errorFields)
.sort()
.map((key) => errorFields[key])
.at(-1) ?? '';
const fieldErrorMessageKey = arrayLastElement(Object.keys(errorFields));
const fieldErrorMessage = fieldErrorMessageKey ? (errorFields[fieldErrorMessageKey] ?? '') : '';

const inputRef = inputProps.ref;

Expand Down
5 changes: 2 additions & 3 deletions src/components/LHNOptionsList/LHNOptionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {isValidDraftComment} from '@libs/DraftCommentUtils';
import getPlatform from '@libs/getPlatform';
import Log from '@libs/Log';
import {getIOUReportIDOfLastAction, getLastMessageTextForReport} from '@libs/OptionsListUtils';
import {getOneTransactionThreadReportID, getOriginalMessage, getSortedReportActionsForDisplay, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getFirstSortedReportActionForDisplay, getOneTransactionThreadReportID, getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {canUserPerformWriteAction} from '@libs/ReportUtils';
import isProductTrainingElementDismissed from '@libs/TooltipUtils';
import variables from '@styles/variables';
Expand Down Expand Up @@ -192,8 +192,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
const hasDraftComment = isValidDraftComment(draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]);

const canUserPerformWrite = canUserPerformWriteAction(item);
const sortedReportActions = getSortedReportActionsForDisplay(itemReportActions, canUserPerformWrite);
const lastReportAction = sortedReportActions.at(0);
const lastReportAction = getFirstSortedReportActionForDisplay(itemReportActions, canUserPerformWrite);

// Get the transaction for the last report action
const lastReportActionTransactionID = isMoneyRequestAction(lastReportAction)
Expand Down
12 changes: 6 additions & 6 deletions src/components/ProductTrainingContext/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import arrayFirstElement from '@src/utils/arrayFirstElement';
import createPressHandler from './createPressHandler';
import type {ProductTrainingTooltipName} from './TOOLTIPS';
import TOOLTIPS from './TOOLTIPS';
Expand Down Expand Up @@ -101,14 +102,13 @@ function ProductTrainingContextProvider({children}: ChildrenProps) {
return null;
}

const sortedTooltips = Array.from(activeTooltips)
.map((name) => ({
const highestPriorityTooltip = arrayFirstElement(
Array.from(activeTooltips).map((name) => ({
name,
priority: TOOLTIPS[name]?.priority ?? 0,
}))
.sort((a, b) => b.priority - a.priority);

const highestPriorityTooltip = sortedTooltips.at(0);
})),
(a, b) => b.priority - a.priority,
);

if (!highestPriorityTooltip) {
return null;
Expand Down
5 changes: 2 additions & 3 deletions src/hooks/useFastSearchFromOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import type {Options as OptionsListType, ReportAndPersonalDetailOptions} from '@
import {filterUserToInvite, isSearchStringMatch} from '@libs/OptionsListUtils';
import Performance from '@libs/Performance';
import type {OptionData} from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
import CONST from '@src/CONST';
import arrayLastElement from '@src/utils/arrayLastElement';

type Options = {
includeUserToInvite: boolean;
Expand Down Expand Up @@ -122,8 +122,7 @@ function useFastSearchFromOptions(
}
const deburredInput = deburr(searchInput);
const searchWords = deburredInput.split(/\s+/);
const searchWordsSorted = StringUtils.sortStringArrayByLength(searchWords);
const longestSearchWord = searchWordsSorted.at(searchWordsSorted.length - 1); // longest word is the last element
const longestSearchWord = arrayLastElement(searchWords, (a, b) => a.length - b.length); // longest word is the last element
if (!longestSearchWord) {
return emptyResult;
}
Expand Down
16 changes: 9 additions & 7 deletions src/libs/ErrorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import type {TranslationPaths} from '@src/languages/types';
import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon';
import type Response from '@src/types/onyx/Response';
import type {ReceiptError} from '@src/types/onyx/Transaction';
import arrayFirstElement from '@src/utils/arrayFirstElement';
import arrayLastElement from '@src/utils/arrayLastElement';
import DateUtils from './DateUtils';
import * as Localize from './Localize';
import {translateLocal} from './Localize';

function getAuthenticateErrorMessage(response: Response): TranslationPaths {
switch (response.jsonCode) {
Expand Down Expand Up @@ -42,7 +44,7 @@ function getAuthenticateErrorMessage(response: Response): TranslationPaths {
* @param error - The translation key for the error message.
*/
function getMicroSecondOnyxErrorWithTranslationKey(error: TranslationPaths, errorKey?: number): Errors {
return {[errorKey ?? DateUtils.getMicroseconds()]: Localize.translateLocal(error)};
return {[errorKey ?? DateUtils.getMicroseconds()]: translateLocal(error)};
}

/**
Expand Down Expand Up @@ -77,7 +79,7 @@ function getLatestErrorMessage<TOnyxData extends OnyxDataWithErrors>(onyxData: O
return '';
}

const key = Object.keys(errors).sort().reverse().at(0) ?? '';
const key = arrayLastElement(Object.keys(errors)) ?? '';
return getErrorMessageWithTranslationData(errors[key] ?? '');
}

Expand All @@ -88,7 +90,7 @@ function getLatestErrorMessageField<TOnyxData extends OnyxDataWithErrors>(onyxDa
return {};
}

const key = Object.keys(errors).sort().reverse().at(0) ?? '';
const key = arrayLastElement(Object.keys(errors)) ?? '';

return {key: errors[key]};
}
Expand All @@ -104,7 +106,7 @@ function getLatestErrorField<TOnyxData extends OnyxDataWithErrorFields>(onyxData
return {};
}

const key = Object.keys(errorsForField).sort().reverse().at(0) ?? '';
const key = arrayLastElement(Object.keys(errorsForField)) ?? '';
return {[key]: getErrorMessageWithTranslationData(errorsForField[key])};
}

Expand All @@ -115,7 +117,7 @@ function getEarliestErrorField<TOnyxData extends OnyxDataWithErrorFields>(onyxDa
return {};
}

const key = Object.keys(errorsForField).sort().at(0) ?? '';
const key = arrayFirstElement(Object.keys(errorsForField)) ?? '';
return {[key]: getErrorMessageWithTranslationData(errorsForField[key])};
}

Expand All @@ -139,7 +141,7 @@ function getLatestError(errors?: Errors): Errors {
return {};
}

const key = Object.keys(errors).sort().reverse().at(0) ?? '';
const key = arrayLastElement(Object.keys(errors)) ?? '';
return {[key]: getErrorMessageWithTranslationData(errors[key])};
}

Expand Down
115 changes: 75 additions & 40 deletions src/libs/ReportActionsUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {format} from 'date-fns';
import {fastMerge, Str} from 'expensify-common';
import clone from 'lodash/clone';
import lodashFindLast from 'lodash/findLast';
import isEmpty from 'lodash/isEmpty';
import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
Expand All @@ -20,6 +19,8 @@
import type {Message, OldDotReportAction, OriginalMessage, ReportActions} from '@src/types/onyx/ReportAction';
import type ReportActionName from '@src/types/onyx/ReportActionName';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import arrayFirstElement from '@src/utils/arrayFirstElement';
import arrayLastElement from '@src/utils/arrayLastElement';
import {convertAmountToDisplayString, convertToDisplayString, convertToShortDisplayString} from './CurrencyUtils';
import DateUtils from './DateUtils';
import {getEnvironmentURL} from './Environment/Environment';
Expand Down Expand Up @@ -57,7 +58,7 @@
type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMentionElement | MemberChangeMessageRoomReferenceElement;

let allReportActions: OnyxCollection<ReportActions>;
Onyx.connect({

Check warning on line 61 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand All @@ -69,7 +70,7 @@
});

let allReports: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 73 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -78,14 +79,14 @@
});

let isNetworkOffline = false;
Onyx.connect({

Check warning on line 82 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NETWORK,
callback: (val) => (isNetworkOffline = val?.isOffline ?? false),
});

let currentUserAccountID: number | undefined;
let currentEmail = '';
Onyx.connect({

Check warning on line 89 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
// When signed out, value is undefined
Expand All @@ -99,7 +100,7 @@
});

let privatePersonalDetails: PrivatePersonalDetails | undefined;
Onyx.connect({

Check warning on line 103 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS,
callback: (personalDetails) => {
privatePersonalDetails = personalDetails;
Expand Down Expand Up @@ -423,43 +424,49 @@
}

/**
* Sort an array of reportActions by their created timestamp first, and reportActionID second
* Gives the comparator for sorting an array of reportActions by their created timestamp first, and reportActionID second
* This gives us a stable order even in the case of multiple reportActions created on the same millisecond
*
*/
function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false): ReportAction[] {
if (!Array.isArray(reportActions)) {
throw new Error(`ReportActionsUtils.getSortedReportActions requires an array, received ${typeof reportActions}`);
function getSortedReportActionsComparator(first: ReportAction, second: ReportAction, shouldSortInDescendingOrder = false): number {
const invertedMultiplier = shouldSortInDescendingOrder ? -1 : 1;

// First sort by action type, ensuring that `CREATED` actions always come first if they have the same or even a later timestamp as another action type
if ((first.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED || second.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) && first.actionName !== second.actionName) {
return (first.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED ? -1 : 1) * invertedMultiplier;
}

const invertedMultiplier = shouldSortInDescendingOrder ? -1 : 1;
// Ensure that neither first's nor second's created property is undefined
if (first.created === undefined || second.created === undefined) {
return (first.created === undefined ? -1 : 1) * invertedMultiplier;
}

const sortedActions = reportActions?.filter(Boolean).sort((first, second) => {
// First sort by action type, ensuring that `CREATED` actions always come first if they have the same or even a later timestamp as another action type
if ((first.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED || second.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) && first.actionName !== second.actionName) {
return (first.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED ? -1 : 1) * invertedMultiplier;
}
// Then sort by timestamp
if (first.created !== second.created) {
return (first.created < second.created ? -1 : 1) * invertedMultiplier;
}

// Ensure that neither first's nor second's created property is undefined
if (first.created === undefined || second.created === undefined) {
return (first.created === undefined ? -1 : 1) * invertedMultiplier;
}
// Ensure that `REPORT_PREVIEW` actions always come after if they have the same timestamp as another action type
if ((first.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW || second.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) && first.actionName !== second.actionName) {
return (first.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW ? 1 : -1) * invertedMultiplier;
}

// Then sort by timestamp
if (first.created !== second.created) {
return (first.created < second.created ? -1 : 1) * invertedMultiplier;
}
// Then fallback on reportActionID as the final sorting criteria. It is a random number,
// but using this will ensure that the order of reportActions with the same created time and action type
// will be consistent across all users and devices
return (first.reportActionID < second.reportActionID ? -1 : 1) * invertedMultiplier;
}

// Ensure that `REPORT_PREVIEW` actions always come after if they have the same timestamp as another action type
if ((first.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW || second.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) && first.actionName !== second.actionName) {
return (first.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW ? 1 : -1) * invertedMultiplier;
}
/**
* Sort an array of reportActions by their created timestamp first, and reportActionID second
* This gives us a stable order even in the case of multiple reportActions created on the same millisecond
*
*/
function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false): ReportAction[] {
if (!Array.isArray(reportActions)) {
throw new Error(`ReportActionsUtils.getSortedReportActions requires an array, received ${typeof reportActions}`);
}

// Then fallback on reportActionID as the final sorting criteria. It is a random number,
// but using this will ensure that the order of reportActions with the same created time and action type
// will be consistent across all users and devices
return (first.reportActionID < second.reportActionID ? -1 : 1) * invertedMultiplier;
});
const sortedActions = reportActions?.filter(Boolean).sort((first, second) => getSortedReportActionsComparator(first, second, shouldSortInDescendingOrder));

return sortedActions;
}
Expand Down Expand Up @@ -541,8 +548,8 @@
return null;
}

const sortedReportActions = getSortedReportActions(iouRequestActions);
return sortedReportActions.at(-1)?.reportActionID ?? null;
const lastSortedReportAction = arrayLastElement(iouRequestActions, getSortedReportActionsComparator);
return lastSortedReportAction?.reportActionID ?? null;
}

/**
Expand Down Expand Up @@ -921,11 +928,8 @@
reportActions = Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {});
}
const visibleReportActions = reportActions.filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action, canUserPerformWriteAction));
const sortedReportActions = getSortedReportActions(visibleReportActions, true);
if (sortedReportActions.length === 0) {
return undefined;
}
return sortedReportActions.at(0);
const lastVisibleAction = arrayFirstElement(visibleReportActions, (el1, el2) => getSortedReportActionsComparator(el1, el2, true));
return lastVisibleAction;
}

function formatLastMessageText(lastMessageText: string | undefined) {
Expand Down Expand Up @@ -984,6 +988,37 @@
}

/**
* This method returns the first report action that are ready for display in the ReportActionsView.
*/
function getFirstSortedReportActionForDisplay(
reportActions: OnyxEntry<ReportActions> | ReportAction[],
canUserPerformWriteAction?: boolean,
shouldIncludeInvisibleActions = false,
): ReportAction | null {
if (!reportActions) {
return null;
}

let filteredReportActions: ReportAction[] = [];

if (shouldIncludeInvisibleActions) {
filteredReportActions = Object.values(reportActions).filter(Boolean);
} else {
filteredReportActions = Object.entries(reportActions)
.filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key, canUserPerformWriteAction))
.map(([, reportAction]) => reportAction);
}

const comparator = (first: ReportAction, second: ReportAction) => {
return getSortedReportActionsComparator(first, second, true);
};

const firstReportAction = arrayFirstElement(filteredReportActions, comparator);
return firstReportAction ? replaceBaseURLInPolicyChangeLogAction(firstReportAction) : null;
}

/**
* This method returns the last report action that are ready for display in the ReportActionsView.
* Helper for filtering out Report Actions that are either:
* - ReportPreview with shouldShow set to false and without a pending action
* - Money request with parent action deleted
Expand Down Expand Up @@ -1039,8 +1074,7 @@
}

const filteredReportActions = filterOutDeprecatedReportActions(reportActions);
const sortedReportActions = getSortedReportActions(filteredReportActions);
return lodashFindLast(sortedReportActions, (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED);
return arrayLastElement(filteredReportActions, getSortedReportActionsComparator, (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED);
}

/**
Expand Down Expand Up @@ -1070,8 +1104,7 @@
}

const reportActions = Object.values((reportActionUpdate.value as ReportActions) ?? {});
const sortedReportActions = getSortedReportActions(reportActions);
return sortedReportActions.at(-1) ?? null;
return arrayLastElement(reportActions, getSortedReportActionsComparator) ?? null;
}

/**
Expand Down Expand Up @@ -2974,7 +3007,9 @@
getReportActionMessageText,
getReportActionText,
getReportPreviewAction,
getSortedReportActionsComparator,
getSortedReportActions,
getFirstSortedReportActionForDisplay,
getSortedReportActionsForDisplay,
getTextFromHtml,
getTrackExpenseActionableWhisper,
Expand Down
Loading
Loading