Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4d662af
refactor: make `onAssignCard` callbcak `cardID` required
chrispader Dec 18, 2025
819154a
fix: update empty table component
chrispader Dec 18, 2025
b9c3a06
fix: remove unused props
chrispader Dec 19, 2025
03170b4
fix: onAssignCard param type
chrispader Dec 19, 2025
e7f4198
fix: remove header subtitle
chrispader Dec 19, 2025
c27127f
fix: URI encode feed name
chrispader Dec 19, 2025
e1fb42b
update types
chrispader Dec 19, 2025
6339899
refactor: remove unused variable
chrispader Dec 19, 2025
796cf51
fix: simplify navigating to card assign flow
chrispader Dec 19, 2025
b05a6fc
feat: extract `CardFeedIcon` component
chrispader Dec 19, 2025
26fbc44
fix: lift cardFeedIcon up in React view hierarchy
chrispader Dec 19, 2025
0d338e7
fix: apply `finallyData` if "simulate failing network request flag is…
chrispader Dec 19, 2025
13b0fb7
refactor: move change of card assign state to `finallyData`
chrispader Dec 19, 2025
1c25f3d
Merge branch 'byoc-bulk-card-assign-r1' into byoc-bulk-card-assign-ui…
chrispader Dec 19, 2025
5f10adc
fix: add back `domainOrWorkspaceAccountID`
chrispader Dec 19, 2025
4de3217
Merge branch 'byoc-bulk-card-assign-r1' into byoc-bulk-card-assign-ui…
chrispader Dec 19, 2025
e5379f7
re-arrange assign card flow
chrispader Dec 19, 2025
91b21bf
fix: commercial cards not shown
chrispader Dec 19, 2025
ba84eae
fix: spell check
chrispader Dec 19, 2025
4eb520e
fix: ESLint and TS errors
chrispader Dec 19, 2025
1d15c84
Merge branch 'byoc-bulk-card-assign-r1' into byoc-bulk-card-assign-ui…
chrispader Dec 19, 2025
1616e5c
fix: invalid type
chrispader Dec 19, 2025
6f9b894
fix: do not inline cardFeedIcon element
chrispader Dec 19, 2025
61f9fd7
fix: run prettier
chrispader Dec 19, 2025
8b3dfc9
fix: TS errors
chrispader Dec 19, 2025
7a8ee1f
fix: more TS errors
chrispader Dec 19, 2025
dd7aa13
refactor: rename arbitrary `data` property to `cardToAssign`
chrispader Dec 19, 2025
f8551e6
Merge branch 'byoc-bulk-card-assign-r1' into byoc-bulk-card-assign-ui…
chrispader Dec 19, 2025
d29a28c
fix: prettier
chrispader Dec 19, 2025
2e4b769
fix: feed route param types
chrispader Dec 19, 2025
00c4724
fix: load members on company cards page open
chrispader Dec 19, 2025
3126a53
fix: remove reference of old "New card" button on member spage
chrispader Dec 19, 2025
6be5734
fix: TS error
chrispader Dec 19, 2025
0f6282e
Merge branch 'byoc-bulk-card-assign-r1' into byoc-bulk-card-assign-ui…
chrispader Dec 19, 2025
6616e52
fix: more lint errors
chrispader Dec 19, 2025
2f9eb11
fix: `filterInactiveCards` test failures
chrispader Dec 19, 2025
de09125
fix: AssignCardFeed ui tests
chrispader Dec 19, 2025
9d6ae23
revert: invalid changes to snapshot type
chrispader Dec 19, 2025
912880c
fix: hide popover label
chrispader Dec 19, 2025
20edc52
fix: TS errors
chrispader Dec 19, 2025
b45985b
Merge branch 'byoc-bulk-card-assign-r1' into byoc-bulk-card-assign-ui…
chrispader Dec 19, 2025
bad449e
fix: table header label styles
chrispader Dec 19, 2025
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
25 changes: 12 additions & 13 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import type {ReimbursementAccountStepToOpen} from './libs/ReimbursementAccountUt
import {getUrlWithParams} from './libs/Url';
import SCREENS from './SCREENS';
import type {Screen} from './SCREENS';
import type {CompanyCardFeedWithDomainID} from './types/onyx';
import type {ConnectionName, SageIntacctMappingName} from './types/onyx/Policy';
import type {CustomFieldType} from './types/onyx/PolicyEmployee';
import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual';

type WorkspaceCompanyCardsAssignCardParams = {
policyID: string;
feed: string;
feed: CompanyCardFeedWithDomainID;
cardID: string;
};

