Skip to content

Commit

Permalink
Merge branch 'main' into ts/centralized-api-types
Browse files Browse the repository at this point in the history
  • Loading branch information
blazejkustra committed Jan 26, 2024
2 parents c5aac10 + 3f702b7 commit c8f663a
Show file tree
Hide file tree
Showing 30 changed files with 361 additions and 225 deletions.
1 change: 1 addition & 0 deletions .github/workflows/reassurePerformanceTests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
npx reassure --baseline
git switch --force --detach -
git merge --no-commit --allow-unrelated-histories "$BASELINE_BRANCH" -X ours
git checkout --ours .
npm install --force
npx reassure --branch
Expand Down
2 changes: 1 addition & 1 deletion contributingGuides/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Additionally if you want to discuss an idea with the open source community witho
```
11. [Open a pull request](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork), and make sure to fill in the required fields.
12. An Expensify engineer and a member from the Contributor-Plus team will be assigned to your pull request automatically to review.
13. Daily updates on weekdays are highly recommended. If you know you won’t be able to provide updates for > 1 week, please comment on the PR or issue how long you plan to be out so that we may plan accordingly. We understand everyone needs a little vacation here and there. Any issue that doesn't receive an update for 1 full week may be considered abandoned and the original contract terminated.
13. Daily updates on weekdays are highly recommended. If you know you won’t be able to provide updates within 48 hours, please comment on the PR or issue stating how long you plan to be out so that we may plan accordingly. We understand everyone needs a little vacation here and there. Any issue that doesn't receive an update for 5 days (including weekend days) may be considered abandoned and the original contract terminated.

#### Submit your pull request for final review
14. When you are ready to submit your pull request for final review, make sure the following checks pass:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Expensify’s Budgets feature allows you to:

{% include faq-begin.md %}
## Can I import budgets as a CSV?
At this time, you cannot import budgets via CSV since we don’t import categories or tags from direct accounting integrations.
At this time, you cannot import budgets via CSV.

## When will I be notified as a budget is hit?
Notifications are sent twice:
Expand Down
7 changes: 0 additions & 7 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3057,13 +3057,6 @@ const CONST = {
*/
MAX_OPTIONS_SELECTOR_PAGE_LENGTH: 500,

/**
* Performance test setup - run the same test multiple times to get a more accurate result
*/
PERFORMANCE_TESTS: {
RUNS: 20,
},

/**
* Bank account names
*/
Expand Down
5 changes: 5 additions & 0 deletions src/components/SelectionList/BaseSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,11 @@ function BaseSelectionList<TItem extends User | RadioItem>(
return;
}

// scroll is unnecessary if multiple options cannot be selected
if (!canSelectMultiple) {
return;
}

// set the focus on the first item when the sections list is changed
if (sections.length > 0) {
updateAndScrollToFocusedIndex(0);
Expand Down
26 changes: 11 additions & 15 deletions src/hooks/useResponsiveLayout.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import type {ParamListBase, RouteProp} from '@react-navigation/native';
import {useRoute} from '@react-navigation/native';
import {navigationRef} from '@libs/Navigation/Navigation';
import NAVIGATORS from '@src/NAVIGATORS';
import useWindowDimensions from './useWindowDimensions';

type RouteParams = ParamListBase & {
params: {isInRHP?: boolean};
};
type ResponsiveLayoutResult = {
shouldUseNarrowLayout: boolean;
isSmallScreenWidth: boolean;
isInModal: boolean;
};
/**
* Hook to determine if we are on mobile devices or in the RHP
* Hook to determine if we are on mobile devices or in the Modal Navigator
*/
export default function useResponsiveLayout(): ResponsiveLayoutResult {
const {isSmallScreenWidth} = useWindowDimensions();
try {
// eslint-disable-next-line react-hooks/rules-of-hooks
const {params} = useRoute<RouteProp<RouteParams, 'params'>>();
return {shouldUseNarrowLayout: isSmallScreenWidth || (params?.isInRHP ?? false)};
} catch (error) {
return {
shouldUseNarrowLayout: isSmallScreenWidth,
};
}
const state = navigationRef?.getRootState();
const lastRoute = state?.routes?.at(-1);
const lastRouteName = lastRoute?.name;
const isInModal = lastRouteName === NAVIGATORS.LEFT_MODAL_NAVIGATOR || lastRouteName === NAVIGATORS.RIGHT_MODAL_NAVIGATOR;
const shouldUseNarrowLayout = isSmallScreenWidth || isInModal;
return {shouldUseNarrowLayout, isSmallScreenWidth, isInModal};
}
28 changes: 27 additions & 1 deletion src/libs/ComposerUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ function insertText(text: string, selection: Selection, textToInsert: string): s
return text.slice(0, selection.start) + textToInsert + text.slice(selection.end, text.length);
}

/**
* Insert a white space at given index of text
* @param text - text that needs whitespace to be appended to
*/
function insertWhiteSpaceAtIndex(text: string, index: number) {
return `${text.slice(0, index)} ${text.slice(index)}`;
}

/**
* Check whether we can skip trigger hotkeys on some specific devices.
*/
Expand All @@ -23,4 +31,22 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo
return (isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen()) || isKeyboardShown;
}

export {getNumberOfLines, updateNumberOfLines, insertText, canSkipTriggerHotkeys};
/**
* Finds the length of common suffix between two texts
*/
function findCommonSuffixLength(str1: string, str2: string, cursorPosition: number) {
let commonSuffixLength = 0;
const minLength = Math.min(str1.length - cursorPosition, str2.length);

for (let i = 1; i <= minLength; i++) {
if (str1.charAt(str1.length - i) === str2.charAt(str2.length - i)) {
commonSuffixLength++;
} else {
break;
}
}

return commonSuffixLength;
}

export {getNumberOfLines, updateNumberOfLines, insertText, canSkipTriggerHotkeys, insertWhiteSpaceAtIndex, findCommonSuffixLength};
2 changes: 1 addition & 1 deletion src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1563,7 +1563,7 @@ function getOptions(
if (includePersonalDetails) {
// Next loop over all personal details removing any that are selectedUsers or recentChats
allPersonalDetailsOptions.forEach((personalDetailOption) => {
if (optionsToExclude.some((optionToExclude) => optionToExclude.login === personalDetailOption.login)) {
if (optionsToExclude.some((optionToExclude) => optionToExclude.login === addSMSDomainIfPhoneNumber(personalDetailOption.login ?? ''))) {
return;
}
const {searchText, participantsList, isChatRoom} = personalDetailOption;
Expand Down
39 changes: 30 additions & 9 deletions src/libs/actions/IOU.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,29 @@ function getReceiptError(receipt, filename, isScanRequest = true) {
: ErrorUtils.getMicroSecondOnyxErrorObject({error: CONST.IOU.RECEIPT_ERROR, source: receipt.source, filename});
}

/**
* Return the object to update hasOutstandingChildRequest
* @param {Object} [policy]
* @param {Boolean} needsToBeManuallySubmitted
* @returns {Object}
*/
function getOutstandingChildRequest(policy, needsToBeManuallySubmitted) {
if (!needsToBeManuallySubmitted) {
return {
hasOutstandingChildRequest: false,
};
}

if (PolicyUtils.isPolicyAdmin(policy)) {
return {
hasOutstandingChildRequest: true,
};
}

// We don't need to update hasOutstandingChildRequest in this case
return {};
}

/**
* Builds the Onyx data for a money request.
*
Expand All @@ -329,7 +352,7 @@ function getReceiptError(receipt, filename, isScanRequest = true) {
* @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts)
* @param {Array} policyTags
* @param {Array} policyCategories
* @param {Boolean} hasOutstandingChildRequest
* @param {Boolean} needsToBeManuallySubmitted
* @returns {Array} - An array containing the optimistic data, success data, and failure data.
*/
function buildOnyxDataForMoneyRequest(
Expand All @@ -348,9 +371,10 @@ function buildOnyxDataForMoneyRequest(
policy,
policyTags,
policyCategories,
hasOutstandingChildRequest = false,
needsToBeManuallySubmitted = true,
) {
const isScanRequest = TransactionUtils.isScanRequest(transaction);
const outstandingChildRequest = getOutstandingChildRequest(needsToBeManuallySubmitted, policy);
const optimisticData = [
{
// Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page
Expand All @@ -361,7 +385,7 @@ function buildOnyxDataForMoneyRequest(
lastReadTime: DateUtils.getDBTime(),
lastMessageTranslationKey: '',
iouReportID: iouReport.reportID,
hasOutstandingChildRequest,
...outstandingChildRequest,
...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}),
},
},
Expand Down Expand Up @@ -506,6 +530,7 @@ function buildOnyxDataForMoneyRequest(
iouReportID: chatReport.iouReportID,
lastReadTime: chatReport.lastReadTime,
pendingFields: null,
hasOutstandingChildRequest: chatReport.hasOutstandingChildRequest,
...(isNewChatReport
? {
errorFields: {
Expand Down Expand Up @@ -687,7 +712,7 @@ function getMoneyRequestInformation(
let iouReport = isNewIOUReport ? null : allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`];

