diff --git a/contributingGuides/TABLE.md b/contributingGuides/TABLE.md new file mode 100644 index 0000000000000..d0bb6d938dc4a --- /dev/null +++ b/contributingGuides/TABLE.md @@ -0,0 +1,249 @@ +# Table Component + +A composable, generic table component with built-in filtering, search, and sorting capabilities. + +## Quick Start + +```tsx +import Table from '@components/Table'; +import type { TableColumn, CompareItemsCallback } from '@components/Table'; + +type Item = { id: string; name: string; status: string }; +type ColumnKey = 'name' | 'status'; + +const columns: Array> = [ + { key: 'name', label: 'Name' }, + { key: 'status', label: 'Status' }, +]; + +function MyTable() { + return ( + + data={items} + columns={columns} + renderItem={({ item }) => } + keyExtractor={(item) => item.id} + > + + + + ); +} +``` + +## Compositional Pattern + +The Table uses a **compound component pattern** where the parent `` manages all state and child components render specific UI parts: + +| Component | Purpose | +|-----------|---------| +| `
` | Parent container that manages state and provides context | +| `` | Renders sortable column headers | +| `` | Renders data rows using FlashList | +| `` | Search input that filters data | +| `` | Dropdown filter buttons | + +### Flexible Composition + +You only include the components you need: + +```tsx +// Minimal: just data rows +
+ +
+ +// With search + + + +
+ +// Full featured + + + + + +
+``` + +## Features + +### Sorting + +Enable by providing `compareItems`: + +```tsx +const compareItems: CompareItemsCallback = (a, b, { columnKey, order }) => { + const multiplier = order === 'asc' ? 1 : -1; + return a[columnKey].localeCompare(b[columnKey]) * multiplier; +}; + + + {/* Clicking headers toggles sort */} + +
+``` + +Header click behavior: `ascending → descending → reset` + +### Searching + +Enable by providing `isItemInSearch`: + +```tsx +const isItemInSearch = (item: Item, searchString: string) => + item.name.toLowerCase().includes(searchString.toLowerCase()); + + + + +
+``` + +### Filtering + +Enable by providing `filters` config and `isItemInFilter`: + +```tsx +import type { FilterConfig, IsItemInFilterCallback } from '@components/Table'; + +const filterConfig: FilterConfig = { + status: { + filterType: 'single-select', // or 'multi-select' + options: [ + { label: 'All', value: 'all' }, + { label: 'Active', value: 'active' }, + { label: 'Inactive', value: 'inactive' }, + ], + default: 'all', + }, +}; + +const isItemInFilter: IsItemInFilterCallback = (item, filterValues) => { + if (filterValues.includes('all')) return true; + return filterValues.includes(item.status); +}; + + + + +
+``` + +## Programmatic Control + +Access table methods via ref: + +```tsx +import type { TableHandle } from '@components/Table'; + +const tableRef = useRef>(null); + +// Update sorting programmatically +tableRef.current?.updateSorting({ columnKey: 'name', order: 'desc' }); + +// Update search +tableRef.current?.updateSearchString('query'); + +// Get current state +const sorting = tableRef.current?.getActiveSorting(); +const searchString = tableRef.current?.getActiveSearchString(); + +// FlashList methods also available +tableRef.current?.scrollToIndex({ index: 0 }); + + + +
+``` + +## Type Parameters + +| Parameter | Description | +|-----------|-------------| +| `T` | Type of items in the data array | +| `ColumnKey` | String literal union of column keys (e.g., `'name' \| 'status'`) | +| `FilterKey` | String literal union of filter keys | + +## Architecture + +### Middleware Pipeline + +Data processing flows through three middlewares: + +``` +data → [Filtering] → [Searching] → [Sorting] → processedData +``` + +Each middleware transforms the data array. The order is fixed: filters first, then search, then sort. + +### Context + +All sub-components access shared state via `TableContext`. You can create custom sub-components using `useTableContext`: + +```tsx +import { useTableContext } from '@components/Table/TableContext'; + +function CustomComponent() { + const { processedData, activeSorting, updateSorting } = useTableContext(); + // Build custom UI using context data... +} +``` + +## Column Configuration + +```tsx +type TableColumn = { + key: ColumnKey; // Unique identifier + label: string; // Display text + styling?: { + flex?: number; // Column width ratio + containerStyles?: StyleProp; + labelStyles?: StyleProp; + }; +}; +``` + +## Props Reference + +### Table Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `data` | `T[]` | Yes | Array of items to display | +| `columns` | `TableColumn[]` | Yes | Column configuration | +| `renderItem` | FlashList's `renderItem` | Yes | Row renderer | +| `keyExtractor` | FlashList's `keyExtractor` | Yes | Unique key generator | +| `compareItems` | `CompareItemsCallback` | No | Sorting comparator | +| `isItemInSearch` | `IsItemInSearchCallback` | No | Search predicate | +| `isItemInFilter` | `IsItemInFilterCallback` | No | Filter predicate | +| `filters` | `FilterConfig` | No | Filter dropdown config | +| `ref` | `Ref>` | No | Ref for programmatic control | + +Plus all FlashList props except `data`. diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 1beef445122e4..73eca3749005d 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3437,6 +3437,7 @@ const CONST = { CSV: 'ccupload', }, FEED_KEY_SEPARATOR: '#', + CARD_NUMBER_MASK_CHAR: 'X', STEP_NAMES: ['1', '2', '3', '4'], STEP: { BANK_CONNECTION: 'BankConnection', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index cd6ee05a69d08..08a7e5b8a2410 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -706,6 +706,9 @@ const ONYXKEYS = { */ WORKSPACE_CARDS_LIST: 'cards_', + /** Collection of objects where each object represents the card assignment that failed because we can't store errors in cardList or card feed due to server-provided IDs that aren't optimistic. */ + FAILED_COMPANY_CARDS_ASSIGNMENTS: 'failedCompanyCardsAssignments_', + /** Expensify cards settings */ PRIVATE_EXPENSIFY_CARD_SETTINGS: 'private_expensifyCardSettings_', @@ -1130,6 +1133,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_BANK_ACCOUNT_METADATA]: OnyxTypes.ExpensifyCardBankAccountMetadata; [ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_MANUAL_BILLING]: boolean; [ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList; + [ONYXKEYS.COLLECTION.FAILED_COMPANY_CARDS_ASSIGNMENTS]: OnyxTypes.FailedCompanyCardAssignments; [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName; [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: OnyxTypes.CardContinuousReconciliation; [ONYXKEYS.COLLECTION.LAST_SELECTED_FEED]: OnyxTypes.CompanyCardFeedWithDomainID; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a909c31d99e09..27d9c8432e8ef 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -16,10 +16,17 @@ 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: CompanyCardFeedWithDomainID; + cardID: string; +}; + // This is a file containing constants for all the routes we want to be able to go to /** @@ -2160,14 +2167,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: { @@ -2180,21 +2187,53 @@ 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', - - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getRoute: (policyID: string, feed: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/assign-card`, 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', + 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/${encodeURIComponent(params.cardID)}/assignee`, backTo), + }, + WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_CARD_SELECTION: { + route: 'workspaces/:policyID/company-cards/:feed/assign-card/:cardID/card-selection', + getRoute: (params: WorkspaceCompanyCardsAssignCardParams) => + `workspaces/${params.policyID}/company-cards/${encodeURIComponent(params.feed)}/assign-card/${encodeURIComponent(params.cardID)}/card-selection` as const, + }, + WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_TRANSACTION_START_DATE: { + route: 'workspaces/:policyID/company-cards/:feed/assign-card/:cardID/transaction-start-date', + getRoute: (params: WorkspaceCompanyCardsAssignCardParams) => + `workspaces/${params.policyID}/company-cards/${encodeURIComponent(params.feed)}/assign-card/${encodeURIComponent(params.cardID)}/transaction-start-date` as const, + }, + WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_CARD_NAME: { + route: 'workspaces/:policyID/company-cards/:feed/assign-card/:cardID/card-name', + getRoute: (params: WorkspaceCompanyCardsAssignCardParams) => + `workspaces/${params.policyID}/company-cards/${encodeURIComponent(params.feed)}/assign-card/${encodeURIComponent(params.cardID)}/card-name` as const, + }, + WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_CONFIRMATION: { + route: 'workspaces/:policyID/company-cards/:feed/assign-card/:cardID/confirmation', + 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/${encodeURIComponent(params.cardID)}/confirmation`, backTo), + }, + WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_INVITE_NEW_MEMBER: { + route: 'workspaces/:policyID/company-cards/:feed/assign-card/:cardID/invite-new-member', + getRoute: (params: WorkspaceCompanyCardsAssignCardParams) => + `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, feed: CompanyCardFeedWithDomainID, cardID: string, 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', - getRoute: (policyID: string, cardID: string, bank: string) => `workspaces/${policyID}/company-cards/${bank}/${cardID}/edit/name` as const, + WORKSPACE_COMPANY_CARD_EDIT_CARD_NAME: { + route: 'workspaces/:policyID/company-cards/:feed/:cardID/edit/name', + getRoute: (policyID: string, cardID: string, feed: CompanyCardFeedWithDomainID) => + `workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/${encodeURIComponent(cardID)}/edit/name` as const, }, WORKSPACE_COMPANY_CARD_EXPORT: { route: 'workspaces/:policyID/company-cards/:bank/:cardID/edit/export', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 548c15d13f665..f226769d6aea4 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -578,7 +578,15 @@ 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', + COMPANY_CARDS_ASSIGN_CARD_CONFIRMATION: 'Workspace_CompanyCards_AssignCard_Confirmation', + COMPANY_CARDS_ASSIGN_CARD_INVITE_NEW_MEMBER: 'Workspace_CompanyCards_AssignCard_Invite_New_Member', COMPANY_CARDS_SELECT_FEED: 'Workspace_CompanyCards_Select_Feed', COMPANY_CARDS_BANK_CONNECTION: 'Workspace_CompanyCards_BankConnection', COMPANY_CARDS_ADD_NEW: 'Workspace_CompanyCards_New', @@ -590,7 +598,7 @@ const SCREENS = { COMPANY_CARDS_SETTINGS_FEED_NAME: 'Workspace_CompanyCards_Settings_Feed_Name', COMPANY_CARDS_SETTINGS_STATEMENT_CLOSE_DATE: 'Workspace_CompanyCards_Settings_Statement_Close_Date', COMPANY_CARD_DETAILS: 'Workspace_CompanyCard_Details', - COMPANY_CARD_NAME: 'Workspace_CompanyCard_Name', + COMPANY_CARD_EDIT_CARD_NAME: 'Workspace_CompanyCard_Edit_Card_Name', COMPANY_CARD_EXPORT: 'Workspace_CompanyCard_Export', EXPENSIFY_CARD: 'Workspace_ExpensifyCard', EXPENSIFY_CARD_DETAILS: 'Workspace_ExpensifyCard_Details', diff --git a/src/components/AccountSwitcherSkeletonView/index.tsx b/src/components/AccountSwitcherSkeletonView/index.tsx index 8712539d17995..b558b2fa35581 100644 --- a/src/components/AccountSwitcherSkeletonView/index.tsx +++ b/src/components/AccountSwitcherSkeletonView/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import {Circle, Rect} from 'react-native-svg'; import type {ValueOf} from 'type-fest'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; @@ -15,9 +16,15 @@ type AccountSwitcherSkeletonViewProps = { /** The size of the avatar */ avatarSize?: ValueOf; + + /** The width of the skeleton view */ + width?: number; + + /** Additional styles for the skeleton view */ + style?: StyleProp; }; -function AccountSwitcherSkeletonView({shouldAnimate = true, avatarSize = CONST.AVATAR_SIZE.DEFAULT}: AccountSwitcherSkeletonViewProps) { +function AccountSwitcherSkeletonView({shouldAnimate = true, avatarSize = CONST.AVATAR_SIZE.DEFAULT, width, style}: AccountSwitcherSkeletonViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -28,11 +35,12 @@ function AccountSwitcherSkeletonView({shouldAnimate = true, avatarSize = CONST.A const rectXTranslation = startPositionX + avatarPlaceholderRadius + styles.gap3.gap; return ( - + ; +}; + +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 ; + } + + if (isPlaidCardFeed) { + return ( + + ); + } + + if (!selectedFeed) { + return null; + } + + return ( + + ); +} + +function ExpensifyCardFeedIcon(iconProps: Partial) { + const {src, ...restIconProps} = iconProps ?? {}; + + const memoizedIllustrations = useMemoizedLazyIllustrations(['ExpensifyCardImage']); + + return ( + + ); +} + +export default CardFeedIcon; diff --git a/src/components/CaretWrapper.tsx b/src/components/CaretWrapper.tsx index dc29fa9c3b3ad..6518f6a53449f 100644 --- a/src/components/CaretWrapper.tsx +++ b/src/components/CaretWrapper.tsx @@ -1,29 +1,32 @@ import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; 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 ChildrenProps from '@src/types/utils/ChildrenProps'; import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; type CaretWrapperProps = ChildrenProps & { style?: StyleProp; + caretWidth?: number; + caretHeight?: number; }; -function CaretWrapper({children, style}: CaretWrapperProps) { +function CaretWrapper({children, style, caretWidth, caretHeight}: CaretWrapperProps) { const theme = useTheme(); const styles = useThemeStyles(); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['DownArrow'] as const); return ( {children} ); diff --git a/src/components/ErrorMessageRow.tsx b/src/components/ErrorMessageRow.tsx index 8086fc714f6ed..9d3274aa34ddc 100644 --- a/src/components/ErrorMessageRow.tsx +++ b/src/components/ErrorMessageRow.tsx @@ -16,17 +16,14 @@ type ErrorMessageRowProps = { /** Additional style object for the error row text */ errorRowTextStyles?: StyleProp; - /** A function to run when the X button next to the error is clicked */ - onClose?: () => void; - - /** Whether we can dismiss the error message */ - canDismissError?: boolean; + /** If passed, an X button next to the error will be shown and which triggers this callback */ + onDismiss?: () => void; /** A function to dismiss error */ dismissError?: () => void; }; -function ErrorMessageRow({errors, errorRowStyles, onClose, canDismissError = true, dismissError, errorRowTextStyles}: ErrorMessageRowProps) { +function ErrorMessageRow({errors, errorRowStyles, onDismiss, dismissError, errorRowTextStyles}: ErrorMessageRowProps) { // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. const errorEntries = Object.entries(errors ?? {}); const filteredErrorEntries = errorEntries.filter((errorEntry): errorEntry is [string, string | ReceiptError | OnyxCommon.TranslationKeyError] => errorEntry[1] !== null); @@ -37,10 +34,9 @@ function ErrorMessageRow({errors, errorRowStyles, onClose, canDismissError = tru ) : null; diff --git a/src/components/FeedSelector.tsx b/src/components/FeedSelector.tsx index 7fa78bcbe2628..8fee5ed822b49 100644 --- a/src/components/FeedSelector.tsx +++ b/src/components/FeedSelector.tsx @@ -1,14 +1,12 @@ import React from 'react'; 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 * as Expensicons from './Icon/Expensicons'; -import PlaidCardFeedIcon from './PlaidCardFeedIcon'; import {PressableWithFeedback} from './Pressable'; +import SearchInputSelectionSkeleton from './Skeletons/SearchInputSelectionSkeleton'; import Text from './Text'; type Props = { @@ -16,10 +14,7 @@ type Props = { onFeedSelect: () => void; /** Icon for the card */ - cardIcon: IconAsset; - - /** Whether to show assign card button */ - shouldChangeLayout?: boolean; + CardFeedIcon: React.ReactNode; /** Feed name */ feedName?: string; @@ -30,34 +25,31 @@ type Props = { /** Whether the RBR indicator should be shown */ shouldShowRBR?: boolean; - /** Image url for plaid bank account */ - plaidUrl?: string | null; + /** Whether the feed selector should render a loading skeleton */ + isLoading?: boolean; }; -function FeedSelector({onFeedSelect, cardIcon, shouldChangeLayout, feedName, supportingText, shouldShowRBR = false, plaidUrl = null}: Props) { +function FeedSelector({onFeedSelect, CardFeedIcon, feedName, supportingText, shouldShowRBR = false, isLoading = false}: Props) { const styles = useThemeStyles(); const theme = useTheme(); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator'] as const); + + if (isLoading) { + return ; + } return ( - {plaidUrl ? ( - - ) : ( - - )} + {CardFeedIcon} + - + {shouldShowRBR && ( )} diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index e4038ee9fd11a..51d398a3e85db 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -188,3 +188,4 @@ function Icon({ } export default Icon; +export type {IconProps}; diff --git a/src/components/ImportSpreadsheetColumns.tsx b/src/components/ImportSpreadsheetColumns.tsx index b1d8b93127404..d05c309da9ed6 100644 --- a/src/components/ImportSpreadsheetColumns.tsx +++ b/src/components/ImportSpreadsheetColumns.tsx @@ -108,7 +108,6 @@ function ImportSpreadsheetColumns({ shouldDisplayErrorAbove errors={errors} errorRowStyles={styles.mv2} - canDismissError={false} >