diff --git a/src/components/ImportedFromAccountingSoftware.tsx b/src/components/ImportedFromAccountingSoftware.tsx new file mode 100644 index 000000000000..48c44573702e --- /dev/null +++ b/src/components/ImportedFromAccountingSoftware.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import {View} from 'react-native'; +import useEnvironment from '@hooks/useEnvironment'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getIntegrationIcon} from '@libs/ReportUtils'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {ConnectionName} from '@src/types/onyx/Policy'; +import Icon from './Icon'; +import Text from './Text'; +import TextBlock from './TextBlock'; +import TextLinkBlock from './TextLinkBlock'; + +type ImportedFromAccountingSoftwareProps = { + /** The policy ID to link to */ + policyID: string; + + /** The name of the current connection */ + currentConnectionName: string; + + /** The connected integration */ + connectedIntegration: ConnectionName | undefined; + + /** The translated text for the "imported from" message */ + translatedText: string; +}; + +function ImportedFromAccountingSoftware({policyID, currentConnectionName, translatedText, connectedIntegration}: ImportedFromAccountingSoftwareProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + const {environmentURL} = useEnvironment(); + const icon = getIntegrationIcon(connectedIntegration); + + return ( + + + + ) : undefined + } + /> + . + + ); +} + +ImportedFromAccountingSoftware.displayName = 'ImportedFromAccountingSoftware'; + +export default ImportedFromAccountingSoftware; diff --git a/src/components/TextLink.tsx b/src/components/TextLink.tsx index 2d390f9811e6..715d25e040a9 100644 --- a/src/components/TextLink.tsx +++ b/src/components/TextLink.tsx @@ -4,7 +4,7 @@ import React, {forwardRef} from 'react'; import type {GestureResponderEvent, Text as RNText, StyleProp, TextStyle} from 'react-native'; import useEnvironment from '@hooks/useEnvironment'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Link from '@userActions/Link'; +import {openLink as openLinkUtil} from '@userActions/Link'; import CONST from '@src/CONST'; import type {TextProps} from './Text'; import Text from './Text'; @@ -40,7 +40,7 @@ function TextLink({href, onPress, children, style, onMouseDown = (event) => even if (onPress) { onPress(event); } else { - Link.openLink(href, environmentURL); + openLinkUtil(href, environmentURL); } }; @@ -79,6 +79,6 @@ function TextLink({href, onPress, children, style, onMouseDown = (event) => even TextLink.displayName = 'TextLink'; -export type {LinkProps, PressProps}; +export type {LinkProps, PressProps, TextLinkProps}; export default forwardRef(TextLink); diff --git a/src/components/TextLinkBlock.tsx b/src/components/TextLinkBlock.tsx new file mode 100644 index 000000000000..ce0454b0ae26 --- /dev/null +++ b/src/components/TextLinkBlock.tsx @@ -0,0 +1,72 @@ +/** + * TextLinkBlock component splits a given text into individual words and displays + * each word within a TextLink component so the link text wraps naturally. + */ +import React, {memo, useMemo} from 'react'; +import type {StyleProp, TextStyle} from 'react-native'; +import {View} from 'react-native'; +import useEnvironment from '@hooks/useEnvironment'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {openLink as openLinkUtil} from '@userActions/Link'; +import CONST from '@src/CONST'; +import {PressableWithoutFeedback} from './Pressable'; +import Text from './Text'; +import TextLink from './TextLink'; +import type {LinkProps, PressProps} from './TextLink'; + +type TextLinkBlockProps = (LinkProps | PressProps) & { + /** Styles to apply to each word */ + style?: StyleProp; + + /** The full text to be split into words */ + text: string; + + /** The full text to be split into words */ + prefixIcon?: React.JSX.Element; +}; + +function TextLinkBlock({text, style, prefixIcon, ...rest}: TextLinkBlockProps) { + const words = useMemo(() => text.match(/(\S+\s*)/g) ?? [], [text]); + + const {environmentURL} = useEnvironment(); + const styles = useThemeStyles(); + + const openLink = () => { + if (!rest.href) { + return; + } + openLinkUtil(rest.href, environmentURL); + }; + + return ( + + {words.map((word, index) => ( + + {prefixIcon && index === 0 && prefixIcon} + {!!prefixIcon && index === 0 && } + + {word} + + + ))} + + ); +} + +TextLinkBlock.displayName = 'TextLinkBlock'; + +export default memo(TextLinkBlock); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index ecbdfc03ae11..f4ecc1c4fe45 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -9,6 +9,7 @@ import EmptyStateComponent from '@components/EmptyStateComponent'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; +import ImportedFromAccountingSoftware from '@components/ImportedFromAccountingSoftware'; import LottieAnimations from '@components/LottieAnimations'; import RenderHTML from '@components/RenderHTML'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -21,7 +22,6 @@ import CustomListHeader from '@components/SelectionListWithModal/CustomListHeade import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; import Switch from '@components/Switch'; import Text from '@components/Text'; -import TextLink from '@components/TextLink'; import useAutoTurnSelectionModeOffWhenHasNoActiveOption from '@hooks/useAutoTurnSelectionModeOffWhenHasNoActiveOption'; import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; import useEnvironment from '@hooks/useEnvironment'; @@ -440,17 +440,13 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const headerContent = ( <> - {!hasSyncError && isConnectionVerified ? ( - - {`${translate('workspace.categories.importedFromAccountingSoftware')} `} - - {`${currentConnectionName} ${translate('workspace.accounting.settings')}`} - - . - + {!hasSyncError && isConnectionVerified && currentConnectionName ? ( + ) : ( {translate('workspace.categories.subtitle')} )} diff --git a/src/pages/workspace/reports/WorkspaceReportsPage.tsx b/src/pages/workspace/reports/WorkspaceReportsPage.tsx index 868af9d3d7d9..90015b13e7cb 100644 --- a/src/pages/workspace/reports/WorkspaceReportsPage.tsx +++ b/src/pages/workspace/reports/WorkspaceReportsPage.tsx @@ -7,6 +7,7 @@ import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {Plus} from '@components/Icon/Expensicons'; import {ReportReceipt} from '@components/Icon/Illustrations'; +import ImportedFromAccountingSoftware from '@components/ImportedFromAccountingSoftware'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -16,8 +17,6 @@ import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; -import TextLink from '@components/TextLink'; -import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -69,7 +68,6 @@ function WorkspaceReportFieldsPage({ const [isReportFieldsWarningModalOpen, setIsReportFieldsWarningModalOpen] = useState(false); const policy = usePolicy(policyID); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`, {canBeMissing: true}); - const {environmentURL} = useEnvironment(); const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy); const hasSyncError = shouldShowSyncError(policy, isSyncInProgress); const connectedIntegration = getConnectedIntegration(policy) ?? connectionSyncProgress?.connectionName; @@ -127,16 +125,14 @@ function WorkspaceReportFieldsPage({ ); const getHeaderText = () => - !hasSyncError && isConnectionVerified ? ( + !hasSyncError && isConnectionVerified && currentConnectionName ? ( - {`${translate('workspace.reportFields.importedFromAccountingSoftware')} `} - - {`${currentConnectionName} ${translate('workspace.accounting.settings')}`} - - . + ) : ( {translate('workspace.reportFields.subtitle')} diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index ebac3e9729ce..558c8effac58 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -9,6 +9,7 @@ import EmptyStateComponent from '@components/EmptyStateComponent'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; +import ImportedFromAccountingSoftware from '@components/ImportedFromAccountingSoftware'; import LottieAnimations from '@components/LottieAnimations'; import RenderHTML from '@components/RenderHTML'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -604,17 +605,13 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const headerContent = ( <> - {!hasSyncError && isConnectionVerified ? ( - - {`${translate('workspace.tags.importedFromAccountingSoftware')} `} - - {`${currentConnectionName} ${translate('workspace.accounting.settings')}`} - - . - + {!hasSyncError && isConnectionVerified && currentConnectionName ? ( + ) : ( {translate('workspace.tags.subtitle')} diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index ce446a3b81ba..7e9d0d3d1ada 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -7,6 +7,7 @@ import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; +import ImportedFromAccountingSoftware from '@components/ImportedFromAccountingSoftware'; import ScreenWrapper from '@components/ScreenWrapper'; import SearchBar from '@components/SearchBar'; import TableListItem from '@components/SelectionList/TableListItem'; @@ -15,9 +16,7 @@ import SelectionListWithModal from '@components/SelectionListWithModal'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; import Switch from '@components/Switch'; import Text from '@components/Text'; -import TextLink from '@components/TextLink'; import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; -import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; @@ -66,7 +65,6 @@ function WorkspaceTaxesPage({ const styles = useThemeStyles(); const theme = useTheme(); const {translate, localeCompare} = useLocalize(); - const {environmentURL} = useEnvironment(); const [selectedTaxesIDs, setSelectedTaxesIDs] = useState([]); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const isMobileSelectionModeEnabled = useMobileSelectionMode(); @@ -362,17 +360,13 @@ function WorkspaceTaxesPage({ const headerContent = ( <> - {!hasSyncError && isConnectionVerified ? ( - - {`${translate('workspace.taxes.importedFromAccountingSoftware')} `} - - {`${currentConnectionName} ${translate('workspace.accounting.settings')}`} - - . - + {!hasSyncError && isConnectionVerified && currentConnectionName ? ( + ) : ( {translate('workspace.taxes.subtitle')} )} diff --git a/src/styles/utils/display.ts b/src/styles/utils/display.ts index 0f0166e135c9..dc2ee88fe68c 100644 --- a/src/styles/utils/display.ts +++ b/src/styles/utils/display.ts @@ -19,6 +19,14 @@ export default { display: 'none', }, + /** + * display: 'contents' - Supported in React Native 0.77+ + * Ref: https://reactnative.dev/blog/2025/01/21/version-0.77#simpler-layouts-with-display-contents + */ + dContents: { + display: 'contents', + }, + /** * Web-only style. */