Expand Down Expand Up @@ -2167,14 +2168,14 @@ const ROUTES = {
},
},
WORKSPACE_COMPANY_CARDS_BANK_CONNECTION: {
route: 'workspaces/:policyID/company-cards/:bankName/bank-connection',
getRoute: (policyID: string | undefined, bankName: string, backTo: string) => {
route: 'workspaces/:policyID/company-cards/:feed/bank-connection',
getRoute: (policyID: string | undefined, feed: CompanyCardFeedWithDomainID, backTo?: string) => {
if (!policyID) {
Log.warn('Invalid policyID is used to build the WORKSPACE_COMPANY_CARDS_BANK_CONNECTION route');
}

// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
return getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${bankName}/bank-connection`, backTo);
return getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${feed}/bank-connection`, backTo);
},
},
WORKSPACE_COMPANY_CARDS_ADD_NEW: {
Expand All @@ -2187,12 +2188,9 @@ const ROUTES = {
route: 'workspaces/:policyID/company-cards/select-feed',
getRoute: (policyID: string) => `workspaces/${policyID}/company-cards/select-feed` as const,
},
WORKSPACE_COMPANY_CARDS_ASSIGN_CARD: {
route: 'workspaces/:policyID/company-cards/:feed/assign-card/:cardID',

getRoute: (params: WorkspaceCompanyCardsAssignCardParams, backTo?: string) =>
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
getUrlWithBackToParam(`workspaces/${params.policyID}/company-cards/${encodeURIComponent(params.feed)}/assign-card/${params.cardID}`, backTo),
WORKSPACE_COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION: {
route: 'workspaces/:policyID/company-cards/:feed/broken-card-feed-connection',
getRoute: (policyID: string, feed: CompanyCardFeedWithDomainID) => `workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/broken-card-feed-connection` as const,
},
WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE: {
route: 'workspaces/:policyID/company-cards/:feed/assign-card/:cardID/assignee',
Expand Down Expand Up @@ -2227,10 +2225,11 @@ const ROUTES = {
`workspaces/${params.policyID}/company-cards/${encodeURIComponent(params.feed)}/assign-card/${encodeURIComponent(params.cardID)}/invite-new-member` as const,
},
WORKSPACE_COMPANY_CARD_DETAILS: {
route: 'workspaces/:policyID/company-cards/:bank/:cardID',
route: 'workspaces/:policyID/company-cards/:feed/:cardID',

// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
getRoute: (policyID: string, cardID: string, bank: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${bank}/${cardID}`, backTo),
getRoute: (policyID: string, cardID: string, feed: CompanyCardFeedWithDomainID, backTo?: string) =>
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/${encodeURIComponent(cardID)}`, backTo),
},
WORKSPACE_COMPANY_CARD_NAME: {
route: 'workspaces/:policyID/company-cards/:bank/:cardID/edit/name',
Expand Down
4 changes: 3 additions & 1 deletion src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,8 +579,10 @@ const SCREENS = {
INITIAL: 'Workspace_Initial',
PROFILE: 'Workspace_Overview',
COMPANY_CARDS: 'Workspace_CompanyCards',
COMPANY_CARDS_ASSIGN_CARD: 'Workspace_CompanyCards_AssignCard',
COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION: 'Workspace_CompanyCards_BrokenCardFeedConnection',
COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE: 'Workspace_CompanyCards_AssignCard_Assignee',
COMPANY_CARDS_ASSIGN_CARD_BANK_CONNECTION: 'Workspace_CompanyCards_AssignCard_Bank_Connection',
COMPANY_CARDS_ASSIGN_CARD_PLAID_CONNECTION: 'Workspace_CompanyCards_AssignCard_Plaid_Connection',
COMPANY_CARDS_ASSIGN_CARD_CARD_SELECTION: 'Workspace_CompanyCards_AssignCard_Card_Selection',
COMPANY_CARDS_ASSIGN_CARD_TRANSACTION_START_DATE: 'Workspace_CompanyCards_AssignCard_Transaction_Start_Date',
COMPANY_CARDS_ASSIGN_CARD_CARD_NAME: 'Workspace_CompanyCards_AssignCard_Card_Name',
Expand Down
67 changes: 67 additions & 0 deletions src/components/CardFeedIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import {useCompanyCardFeedIcons} from '@hooks/useCompanyCardIcons';
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useThemeIllustrations from '@hooks/useThemeIllustrations';
import {getCardFeedIcon, getPlaidInstitutionIconUrl, getPlaidInstitutionId} from '@libs/CardUtils';
import type {CompanyCardFeed, CompanyCardFeedWithDomainID} from '@src/types/onyx';
import type {IconProps} from './Icon';
import Icon from './Icon';
import PlaidCardFeedIcon from './PlaidCardFeedIcon';

type CardFeedIconProps = {
isExpensifyCardFeed?: boolean;
selectedFeed?: CompanyCardFeedWithDomainID | undefined;
iconProps?: Partial<IconProps>;
};

function CardFeedIcon({iconProps, selectedFeed, isExpensifyCardFeed = false}: CardFeedIconProps) {
const {src, ...restIconProps} = iconProps ?? {};

const illustrations = useThemeIllustrations();
const companyCardFeedIcons = useCompanyCardFeedIcons();

const isPlaidCardFeed = !!getPlaidInstitutionId(selectedFeed);

if (isExpensifyCardFeed) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <ExpensifyCardFeedIcon {...iconProps} />;
}

if (isPlaidCardFeed) {
return (
<PlaidCardFeedIcon
plaidUrl={getPlaidInstitutionIconUrl(selectedFeed)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...restIconProps}
/>
);
}

if (!selectedFeed) {
return null;
}

return (
<Icon
src={src ?? getCardFeedIcon(selectedFeed as CompanyCardFeed, illustrations, companyCardFeedIcons)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...restIconProps}
/>
);
}

function ExpensifyCardFeedIcon(iconProps: Partial<IconProps>) {
const {src, ...restIconProps} = iconProps ?? {};

const memoizedIllustrations = useMemoizedLazyIllustrations(['ExpensifyCardImage']);

return (
<Icon
src={memoizedIllustrations.ExpensifyCardImage}
// eslint-disable-next-line react/jsx-props-no-spreading
{...restIconProps}
/>
);
}

export default CardFeedIcon;
27 changes: 7 additions & 20 deletions src/components/FeedSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ import {View} from 'react-native';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import type IconAsset from '@src/types/utils/IconAsset';
import CaretWrapper from './CaretWrapper';
import Icon from './Icon';
import PlaidCardFeedIcon from './PlaidCardFeedIcon';
import {PressableWithFeedback} from './Pressable';
import Text from './Text';

Expand All @@ -16,7 +13,7 @@ type Props = {
onFeedSelect: () => void;

/** Icon for the card */
cardIcon: IconAsset;
CardFeedIcon: React.ReactNode;

/** Feed name */
feedName?: string;
Expand All @@ -26,32 +23,22 @@ type Props = {

/** Whether the RBR indicator should be shown */
shouldShowRBR?: boolean;

/** Image url for plaid bank account */
plaidUrl?: string | null;
};

function FeedSelector({onFeedSelect, cardIcon, feedName, supportingText, shouldShowRBR = false, plaidUrl = null}: Props) {
function FeedSelector({onFeedSelect, CardFeedIcon, feedName, supportingText, shouldShowRBR = false}: Props) {
const styles = useThemeStyles();
const theme = useTheme();
const Expensicons = useMemoizedLazyExpensifyIcons(['DotIndicator'] as const);
const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator'] as const);

return (
<PressableWithFeedback
onPress={onFeedSelect}
wrapperStyle={styles.flexShrink1}
style={[styles.flexRow, styles.alignItemsCenter, styles.gap3]}
accessibilityLabel={feedName ?? ''}
>
{plaidUrl ? (
<PlaidCardFeedIcon plaidUrl={plaidUrl} />
) : (
<Icon
src={cardIcon}
height={variables.cardIconHeight}
width={variables.cardIconWidth}
additionalStyles={styles.cardIcon}
/>
)}
{CardFeedIcon}

<View style={styles.flex1}>
<View style={[styles.flexRow, styles.gap1, styles.alignItemsCenter]}>
<CaretWrapper style={styles.flex1}>
Expand All @@ -64,7 +51,7 @@ function FeedSelector({onFeedSelect, cardIcon, feedName, supportingText, shouldS
</CaretWrapper>
{shouldShowRBR && (
<Icon
src={Expensicons.DotIndicator}
src={expensifyIcons.DotIndicator}
fill={theme.danger}
/>
)}
Expand Down
1 change: 1 addition & 0 deletions src/components/Icon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,4 @@ function Icon({
}

export default Icon;
export type {IconProps};
8 changes: 4 additions & 4 deletions src/components/Search/FilterDropdowns/DropdownButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ type DropdownButtonProps = {
/** Button label style */
labelStyle?: StyleProp<TextStyle>;

/** Carret wrapper style */
carretWrapperStyle?: StyleProp<ViewStyle>;
/** Caret wrapper style */
caretWrapperStyle?: StyleProp<ViewStyle>;

/** Wrapper style for the outer view */
wrapperStyle?: StyleProp<ViewStyle>;
Expand All @@ -58,7 +58,7 @@ const ANCHOR_ORIGIN = {
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
};

function DropdownButton({label, value, viewportOffsetTop, PopoverComponent, medium = false, labelStyle, innerStyles, carretWrapperStyle, wrapperStyle}: DropdownButtonProps) {
function DropdownButton({label, value, viewportOffsetTop, PopoverComponent, medium = false, labelStyle, innerStyles, caretWrapperStyle, wrapperStyle}: DropdownButtonProps) {
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to distinguish RHL and narrow layout
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
Expand Down Expand Up @@ -138,7 +138,7 @@ function DropdownButton({label, value, viewportOffsetTop, PopoverComponent, medi
{...(medium ? {medium: true} : {small: true})}
>
<CaretWrapper
style={[styles.flex1, styles.mw100, carretWrapperStyle]}
style={[styles.flex1, styles.mw100, caretWrapperStyle]}
caretWidth={variables.iconSizeSmall}
caretHeight={variables.iconSizeSmall}
>
Expand Down
6 changes: 4 additions & 2 deletions src/components/Search/FilterDropdowns/SingleSelectPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type SingleSelectItem<T> = {

type SingleSelectPopupProps<T> = {
/** The label to show when in an overlay on mobile */
label: string;
label?: string;

/** The list of all items to show up in the list */
items: Array<SingleSelectItem<T>>;
Expand Down Expand Up @@ -107,9 +107,11 @@ function SingleSelectPopup<T extends string>({label, value, items, closeOverlay,
[searchTerm, isSearchable, searchPlaceholder, translate, setSearchTerm, noResultsFound],
);

const shouldShowLabel = isSmallScreenWidth && !!label;

return (
<View style={[!isSmallScreenWidth && styles.pv4, styles.gap2]}>
{isSmallScreenWidth && <Text style={[styles.textLabel, styles.textSupporting, styles.ph5, styles.pv1]}>{label}</Text>}
{shouldShowLabel && <Text style={[styles.textLabel, styles.textSupporting, styles.ph5, styles.pv1]}>{label}</Text>}

<View style={[styles.getSelectionListPopoverHeight(options.length || 1, windowHeight, isSearchable ?? false)]}>
<SelectionList
Expand Down
3 changes: 2 additions & 1 deletion src/components/Table/TableBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ function TableBody<T>({contentContainerStyle, ...props}: TableBodyProps) {
return (
<View
style={styles.flex1}
{...props} // eslint-disable-line react/jsx-props-no-spreading
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
<FlashList<T>
data={filteredAndSortedData}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ function createSingleSelectPopover<FilterKey extends string = string>({filterKey

return (
<SingleSelectPopup
label={filterKey}
label={filterConfig.showLabel ? filterKey : undefined}
items={filterConfig.options.map((option) => ({
text: option.label,
value: option.value,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Table/TableFilterButtons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ function FilterItemRenderer({item}: FilterItemRendererProps) {
innerStyles={[styles.gap2, shouldShowResponsiveLayout && styles.mw100]}
wrapperStyle={shouldShowResponsiveLayout && styles.w100}
labelStyle={styles.fontSizeLabel}
carretWrapperStyle={styles.gap2}
caretWrapperStyle={styles.gap2}
medium
/>
);
Expand Down
3 changes: 2 additions & 1 deletion src/components/Table/TableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ function TableHeaderColumn<T, ColumnKey extends string = string>({column}: {colu
accessible
accessibilityLabel={column.label}
accessibilityRole="button"
style={[column.styling?.labelStyles, styles.flexRow, styles.alignItemsCenter, column.styling?.flex ? {flex: column.styling.flex} : styles.flex1, column.styling?.containerStyles]}
style={[styles.flexRow, styles.alignItemsCenter, column.styling?.flex ? {flex: column.styling.flex} : styles.flex1, column.styling?.containerStyles]}
onPress={() => toggleSorting(column.key)}
>
<Text
Expand All @@ -129,6 +129,7 @@ function TableHeaderColumn<T, ColumnKey extends string = string>({column}: {colu
style={[
styles.lh16,
isSortingByColumn ? styles.textMicroBoldSupporting : [styles.textMicroSupporting, styles.pr1, {marginRight: variables.iconSizeExtraSmall, marginBottom: 1, marginTop: 1}],
column.styling?.labelStyles,
]}
>
{column.label}
Expand Down
1 change: 1 addition & 0 deletions src/components/Table/middlewares/filtering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {Middleware, MiddlewareHookResult} from './types';
* @template FilterKey - The type of filter keys.
*/
type FilterConfigEntry = {
showLabel?: boolean;
filterType?: 'multi-select' | 'single-select';
options: Array<{label: string; value: string}>;
default?: string;
Expand Down
Loading
Loading