diff --git a/assets/images/companyCards/amex.svg b/assets/images/companyCards/amex.svg new file mode 100644 index 000000000000..73e8164cdc63 --- /dev/null +++ b/assets/images/companyCards/amex.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/companyCards/mastercard.svg b/assets/images/companyCards/mastercard.svg new file mode 100644 index 000000000000..dcfac5eb33dd --- /dev/null +++ b/assets/images/companyCards/mastercard.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/companyCards/visa.svg b/assets/images/companyCards/visa.svg new file mode 100644 index 000000000000..4a7a73b66639 --- /dev/null +++ b/assets/images/companyCards/visa.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CONST.ts b/src/CONST.ts index 175aa8cd3c16..8100bfb68274 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2276,6 +2276,13 @@ const CONST = { MENTION_ICON: 'mention-icon', SMALL_NORMAL: 'small-normal', }, + COMPANY_CARD: { + FEED_BANK_NAME: { + MASTER_CARD: 'cdf', + VISA: 'vcf', + AMEX: 'gl1025', + }, + }, EXPENSIFY_CARD: { BANK: 'Expensify Card', FRAUD_TYPES: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 101656ae1ea1..6a1fc8a629ed 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -461,6 +461,8 @@ const ONYXKEYS = { // Shared NVPs /** Collection of objects where each object represents the owner of the workspace that is past due billing AND the user is a member of. */ SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END: 'sharedNVP_private_billingGracePeriodEnd_', + + /** The collection of card feeds */ SHARED_NVP_PRIVATE_DOMAIN_MEMBER: 'sharedNVP_private_domain_member_', /** @@ -477,6 +479,9 @@ const ONYXKEYS = { /** The value that indicates whether Continuous Reconciliation should be used on the domain */ EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION: 'expensifyCard_useContinuousReconciliation_', + + /** Currently displaying feed */ + LAST_SELECTED_FEED: 'lastSelectedFeed_', }, /** List of Form ids */ @@ -762,11 +767,12 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; [ONYXKEYS.COLLECTION.SNAPSHOT]: OnyxTypes.SearchResults; [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod; - [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER]: OnyxTypes.CompanyCards; + [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER]: OnyxTypes.CardFeeds; [ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS]: OnyxTypes.ExpensifyCardSettings; [ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList; [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName; [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean; + [ONYXKEYS.COLLECTION.LAST_SELECTED_FEED]: string; }; type OnyxValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2db861121375..2e8421532048 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -888,6 +888,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/reportFields/:reportFieldID/edit/initialValue', getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportFields/${encodeURIComponent(reportFieldID)}/edit/initialValue` as const, }, + WORKSPACE_COMPANY_CARDS_SELECT_FEED: { + route: 'settings/workspaces/:policyID/company-cards/select-feed', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/company-cards/select-feed` as const, + }, WORKSPACE_EXPENSIFY_CARD: { route: 'settings/workspaces/:policyID/expensify-card', getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 7ffca6889688..9634cc1b02ac 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -365,6 +365,7 @@ const SCREENS = { RATE_AND_UNIT_RATE: 'Workspace_RateAndUnit_Rate', RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit', COMPANY_CARDS: 'Workspace_CompanyCards', + COMPANY_CARDS_SELECT_FEED: 'Workspace_CompanyCards_Select_Feed', EXPENSIFY_CARD: 'Workspace_ExpensifyCard', EXPENSIFY_CARD_DETAILS: 'Workspace_ExpensifyCard_Details', EXPENSIFY_CARD_LIMIT: 'Workspace_ExpensifyCard_Limit', diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index f8e1b4ef5e31..060f31647b7c 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -1,3 +1,6 @@ +import AmexCompanyCards from '@assets/images/companyCards/amex.svg'; +import MasterCardCompanyCards from '@assets/images/companyCards/mastercard.svg'; +import VisaCompanyCards from '@assets/images/companyCards/visa.svg'; import EmptyCardState from '@assets/images/emptystate__expensifycard.svg'; import ExpensifyCardIllustration from '@assets/images/expensifyCard/cardIllustration.svg'; import LaptopwithSecondScreenandHourglass from '@assets/images/LaptopwithSecondScreenandHourglass.svg'; @@ -220,4 +223,7 @@ export { BigVault, Filters, Rules, + AmexCompanyCards, + MasterCardCompanyCards, + VisaCompanyCards, }; diff --git a/src/languages/en.ts b/src/languages/en.ts index 0878b4875f29..49aad695995e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2750,6 +2750,10 @@ export default { control: 'Control', collect: 'Collect', }, + companyCards: { + addCompanyCards: 'Add company cards', + selectCardFeed: 'Select card feed', + }, expensifyCard: { issueAndManageCards: 'Issue and manage your Expensify Cards', getStartedIssuing: 'Get started by issuing your first virtual or physical card.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 660ab88cb92c..dd4d94d47b78 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2797,6 +2797,10 @@ export default { control: 'Control', collect: 'Recolectar', }, + companyCards: { + addCompanyCards: 'Agregar tarjetas de empresa', + selectCardFeed: 'Seleccionar feed de tarjetas', + }, expensifyCard: { issueAndManageCards: 'Emitir y gestionar Tarjetas Expensify', getStartedIssuing: 'Empieza emitiendo tu primera tarjeta virtual o fĂ­sica.', diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index e0041dde9934..2398bc1e729a 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -2,12 +2,14 @@ import lodash from 'lodash'; import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import * as Illustrations from '@src/components/Icon/Illustrations'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {BankAccountList, Card, CardList, PersonalDetailsList, WorkspaceCardsList} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type IconAsset from '@src/types/utils/IconAsset'; import localeCompare from './LocaleCompare'; import * as Localize from './Localize'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; @@ -180,6 +182,18 @@ function sortCardsByCardholderName(cardsList: OnyxEntry, per }); } +function getCardFeedIcon(cardFeed: string): IconAsset { + if (cardFeed.startsWith(CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD)) { + return Illustrations.MasterCardCompanyCards; + } + + if (cardFeed.startsWith(CONST.COMPANY_CARD.FEED_BANK_NAME.VISA)) { + return Illustrations.VisaCompanyCards; + } + + return Illustrations.AmexCompanyCards; +} + export { isExpensifyCard, isCorporateCard, @@ -195,4 +209,5 @@ export { getTranslationKeyForLimitType, getEligibleBankAccountsForCard, sortCardsByCardholderName, + getCardFeedIcon, }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 40e8d5a106ef..187e71df4d98 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -421,6 +421,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/taxes/WorkspaceTaxCodePage').default, [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsName').default, [SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite').default, + [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require('../../../../pages/workspace/expensifyCard/issueNew/IssueNewCardPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceCardSettingsPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceSettlementAccountPage').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index c7939268fa12..19e889092fb2 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -160,7 +160,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE, ], [SCREENS.WORKSPACE.INVOICES]: [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME, SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE], - [SCREENS.WORKSPACE.COMPANY_CARDS]: [], + [SCREENS.WORKSPACE.COMPANY_CARDS]: [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED], [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [ SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW, SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 43e7ece014eb..6ee3b14b64ed 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -481,6 +481,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE]: { path: ROUTES.WORKSPACE_INVOICES_COMPANY_WEBSITE.route, }, + [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED]: { + path: ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.route, + }, [SCREENS.WORKSPACE.EXPENSIFY_CARD_LIMIT]: { path: ROUTES.WORKSPACE_EXPENSIFY_CARD_LIMIT.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index d1bfb4fa4718..4e5e3a2c6fac 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -672,6 +672,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE]: { policyID: string; }; + [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED]: { + policyID: string; + }; [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: { policyID: string; }; diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 747df5d3998e..61a86be77323 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -617,6 +617,16 @@ function toggleContinuousReconciliation(workspaceAccountID: number, shouldUseCon }); } +function updateSelectedFeed(feed: string, policyID: string) { + Onyx.update([ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, + value: feed, + }, + ]); +} + export { requestReplacementExpensifyCard, activatePhysicalExpensifyCard, @@ -635,5 +645,6 @@ export { openCardDetailsPage, toggleContinuousReconciliation, updateExpensifyCardLimitType, + updateSelectedFeed, }; export type {ReplacementReason}; diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 2f93c7416479..e1eb7e24130a 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -71,7 +71,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const policyID = policy?.id; const workspaceAccountID = policy?.workspaceAccountID ?? -1; const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID.toString()}${CONST.EXPENSIFY_CARD.BANK}`); - const [companyCardsList] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID.toString()}`); + const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID.toString()}`); const [isOrganizeWarningModalOpen, setIsOrganizeWarningModalOpen] = useState(false); const [isIntegrateWarningModalOpen, setIsIntegrateWarningModalOpen] = useState(false); const [isReportFieldsWarningModalOpen, setIsReportFieldsWarningModalOpen] = useState(false); @@ -126,7 +126,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro subtitleTranslationKey: 'workspace.moreFeatures.companyCards.subtitle', isActive: policy?.areCompanyCardsEnabled ?? false, pendingAction: policy?.pendingFields?.areCompanyCardsEnabled, - disabled: !isEmptyObject(companyCardsList), + disabled: !isEmptyObject(cardFeeds?.companyCards), action: (isEnabled: boolean) => { if (!policyID) { return; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx new file mode 100644 index 000000000000..c745774d364d --- /dev/null +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx @@ -0,0 +1,123 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as CardUtils from '@libs/CardUtils'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import Navigation from '@navigation/Navigation'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import variables from '@styles/variables'; +import * as Card from '@userActions/Card'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {CardFeeds} from '@src/types/onyx'; + +type CardFeedListItem = ListItem & { + /** Card feed value */ + value: string; +}; + +const mockedData: CardFeeds = { + companyCards: { + cdfbmo: { + pending: false, + asrEnabled: true, + forceReimbursable: 'force_no', + liabilityType: 'corporate', + preferredPolicy: '', + reportTitleFormat: '{report:card}{report:bank}{report:submit:from}{report:total}{report:enddate:MMMM}', + statementPeriodEndDay: 'LAST_DAY_OF_MONTH', + }, + }, + companyCardNicknames: { + cdfbmo: 'BMO MasterCard', + }, +}; + +type WorkspaceCompanyCardFeedSelectorPageProps = StackScreenProps; + +function WorkspaceCompanyCardFeedSelectorPage({route}: WorkspaceCompanyCardFeedSelectorPageProps) { + const {policyID} = route.params; + + const {translate} = useLocalize(); + const styles = useThemeStyles(); + // TODO: use data form onyx instead of mocked one when API is implemented + // const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); + const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`); + + const cardFeeds = mockedData; + const defaultFeed = Object.keys(cardFeeds?.companyCards ?? {})[0]; + const selectedFeed = lastSelectedFeed ?? defaultFeed; + + const feeds: CardFeedListItem[] = Object.entries(cardFeeds?.companyCardNicknames ?? {}).map(([key, value]) => ({ + value: key, + text: value, + keyForList: key, + isSelected: key === selectedFeed, + leftElement: ( + + ), + })); + + const goBack = () => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + + const selectFeed = (feed: CardFeedListItem) => { + Card.updateSelectedFeed(feed.value, policyID); + goBack(); + }; + + return ( + + + + { + // TODO: navigate to Add Feed flow when it's implemented + }} + /> + } + /> + + + ); +} + +WorkspaceCompanyCardFeedSelectorPage.displayName = 'WorkspaceCompanyCardFeedSelectorPage'; + +export default WorkspaceCompanyCardFeedSelectorPage; diff --git a/src/types/onyx/CardFeeds.ts b/src/types/onyx/CardFeeds.ts new file mode 100644 index 000000000000..59d93a192cf8 --- /dev/null +++ b/src/types/onyx/CardFeeds.ts @@ -0,0 +1,34 @@ +/** Card feed data */ +type CardFeedData = { + /** Whether any actions are pending */ + pending: boolean; + + /** Determines if Automated Statement Reconciliation (ASR) is enabled for the cards */ + asrEnabled: boolean; + + /** Specifies if the expenses on this card should be force reimbursable */ + forceReimbursable: string; + + /** Defines the type of liability for the card */ + liabilityType: string; + + /** Preferred policy */ + preferredPolicy: string; + + /** Specifies the format for the report title related to this card */ + reportTitleFormat: string; + + /** Indicates the day when the statement period for this card ends */ + statementPeriodEndDay: string; +}; + +/** Card feeds model */ +type CardFeeds = { + /** Company cards feeds */ + companyCards: Record; + + /** User-friendly feed nicknames */ + companyCardNicknames: Record; +}; + +export default CardFeeds; diff --git a/src/types/onyx/CompanyCards.ts b/src/types/onyx/CompanyCards.ts deleted file mode 100644 index 17ebbaf98bb1..000000000000 --- a/src/types/onyx/CompanyCards.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** Model of CompanyCard's Shared NVP record */ -// TODO update information here during implementation Add Company Card flow -type CompanyCards = { - /** Company cards object */ - companyCards: { - /** Company card info key */ - cdfbmo: CompanyCardInfo; - }; - /** Company cards nicknames */ - companyCardNicknames: { - /** Company cards info key */ - cdfbmo: string; - }; -}; -/** - * Model of company card information - */ -type CompanyCardInfo = { - /** Company card pending state */ - pending: boolean; - - /** Company card asr state */ - asrEnabled: boolean; - - /** Company card force reimbursable value */ - forceReimbursable: string; - - /** Company card liability type */ - liabilityType: string; - - /** Company card preferred policy */ - preferredPolicy: string; - - /** Company card report title format */ - reportTitleFormat: string; - - /** Company card statement period */ - statementPeriodEndDay: string; -}; - -export default CompanyCards; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index ca7dc271f84a..54ee7fe25892 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -10,7 +10,7 @@ import type BlockedFromConcierge from './BlockedFromConcierge'; import type CancellationDetails from './CancellationDetails'; import type Card from './Card'; import type {CardList, IssueNewCard, WorkspaceCardsList} from './Card'; -import type CompanyCards from './CompanyCards'; +import type CardFeeds from './CardFeeds'; import type {CapturedLogs, Log} from './Console'; import type Credentials from './Credentials'; import type Currency from './Currency'; @@ -115,7 +115,6 @@ export type { Credentials, Currency, CurrencyList, - CompanyCards, CustomStatusDraft, DismissedReferralBanners, Download, @@ -222,4 +221,5 @@ export type { ApprovalWorkflowOnyx, MobileSelectionMode, WorkspaceTooltip, + CardFeeds, };