Skip to content

Commit

Permalink
Merge branch 'Expensify:main' into Github-Actions-Fix-Engineer-is-not…
Browse files Browse the repository at this point in the history
…-tagged-and-assigned-in-Deploy-Checklist-to-complete-the-QA-for-PRs-with-Internal-QA
  • Loading branch information
rayane-djouah authored Mar 7, 2024
2 parents 010fb3e + 9d56f35 commit 1554f25
Show file tree
Hide file tree
Showing 93 changed files with 1,814 additions and 875 deletions.
1 change: 1 addition & 0 deletions docs/redirects.csv
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/
https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription,https://use.expensify.com/
https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription,https://use.expensify.com/
https://help.expensify.com/articles/expensify-classic/settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts
https://help.expensify.com/articles/expensify-classic/settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
5 changes: 5 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ const CONST = {
LIMIT: 50,
TYPE: {
ADDCOMMENT: 'ADDCOMMENT',
ACTIONABLEJOINREQUEST: 'ACTIONABLEJOINREQUEST',
APPROVED: 'APPROVED',
CHRONOSOOOLIST: 'CHRONOSOOOLIST',
CLOSED: 'CLOSED',
Expand Down Expand Up @@ -678,6 +679,10 @@ const CONST = {
INVITE: 'invited',
NOTHING: 'nothing',
},
ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION: {
ACCEPT: 'accept',
DECLINE: 'decline',
},
ARCHIVE_REASON: {
DEFAULT: 'default',
ACCOUNT_CLOSED: 'accountClosed',
Expand Down
6 changes: 4 additions & 2 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {OnyxEntry} from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type CONST from './CONST';
import type * as FormTypes from './types/form';
Expand Down Expand Up @@ -287,6 +287,7 @@ const ONYXKEYS = {
POLICY_MEMBERS: 'policyMembers_',
POLICY_DRAFTS: 'policyDrafts_',
POLICY_MEMBERS_DRAFTS: 'policyMembersDrafts_',
POLICY_JOIN_MEMBER: 'policyJoinMember_',
POLICY_CATEGORIES: 'policyCategories_',
POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_',
POLICY_TAGS: 'policyTags_',
Expand Down Expand Up @@ -487,6 +488,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.SELECTED_TAB]: string;
[ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string;
[ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep;
[ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember;
};

type OnyxValuesMapping = {
Expand Down Expand Up @@ -589,7 +591,7 @@ type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping;
type OnyxValueKey = keyof OnyxValuesMapping;

type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey;
type OnyxValue<TOnyxKey extends OnyxKey> = OnyxEntry<OnyxValues[TOnyxKey]>;
type OnyxValue<TOnyxKey extends OnyxKey> = TOnyxKey extends keyof OnyxCollectionValuesMapping ? OnyxCollection<OnyxValues[TOnyxKey]> : OnyxEntry<OnyxValues[TOnyxKey]>;

type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude<AllOnyxKeys, OnyxKey>}`;
/** If this type errors, it means that the `OnyxKey` type is missing some keys. */
Expand Down
13 changes: 13 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,10 @@ const ROUTES = {
route: 'workspace/:policyID/avatar',
getRoute: (policyID: string) => `workspace/${policyID}/avatar` as const,
},
WORKSPACE_JOIN_USER: {
route: 'workspace/:policyID/join',
getRoute: (policyID: string, inviterEmail: string) => `workspace/${policyID}/join?email=${inviterEmail}` as const,
},
WORKSPACE_SETTINGS_CURRENCY: {
route: 'workspace/:policyID/settings/currency',
getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const,
Expand Down Expand Up @@ -554,6 +558,15 @@ const ROUTES = {
route: 'workspace/:policyID/tags',
getRoute: (policyID: string) => `workspace/${policyID}/tags` as const,
},
WORKSPACE_MEMBER_DETAILS: {
route: 'workspace/:policyID/members/:accountID',
getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}`, backTo),
},
WORKSPACE_MEMBER_ROLE_SELECTION: {
route: 'workspace/:policyID/members/:accountID/role-selection',
getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}/role-selection`, backTo),
},

// Referral program promotion
REFERRAL_DETAILS_MODAL: {
route: 'referral/:contentType',
Expand Down
3 changes: 3 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const SCREENS = {
SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop',
DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect',
SAML_SIGN_IN: 'SAMLSignIn',
WORKSPACE_JOIN_USER: 'WorkspaceJoinUser',

MONEY_REQUEST: {
MANUAL_TAB: 'manual',
Expand Down Expand Up @@ -226,6 +227,8 @@ const SCREENS = {
CATEGORY_CREATE: 'Category_Create',
CATEGORY_SETTINGS: 'Category_Settings',
CATEGORIES_SETTINGS: 'Categories_Settings',
MEMBER_DETAILS: 'Workspace_Member_Details',
MEMBER_DETAILS_ROLE_SELECTION: 'Workspace_Member_Details_Role_Selection',
},

EDIT_REQUEST: {
Expand Down
34 changes: 33 additions & 1 deletion src/components/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import React, {useCallback} from 'react';
import type {GestureResponderEvent, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type IconAsset from '@src/types/utils/IconAsset';
import Icon from './Icon';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import Text from './Text';

Expand Down Expand Up @@ -31,11 +35,29 @@ type BadgeProps = {

/** Callback to be called on onPress */
onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void;

/** The icon asset to display to the left of the text */
icon?: IconAsset | null;

/** Any additional styles to pass to the left icon container. */
iconStyles?: StyleProp<ViewStyle>;
};

function Badge({success = false, error = false, pressable = false, text, environment = CONST.ENVIRONMENT.DEV, badgeStyles, textStyles, onPress = () => {}}: BadgeProps) {
function Badge({
success = false,
error = false,
pressable = false,
text,
environment = CONST.ENVIRONMENT.DEV,
badgeStyles,
textStyles,
onPress = () => {},
icon,
iconStyles = [],
}: BadgeProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const theme = useTheme();
const textColorStyles = success || error ? styles.textWhite : undefined;
const Wrapper = pressable ? PressableWithoutFeedback : View;

Expand All @@ -53,6 +75,16 @@ function Badge({success = false, error = false, pressable = false, text, environ
aria-label={!pressable ? text : undefined}
accessible={false}
>
{icon && (
<View style={[styles.mr2, iconStyles]}>
<Icon
width={variables.iconSizeExtraSmall}
height={variables.iconSizeExtraSmall}
src={icon}
fill={theme.icon}
/>
</View>
)}
<Text
style={[styles.badgeText, textColorStyles, textStyles]}
numberOfLines={1}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ function Button(
accessibilityLabel = '',
...rest
}: ButtonProps,
ref: ForwardedRef<View>,
ref: ForwardedRef<View | HTMLDivElement>,
) {
const theme = useTheme();
const styles = useThemeStyles();
Expand Down
11 changes: 6 additions & 5 deletions src/components/FloatingActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useEffect, useRef} from 'react';
import type {GestureResponderEvent, Role} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {GestureResponderEvent, Role, Text} from 'react-native';
import {Platform, View} from 'react-native';
import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import Svg, {Path} from 'react-native-svg';
Expand Down Expand Up @@ -58,12 +59,12 @@ type FloatingActionButtonProps = {
role: Role;
};

function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef<HTMLDivElement | View>) {
function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef<HTMLDivElement | View | Text>) {
const {success, buttonDefaultBG, textLight, textDark} = useTheme();
const styles = useThemeStyles();
const borderRadius = styles.floatingActionButton.borderRadius;
const {translate} = useLocalize();
const fabPressable = useRef<HTMLDivElement | View | null>(null);
const fabPressable = useRef<HTMLDivElement | View | Text | null>(null);
const sharedValue = useSharedValue(isActive ? 1 : 0);
const buttonRef = ref;

Expand Down Expand Up @@ -112,9 +113,9 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
<View style={styles.bottomTabBarItem}>
<AnimatedPressable
ref={(el) => {
fabPressable.current = el;
fabPressable.current = el ?? null;
if (buttonRef && 'current' in buttonRef) {
buttonRef.current = el;
buttonRef.current = el ?? null;
}
}}
accessibilityLabel={accessibilityLabel}
Expand Down
12 changes: 6 additions & 6 deletions src/components/KYCWall/BaseKYCWall.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Dimensions} from 'react-native';
import type {EmitterSubscription, GestureResponderEvent} from 'react-native';
import type {EmitterSubscription, GestureResponderEvent, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu';
Expand Down Expand Up @@ -69,8 +69,8 @@ function KYCWall({
walletTerms,
shouldShowPersonalBankAccountOption = false,
}: BaseKYCWallProps) {
const anchorRef = useRef<HTMLDivElement>(null);
const transferBalanceButtonRef = useRef<HTMLElement | null>(null);
const anchorRef = useRef<HTMLDivElement | View | null>(null);
const transferBalanceButtonRef = useRef<HTMLDivElement | View | null>(null);

const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false);

Expand Down Expand Up @@ -111,7 +111,7 @@ function KYCWall({
return;
}

const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current);
const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current as HTMLDivElement);
const position = getAnchorPosition(buttonPosition);

setPositionAddPaymentMenu(position);
Expand Down Expand Up @@ -162,7 +162,7 @@ function KYCWall({
}

// Use event target as fallback if anchorRef is null for safety
const targetElement = anchorRef.current ?? (event?.currentTarget as HTMLElement);
const targetElement = anchorRef.current ?? (event?.currentTarget as HTMLDivElement);

transferBalanceButtonRef.current = targetElement;

Expand All @@ -181,7 +181,7 @@ function KYCWall({
return;
}

const clickedElementLocation = getClickedTargetLocation(targetElement);
const clickedElementLocation = getClickedTargetLocation(targetElement as HTMLDivElement);
const position = getAnchorPosition(clickedElementLocation);

setPositionAddPaymentMenu(position);
Expand Down
16 changes: 5 additions & 11 deletions src/components/MoneyRequestHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as HeaderUtils from '@libs/HeaderUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
Expand Down Expand Up @@ -79,16 +78,11 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction);
const isPending = TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction);

const isRequestModifiable = !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction);
const canModifyRequest = isActionOwner && !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction);
let canDeleteRequest = canModifyRequest;
const isDeletedParentAction = ReportActionsUtils.isDeletedAction(parentReportAction);
const canHoldOrUnholdRequest = !isSettled && !isApproved && !isDeletedParentAction;

if (ReportUtils.isPaidGroupPolicyExpenseReport(moneyRequestReport)) {
// If it's a paid policy expense report, only allow deleting the request if it's in draft state or instantly submitted state or the user is the policy admin
canDeleteRequest =
canDeleteRequest &&
(ReportUtils.isDraftExpenseReport(moneyRequestReport) || ReportUtils.isExpenseReportWithInstantSubmittedState(moneyRequestReport) || PolicyUtils.isPolicyAdmin(policy));
}
// If the report supports adding transactions to it, then it also supports deleting transactions from it.
const canDeleteRequest = isActionOwner && ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) && !isDeletedParentAction;

const changeMoneyRequestStatus = () => {
if (isOnHold) {
Expand All @@ -108,7 +102,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
}, [canDeleteRequest]);

const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(report)];
if (isRequestModifiable) {
if (canHoldOrUnholdRequest) {
const isRequestIOU = parentReport?.type === 'iou';
const isHoldCreator = ReportUtils.isHoldCreator(transaction, report?.reportID) && isRequestIOU;
const canModifyStatus = isPolicyAdmin || isActionOwner || isApprover;
Expand Down
26 changes: 26 additions & 0 deletions src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {useIsFocused} from '@react-navigation/native';
import {format} from 'date-fns';
import Str from 'expensify-common/lib/str';
import {isUndefined} from 'lodash';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
Expand Down Expand Up @@ -491,6 +492,31 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
IOU.setMoneyRequestMerchant(transaction.transactionID, distanceMerchant, true);
}, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction]);

// Auto select the category if there is only one enabled category and it is required
useEffect(() => {
const enabledCategories = _.filter(policyCategories, (category) => category.enabled);
if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) {
return;
}
IOU.setMoneyRequestCategory(transaction.transactionID, enabledCategories[0].name);
}, [iouCategory, shouldShowCategories, policyCategories, transaction, isCategoryRequired]);

// Auto select the tag if there is only one enabled tag and it is required
useEffect(() => {
let updatedTagsString = TransactionUtils.getTag(transaction);
policyTagLists.forEach((tagList, index) => {
const enabledTags = _.filter(tagList.tags, (tag) => tag.enabled);
const isTagListRequired = isUndefined(tagList.required) ? false : tagList.required && canUseViolations;
if (!isTagListRequired || enabledTags.length !== 1 || TransactionUtils.getTag(transaction, index)) {
return;
}
updatedTagsString = IOUUtils.insertTagIntoTransactionTagsString(updatedTagsString, enabledTags[0] ? enabledTags[0].name : '', index);
});
if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) {
IOU.setMoneyRequestTag(transaction.transactionID, updatedTagsString);
}
}, [policyTagLists, transaction, policyTags, isTagRequired, canUseViolations]);

/**
* @param {Object} option
*/
Expand Down
6 changes: 4 additions & 2 deletions src/components/OnyxProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import createOnyxContext from './createOnyxContext';
const [withNetwork, NetworkProvider, NetworkContext] = createOnyxContext(ONYXKEYS.NETWORK);
const [withPersonalDetails, PersonalDetailsProvider, , usePersonalDetails] = createOnyxContext(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [withCurrentDate, CurrentDateProvider] = createOnyxContext(ONYXKEYS.CURRENT_DATE);
const [withReportActionsDrafts, ReportActionsDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
const [withBlockedFromConcierge, BlockedFromConciergeProvider] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
const [withReportActionsDrafts, ReportActionsDraftsProvider, , useReportActionsDrafts] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
const [withBlockedFromConcierge, BlockedFromConciergeProvider, , useBlockedFromConcierge] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
const [withBetas, BetasProvider, BetasContext, useBetas] = createOnyxContext(ONYXKEYS.BETAS);
const [withReportCommentDrafts, ReportCommentDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT);
const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = createOnyxContext(ONYXKEYS.PREFERRED_THEME);
Expand Down Expand Up @@ -66,5 +66,7 @@ export {
useFrequentlyUsedEmojis,
withPreferredEmojiSkinTone,
PreferredEmojiSkinToneContext,
useBlockedFromConcierge,
useReportActionsDrafts,
useSession,
};
5 changes: 3 additions & 2 deletions src/components/Popover/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {RefObject} from 'react';
import type {View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {Text, View} from 'react-native';
import type {PopoverAnchorPosition} from '@components/Modal/types';
import type BaseModalProps from '@components/Modal/types';
import type {WindowDimensionsProps} from '@components/withWindowDimensions/types';
Expand All @@ -20,7 +21,7 @@ type PopoverProps = BaseModalProps &
anchorAlignment?: AnchorAlignment;

/** The anchor ref of the popover */
anchorRef: RefObject<View | HTMLDivElement>;
anchorRef: RefObject<View | HTMLDivElement | Text>;

/** Whether disable the animations */
disableAnimation?: boolean;
Expand Down
7 changes: 4 additions & 3 deletions src/components/PopoverProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {RefObject} from 'react';
import React, {createContext, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {Text, View} from 'react-native';
import type {AnchorRef, PopoverContextProps, PopoverContextValue} from './types';

const PopoverContext = createContext<PopoverContextValue>({
Expand All @@ -10,7 +11,7 @@ const PopoverContext = createContext<PopoverContextValue>({
isOpen: false,
});

function elementContains(ref: RefObject<View | HTMLElement> | undefined, target: EventTarget | null) {
function elementContains(ref: RefObject<View | HTMLElement | Text> | undefined, target: EventTarget | null) {
if (ref?.current && 'contains' in ref.current && ref?.current?.contains(target as Node)) {
return true;
}
Expand All @@ -21,7 +22,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
const [isOpen, setIsOpen] = useState(false);
const activePopoverRef = useRef<AnchorRef | null>(null);

const closePopover = useCallback((anchorRef?: RefObject<View | HTMLElement>): boolean => {
const closePopover = useCallback((anchorRef?: RefObject<View | HTMLElement | Text>): boolean => {
if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) {
return false;
}
Expand Down
Loading

0 comments on commit 1554f25

Please sign in to comment.