diff --git a/src/components/BlockingViews/BlockingView.tsx b/src/components/BlockingViews/BlockingView.tsx index 363342778fc0..912637d265f3 100644 --- a/src/components/BlockingViews/BlockingView.tsx +++ b/src/components/BlockingViews/BlockingView.tsx @@ -36,6 +36,9 @@ type BaseBlockingViewProps = { /** Render custom subtitle */ CustomSubtitle?: React.ReactElement; + + /** Additional styles to apply to the container */ + containerStyle?: StyleProp; }; type BlockingViewIconProps = { @@ -81,6 +84,7 @@ function BlockingView({ animationStyles = [], animationWebStyle = {}, CustomSubtitle, + containerStyle, }: BlockingViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -117,7 +121,7 @@ function BlockingView({ }, [styles, subtitleText, shouldEmbedLinkWithSubtitle, CustomSubtitle]); return ( - + {animation && ( ( headerContent, footerContent, listFooterContent, + listEmptyContent, showScrollIndicator = true, showLoadingPlaceholder = false, showConfirmButton = false, @@ -99,6 +100,7 @@ function BaseSelectionList( const itemFocusTimeoutRef = useRef(null); const [currentPage, setCurrentPage] = useState(1); const isTextInputFocusedRef = useRef(false); + const isEmptyList = sections.length === 0; const incrementPage = () => setCurrentPage((prev) => prev + 1); @@ -642,8 +644,11 @@ function BaseSelectionList( testID="selection-list" onLayout={onSectionListLayout} style={(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0} + ListHeaderComponent={listHeaderContent} ListFooterComponent={listFooterContent ?? ShowMoreButtonInstance} - ListHeaderComponent={listHeaderContent && listHeaderContent} + ListEmptyComponent={listEmptyContent} + contentContainerStyle={isEmptyList && listEmptyContent ? styles.flexGrow1 : undefined} + scrollEnabled={!isEmptyList || !listEmptyContent} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} /> diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 4c9b72634c46..53816733603e 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -330,6 +330,9 @@ type BaseSelectionListProps = Partial & { /** Custom content to display in the footer of list component. If present ShowMore button won't be displayed */ listFooterContent?: React.JSX.Element | null; + /** Content to display if the list is empty */ + listEmptyContent?: React.JSX.Element | null; + /** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */ shouldUseDynamicMaxToRenderPerBatch?: boolean; diff --git a/src/components/SelectionScreen.tsx b/src/components/SelectionScreen.tsx index a2ab477accef..34ec10fb00c2 100644 --- a/src/components/SelectionScreen.tsx +++ b/src/components/SelectionScreen.tsx @@ -26,6 +26,9 @@ type SelectionScreenProps = { /** Custom content to display in the header */ headerContent?: React.ReactNode; + /** Content to display if the list is empty */ + listEmptyContent?: React.JSX.Element | null; + /** Sections for the section list */ sections: Array>; @@ -58,6 +61,7 @@ function SelectionScreen({ displayName, title, headerContent, + listEmptyContent, sections, listItem, initiallyFocusedOptionKey, @@ -92,6 +96,7 @@ function SelectionScreen({ showScrollIndicator shouldShowTooltips={false} initiallyFocusedOptionKey={initiallyFocusedOptionKey} + listEmptyContent={listEmptyContent} /> diff --git a/src/languages/en.ts b/src/languages/en.ts index cc806558be31..b6fad270c1c4 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2037,6 +2037,8 @@ export default { [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.CHECK}Error`]: 'Check is not available when locations are enabled. Please select a different export option.', [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY}Error`]: 'Journal entry is not available when taxes enabled. please select a different export option.', }, + noAccountsFound: 'No accounts found', + noAccountsFoundDescription: 'Add the account in Quickbooks Online and sync the connection again', }, xero: { organization: 'Xero organization', @@ -2115,6 +2117,8 @@ export default { }, exportPreferredExporterNote: 'This can be any workspace admin, but must be a domain admin if you set different export accounts for individual company cards in domain settings.', exportPreferredExporterSubNote: 'Once set, the preferred exporter will see reports for export in their account.', + noAccountsFound: 'No accounts found', + noAccountsFoundDescription: 'Add the account in Xero and sync the connection again', }, type: { free: 'Free', diff --git a/src/languages/es.ts b/src/languages/es.ts index 6b520ea0d52f..e54f3201bd39 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2070,6 +2070,8 @@ export default { [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY}Error`]: 'El asiento de diario no está disponible cuando los impuestos están habilitados. seleccione una opción de exportación diferente.', }, + noAccountsFound: 'No se ha encontrado ninguna cuenta', + noAccountsFoundDescription: 'Añade la cuenta en Quickbooks Online y sincroniza de nuevo la conexión', }, xero: { organization: 'Organización Xero', @@ -2152,6 +2154,8 @@ export default { exportPreferredExporterNote: 'Puede ser cualquier administrador del espacio de trabajo, pero debe ser un administrador de dominio si configura diferentes cuentas de exportación para tarjetas de empresa individuales en la configuración del dominio.', exportPreferredExporterSubNote: 'Una vez configurado, el exportador preferido verá los informes para exportar en su cuenta.', + noAccountsFound: 'No se ha encontrado ninguna cuenta', + noAccountsFoundDescription: 'Añade la cuenta en Xero y sincroniza de nuevo la conexión', }, type: { free: 'Gratis', diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index 5716812ced16..6ec4552646ad 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -227,8 +227,8 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli <> ( + + ), + [translate, styles.pb10], + ); + return ( diff --git a/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx index 346c74e35fe8..8fd133fddd9e 100644 --- a/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx @@ -1,6 +1,8 @@ import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; +import BlockingView from '@components/BlockingViews/BlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -13,6 +15,7 @@ import Navigation from '@libs/Navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -59,6 +62,20 @@ function QuickbooksInvoiceAccountSelectPage({policy}: WithPolicyConnectionsProps [policyID], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx index 98d81a480ddd..b9cf3cf7cf71 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx @@ -1,5 +1,7 @@ import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -12,6 +14,7 @@ import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Account} from '@src/types/onyx/Policy'; @@ -62,6 +65,20 @@ function QuickbooksCompanyCardExpenseAccountSelectPage({policy}: WithPolicyConne [nonReimbursableExpensesAccount, policyID], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( {translate(`workspace.qbo.accounts.${nonReimbursableExpensesExportDestination}AccountDescription`)} ) : null } - sections={[{data}]} + sections={data.length ? [{data}] : []} ListItem={RadioListItem} onSelectRow={selectExportAccount} initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx index b7c95811b90a..ea34b453fef4 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx @@ -1,5 +1,7 @@ import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -12,6 +14,7 @@ import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Account} from '@src/types/onyx/Policy'; @@ -48,6 +51,20 @@ function QuickbooksExportInvoiceAccountSelectPage({policy}: WithPolicyConnection [receivableAccount, policyID], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( {translate('workspace.qbo.exportInvoicesDescription')}} - sections={[{data}]} + sections={data.length ? [{data}] : []} ListItem={RadioListItem} onSelectRow={selectExportInvoice} initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx index 729914176a0d..6cc62e2171f5 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx @@ -1,5 +1,7 @@ import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -12,6 +14,7 @@ import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -34,7 +37,7 @@ function QuickbooksNonReimbursableDefaultVendorSelectPage({policy}: WithPolicyCo keyForList: vendor.name, isSelected: vendor.id === nonReimbursableBillDefaultVendor, })) ?? []; - return [{data}]; + return data.length ? [{data}] : []; }, [nonReimbursableBillDefaultVendor, vendors]); const selectVendor = useCallback( @@ -47,6 +50,20 @@ function QuickbooksNonReimbursableDefaultVendorSelectPage({policy}: WithPolicyCo [nonReimbursableBillDefaultVendor, policyID], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( mode.isSelected)?.keyForList} + initiallyFocusedOptionKey={sections[0]?.data.find((mode) => mode.isSelected)?.keyForList} + listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx index 0d1c3c7b9ca9..a6709e74977a 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx @@ -1,5 +1,7 @@ import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -12,6 +14,7 @@ import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Account} from '@src/types/onyx/Policy'; @@ -86,6 +89,20 @@ function QuickbooksOutOfPocketExpenseAccountSelectPage({policy}: WithPolicyConne [reimbursableExpensesAccount, policyID], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( {description}} - sections={[{data}]} + sections={data.length ? [{data}] : []} ListItem={RadioListItem} onSelectRow={selectExportAccount} initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/xero/advanced/XeroBillPaymentAccountSelectorPage.tsx b/src/pages/workspace/accounting/xero/advanced/XeroBillPaymentAccountSelectorPage.tsx index 11ac106f516c..34fcd46af5c2 100644 --- a/src/pages/workspace/accounting/xero/advanced/XeroBillPaymentAccountSelectorPage.tsx +++ b/src/pages/workspace/accounting/xero/advanced/XeroBillPaymentAccountSelectorPage.tsx @@ -1,5 +1,7 @@ import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; import RadioListItem from '@components/SelectionList/RadioListItem'; import type {SelectorType} from '@components/SelectionScreen'; import SelectionScreen from '@components/SelectionScreen'; @@ -10,6 +12,7 @@ import * as Connections from '@libs/actions/connections'; import Navigation from '@libs/Navigation/Navigation'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -54,13 +57,27 @@ function XeroBillPaymentAccountSelectorPage({policy}: WithPolicyConnectionsProps [policyID], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_ADVANCED.getRoute(policyID))} title="workspace.xero.advancedConfig.xeroBillPaymentAccount" + listEmptyContent={listEmptyContent} /> ); } diff --git a/src/pages/workspace/accounting/xero/advanced/XeroInvoiceAccountSelectorPage.tsx b/src/pages/workspace/accounting/xero/advanced/XeroInvoiceAccountSelectorPage.tsx index ba7749fef4f2..bc6e4f54d80a 100644 --- a/src/pages/workspace/accounting/xero/advanced/XeroInvoiceAccountSelectorPage.tsx +++ b/src/pages/workspace/accounting/xero/advanced/XeroInvoiceAccountSelectorPage.tsx @@ -1,5 +1,7 @@ import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; import RadioListItem from '@components/SelectionList/RadioListItem'; import type {SelectorType} from '@components/SelectionScreen'; import SelectionScreen from '@components/SelectionScreen'; @@ -10,6 +12,7 @@ import * as Connections from '@libs/actions/connections'; import Navigation from '@libs/Navigation/Navigation'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -54,13 +57,27 @@ function XeroInvoiceAccountSelectorPage({policy}: WithPolicyConnectionsProps) { [policyID], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_ADVANCED.getRoute(policyID))} title="workspace.xero.advancedConfig.xeroInvoiceCollectionAccount" + listEmptyContent={listEmptyContent} /> ); } diff --git a/src/pages/workspace/accounting/xero/export/XeroBankAccountSelectPage.tsx b/src/pages/workspace/accounting/xero/export/XeroBankAccountSelectPage.tsx index 897ed0b37d78..f818c5b3fba6 100644 --- a/src/pages/workspace/accounting/xero/export/XeroBankAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/xero/export/XeroBankAccountSelectPage.tsx @@ -1,5 +1,7 @@ import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; import RadioListItem from '@components/SelectionList/RadioListItem'; import type {SelectorType} from '@components/SelectionScreen'; import SelectionScreen from '@components/SelectionScreen'; @@ -10,6 +12,7 @@ import * as Connections from '@libs/actions/connections'; import Navigation from '@libs/Navigation/Navigation'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -56,19 +59,34 @@ function XeroBankAccountSelectPage({policy}: WithPolicyConnectionsProps) { [policyID, initiallyFocusedOptionKey], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.getRoute(policyID))} title="workspace.xero.xeroBankAccount" + listEmptyContent={listEmptyContent} /> ); } diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts index 4584a38e8171..c963594684cb 100644 --- a/src/styles/utils/spacing.ts +++ b/src/styles/utils/spacing.ts @@ -601,6 +601,10 @@ export default { paddingBottom: 56, }, + pb10: { + paddingBottom: 40, + }, + pb20: { paddingBottom: 80, }, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 7ab469af9533..9b9c34519f4d 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -127,8 +127,8 @@ export default { borderTopWidth: 1, emptyLHNIconWidth: 24, // iconSizeSmall + 4*2 horizontal margin emptyLHNIconHeight: 16, - emptyWorkspaceIconWidth: 84, - emptyWorkspaceIconHeight: 84, + emptyListIconWidth: 136, + emptyListIconHeight: 144, modalTopIconWidth: 200, modalTopIconHeight: 164, modalTopBigIconHeight: 244,