Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 66 additions & 0 deletions src/components/ImportedFromAccountingSoftware.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View style={[styles.alignItemsCenter, styles.flexRow, styles.flexWrap]}>
<TextBlock
textStyles={[styles.textNormal, styles.colorMuted]}
text={`${translatedText} `}
/>
<TextLinkBlock
style={[styles.textNormal, styles.link]}
href={`${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(policyID)}`}
text={`${currentConnectionName} ${translate('workspace.accounting.settings')}`}
prefixIcon={
icon ? (
<Icon
src={icon}
height={variables.iconSizeMedium}
width={variables.iconSizeMedium}
additionalStyles={[StyleUtils.getAvatarBorderStyle(CONST.AVATAR_SIZE.SMALLER, ''), styles.appBG]}
/>
) : undefined
}
/>
<Text style={[styles.textNormal, styles.colorMuted]}>.</Text>
</View>
);
}

ImportedFromAccountingSoftware.displayName = 'ImportedFromAccountingSoftware';

export default ImportedFromAccountingSoftware;
6 changes: 3 additions & 3 deletions src/components/TextLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
};

Expand Down Expand Up @@ -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);
72 changes: 72 additions & 0 deletions src/components/TextLinkBlock.tsx
Original file line number Diff line number Diff line change
@@ -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<TextStyle>;