// Check if the Scheduled Submit is enabled in case of expense report
let needsToBeManuallySubmitted = false;
let needsToBeManuallySubmitted = true;
let isFromPaidPolicy = false;
if (isPolicyExpenseChat) {
isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy);
Expand Down Expand Up @@ -806,10 +831,6 @@ function getMoneyRequestInformation(
}
: undefined;

// The policy expense chat should have the GBR only when its a paid policy and the scheduled submit is turned off
// so the employee has to submit to their manager manually.
const hasOutstandingChildRequest = isPolicyExpenseChat && needsToBeManuallySubmitted;

// STEP 5: Build Onyx Data
const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest(
chatReport,
Expand All @@ -827,7 +848,7 @@ function getMoneyRequestInformation(
policy,
policyTags,
policyCategories,
hasOutstandingChildRequest,
needsToBeManuallySubmitted,
);

return {
Expand Down
6 changes: 3 additions & 3 deletions src/libs/calculateAnchorPosition.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-console */
import type {View} from 'react-native';
/* eslint-disable no-restricted-imports */
import type {Text as RNText, View} from 'react-native';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
Expand All @@ -13,7 +13,7 @@ type AnchorOrigin = {
/**
* Gets the x,y position of the passed in component for the purpose of anchoring another component to it.
*/
export default function calculateAnchorPosition(anchorComponent: View, anchorOrigin?: AnchorOrigin): Promise<AnchorPosition> {
export default function calculateAnchorPosition(anchorComponent: View | RNText, anchorOrigin?: AnchorOrigin): Promise<AnchorPosition> {
return new Promise((resolve) => {
if (!anchorComponent) {
return resolve({horizontal: 0, vertical: 0});
Expand Down
5 changes: 4 additions & 1 deletion src/pages/RoomInvitePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ function RoomInvitePage(props) {

// Any existing participants and Expensify emails should not be eligible for invitation
const excludedUsers = useMemo(
() => [...PersonalDetailsUtils.getLoginsByAccountIDs(lodashGet(props.report, 'visibleChatMemberAccountIDs', [])), ...CONST.EXPENSIFY_EMAILS],
() =>
_.map([...PersonalDetailsUtils.getLoginsByAccountIDs(lodashGet(props.report, 'visibleChatMemberAccountIDs', [])), ...CONST.EXPENSIFY_EMAILS], (participant) =>
OptionsListUtils.addSMSDomainIfPhoneNumber(participant),
),
[props.report],
);

Expand Down
58 changes: 48 additions & 10 deletions src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import lodashIsEqual from 'lodash/isEqual';
import type {MutableRefObject, RefObject} from 'react';
import React, {memo, useMemo, useRef, useState} from 'react';
import {InteractionManager, View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {GestureResponderEvent, Text as RNText, View as ViewType} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {ContextMenuItemHandle} from '@components/ContextMenuItem';
Expand All @@ -12,15 +14,16 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as ReportUtils from '@libs/ReportUtils';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Beta, ReportAction, ReportActions} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {ContextMenuActionPayload} from './ContextMenuActions';
import type {ContextMenuAction, ContextMenuActionPayload} from './ContextMenuActions';
import ContextMenuActions from './ContextMenuActions';
import type {ContextMenuType} from './ReportActionContextMenu';
import {hideContextMenu} from './ReportActionContextMenu';
import {hideContextMenu, showContextMenu} from './ReportActionContextMenu';

type BaseReportActionContextMenuOnyxProps = {
/** Beta features list */
Expand Down Expand Up @@ -78,7 +81,11 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & {
/** Content Ref */
contentRef?: RefObject<View>;

/** Function to check if context menu is active */
checkIfContextMenuActive?: () => void;

/** List of disabled actions */
disabledActions?: ContextMenuAction[];
};

type MenuItemRefs = Record<string, ContextMenuItemHandle | null>;
Expand All @@ -100,6 +107,7 @@ function BaseReportActionContextMenu({
betas,
reportActions,
checkIfContextMenuActive,
disabledActions = [],
}: BaseReportActionContextMenuProps) {
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
Expand All @@ -117,13 +125,22 @@ function BaseReportActionContextMenu({
}, [reportActions, reportActionID]);

const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen);
let filteredContextMenuActions = ContextMenuActions.filter((contextAction) =>
contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline, isMini),
let filteredContextMenuActions = ContextMenuActions.filter(
(contextAction) =>
!disabledActions.includes(contextAction) &&
contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline, isMini),
);
filteredContextMenuActions =
isMini && filteredContextMenuActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS
? ([...filteredContextMenuActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1), filteredContextMenuActions.at(-1)] as typeof filteredContextMenuActions)
: filteredContextMenuActions;

if (isMini) {
const menuAction = filteredContextMenuActions.at(-1);
const otherActions = filteredContextMenuActions.slice(0, -1);
if (otherActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS && menuAction) {
filteredContextMenuActions = otherActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1);
filteredContextMenuActions.push(menuAction);
} else {
filteredContextMenuActions = otherActions;
}
}

// Context menu actions that are not rendered as menu items are excluded from arrow navigation
const nonMenuItemActionIndexes = filteredContextMenuActions.map((contextAction, index) =>
Expand Down Expand Up @@ -172,6 +189,28 @@ function BaseReportActionContextMenu({
{isActive: shouldEnableArrowNavigation},
);

const openOverflowMenu = (event: GestureResponderEvent | MouseEvent) => {
const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction);
const originalReport = ReportUtils.getReport(originalReportID);
showContextMenu(
CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
event,
selection,
anchor?.current as ViewType | RNText | null,
reportID,
reportAction?.reportActionID,
originalReportID,
draftMessage,
checkIfContextMenuActive,
checkIfContextMenuActive,
ReportUtils.isArchivedRoom(originalReport),
ReportUtils.chatIncludesChronos(originalReport),
undefined,
undefined,
filteredContextMenuActions,
);
};

return (
(isVisible || shouldKeepOpen) && (
<View
Expand All @@ -188,8 +227,7 @@ function BaseReportActionContextMenu({
close: () => setShouldKeepOpen(false),
openContextMenu: () => setShouldKeepOpen(true),
interceptAnonymousUser,
anchor,
checkIfContextMenuActive,
openOverflowMenu,
};

if ('renderContent' in contextAction) {
Expand Down
Loading

0 comments on commit c8f663a

Please sign in to comment.