Skip to content

Commit dad99dc

Browse files
authored
Merge pull request #47353 from rushatgabhane/show-copilot
2 parents 488d5ff + aa5dabe commit dad99dc

31 files changed

Lines changed: 1176 additions & 67 deletions

assets/images/user-plus.svg

Lines changed: 11 additions & 0 deletions
Loading

src/CONST.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,7 @@ const CONST = {
715715
"https://help.expensify.com/articles/expensify-classic/connections/sage-intacct/Sage-Intacct-Troubleshooting#:~:text=First%20make%20sure%20that%20you,your%20company's%20Web%20Services%20authorizations.",
716716
PRICING: `https://www.expensify.com/pricing`,
717717
CUSTOM_REPORT_NAME_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates',
718+
COPILOT_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Assign-or-remove-a-Copilot',
718719
// Use Environment.getEnvironmentURL to get the complete URL with port number
719720
DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:',
720721
OLDDOT_URLS: {
@@ -4084,8 +4085,8 @@ const CONST = {
40844085
GETCODE: 'GETCODE',
40854086
},
40864087
DELEGATE_ROLE: {
4087-
SUBMITTER: 'submitter',
40884088
ALL: 'all',
4089+
SUBMITTER: 'submitter',
40894090
},
40904091
DELEGATE_ROLE_HELPDOT_ARTICLE_LINK: 'https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/',
40914092
STRIPE_GBP_AUTH_STATUSES: {

src/ROUTES.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,19 @@ const ROUTES = {
134134
SETTINGS_WORKSPACES: 'settings/workspaces',
135135
SETTINGS_SECURITY: 'settings/security',
136136
SETTINGS_CLOSE: 'settings/security/closeAccount',
137+
SETTINGS_ADD_DELEGATE: 'settings/security/delegate',
138+
SETTINGS_DELEGATE_ROLE: {
139+
route: 'settings/security/delegate/:login/role/:role',
140+
getRoute: (login: string, role?: string) => `settings/security/delegate/${encodeURIComponent(login)}/role/${role}` as const,
141+
},
142+
SETTINGS_DELEGATE_CONFIRM: {
143+
route: 'settings/security/delegate/:login/role/:role/confirm',
144+
getRoute: (login: string, role: string) => `settings/security/delegate/${encodeURIComponent(login)}/role/${role}/confirm` as const,
145+
},
146+
SETTINGS_DELEGATE_MAGIC_CODE: {
147+
route: 'settings/security/delegate/:login/role/:role/magic-code',
148+
getRoute: (login: string, role: string) => `settings/security/delegate/${encodeURIComponent(login)}/role/${role}/magic-code` as const,
149+
},
137150
SETTINGS_ABOUT: 'settings/about',
138151
SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links',
139152
SETTINGS_WALLET: 'settings/wallet',

src/SCREENS.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ const SCREENS = {
130130
CHANGE_PAYMENT_CURRENCY: 'Settings_Subscription_Change_Payment_Currency',
131131
REQUEST_EARLY_CANCELLATION: 'Settings_Subscription_RequestEarlyCancellation',
132132
},
133+
DELEGATE: {
134+
ADD_DELEGATE: 'Settings_Delegate_Add',
135+
DELEGATE_ROLE: 'Settings_Delegate_Role',
136+
DELEGATE_CONFIRM: 'Settings_Delegate_Confirm',
137+
DELEGATE_MAGIC_CODE: 'Settings_Delegate_Magic_Code',
138+
},
133139
},
134140
SAVE_THE_WORLD: {
135141
ROOT: 'SaveTheWorld_Root',

src/components/AccountSwitcher.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
1010
import useTheme from '@hooks/useTheme';
1111
import useThemeStyles from '@hooks/useThemeStyles';
1212
import {clearDelegatorErrors, connect, disconnect} from '@libs/actions/Delegate';
13+
import * as ErrorUtils from '@libs/ErrorUtils';
1314
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
1415
import variables from '@styles/variables';
1516
import * as Modal from '@userActions/Modal';
1617
import CONST from '@src/CONST';
17-
import type {TranslationPaths} from '@src/languages/types';
1818
import ONYXKEYS from '@src/ONYXKEYS';
1919
import type {PersonalDetails} from '@src/types/onyx';
20+
import type {Errors} from '@src/types/onyx/OnyxCommon';
2021
import Avatar from './Avatar';
2122
import ConfirmModal from './ConfirmModal';
2223
import Icon from './Icon';
@@ -46,7 +47,8 @@ function AccountSwitcher() {
4647
const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false;
4748
const canSwitchAccounts = canUseNewDotCopilot && (delegators.length > 0 || isActingAsDelegate);
4849

49-
const createBaseMenuItem = (personalDetails: PersonalDetails | undefined, error?: TranslationPaths, additionalProps: MenuItemWithLink = {}): MenuItemWithLink => {
50+
const createBaseMenuItem = (personalDetails: PersonalDetails | undefined, errors?: Errors, additionalProps: MenuItemWithLink = {}): MenuItemWithLink => {
51+
const error = Object.values(errors ?? {})[0] ?? '';
5052
return {
5153
title: personalDetails?.displayName ?? personalDetails?.login,
5254
description: Str.removeSMSDomain(personalDetails?.login ?? ''),
@@ -55,7 +57,7 @@ function AccountSwitcher() {
5557
iconType: CONST.ICON_TYPE_AVATAR,
5658
outerWrapperStyle: shouldUseNarrowLayout ? {} : styles.accountSwitcherPopover,
5759
numberOfLinesDescription: 1,
58-
errorText: error ? translate(error) : '',
60+
errorText: error ?? '',
5961
shouldShowRedDotIndicator: !!error,
6062
errorTextStyle: styles.mt2,
6163
...additionalProps,
@@ -81,7 +83,7 @@ function AccountSwitcher() {
8183
}
8284

8385
const delegatePersonalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
84-
const error = account?.delegatedAccess?.error;
86+
const error = ErrorUtils.getLatestErrorField(account?.delegatedAccess, 'connect');
8587

8688
return [
8789
createBaseMenuItem(delegatePersonalDetails, error, {
@@ -100,7 +102,8 @@ function AccountSwitcher() {
100102

101103
const delegatorMenuItems: MenuItemProps[] = delegators
102104
.filter(({email}) => email !== currentUserPersonalDetails.login)
103-
.map(({email, role, error}, index) => {
105+
.map(({email, role, errorFields}, index) => {
106+
const error = ErrorUtils.getLatestErrorField({errorFields}, 'connect');
104107
const personalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(email);
105108
return createBaseMenuItem(personalDetails, error, {
106109
badgeText: translate('delegate.role', role),
@@ -177,7 +180,7 @@ function AccountSwitcher() {
177180
anchorPosition={styles.accountSwitcherAnchorPosition}
178181
>
179182
<View style={styles.pb4}>
180-
<Text style={[styles.createMenuHeaderText, styles.ph5, styles.pb2, styles.pt4]}>{translate('delegate.switchAccount')}</Text>
183+
<Text style={[styles.createMenuHeaderText, styles.ph5, styles.pb3, !shouldUseNarrowLayout && styles.pt4]}>{translate('delegate.switchAccount')}</Text>
181184
<MenuItemList
182185
menuItems={menuItems()}
183186
shouldUseSingleExecution

src/components/Icon/Expensicons.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ import Unlock from '@assets/images/unlock.svg';
185185
import UploadAlt from '@assets/images/upload-alt.svg';
186186
import Upload from '@assets/images/upload.svg';
187187
import UserCheck from '@assets/images/user-check.svg';
188+
import UserPlus from '@assets/images/user-plus.svg';
188189
import User from '@assets/images/user.svg';
189190
import Users from '@assets/images/users.svg';
190191
import VolumeHigh from '@assets/images/volume-high.svg';
@@ -391,6 +392,7 @@ export {
391392
CalendarSolid,
392393
Filter,
393394
CaretUpDown,
395+
UserPlus,
394396
Feed,
395397
Table,
396398
SpreadsheetComputer,

src/components/MenuItemList.tsx

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import type {GestureResponderEvent, View} from 'react-native';
33
import useSingleExecution from '@hooks/useSingleExecution';
44
import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
55
import CONST from '@src/CONST';
6+
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
67
import type {MenuItemProps} from './MenuItem';
78
import MenuItem from './MenuItem';
9+
import OfflineWithFeedback from './OfflineWithFeedback';
810

911
type MenuItemLink = string | (() => Promise<string>);
1012

@@ -14,6 +16,18 @@ type MenuItemWithLink = MenuItemProps & {
1416

1517
/** A unique key for the menu item */
1618
key?: string;
19+
20+
/** The pending action for the menu item */
21+
pendingAction?: OnyxCommon.PendingAction | null;
22+
23+
/** A function to dismiss the pending action */
24+
onPendingActionDismiss?: () => void;
25+
26+
/** The error for the menu item */
27+
error?: OnyxCommon.Errors | null;
28+
29+
/** Whether we should force opacity */
30+
shouldForceOpacity?: boolean;
1731
};
1832

1933
type MenuItemListProps = {
@@ -45,16 +59,23 @@ function MenuItemList({menuItems = [], shouldUseSingleExecution = false}: MenuIt
4559
return (
4660
<>
4761
{menuItems.map((menuItemProps) => (
48-
<MenuItem
49-
key={menuItemProps.key ?? menuItemProps.title}
50-
onSecondaryInteraction={menuItemProps.link !== undefined ? (e) => secondaryInteraction(menuItemProps.link, e) : undefined}
51-
ref={popoverAnchor}
52-
shouldBlockSelection={!!menuItemProps.link}
53-
// eslint-disable-next-line react/jsx-props-no-spreading
54-
{...menuItemProps}
55-
disabled={!!menuItemProps.disabled || isExecuting}
56-
onPress={shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress}
57-
/>
62+
<OfflineWithFeedback
63+
pendingAction={menuItemProps.pendingAction}
64+
onClose={menuItemProps.onPendingActionDismiss}
65+
errors={menuItemProps.error}
66+
shouldForceOpacity={menuItemProps.shouldForceOpacity}
67+
>
68+
<MenuItem
69+
key={menuItemProps.key ?? menuItemProps.title}
70+
onSecondaryInteraction={menuItemProps.link !== undefined ? (e) => secondaryInteraction(menuItemProps.link, e) : undefined}
71+
ref={popoverAnchor}
72+
shouldBlockSelection={!!menuItemProps.link}
73+
// eslint-disable-next-line react/jsx-props-no-spreading
74+
{...menuItemProps}
75+
disabled={!!menuItemProps.disabled || isExecuting}
76+
onPress={shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress}
77+
/>
78+
</OfflineWithFeedback>
5879
))}
5980
</>
6081
);

src/components/OfflineWithFeedback.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ type OfflineWithFeedbackProps = ChildrenProps & {
6060

6161
/** Whether we should render the error message above the children */
6262
shouldDisplayErrorAbove?: boolean;
63+
64+
/** Whether we should force opacity */
65+
shouldForceOpacity?: boolean;
6366
};
6467

6568
type StrikethroughProps = Partial<ChildrenProps> & {style: AllStyles[]};
@@ -78,6 +81,7 @@ function OfflineWithFeedback({
7881
shouldShowErrorMessages = true,
7982
style,
8083
shouldDisplayErrorAbove = false,
84+
shouldForceOpacity = false,
8185
...rest
8286
}: OfflineWithFeedbackProps) {
8387
const styles = useThemeStyles();
@@ -89,7 +93,7 @@ function OfflineWithFeedback({
8993
const isOfflinePendingAction = !!isOffline && !!pendingAction;
9094
const isUpdateOrDeleteError = hasErrors && (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE);
9195
const isAddError = hasErrors && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD;
92-
const needsOpacity = !shouldDisableOpacity && ((isOfflinePendingAction && !isUpdateOrDeleteError) || isAddError);
96+
const needsOpacity = (!shouldDisableOpacity && ((isOfflinePendingAction && !isUpdateOrDeleteError) || isAddError)) || shouldForceOpacity;
9397
const needsStrikeThrough = !shouldDisableStrikeThrough && isOffline && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
9498
const hideChildren = shouldHideOnDelete && !isOffline && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && !hasErrors;
9599
let children = rest.children;

src/components/SelectionList/BaseSelectionList.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ function BaseSelectionList<TItem extends ListItem>(
8383
listHeaderWrapperStyle,
8484
isRowMultilineSupported = false,
8585
isAlternateTextMultilineSupported = false,
86+
alternateTextNumberOfLines = 2,
8687
textInputRef,
8788
headerMessageStyle,
8889
shouldHideListOnInitialRender = true,
@@ -467,6 +468,7 @@ function BaseSelectionList<TItem extends ListItem>(
467468
keyForList={item.keyForList ?? ''}
468469
isMultilineSupported={isRowMultilineSupported}
469470
isAlternateTextMultilineSupported={isAlternateTextMultilineSupported}
471+
alternateTextNumberOfLines={alternateTextNumberOfLines}
470472
onFocus={() => {
471473
if (isDisabled) {
472474
return;

src/components/SelectionList/RadioListItem.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ function RadioListItem<TItem extends ListItem>({
1818
rightHandSideComponent,
1919
isMultilineSupported = false,
2020
isAlternateTextMultilineSupported = false,
21+
alternateTextNumberOfLines = 2,
2122
onFocus,
2223
shouldSyncFocus,
2324
}: RadioListItemProps<TItem>) {
@@ -71,7 +72,7 @@ function RadioListItem<TItem extends ListItem>({
7172
isAlternateTextMultilineSupported ? styles.preWrap : styles.pre,
7273
isAlternateTextMultilineSupported ? {maxWidth: alternateTextMaxWidth} : null,
7374
]}
74-
numberOfLines={isAlternateTextMultilineSupported ? 2 : 1}
75+
numberOfLines={isAlternateTextMultilineSupported ? alternateTextNumberOfLines : 1}
7576
/>
7677
)}
7778
</View>

0 commit comments

Comments
 (0)