/** 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 (
<PressableWithoutFeedback
role={CONST.ROLE.BUTTON}
style={styles.dContents}
onPress={openLink}
accessible
accessibilityLabel={rest.href ?? CONST.ROLE.BUTTON}
>
{words.map((word, index) => (
<View
// eslint-disable-next-line react/no-array-index-key
key={`${word}-${index}`}
style={[styles.dInlineFlex, styles.alignItemsCenter, styles.flexRow]}
>
{prefixIcon && index === 0 && prefixIcon}
{!!prefixIcon && index === 0 && <Text> </Text>}
Comment on lines +55 to +56

@fedirjh fedirjh Aug 18, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{prefixIcon && index === 0 && prefixIcon}
{!!prefixIcon && index === 0 && <Text> </Text>}
{!!(prefixIcon && index === 0) && (
<TextLink
style={style}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
>
{prefixIcon}{' '}
</TextLink>
)}

Let's make the icon as a link as well.

<TextLink
style={style}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
>
{word}
</TextLink>
</View>
))}
</PressableWithoutFeedback>
);
}

TextLinkBlock.displayName = 'TextLinkBlock';

export default memo(TextLinkBlock);
20 changes: 8 additions & 12 deletions src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -440,17 +440,13 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
const headerContent = (
<>
<View style={[styles.ph5, styles.pb5, styles.pt3, shouldUseNarrowLayout ? styles.workspaceSectionMobile : styles.workspaceSection]}>
{!hasSyncError && isConnectionVerified ? (
<Text>
<Text style={[styles.textNormal, styles.colorMuted]}>{`${translate('workspace.categories.importedFromAccountingSoftware')} `}</Text>
<TextLink
style={[styles.textNormal, styles.link]}
href={`${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(policyId)}`}
>
{`${currentConnectionName} ${translate('workspace.accounting.settings')}`}
</TextLink>
<Text style={[styles.textNormal, styles.colorMuted]}>.</Text>
</Text>
{!hasSyncError && isConnectionVerified && currentConnectionName ? (
<ImportedFromAccountingSoftware
policyID={policyId}
currentConnectionName={currentConnectionName}
connectedIntegration={connectedIntegration}
translatedText={translate('workspace.categories.importedFromAccountingSoftware')}
/>
) : (
<Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.categories.subtitle')}</Text>
)}
Expand Down
20 changes: 8 additions & 12 deletions src/pages/workspace/reports/WorkspaceReportsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -127,16 +125,14 @@ function WorkspaceReportFieldsPage({
);

const getHeaderText = () =>
!hasSyncError && isConnectionVerified ? (
!hasSyncError && isConnectionVerified && currentConnectionName ? (
<Text style={[styles.mr5, styles.mt1]}>
<Text style={[styles.textNormal, styles.colorMuted]}>{`${translate('workspace.reportFields.importedFromAccountingSoftware')} `}</Text>
<TextLink
style={[styles.textNormal, styles.link]}
href={`${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(policyID)}`}
>
{`${currentConnectionName} ${translate('workspace.accounting.settings')}`}
</TextLink>
<Text style={[styles.textNormal, styles.colorMuted]}>.</Text>
<ImportedFromAccountingSoftware
policyID={policyID}
currentConnectionName={currentConnectionName}
connectedIntegration={connectedIntegration}
translatedText={translate('workspace.reportFields.importedFromAccountingSoftware')}
/>
</Text>
) : (
<Text style={[styles.textNormal, styles.colorMuted, styles.mr5, styles.mt1]}>{translate('workspace.reportFields.subtitle')}</Text>
Expand Down
19 changes: 8 additions & 11 deletions src/pages/workspace/tags/WorkspaceTagsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -604,17 +605,13 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
const headerContent = (
<>
<View style={[styles.ph5, styles.pb5, styles.pt3, shouldUseNarrowLayout ? styles.workspaceSectionMobile : undefined]}>
{!hasSyncError && isConnectionVerified ? (
<Text>
<Text style={[styles.textNormal, styles.colorMuted]}>{`${translate('workspace.tags.importedFromAccountingSoftware')} `}</Text>
<TextLink
style={[styles.textNormal, styles.link]}
href={`${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(policyID)}`}
>
{`${currentConnectionName} ${translate('workspace.accounting.settings')}`}
</TextLink>
<Text style={[styles.textNormal, styles.colorMuted]}>.</Text>
</Text>
{!hasSyncError && isConnectionVerified && currentConnectionName ? (
<ImportedFromAccountingSoftware
policyID={policyID}
currentConnectionName={currentConnectionName}
connectedIntegration={connectedIntegration}
translatedText={translate('workspace.tags.importedFromAccountingSoftware')}
/>
) : (
<Text style={[styles.textNormal, styles.colorMuted]}>
{translate('workspace.tags.subtitle')}
Expand Down
22 changes: 8 additions & 14 deletions src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -66,7 +65,6 @@ function WorkspaceTaxesPage({
const styles = useThemeStyles();
const theme = useTheme();
const {translate, localeCompare} = useLocalize();
const {environmentURL} = useEnvironment();
const [selectedTaxesIDs, setSelectedTaxesIDs] = useState<string[]>([]);
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const isMobileSelectionModeEnabled = useMobileSelectionMode();
Expand Down Expand Up @@ -362,17 +360,13 @@ function WorkspaceTaxesPage({
const headerContent = (
<>
<View style={[styles.ph5, styles.pb5, styles.pt3, shouldUseNarrowLayout ? styles.workspaceSectionMobile : styles.workspaceSection]}>
{!hasSyncError && isConnectionVerified ? (
<Text>
<Text style={[styles.textNormal, styles.colorMuted]}>{`${translate('workspace.taxes.importedFromAccountingSoftware')} `}</Text>
<TextLink
style={[styles.textNormal, styles.link]}
href={`${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(policyID)}`}
>
{`${currentConnectionName} ${translate('workspace.accounting.settings')}`}
</TextLink>
<Text style={[styles.textNormal, styles.colorMuted]}>.</Text>
</Text>
{!hasSyncError && isConnectionVerified && currentConnectionName ? (
<ImportedFromAccountingSoftware
policyID={policyID}
currentConnectionName={currentConnectionName}
connectedIntegration={connectedIntegration}
translatedText={translate('workspace.taxes.importedFromAccountingSoftware')}
/>
) : (
<Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.taxes.subtitle')}</Text>
)}
Expand Down
8 changes: 8 additions & 0 deletions src/styles/utils/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Loading