diff --git a/Mobile-Expensify b/Mobile-Expensify index 72b75fcae7bc..778d69514a1e 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 72b75fcae7bcf934c72700cbf2a3a09e99ff9dde +Subproject commit 778d69514a1ea7a193fcb7cbfba2afda770cd8b7 diff --git a/android/app/build.gradle b/android/app/build.gradle index 6d12e4a11c5c..fa6bf3b1576b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -114,8 +114,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009011003 - versionName "9.1.10-3" + versionCode 1009011004 + versionName "9.1.10-4" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 690dc8ee674c..168ebc8da002 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -44,7 +44,7 @@ CFBundleVersion - 9.1.10.3 + 9.1.10.4 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index ef0e833f212e..e59650126d3a 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.1.10.3 + 9.1.10.4 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 76c07bb4b96b..22c334f727c5 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.1.10 CFBundleVersion - 9.1.10.3 + 9.1.10.4 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index cd87d0896967..9bdc6f3c60a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.1.10-3", + "version": "9.1.10-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.1.10-3", + "version": "9.1.10-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d8e6431b6706..7005a913cf43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.1.10-3", + "version": "9.1.10-4", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.ts b/src/CONST.ts index fb4e6a8d0dbe..da92524cdef5 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3445,6 +3445,7 @@ const CONST = { WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH: 256, REPORT_NAME_LIMIT: 100, TITLE_CHARACTER_LIMIT: 100, + TASK_TITLE_CHARACTER_LIMIT: 10000, DESCRIPTION_LIMIT: 1000, SEARCH_QUERY_LIMIT: 1000, WORKSPACE_NAME_CHARACTER_LIMIT: 80, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 8bf810e70a6b..c20fc04371c6 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -524,28 +524,48 @@ const ROUTES = { }, MONEY_REQUEST_STEP_AMOUNT: { route: ':action/:iouType/amount/:transactionID/:reportID/:pageIndex?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, pageIndex: string, backTo = '') => - getUrlWithBackToParam(`${action as string}/${iouType as string}/amount/${transactionID}/${reportID}/${pageIndex}`, backTo), + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, pageIndex: string, backTo = '') => { + if (!transactionID || !reportID) { + Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_AMOUNT route'); + } + return getUrlWithBackToParam(`${action as string}/${iouType as string}/amount/${transactionID}/${reportID}/${pageIndex}`, backTo); + }, }, MONEY_REQUEST_STEP_TAX_RATE: { route: ':action/:iouType/taxRate/:transactionID/:reportID?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`${action as string}/${iouType as string}/taxRate/${transactionID}/${reportID}`, backTo), + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '') => { + if (!transactionID || !reportID) { + Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_TAX_RATE route'); + } + return getUrlWithBackToParam(`${action as string}/${iouType as string}/taxRate/${transactionID}/${reportID}`, backTo); + }, }, MONEY_REQUEST_STEP_TAX_AMOUNT: { route: ':action/:iouType/taxAmount/:transactionID/:reportID?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`${action as string}/${iouType as string}/taxAmount/${transactionID}/${reportID}`, backTo), + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '') => { + if (!transactionID || !reportID) { + Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_TAX_AMOUNT route'); + } + return getUrlWithBackToParam(`${action as string}/${iouType as string}/taxAmount/${transactionID}/${reportID}`, backTo); + }, }, MONEY_REQUEST_STEP_CATEGORY: { route: ':action/:iouType/category/:transactionID/:reportID/:reportActionID?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string | undefined, backTo = '', reportActionID?: string) => - getUrlWithBackToParam(`${action as string}/${iouType as string}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', reportActionID?: string) => { + if (!transactionID || !reportID) { + Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_CATEGORY route'); + } + return getUrlWithBackToParam(`${action as string}/${iouType as string}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo); + }, }, MONEY_REQUEST_ATTENDEE: { route: ':action/:iouType/attendees/:transactionID/:reportID', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`${action as string}/${iouType as string}/attendees/${transactionID}/${reportID}`, backTo), + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '') => { + if (!transactionID || !reportID) { + Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_ATTENDEE route'); + } + return getUrlWithBackToParam(`${action as string}/${iouType as string}/attendees/${transactionID}/${reportID}`, backTo); + }, }, MONEY_REQUEST_UPGRADE: { route: ':action/:iouType/upgrade/:transactionID/:reportID', @@ -680,28 +700,48 @@ const ROUTES = { }, MONEY_REQUEST_STEP_DATE: { route: ':action/:iouType/date/:transactionID/:reportID/:reportActionID?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '', reportActionID?: string) => - getUrlWithBackToParam(`${action as string}/${iouType as string}/date/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', reportActionID?: string) => { + if (!transactionID || !reportID) { + Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_DATE route'); + } + return getUrlWithBackToParam(`${action as string}/${iouType as string}/date/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo); + }, }, MONEY_REQUEST_STEP_DESCRIPTION: { route: ':action/:iouType/description/:transactionID/:reportID/:reportActionID?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '', reportActionID?: string) => - getUrlWithBackToParam(`${action as string}/${iouType as string}/description/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', reportActionID?: string) => { + if (!transactionID || !reportID) { + Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_DESCRIPTION route'); + } + return getUrlWithBackToParam(`${action as string}/${iouType as string}/description/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo); + }, }, MONEY_REQUEST_STEP_DISTANCE: { route: ':action/:iouType/distance/:transactionID/:reportID', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`${action as string}/${iouType as string}/distance/${transactionID}/${reportID}`, backTo), + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '') => { + if (!transactionID || !reportID) { + Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_DISTANCE route'); + } + return getUrlWithBackToParam(`${action as string}/${iouType as string}/distance/${transactionID}/${reportID}`, backTo); + }, }, MONEY_REQUEST_STEP_DISTANCE_RATE: { route: ':action/:iouType/distanceRate/:transactionID/:reportID', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`${action as string}/${iouType as string}/distanceRate/${transactionID}/${reportID}`, backTo), + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '') => { + if (!transactionID || !reportID) { + Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_DISTANCE_RATE route'); + } + return getUrlWithBackToParam(`${action as string}/${iouType as string}/distanceRate/${transactionID}/${reportID}`, backTo); + }, }, MONEY_REQUEST_STEP_MERCHANT: { route: ':action/:iouType/merchant/:transactionID/:reportID', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`${action as string}/${iouType as string}/merchant/${transactionID}/${reportID}`, backTo), + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '') => { + if (!transactionID || !reportID) { + Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_MERCHANT route'); + } + return getUrlWithBackToParam(`${action as string}/${iouType as string}/merchant/${transactionID}/${reportID}`, backTo); + }, }, MONEY_REQUEST_STEP_PARTICIPANTS: { route: ':action/:iouType/participants/:transactionID/:reportID', @@ -1657,12 +1697,21 @@ const ROUTES = { TRACK_TRAINING_MODAL: 'track-training', TRAVEL_TRIP_SUMMARY: { route: 'r/:reportID/trip/:transactionID', - getRoute: (reportID: string, transactionID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}`, backTo), + getRoute: (reportID: string | undefined, transactionID: string | undefined, backTo?: string) => { + if (!reportID || !transactionID) { + Log.warn('Invalid reportID or transactionID is used to build the TRAVEL_TRIP_SUMMARY route'); + } + return getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}`, backTo); + }, }, TRAVEL_TRIP_DETAILS: { route: 'r/:reportID/trip/:transactionID/:reservationIndex', - getRoute: (reportID: string, transactionID: string, reservationIndex: number, backTo?: string) => - getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}/${reservationIndex}`, backTo), + getRoute: (reportID: string | undefined, transactionID: string | undefined, reservationIndex: number, backTo?: string) => { + if (!reportID || !transactionID) { + Log.warn('Invalid reportID or transactionID is used to build the TRAVEL_TRIP_DETAILS route'); + } + return getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}/${reservationIndex}`, backTo); + }, }, TRAVEL_DOMAIN_SELECTOR: 'travel/domain-selector', TRAVEL_DOMAIN_PERMISSION_INFO: { diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx index d30f617c2a5f..ee0fa8b81588 100644 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx +++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx @@ -9,7 +9,7 @@ import Tooltip from '@components/Tooltip'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {BaseAnchorForCommentsOnlyProps, LinkProps} from './types'; @@ -17,7 +17,19 @@ import type {BaseAnchorForCommentsOnlyProps, LinkProps} from './types'; /* * This is a default anchor component for regular links. */ -function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', target = '', children = null, style, onPress, linkHasImage, ...rest}: BaseAnchorForCommentsOnlyProps) { +function BaseAnchorForCommentsOnly({ + onPressIn, + onPressOut, + href = '', + rel = '', + target = '', + children = null, + style, + onPress, + linkHasImage, + wrapperStyle, + ...rest +}: BaseAnchorForCommentsOnlyProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const linkRef = useRef(null); @@ -38,7 +50,7 @@ function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', } else { linkProps.href = href; } - const defaultTextStyle = DeviceCapabilities.canUseTouchScreen() || shouldUseNarrowLayout ? {} : {...styles.userSelectText, ...styles.cursorPointer}; + const defaultTextStyle = canUseTouchScreen() || shouldUseNarrowLayout ? {} : {...styles.userSelectText, ...styles.cursorPointer}; const isEmail = Str.isValidEmail(href.replace(/mailto:/i, '')); const linkHref = !linkHasImage ? href : undefined; @@ -62,6 +74,7 @@ function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', onPressOut={onPressOut} role={CONST.ROLE.LINK} accessibilityLabel={href} + wrapperStyle={wrapperStyle} > ; + /** Any additional styles to apply to the wrapper */ + wrapperStyle?: StyleProp; + /** Press handler for the link, when not passed, default href is used to create a link like behaviour */ onPress?: () => void; diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 12b515194928..57d9699dd697 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -32,6 +32,11 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim tagName: 'edited', contentModel: HTMLContentModel.textual, }), + 'task-title': HTMLElementModel.fromCustomModel({ + tagName: 'task-title', + contentModel: HTMLContentModel.block, + mixedUAStyles: {...styles.taskTitleMenuItem}, + }), 'alert-text': HTMLElementModel.fromCustomModel({ tagName: 'alert-text', mixedUAStyles: {...styles.formError, ...styles.mb0}, @@ -119,6 +124,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim styles.mutedNormalTextLabel, styles.onlyEmojisText, styles.onlyEmojisTextLineHeight, + styles.taskTitleMenuItem, ], ); /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx index 493ddec5a5d0..b4d163306cb3 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx @@ -8,8 +8,8 @@ import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils import Text from '@components/Text'; import useEnvironment from '@hooks/useEnvironment'; import useThemeStyles from '@hooks/useThemeStyles'; +import {getInternalExpensifyPath, getInternalNewExpensifyPath, openLink} from '@libs/actions/Link'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; -import * as Link from '@userActions/Link'; import CONST from '@src/CONST'; type AnchorRendererProps = CustomRendererProps & { @@ -27,22 +27,24 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { const displayName = tNodeChild && 'data' in tNodeChild && typeof tNodeChild.data === 'string' ? tNodeChild.data : ''; const attrHref = htmlAttribs.href || htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] || ''; const parentStyle = tnode.parent?.styles?.nativeTextRet ?? {}; - const internalNewExpensifyPath = Link.getInternalNewExpensifyPath(attrHref); - const internalExpensifyPath = Link.getInternalExpensifyPath(attrHref); + const internalNewExpensifyPath = getInternalNewExpensifyPath(attrHref); + const internalExpensifyPath = getInternalExpensifyPath(attrHref); const isVideo = attrHref && Str.isVideo(attrHref); const linkHasImage = tnode.tagName === 'a' && tnode.children.some((child) => child.tagName === 'img'); const isDeleted = HTMLEngineUtils.isDeletedNode(tnode); + const isChildOfTaskTitle = HTMLEngineUtils.isChildOfTaskTitle(tnode); + const textDecorationLineStyle = isDeleted ? styles.underlineLineThrough : {}; - if (!HTMLEngineUtils.isChildOfComment(tnode)) { + if (!HTMLEngineUtils.isChildOfComment(tnode) && !isChildOfTaskTitle) { // This is not a comment from a chat, the AnchorForCommentsOnly uses a Pressable to create a context menu on right click. // We don't have this behaviour in other links in NewDot // TODO: We should use TextLink, but I'm leaving it as Text for now because TextLink breaks the alignment in Android. return ( Link.openLink(attrHref, environmentURL, isAttachment)} + onPress={() => openLink(attrHref, environmentURL, isAttachment)} suppressHighlighting > @@ -70,10 +72,10 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { // eslint-disable-next-line react/jsx-props-no-multi-spaces target={htmlAttribs.target || '_blank'} rel={htmlAttribs.rel || 'noopener noreferrer'} - style={[style, parentStyle, textDecorationLineStyle, styles.textUnderlinePositionUnder, styles.textDecorationSkipInkNone]} + style={[style, parentStyle, textDecorationLineStyle, styles.textUnderlinePositionUnder, styles.textDecorationSkipInkNone, isChildOfTaskTitle && styles.taskTitleMenuItem]} key={key} // Only pass the press handler for internal links. For public links or whitelisted internal links fallback to default link handling - onPress={internalNewExpensifyPath || internalExpensifyPath ? () => Link.openLink(attrHref, environmentURL, isAttachment) : undefined} + onPress={internalNewExpensifyPath || internalExpensifyPath ? () => openLink(attrHref, environmentURL, isAttachment) : undefined} linkHasImage={linkHasImage} > ) { + const styles = useThemeStyles(); + const isChildOfTaskTitle = HTMLEngineUtils.isChildOfTaskTitle(tnode); + + return 'data' in tnode ? ( + {tnode.data} + ) : ( + { + return ( + + {props.childElement} + + ); + }} + /> + ); +} + +EMRenderer.displayName = 'EMRenderer'; + +export default EMRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/HeadingRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/HeadingRenderer.tsx new file mode 100644 index 000000000000..9d7c402e17fd --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/HeadingRenderer.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; +import {TNodeChildrenRenderer} from 'react-native-render-html'; +import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; + +function HeadingRenderer({tnode}: CustomRendererProps) { + const styles = useThemeStyles(); + const isChildOfTaskTitle = HTMLEngineUtils.isChildOfTaskTitle(tnode); + + return ( + { + return ( + + {props.childElement} + + ); + }} + /> + ); +} + +HeadingRenderer.displayName = 'HeadingRenderer'; + +export default HeadingRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx index 1db7df36fb8c..dd20a95de0ea 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx @@ -2,11 +2,14 @@ import React from 'react'; import {View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; import type {CustomRendererProps, TBlock} from 'react-native-render-html'; +import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import {ShowContextMenuContext, showContextMenuForReport} from '@components/ShowContextMenuContext'; +import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ReportUtils from '@libs/ReportUtils'; +import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; type PreRendererProps = CustomRendererProps & { @@ -28,9 +31,13 @@ type PreRendererProps = CustomRendererProps & { function PreRenderer({TDefaultRenderer, onPressIn, onPressOut, onLongPress, ...defaultRendererProps}: PreRendererProps) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const isLast = defaultRendererProps.renderIndex === defaultRendererProps.renderLength - 1; + const isInsideTaskTitle = HTMLEngineUtils.isChildOfTaskTitle(defaultRendererProps.tnode); + const fontSize = StyleUtils.getCodeFontSize(false, isInsideTaskTitle); + return ( @@ -43,15 +50,17 @@ function PreRenderer({TDefaultRenderer, onPressIn, onPressOut, onLongPress, ...d if (isDisabled) { return; } - showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedNonExpenseReport(report, reportNameValuePairs)); + showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, isArchivedNonExpenseReport(report, reportNameValuePairs)); }} shouldUseHapticsOnLongPress role={CONST.ROLE.PRESENTATION} accessibilityLabel={translate('accessibilityHints.prestyledText')} > - {/* eslint-disable-next-line react/jsx-props-no-spreading */} - + + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + )} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/StrongRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/StrongRenderer.tsx new file mode 100644 index 000000000000..d21de8e6e170 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/StrongRenderer.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; +import {TNodeChildrenRenderer} from 'react-native-render-html'; +import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; + +function StrongRenderer({tnode}: CustomRendererProps) { + const styles = useThemeStyles(); + const isChildOfTaskTitle = HTMLEngineUtils.isChildOfTaskTitle(tnode); + + return 'data' in tnode ? ( + {tnode.data} + ) : ( + { + return ( + + {props.childElement} + + ); + }} + /> + ); +} + +StrongRenderer.displayName = 'StrongRenderer'; + +export default StrongRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/TaskTitleRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/TaskTitleRenderer.tsx new file mode 100644 index 000000000000..545df0ba26ab --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/TaskTitleRenderer.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; +import {TNodeChildrenRenderer} from 'react-native-render-html'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; + +function TaskTitleRenderer({tnode}: CustomRendererProps) { + const styles = useThemeStyles(); + + return ( + { + return ( + + {props.childElement} + + ); + }} + /> + ); +} + +TaskTitleRenderer.displayName = 'TaskTitleRenderer'; + +export default TaskTitleRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index 91ed66f8b931..b8bd12da24fe 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -4,12 +4,16 @@ import CodeRenderer from './CodeRenderer'; import DeletedActionRenderer from './DeletedActionRenderer'; import EditedRenderer from './EditedRenderer'; import EmojiRenderer from './EmojiRenderer'; +import EMRenderer from './EMRenderer'; +import HeadingRenderer from './HeadingRenderer'; import ImageRenderer from './ImageRenderer'; import MentionHereRenderer from './MentionHereRenderer'; import MentionReportRenderer from './MentionReportRenderer'; import MentionUserRenderer from './MentionUserRenderer'; import NextStepEmailRenderer from './NextStepEmailRenderer'; import PreRenderer from './PreRenderer'; +import StrongRenderer from './StrongRenderer'; +import TaskTitleRenderer from './TaskTitleRenderer'; import VideoRenderer from './VideoRenderer'; /** @@ -21,11 +25,15 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { code: CodeRenderer, img: ImageRenderer, video: VideoRenderer, + h1: HeadingRenderer, + strong: StrongRenderer, + em: EMRenderer, // Custom tag renderers edited: EditedRenderer, pre: PreRenderer, /* eslint-disable @typescript-eslint/naming-convention */ + 'task-title': TaskTitleRenderer, 'mention-user': MentionUserRenderer, 'mention-report': MentionReportRenderer, 'mention-here': MentionHereRenderer, diff --git a/src/components/HTMLEngineProvider/htmlEngineUtils.ts b/src/components/HTMLEngineProvider/htmlEngineUtils.ts index fba467add14b..f94339820fbf 100644 --- a/src/components/HTMLEngineProvider/htmlEngineUtils.ts +++ b/src/components/HTMLEngineProvider/htmlEngineUtils.ts @@ -59,6 +59,10 @@ function isChildOfH1(tnode: TNode): boolean { return isChildOfNode(tnode, (node) => node.domNode?.name !== undefined && node.domNode.name.toLowerCase() === 'h1'); } +function isChildOfTaskTitle(tnode: TNode): boolean { + return isChildOfNode(tnode, (node) => node.domNode?.name !== undefined && node.domNode.name.toLowerCase() === 'task-title'); +} + /** * Check if the parent node has deleted style. */ @@ -67,4 +71,4 @@ function isDeletedNode(tnode: TNode): boolean { return 'textDecorationLine' in parentStyle && parentStyle.textDecorationLine === 'line-through'; } -export {computeEmbeddedMaxWidth, isChildOfComment, isCommentTag, isChildOfH1, isDeletedNode}; +export {computeEmbeddedMaxWidth, isChildOfComment, isCommentTag, isChildOfH1, isDeletedNode, isChildOfTaskTitle}; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 08f0cbc3fcf6..7e9f631c32f4 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -33,6 +33,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {TransactionDuplicateNavigatorParamList} from '@libs/Navigation/types'; import {getAvatarsForAccountIDs} from '@libs/OptionsListUtils'; +import Parser from '@libs/Parser'; import {getCleanedTagName} from '@libs/PolicyUtils'; import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils'; import {getOriginalMessage, getReportAction, isMessageDeleted, isMoneyRequestAction as isMoneyRequestActionReportActionsUtils} from '@libs/ReportActionsUtils'; @@ -140,7 +141,7 @@ function MoneyRequestPreviewContent({ category, } = useMemo>(() => getTransactionDetails(transaction) ?? {}, [transaction]); - const description = truncate(StringUtils.lineBreaksToSpaces(requestComment), {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); + const description = truncate(StringUtils.lineBreaksToSpaces(Parser.htmlToMarkdown(requestComment ?? '')), {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const hasReceipt = hasReceiptTransactionUtils(transaction); const isScanning = hasReceipt && isReceiptBeingScanned(transaction); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index b26c36830798..8296ff2b436a 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -585,7 +585,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals 0; @@ -83,7 +79,6 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che const avatarSize = CONST.AVATAR_SIZE.SMALL; const isDeletedParentAction = isCanceledTaskReport(taskReport, action); const iconWrapperStyle = StyleUtils.getTaskPreviewIconWrapper(hasAssignee ? avatarSize : undefined); - const titleStyle = StyleUtils.getTaskPreviewTitleStyle(iconWrapperStyle.height, isTaskCompleted); const shouldShowGreenDotIndicator = isOpenTaskReport(taskReport, action) && isReportManager(taskReport); if (isDeletedParentAction) { @@ -131,7 +126,9 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che )} - {taskTitle} + + ${taskReport?.reportName ?? action?.childReportName ?? ''}`} /> + {shouldShowGreenDotIndicator && ( diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 01c8c1add718..decb20f8212a 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -10,6 +10,7 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; +import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; @@ -41,13 +42,14 @@ function TaskView({report}: TaskViewProps) { useEffect(() => { setTaskReport(report); }, [report]); - - const taskTitle = convertToLTR(report?.reportName ?? ''); + const taskTitle = `${convertToLTR(report?.reportName ?? '')}`; const assigneeTooltipDetails = getDisplayNamesWithTooltips(getPersonalDetailsForAccountIDs(report?.managerID ? [report?.managerID] : [], personalDetails), false); + const isOpen = isOpenTaskReport(report); const isCompleted = isCompletedTaskReport(report); const canModifyTask = canModifyTaskUtil(report, currentUserPersonalDetails.accountID); const canActionTask = canActionTaskUtil(report, currentUserPersonalDetails.accountID); + const disableState = !canModifyTask; const isDisableInteractive = !canModifyTask || !isOpen; const {translate} = useLocalize(); @@ -107,12 +109,7 @@ function TaskView({report}: TaskViewProps) { disabled={!canActionTask} /> - - {taskTitle} - + {!isDisableInteractive && ( diff --git a/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts b/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts index 4b1d40dadaa3..96eb81324b81 100644 --- a/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts +++ b/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts @@ -28,6 +28,7 @@ type CategorizeTrackedExpenseParams = { adminsCreatedReportActionID?: string; guidedSetupData?: string; engagementChoice?: string; + description?: string; }; export default CategorizeTrackedExpenseParams; diff --git a/src/libs/API/parameters/CompleteSplitBillParams.ts b/src/libs/API/parameters/CompleteSplitBillParams.ts index 123ee7b05257..69faf7f0a677 100644 --- a/src/libs/API/parameters/CompleteSplitBillParams.ts +++ b/src/libs/API/parameters/CompleteSplitBillParams.ts @@ -11,6 +11,7 @@ type CompleteSplitBillParams = { taxCode?: string; taxAmount?: number; billable?: boolean; + description?: string; }; export default CompleteSplitBillParams; diff --git a/src/libs/API/parameters/CreateDistanceRequestParams.ts b/src/libs/API/parameters/CreateDistanceRequestParams.ts index 81385d092530..915a3e4cc133 100644 --- a/src/libs/API/parameters/CreateDistanceRequestParams.ts +++ b/src/libs/API/parameters/CreateDistanceRequestParams.ts @@ -20,6 +20,7 @@ type CreateDistanceRequestParams = { payerEmail?: string; splits?: string; chatType?: string; + description?: string; }; export default CreateDistanceRequestParams; diff --git a/src/libs/API/parameters/CreateTaskParams.ts b/src/libs/API/parameters/CreateTaskParams.ts index 0ead163c623b..a883417a3f79 100644 --- a/src/libs/API/parameters/CreateTaskParams.ts +++ b/src/libs/API/parameters/CreateTaskParams.ts @@ -3,7 +3,7 @@ type CreateTaskParams = { parentReportID?: string; taskReportID?: string; createdTaskReportActionID?: string; - title?: string; + htmlTitle?: string | {text: string; html: string}; description?: string; assignee?: string; assigneeAccountID?: number; diff --git a/src/libs/API/parameters/EditTaskParams.ts b/src/libs/API/parameters/EditTaskParams.ts index 01595b7928c5..28cb9268a576 100644 --- a/src/libs/API/parameters/EditTaskParams.ts +++ b/src/libs/API/parameters/EditTaskParams.ts @@ -1,6 +1,6 @@ type EditTaskParams = { taskReportID?: string; - title?: string; + htmlTitle?: string; description?: string; editedTaskReportActionID?: string; }; diff --git a/src/libs/API/parameters/RequestMoneyParams.ts b/src/libs/API/parameters/RequestMoneyParams.ts index 4fe0a0a29e9f..ea375f32fe0d 100644 --- a/src/libs/API/parameters/RequestMoneyParams.ts +++ b/src/libs/API/parameters/RequestMoneyParams.ts @@ -28,6 +28,7 @@ type RequestMoneyParams = { transactionThreadReportID: string; createdReportActionIDForThread: string | undefined; reimbursible?: boolean; + description?: string; }; export default RequestMoneyParams; diff --git a/src/libs/API/parameters/SendInvoiceParams.ts b/src/libs/API/parameters/SendInvoiceParams.ts index 2b172ee6ce6d..4ef7b4023bc6 100644 --- a/src/libs/API/parameters/SendInvoiceParams.ts +++ b/src/libs/API/parameters/SendInvoiceParams.ts @@ -23,6 +23,7 @@ type SendInvoiceParams = RequireAtLeastOne< createdIOUReportActionID: string; createdReportActionIDForThread: string | undefined; reportActionID: string; + description?: string; }, 'receiverEmail' | 'receiverInvoiceRoomID' >; diff --git a/src/libs/API/parameters/ShareTrackedExpenseParams.ts b/src/libs/API/parameters/ShareTrackedExpenseParams.ts index c851af4e227b..be143ae82ed8 100644 --- a/src/libs/API/parameters/ShareTrackedExpenseParams.ts +++ b/src/libs/API/parameters/ShareTrackedExpenseParams.ts @@ -28,6 +28,7 @@ type ShareTrackedExpenseParams = { adminsCreatedReportActionID?: string; engagementChoice?: string; guidedSetupData?: string; + description?: string; }; export default ShareTrackedExpenseParams; diff --git a/src/libs/API/parameters/SplitBillParams.ts b/src/libs/API/parameters/SplitBillParams.ts index 76252abe3292..3fda11b9ca98 100644 --- a/src/libs/API/parameters/SplitBillParams.ts +++ b/src/libs/API/parameters/SplitBillParams.ts @@ -17,6 +17,7 @@ type SplitBillParams = { splitPayerAccountIDs: number[]; taxCode: string; taxAmount: number; + description?: string; }; export default SplitBillParams; diff --git a/src/libs/API/parameters/StartSplitBillParams.ts b/src/libs/API/parameters/StartSplitBillParams.ts index 499073c88de9..10f1029a0fba 100644 --- a/src/libs/API/parameters/StartSplitBillParams.ts +++ b/src/libs/API/parameters/StartSplitBillParams.ts @@ -16,6 +16,7 @@ type StartSplitBillParams = { chatType?: string; taxCode?: string; taxAmount?: number; + description?: string; }; export default StartSplitBillParams; diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts index 89b8d79e3b9a..e11b119e2f83 100644 --- a/src/libs/API/parameters/TrackExpenseParams.ts +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -28,6 +28,7 @@ type TrackExpenseParams = { waypoints?: string; actionableWhisperReportActionID?: string; customUnitRateID?: string; + description?: string; }; export default TrackExpenseParams; diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 465067a045c4..0254059e4047 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -9,6 +9,7 @@ import {convertToDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; import {translateLocal} from './Localize'; import Log from './Log'; +import Parser from './Parser'; import {getCleanedTagName, getSortedTagKeys} from './PolicyUtils'; import {getOriginalMessage, isModifiedExpenseAction} from './ReportActionsUtils'; // eslint-disable-next-line import/no-cycle @@ -222,8 +223,8 @@ function getForReportAction({ const hasModifiedComment = isReportActionOriginalMessageAnObject && 'oldComment' in reportActionOriginalMessage && 'newComment' in reportActionOriginalMessage; if (hasModifiedComment) { buildMessageFragmentForValue( - reportActionOriginalMessage?.newComment ?? '', - reportActionOriginalMessage?.oldComment ?? '', + Parser.htmlToMarkdown(reportActionOriginalMessage?.newComment ?? ''), + Parser.htmlToMarkdown(reportActionOriginalMessage?.oldComment ?? ''), translateLocal('common.description'), true, setFragments, diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts index f4ecfe3700f1..92011e28f687 100644 --- a/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts @@ -5,7 +5,7 @@ import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import getParamsFromRoute from '@libs/Navigation/helpers/getParamsFromRoute'; import navigationRef from '@libs/Navigation/navigationRef'; import type {NavigationPartialRoute} from '@libs/Navigation/types'; -import * as PolicyUtils from '@libs/PolicyUtils'; +import {shouldDisplayPolicyNotFoundPage} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {SplitNavigatorRouterOptions} from './types'; @@ -26,21 +26,26 @@ function getRoutePolicyID(route: NavigationPartialRoute): string | undefined { function adaptStateIfNecessary({state, options: {sidebarScreen, defaultCentralScreen, parentRoute}}: AdaptStateIfNecessaryArgs) { const isNarrowLayout = getIsNarrowLayout(); + const rootState = navigationRef.getRootState(); const lastRoute = state.routes.at(-1) as NavigationPartialRoute; const routePolicyID = getRoutePolicyID(lastRoute); // If invalid policy page is displayed on narrow layout, sidebar screen should not be pushed to the navigation state to avoid adding reduntant not found page if (isNarrowLayout && !!routePolicyID) { - if (PolicyUtils.shouldDisplayPolicyNotFoundPage(routePolicyID)) { + if (shouldDisplayPolicyNotFoundPage(routePolicyID)) { return; } } + // When initializing the app on a small screen with the center screen as the initial screen, the sidebar must also be split to allow users to swipe back. + const isInitialRoute = !rootState || rootState.routes.length === 1; + const shouldSplitHaveSidebar = isInitialRoute || !isNarrowLayout; + // If the screen is wide, there should be at least two screens inside: // - sidebarScreen to cover left pane. // - defaultCentralScreen to cover central pane. - if (!isAtLeastOneInState(state, sidebarScreen)) { + if (!isAtLeastOneInState(state, sidebarScreen) && shouldSplitHaveSidebar) { const paramsFromRoute = getParamsFromRoute(sidebarScreen); const copiedParams = pick(lastRoute?.params, paramsFromRoute); @@ -65,8 +70,6 @@ function adaptStateIfNecessary({state, options: {sidebarScreen, defaultCentralSc // - defaultCentralScreen to cover central pane. if (!isNarrowLayout) { if (state.routes.length === 1 && state.routes[0].name === sidebarScreen) { - const rootState = navigationRef.getRootState(); - const previousSameNavigator = rootState?.routes.filter((route) => route.name === parentRoute.name).at(-2); // If we have optimization for not rendering all split navigators, then last selected option may not be in the state. In this case state has to be read from the preserved state. diff --git a/src/libs/Navigation/helpers/resetPolicyIDInNavigationState.ts b/src/libs/Navigation/helpers/resetPolicyIDInNavigationState.ts index 6286c4cc816a..ab069bc4368c 100644 --- a/src/libs/Navigation/helpers/resetPolicyIDInNavigationState.ts +++ b/src/libs/Navigation/helpers/resetPolicyIDInNavigationState.ts @@ -10,8 +10,7 @@ import SCREENS from '@src/SCREENS'; */ function resetPolicyIDInNavigationState() { const rootState = navigationRef.getRootState(); - const lastPolicyRoute = rootState?.routes?.findLast((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR || route.name === SCREENS.SEARCH.ROOT); - + const lastPolicyRoute = rootState?.routes?.findLast((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR || route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); if (!lastPolicyRoute) { return; } @@ -21,14 +20,18 @@ function resetPolicyIDInNavigationState() { return; } - const {q, ...rest} = lastPolicyRoute.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT]; + const lastSearchRoute = lastPolicyRoute.state?.routes.findLast((route) => route.name === SCREENS.SEARCH.ROOT); + if (!lastSearchRoute || !lastSearchRoute.params) { + return; + } + const {q, ...rest} = lastSearchRoute.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT]; const queryJSON = SearchQueryUtils.buildSearchQueryJSON(q); if (!queryJSON || !queryJSON.policyID) { return; } delete queryJSON.policyID; - Navigation.setParams({q: SearchQueryUtils.buildSearchQueryString(queryJSON), ...rest}, lastPolicyRoute.key); + Navigation.setParams({q: SearchQueryUtils.buildSearchQueryString(queryJSON), ...rest}, lastSearchRoute.key); } export default resetPolicyIDInNavigationState; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 4910254d5cfc..929ea5f7229a 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -215,7 +215,7 @@ type GetOptionsConfig = { includeRecentReports?: boolean; includeSelectedOptions?: boolean; recentAttendees?: Attendee[]; - excludeHiddenReports?: boolean; + excludeHiddenThreads?: boolean; canShowManagerMcTest?: boolean; } & GetValidReportsConfig; @@ -1484,7 +1484,7 @@ function getValidOptions( selectedOptions = [], shouldSeparateSelfDMChat = false, shouldSeparateWorkspaceChat = false, - excludeHiddenReports = false, + excludeHiddenThreads = false, canShowManagerMcTest = false, ...config }: GetOptionsConfig = {}, @@ -1579,8 +1579,8 @@ function getValidOptions( } } - if (excludeHiddenReports) { - recentReportOptions = recentReportOptions.filter((option) => option.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + if (excludeHiddenThreads) { + recentReportOptions = recentReportOptions.filter((option) => !option.isThread || option.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); } return { @@ -1613,7 +1613,7 @@ function getSearchOptions(options: OptionList, betas: Beta[] = [], isUsedInChatF includeTasks: true, includeSelfDM: true, shouldBoldTitleByDefault: !isUsedInChatFinder, - excludeHiddenReports: true, + excludeHiddenThreads: true, }); const orderedOptions = orderOptions(optionList); Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d04c3356d792..405c978227a8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -553,6 +553,7 @@ type TransactionDetails = { billable: boolean; tag: string; mccGroup?: ValueOf; + description?: string; cardID: number; originalAmount: number; originalCurrency: string; @@ -3507,6 +3508,7 @@ function getTransactionDetails(transaction: OnyxInputOrEntry, creat function getTransactionCommentObject(transaction: OnyxEntry): Comment { return { ...transaction?.comment, + comment: Parser.htmlToMarkdown(transaction?.comment?.comment ?? ''), waypoints: getWaypoints(transaction), }; } @@ -4454,6 +4456,14 @@ function getReportNameInternal({ return getIOUUnapprovedMessage(parentReportAction); } + if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { + return translateLocal('parentReportAction.deletedTask'); + } + + if (isTaskReport(report)) { + return Parser.htmlToText(report?.reportName ?? ''); + } + if (isChatThread(report)) { if (!isEmptyObject(parentReportAction) && isTransactionThread(parentReportAction)) { formattedName = getTransactionReportName({reportAction: parentReportAction, transactions, reports}); @@ -4512,15 +4522,11 @@ function getReportNameInternal({ return translateLocal('parentReportAction.deletedReport'); } - if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { - return translateLocal('parentReportAction.deletedTask'); - } - if (isGroupChat(report)) { return getGroupChatName(undefined, true, report) ?? ''; } - if (isChatRoom(report) || isTaskReport(report)) { + if (isChatRoom(report)) { formattedName = report?.reportName; } @@ -6363,7 +6369,7 @@ function buildOptimisticEditedTaskFieldReportAction({title, description}: Task): { type: CONST.REPORT.MESSAGE.TYPE.COMMENT, text: changelog, - html: description ? getParsedComment(changelog) : changelog, + html: getParsedComment(changelog), }, ], person: [ @@ -6715,7 +6721,7 @@ function buildOptimisticTaskReport( return { reportID: generateReportID(), - reportName: title, + reportName: getParsedComment(title ?? ''), description: getParsedComment(description ?? ''), ownerAccountID, participants, diff --git a/src/libs/TaskUtils.ts b/src/libs/TaskUtils.ts index ac66bdebd42c..1533da4963d0 100644 --- a/src/libs/TaskUtils.ts +++ b/src/libs/TaskUtils.ts @@ -8,6 +8,7 @@ import type {Message} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import {translateLocal} from './Localize'; import Navigation from './Navigation/Navigation'; +import Parser from './Parser'; import {getReportActionHtml, getReportActionText} from './ReportActionsUtils'; let allReports: OnyxCollection = {}; @@ -53,9 +54,10 @@ function getTaskReportActionMessage(action: OnyxEntry): Pick, fallbackTitle = ''): string { // We need to check for reportID, not just reportName, because when a receiver opens the task for the first time, - // an optimistic report is created with the only property – reportName: 'Chat report', + // an optimistic report is created with the only property - reportName: 'Chat report', // and it will be displayed as the task title without checking for reportID to be present. - return taskReport?.reportID && taskReport.reportName ? taskReport.reportName : fallbackTitle; + const title = taskReport?.reportID && taskReport.reportName ? taskReport.reportName : fallbackTitle; + return Parser.htmlToText(title); } function getTaskTitle(taskReportID: string | undefined, fallbackTitle = ''): string { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 805f00e89866..d6f30969cab4 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -108,6 +108,7 @@ import { getMoneyRequestSpendBreakdown, getOptimisticDataForParentReportAction, getOutstandingChildRequest, + getParsedComment, getPersonalDetailsForAccountID, getReportNameValuePairs, getReportOrDraftReport, @@ -3978,13 +3979,16 @@ function updateMoneyRequestDate( /** Updates the billable field of an expense */ function updateMoneyRequestBillable( - transactionID: string, - transactionThreadReportID: string, + transactionID: string | undefined, + transactionThreadReportID: string | undefined, value: boolean, policy: OnyxEntry, policyTagList: OnyxEntry, policyCategories: OnyxEntry, ) { + if (!transactionID || !transactionThreadReportID) { + return; + } const transactionChanges: TransactionChanges = { billable: value, }; @@ -4193,8 +4197,9 @@ function updateMoneyRequestDescription( policyTagList: OnyxEntry, policyCategories: OnyxEntry, ) { + const parsedComment = getParsedComment(comment); const transactionChanges: TransactionChanges = { - comment, + comment: parsedComment, }; const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null; @@ -4205,6 +4210,7 @@ function updateMoneyRequestDescription( data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories); } const {params, onyxData} = data; + params.description = parsedComment; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DESCRIPTION, params, onyxData); } @@ -4490,6 +4496,7 @@ function categorizeTrackedExpense(trackedExpenseParams: TrackedExpenseParams) { adminsCreatedReportActionID: createdWorkspaceParams?.adminsCreatedReportActionID, engagementChoice: createdWorkspaceParams?.engagementChoice, guidedSetupData: createdWorkspaceParams?.guidedSetupData, + description: transactionParams.comment, }; API.write(WRITE_COMMANDS.CATEGORIZE_TRACKED_EXPENSE, parameters, {optimisticData, successData, failureData}); @@ -4550,6 +4557,7 @@ function shareTrackedExpense(trackedExpenseParams: TrackedExpenseParams) { adminsCreatedReportActionID: createdWorkspaceParams?.adminsCreatedReportActionID, engagementChoice: createdWorkspaceParams?.engagementChoice, guidedSetupData: createdWorkspaceParams?.guidedSetupData, + description: transactionParams.comment, }; API.write(WRITE_COMMANDS.SHARE_TRACKED_EXPENSE, parameters, {optimisticData, successData, failureData}); @@ -4561,6 +4569,8 @@ function shareTrackedExpense(trackedExpenseParams: TrackedExpenseParams) { function requestMoney(requestMoneyInformation: RequestMoneyInformation) { const {report, participantParams, policyParams = {}, transactionParams, gpsPoints, action, reimbursible} = requestMoneyInformation; const {payeeAccountID} = participantParams; + const parsedComment = getParsedComment(transactionParams.comment ?? ''); + transactionParams.comment = parsedComment; const { amount, currency, @@ -4698,6 +4708,7 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation) { transactionThreadReportID, createdReportActionIDForThread, reimbursible, + description: parsedComment, }; // eslint-disable-next-line rulesdir/no-multiple-api-calls @@ -4807,6 +4818,11 @@ function sendInvoice( companyName?: string, companyWebsite?: string, ) { + const parsedComment = getParsedComment(transaction?.comment?.comment?.trim() ?? ''); + if (transaction?.comment) { + // eslint-disable-next-line no-param-reassign + transaction.comment.comment = parsedComment; + } const { senderWorkspaceID, receiver, @@ -4830,7 +4846,7 @@ function sendInvoice( accountID: currentUserAccountID, amount: transaction?.amount ?? 0, currency: transaction?.currency ?? '', - comment: transaction?.comment?.comment?.trim() ?? '', + comment: parsedComment, merchant: transaction?.merchant ?? '', category: transaction?.category, date: transaction?.created ?? '', @@ -4842,6 +4858,7 @@ function sendInvoice( transactionThreadReportID, companyName, companyWebsite, + description: parsedComment, ...(invoiceChatReport?.reportID ? {receiverInvoiceRoomID: invoiceChatReport.reportID} : {receiverEmail: receiver.login ?? ''}), }; @@ -4864,6 +4881,8 @@ function trackExpense(params: CreateTrackExpenseParams) { const {report, action, isDraftPolicy, participantParams, policyParams: policyData = {}, transactionParams: transactionData} = params; const {participant, payeeAccountID, payeeEmail} = participantParams; const {policy, policyCategories, policyTagList} = policyData; + const parsedComment = getParsedComment(transactionData.comment ?? ''); + transactionData.comment = parsedComment; const { amount, currency, @@ -5066,6 +5085,7 @@ function trackExpense(params: CreateTrackExpenseParams) { createdReportActionIDForThread, waypoints: sanitizedWaypoints, customUnitRateID, + description: parsedComment, }; if (actionableWhisperReportActionIDParam) { parameters.actionableWhisperReportActionID = actionableWhisperReportActionIDParam; @@ -5609,6 +5629,7 @@ function splitBill({ taxCode = '', taxAmount = 0, }: SplitBillActionsParams) { + const parsedComment = getParsedComment(comment); const {splitData, splits, onyxData} = createSplitsAndOnyxData({ participants, currentUserLogin, @@ -5616,7 +5637,7 @@ function splitBill({ existingSplitChatReportID, transactionParams: { amount, - comment, + comment: parsedComment, currency, merchant, created, @@ -5635,7 +5656,7 @@ function splitBill({ amount, splits: JSON.stringify(splits), currency, - comment, + comment: parsedComment, category, merchant, created, @@ -5649,6 +5670,7 @@ function splitBill({ splitPayerAccountIDs, taxCode, taxAmount, + description: parsedComment, }; API.write(WRITE_COMMANDS.SPLIT_BILL, parameters, onyxData); @@ -5680,6 +5702,7 @@ function splitBillAndOpenReport({ taxAmount = 0, existingSplitChatReportID, }: SplitBillActionsParams) { + const parsedComment = getParsedComment(comment); const {splitData, splits, onyxData} = createSplitsAndOnyxData({ participants, currentUserLogin, @@ -5687,7 +5710,7 @@ function splitBillAndOpenReport({ existingSplitChatReportID, transactionParams: { amount, - comment, + comment: parsedComment, currency, merchant, created, @@ -5708,7 +5731,7 @@ function splitBillAndOpenReport({ currency, merchant, created, - comment, + comment: parsedComment, category, tag, billable, @@ -5720,6 +5743,7 @@ function splitBillAndOpenReport({ splitPayerAccountIDs, taxCode, taxAmount, + description: parsedComment, }; API.write(WRITE_COMMANDS.SPLIT_BILL_AND_OPEN_REPORT, parameters, onyxData); @@ -5767,6 +5791,7 @@ function startSplitBill({ const participantAccountIDs = participants.map((participant) => Number(participant.accountID)); const {splitChatReport, existingSplitChatReport} = getOrCreateOptimisticSplitChatReport(existingSplitChatReportID, participants, participantAccountIDs, currentUserAccountID); const isOwnPolicyExpenseChat = !!splitChatReport.isOwnPolicyExpenseChat; + const parsedComment = getParsedComment(comment); const {name: filename, source, state = CONST.IOU.RECEIPT_STATE.SCANREADY} = receipt; const receiptObject: Receipt = {state, source}; @@ -5777,7 +5802,7 @@ function startSplitBill({ amount: 0, currency, reportID: CONST.REPORT.SPLIT_REPORTID, - comment, + comment: parsedComment, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, receipt: receiptObject, category, @@ -5795,7 +5820,7 @@ function startSplitBill({ CONST.IOU.REPORT_ACTION_TYPE.SPLIT, 0, CONST.CURRENCY.USD, - comment, + parsedComment, participants, splitTransaction.transactionID, undefined, @@ -6032,7 +6057,7 @@ function startSplitBill({ transactionID: splitTransaction.transactionID, splits: JSON.stringify(splits), receipt, - comment, + comment: parsedComment, category, tag, currency, @@ -6042,6 +6067,7 @@ function startSplitBill({ chatType: splitChatReport?.chatType, taxCode, taxAmount, + description: parsedComment, }; API.write(WRITE_COMMANDS.START_SPLIT_BILL, parameters, {optimisticData, successData, failureData}); @@ -6065,6 +6091,11 @@ function completeSplitBill( sessionAccountID: number, sessionEmail?: string, ) { + const parsedComment = getParsedComment(updatedTransaction?.comment?.comment ?? ''); + if (updatedTransaction?.comment) { + // eslint-disable-next-line no-param-reassign + updatedTransaction.comment.comment = parsedComment; + } const currentUserEmailForIOUSplit = addSMSDomainIfPhoneNumber(sessionEmail); const transactionID = updatedTransaction?.transactionID; const unmodifiedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; @@ -6191,7 +6222,7 @@ function completeSplitBill( amount: isPolicyExpenseChat ? -splitAmount : splitAmount, currency: currency ?? '', reportID: oneOnOneIOUReport?.reportID, - comment: updatedTransaction?.comment?.comment, + comment: parsedComment, created: updatedTransaction?.modifiedCreated, merchant: updatedTransaction?.modifiedMerchant, receipt: {...updatedTransaction?.receipt, state: CONST.IOU.RECEIPT_STATE.OPEN}, @@ -6211,7 +6242,7 @@ function completeSplitBill( CONST.IOU.REPORT_ACTION_TYPE.CREATE, splitAmount, currency ?? '', - updatedTransaction?.comment?.comment ?? '', + parsedComment, currentUserEmailForIOUSplit, [participant], oneOnOneTransaction.transactionID, @@ -6295,6 +6326,7 @@ function completeSplitBill( taxCode: transactionTaxCode, taxAmount: transactionTaxAmount, billable: transactionBillable, + description: parsedComment, }; API.write(WRITE_COMMANDS.COMPLETE_SPLIT_BILL, parameters, {optimisticData, successData, failureData}); @@ -6339,6 +6371,8 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest policyParams = {}, } = distanceRequestInformation; const {policy, policyCategories, policyTagList} = policyParams; + const parsedComment = getParsedComment(transactionParams.comment); + transactionParams.comment = parsedComment; const {amount, comment, currency, created, category, tag, taxAmount, taxCode, merchant, billable, validWaypoints, customUnitRateID = '', splitShares = {}} = transactionParams; // If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function @@ -6399,6 +6433,7 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest billable, splits: JSON.stringify(splits), chatType: splitData.chatType, + description: parsedComment, }; } else { const participant = participants.at(0) ?? {}; @@ -6465,6 +6500,7 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest createdReportActionIDForThread, payerEmail, customUnitRateID, + description: parsedComment, }; } diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index c0876c9ea5ca..0c3fc2b251a2 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -115,7 +115,7 @@ function clearOutTaskInfo(skipConfirmation = false) { * 3b. The TaskReportAction on the assignee chat report */ function createTaskAndNavigate( - parentReportID: string, + parentReportID: string | undefined, title: string, description: string, assigneeEmail: string, @@ -124,6 +124,9 @@ function createTaskAndNavigate( policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE, isCreatedUsingMarkdown = false, ) { + if (!parentReportID) { + return; + } const optimisticTaskReport = ReportUtils.buildOptimisticTaskReport(currentUserAccountID, parentReportID, assigneeAccountID, title, description, policyID); const assigneeChatReportID = assigneeChatReport?.reportID; @@ -321,7 +324,7 @@ function createTaskAndNavigate( parentReportID, taskReportID: optimisticTaskReport.reportID, createdTaskReportActionID: optimisticTaskCreatedAction.reportActionID, - title: optimisticTaskReport.reportName, + htmlTitle: optimisticTaskReport.reportName, description: optimisticTaskReport.description, assignee: assigneeEmail, assigneeAccountID, @@ -544,8 +547,10 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task // Create the EditedReportAction on the task const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskFieldReportAction({title, description}); - // Sometimes title or description is undefined, so we need to check for that, and we provide it to multiple functions - const reportName = (title ?? report?.reportName)?.trim(); + // Ensure title is defined before parsing it with getParsedComment. If title is undefined, fall back to reportName from report. + // Trim the final parsed title for consistency. + const reportName = title ? ReportUtils.getParsedComment(title) : report?.reportName ?? ''; + const parsedTitle = (reportName ?? '').trim(); // Description can be unset, so we default to an empty string if so const newDescription = typeof description === 'string' ? ReportUtils.getParsedComment(description) : report.description; @@ -561,7 +566,7 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, value: { - reportName, + reportName: parsedTitle, description: reportDescription, pendingFields: { ...(title && {reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), @@ -582,6 +587,8 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, value: { + reportName: parsedTitle, + description: reportDescription, pendingFields: { ...(title && {reportName: null}), ...(description && {description: null}), @@ -608,7 +615,7 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task const parameters: EditTaskParams = { taskReportID: report.reportID, - title: reportName, + htmlTitle: parsedTitle, description: reportDescription, editedTaskReportActionID: editTaskReportAction.reportActionID, }; diff --git a/src/pages/Search/SearchPageNarrow.tsx b/src/pages/Search/SearchPageNarrow.tsx index 44a6b050bee2..55ca5ba93298 100644 --- a/src/pages/Search/SearchPageNarrow.tsx +++ b/src/pages/Search/SearchPageNarrow.tsx @@ -147,7 +147,7 @@ function SearchPageNarrow({queryJSON, policyID, searchName, shouldGroupByReports - + diff --git a/src/pages/tasks/NewTaskDetailsPage.tsx b/src/pages/tasks/NewTaskDetailsPage.tsx index f225727f10c3..47a5921c51a3 100644 --- a/src/pages/tasks/NewTaskDetailsPage.tsx +++ b/src/pages/tasks/NewTaskDetailsPage.tsx @@ -1,7 +1,6 @@ -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; @@ -25,20 +24,17 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/NewTaskForm'; -import type {Task} from '@src/types/onyx'; -type NewTaskDetailsPageOnyxProps = { - /** Task Creation Data */ - task: OnyxEntry; -}; +type NewTaskDetailsPageProps = PlatformStackScreenProps; -type NewTaskDetailsPageProps = NewTaskDetailsPageOnyxProps & PlatformStackScreenProps; - -function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) { +function NewTaskDetailsPage({route}: NewTaskDetailsPageProps) { + const [task] = useOnyx(ONYXKEYS.TASK); const styles = useThemeStyles(); const {translate} = useLocalize(); const [taskTitle, setTaskTitle] = useState(task?.title ?? ''); const [taskDescription, setTaskDescription] = useState(task?.description ?? ''); + const titleDefaultValue = useMemo(() => Parser.htmlToMarkdown(Parser.replace(taskTitle)), [taskTitle]); + const descriptionDefaultValue = useMemo(() => Parser.htmlToMarkdown(Parser.replace(taskDescription)), [taskDescription]); const {inputCallbackRef} = useAutoFocusInput(); @@ -47,7 +43,7 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) { const buttonText = skipConfirmation ? translate('newTaskPage.assignTask') : translate('common.next'); useEffect(() => { - setTaskTitle(task?.title ?? ''); + setTaskTitle(Parser.htmlToMarkdown(Parser.replace(task?.title ?? ''))); setTaskDescription(Parser.htmlToMarkdown(Parser.replace(task?.description ?? ''))); }, [task]); @@ -57,8 +53,8 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) { if (!values.taskTitle) { // We error if the user doesn't enter a task name addErrorMessage(errors, 'taskTitle', translate('newTaskPage.pleaseEnterTaskName')); - } else if (values.taskTitle.length > CONST.TITLE_CHARACTER_LIMIT) { - addErrorMessage(errors, 'taskTitle', translate('common.error.characterLimitExceedCounter', {length: values.taskTitle.length, limit: CONST.TITLE_CHARACTER_LIMIT})); + } else if (values.taskTitle.length > CONST.TASK_TITLE_CHARACTER_LIMIT) { + addErrorMessage(errors, 'taskTitle', translate('common.error.characterLimitExceedCounter', {length: values.taskTitle.length, limit: CONST.TASK_TITLE_CHARACTER_LIMIT})); } const taskDescriptionLength = getCommentLength(values.taskDescription); if (taskDescriptionLength > CONST.DESCRIPTION_LIMIT) { @@ -76,7 +72,7 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) { if (skipConfirmation) { setShareDestinationValue(task?.parentReportID); playSound(SOUNDS.DONE); - createTaskAndNavigate(task?.parentReportID ?? '-1', values.taskTitle, values.taskDescription ?? '', task?.assignee ?? '', task.assigneeAccountID, task.assigneeChatReport); + createTaskAndNavigate(task?.parentReportID, values.taskTitle, values.taskDescription ?? '', task?.assignee ?? '', task.assigneeAccountID, task.assigneeChatReport); } else { Navigation.navigate(ROUTES.NEW_TASK.getRoute(backTo)); } @@ -110,9 +106,13 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) { inputID={INPUT_IDS.TASK_TITLE} label={translate('task.title')} accessibilityLabel={translate('task.title')} + defaultValue={titleDefaultValue} value={taskTitle} onValueChange={setTaskTitle} autoCorrect={false} + type="markdown" + autoGrowHeight + maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight} /> @@ -126,7 +126,7 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) { autoGrowHeight maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight} shouldSubmitForm - defaultValue={Parser.htmlToMarkdown(Parser.replace(taskDescription))} + defaultValue={descriptionDefaultValue} value={taskDescription} onValueChange={setTaskDescription} type="markdown" @@ -139,8 +139,4 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) { NewTaskDetailsPage.displayName = 'NewTaskDetailsPage'; -export default withOnyx({ - task: { - key: ONYXKEYS.TASK, - }, -})(NewTaskDetailsPage); +export default NewTaskDetailsPage; diff --git a/src/pages/tasks/NewTaskPage.tsx b/src/pages/tasks/NewTaskPage.tsx index fa7ec8df7d24..33fef0a065ca 100644 --- a/src/pages/tasks/NewTaskPage.tsx +++ b/src/pages/tasks/NewTaskPage.tsx @@ -14,14 +14,14 @@ import useLocalize from '@hooks/useLocalize'; import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; import blurActiveElement from '@libs/Accessibility/blurActiveElement'; -import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; +import {createTaskAndNavigate, dismissModalAndClearOutTaskInfo, getAssignee, getShareDestination, setShareDestinationValue} from '@libs/actions/Task'; +import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {NewTaskNavigatorParamList} from '@libs/Navigation/types'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; +import {getDisplayNamesWithTooltips, isAllowedToComment} from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; -import * as TaskActions from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -36,20 +36,17 @@ function NewTaskPage({route}: NewTaskPageProps) { const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const styles = useThemeStyles(); const {translate} = useLocalize(); - const assignee = useMemo(() => TaskActions.getAssignee(task?.assigneeAccountID ?? -1, personalDetails), [task?.assigneeAccountID, personalDetails]); - const assigneeTooltipDetails = ReportUtils.getDisplayNamesWithTooltips( - OptionsListUtils.getPersonalDetailsForAccountIDs(task?.assigneeAccountID ? [task.assigneeAccountID] : [], personalDetails), - false, - ); + const assignee = useMemo(() => getAssignee(task?.assigneeAccountID ?? CONST.DEFAULT_NUMBER_ID, personalDetails), [task?.assigneeAccountID, personalDetails]); + const assigneeTooltipDetails = getDisplayNamesWithTooltips(getPersonalDetailsForAccountIDs(task?.assigneeAccountID ? [task.assigneeAccountID] : [], personalDetails), false); const shareDestination = useMemo( - () => (task?.shareDestination ? TaskActions.getShareDestination(task.shareDestination, reports, personalDetails) : undefined), + () => (task?.shareDestination ? getShareDestination(task.shareDestination, reports, personalDetails) : undefined), [task?.shareDestination, reports, personalDetails], ); const parentReport = useMemo(() => (task?.shareDestination ? reports?.[`${ONYXKEYS.COLLECTION.REPORT}${task.shareDestination}`] : undefined), [reports, task?.shareDestination]); const [errorMessage, setErrorMessage] = useState(''); const hasDestinationError = task?.skipConfirmation && !task?.parentReportID; - const isAllowedToCreateTask = useMemo(() => isEmptyObject(parentReport) || ReportUtils.isAllowedToComment(parentReport), [parentReport]); + const isAllowedToCreateTask = useMemo(() => isEmptyObject(parentReport) || isAllowedToComment(parentReport), [parentReport]); const {paddingBottom} = useStyledSafeAreaInsets(); @@ -74,7 +71,7 @@ function NewTaskPage({route}: NewTaskPageProps) { // this allows us to go ahead and set that report as the share destination // and disable the share destination selector if (task?.parentReportID) { - TaskActions.setShareDestinationValue(task.parentReportID); + setShareDestinationValue(task.parentReportID); } }, [task?.assignee, task?.assigneeAccountID, task?.description, task?.parentReportID, task?.shareDestination, task?.title]); @@ -97,15 +94,7 @@ function NewTaskPage({route}: NewTaskPageProps) { } playSound(SOUNDS.DONE); - TaskActions.createTaskAndNavigate( - parentReport?.reportID ?? '-1', - task.title, - task?.description ?? '', - task?.assignee ?? '', - task.assigneeAccountID, - task.assigneeChatReport, - parentReport?.policyID, - ); + createTaskAndNavigate(parentReport?.reportID, task.title, task?.description ?? '', task?.assignee ?? '', task.assigneeAccountID, task.assigneeChatReport, parentReport?.policyID); }; return ( @@ -115,7 +104,7 @@ function NewTaskPage({route}: NewTaskPageProps) { > TaskActions.dismissModalAndClearOutTaskInfo()} + onBackButtonPress={() => dismissModalAndClearOutTaskInfo()} shouldShowLink={false} > Navigation.navigate(ROUTES.NEW_TASK_TITLE.getRoute(backTo))} shouldShowRightIcon rightLabel={translate('common.required')} + shouldParseTitle /> Navigation.navigate(ROUTES.NEW_TASK_ASSIGNEE.getRoute(backTo))} shouldShowRightIcon diff --git a/src/pages/tasks/NewTaskTitlePage.tsx b/src/pages/tasks/NewTaskTitlePage.tsx index 88a44aca8501..5824b095c665 100644 --- a/src/pages/tasks/NewTaskTitlePage.tsx +++ b/src/pages/tasks/NewTaskTitlePage.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapperWithRef from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; @@ -11,25 +10,24 @@ import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; +import {setTitleValue} from '@libs/actions/Task'; +import {addErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {NewTaskNavigatorParamList} from '@libs/Navigation/types'; -import * as TaskActions from '@userActions/Task'; +import Parser from '@libs/Parser'; +import {getCommentLength} from '@libs/ReportUtils'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/NewTaskForm'; -import type {Task} from '@src/types/onyx'; -type NewTaskTitlePageOnyxProps = { - /** Task Creation Data */ - task: OnyxEntry; -}; -type NewTaskTitlePageProps = NewTaskTitlePageOnyxProps & PlatformStackScreenProps; +type NewTaskTitlePageProps = PlatformStackScreenProps; -function NewTaskTitlePage({task, route}: NewTaskTitlePageProps) { +function NewTaskTitlePage({route}: NewTaskTitlePageProps) { + const [task] = useOnyx(ONYXKEYS.TASK); const styles = useThemeStyles(); const {inputCallbackRef} = useAutoFocusInput(); @@ -39,11 +37,13 @@ function NewTaskTitlePage({task, route}: NewTaskTitlePageProps) { const validate = (values: FormOnyxValues): FormInputErrors => { const errors = {}; + const parsedTitleLength = getCommentLength(values.taskTitle); + if (!values.taskTitle) { // We error if the user doesn't enter a task name - ErrorUtils.addErrorMessage(errors, 'taskTitle', translate('newTaskPage.pleaseEnterTaskName')); - } else if (values.taskTitle.length > CONST.TITLE_CHARACTER_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'taskTitle', translate('common.error.characterLimitExceedCounter', {length: values.taskTitle.length, limit: CONST.TITLE_CHARACTER_LIMIT})); + addErrorMessage(errors, 'taskTitle', translate('newTaskPage.pleaseEnterTaskName')); + } else if (parsedTitleLength > CONST.TASK_TITLE_CHARACTER_LIMIT) { + addErrorMessage(errors, 'taskTitle', translate('common.error.characterLimitExceedCounter', {length: parsedTitleLength, limit: CONST.TASK_TITLE_CHARACTER_LIMIT})); } return errors; @@ -52,7 +52,7 @@ function NewTaskTitlePage({task, route}: NewTaskTitlePageProps) { // On submit, we want to call the assignTask function and wait to validate // the response const onSubmit = (values: FormOnyxValues) => { - TaskActions.setTitleValue(values.taskTitle); + setTitleValue(values.taskTitle); goBack(); }; @@ -79,11 +79,14 @@ function NewTaskTitlePage({task, route}: NewTaskTitlePageProps) { @@ -93,8 +96,4 @@ function NewTaskTitlePage({task, route}: NewTaskTitlePageProps) { NewTaskTitlePage.displayName = 'NewTaskTitlePage'; -export default withOnyx({ - task: { - key: ONYXKEYS.TASK, - }, -})(NewTaskTitlePage); +export default NewTaskTitlePage; diff --git a/src/pages/tasks/TaskTitlePage.tsx b/src/pages/tasks/TaskTitlePage.tsx index d767b5b9da3c..30beb0bd700d 100644 --- a/src/pages/tasks/TaskTitlePage.tsx +++ b/src/pages/tasks/TaskTitlePage.tsx @@ -13,14 +13,17 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; +import {canModifyTask as canModifyTaskTaskUtils, editTask} from '@libs/actions/Task'; +import {addErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {TaskDetailsNavigatorParamList} from '@libs/Navigation/types'; -import * as ReportUtils from '@libs/ReportUtils'; +import Parser from '@libs/Parser'; +import {getCommentLength, getParsedComment, isOpenTaskReport, isTaskReport} from '@libs/ReportUtils'; +import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound'; -import * as Task from '@userActions/Task'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -38,10 +41,13 @@ function TaskTitlePage({report, currentUserPersonalDetails}: TaskTitlePageProps) ({title}: FormOnyxValues): FormInputErrors => { const errors: FormInputErrors = {}; - if (!title) { - ErrorUtils.addErrorMessage(errors, INPUT_IDS.TITLE, translate('newTaskPage.pleaseEnterTaskName')); - } else if (title.length > CONST.TITLE_CHARACTER_LIMIT) { - ErrorUtils.addErrorMessage(errors, INPUT_IDS.TITLE, translate('common.error.characterLimitExceedCounter', {length: title.length, limit: CONST.TITLE_CHARACTER_LIMIT})); + const parsedTitle = getParsedComment(title); + const parsedTitleLength = getCommentLength(parsedTitle); + + if (!parsedTitle) { + addErrorMessage(errors, INPUT_IDS.TITLE, translate('newTaskPage.pleaseEnterTaskName')); + } else if (parsedTitleLength > CONST.TASK_TITLE_CHARACTER_LIMIT) { + addErrorMessage(errors, INPUT_IDS.TITLE, translate('common.error.characterLimitExceedCounter', {length: parsedTitleLength, limit: CONST.TASK_TITLE_CHARACTER_LIMIT})); } return errors; @@ -51,10 +57,10 @@ function TaskTitlePage({report, currentUserPersonalDetails}: TaskTitlePageProps) const submit = useCallback( (values: FormOnyxValues) => { - if (values.title !== report?.reportName && !isEmptyObject(report)) { + if (values.title !== Parser.htmlToMarkdown(report?.reportName ?? '') && !isEmptyObject(report)) { // Set the title of the report in the store and then call EditTask API // to update the title of the report on the server - Task.editTask(report, {title: values.title}); + editTask(report, {title: values.title}); } Navigation.dismissModal(report?.reportID); @@ -62,16 +68,16 @@ function TaskTitlePage({report, currentUserPersonalDetails}: TaskTitlePageProps) [report], ); - if (!ReportUtils.isTaskReport(report)) { + if (!isTaskReport(report)) { Navigation.isNavigationReady().then(() => { Navigation.dismissModal(report?.reportID); }); } const inputRef = useRef(null); - const isOpen = ReportUtils.isOpenTaskReport(report); - const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID); - const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); + const isOpen = isOpenTaskReport(report); + const canModifyTask = canModifyTaskTaskUtils(report, currentUserPersonalDetails.accountID); + const isTaskNonEditable = isTaskReport(report) && (!canModifyTask || !isOpen); return ( { if (!element) { return; } if (!inputRef.current && didScreenTransitionEnd) { - element.focus(); + updateMultilineInputRange(inputRef.current); } inputRef.current = element; }} + autoGrowHeight + maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight} + shouldSubmitForm={false} + type="markdown" /> diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx index d57941f5d7ca..d5c669687eee 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx @@ -82,7 +82,7 @@ function WorkspaceCompanyCardFeedSelectorPage({route}: WorkspaceCompanyCardFeedS Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ADD_NEW.getRoute(policyID)); }; - const goBack = () => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + const goBack = () => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); const selectFeed = (feed: CardFeedListItem) => { updateSelectedFeed(feed.value, policyID); diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index e312e82bd8c4..0e4352abe230 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -89,8 +89,8 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.glCodes.alias, isQuickSettingsFlow - ? ROUTES.SETTINGS_TAG_GL_CODE.getRoute(policy?.id ?? `${CONST.DEFAULT_NUMBER_ID}`, orderWeight, tagName, backTo) - : ROUTES.WORKSPACE_TAG_GL_CODE.getRoute(policy?.id ?? `${CONST.DEFAULT_NUMBER_ID}`, orderWeight, tagName), + ? ROUTES.SETTINGS_TAG_GL_CODE.getRoute(policyID, orderWeight, tagName, backTo) + : ROUTES.WORKSPACE_TAG_GL_CODE.getRoute(policyID, orderWeight, tagName), ), ); return; diff --git a/src/styles/index.ts b/src/styles/index.ts index 9b82c1c672b6..26468fc49ee6 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -131,6 +131,10 @@ const headlineFont = { ...FontUtils.fontFamily.platform.EXP_NEW_KANSAS_MEDIUM, } satisfies TextStyle; +const headlineItalicFont = { + ...FontUtils.fontFamily.platform.EXP_NEW_KANSAS_MEDIUM_ITALIC, +} satisfies TextStyle; + const modalNavigatorContainer = (isSmallScreenWidth: boolean) => ({ position: 'absolute', @@ -157,12 +161,6 @@ const webViewStyles = (theme: ThemeColors) => textDecorationStyle: 'solid', }, - strong: { - // We set fontFamily and fontWeight directly in order to avoid overriding fontStyle. - fontFamily: FontUtils.fontFamily.platform.EXP_NEUE_BOLD.fontFamily, - fontWeight: FontUtils.fontFamily.platform.EXP_NEUE_BOLD.fontWeight, - }, - a: link(theme), ul: { @@ -192,7 +190,7 @@ const webViewStyles = (theme: ThemeColors) => ...baseCodeTagStyles(theme), paddingVertical: 8, paddingHorizontal: 12, - fontSize: 13, + fontSize: undefined, ...FontUtils.fontFamily.platform.MONOSPACE, marginTop: 0, marginBottom: 0, @@ -203,7 +201,6 @@ const webViewStyles = (theme: ThemeColors) => paddingLeft: 5, paddingRight: 5, fontFamily: FontUtils.fontFamily.platform.MONOSPACE.fontFamily, - // Font size is determined by getCodeFontSize function in `StyleUtils.js` }, img: { @@ -226,7 +223,8 @@ const webViewStyles = (theme: ThemeColors) => marginBottom: 0, }, h1: { - fontSize: variables.fontSizeLarge, + fontSize: undefined, + fontWeight: undefined, marginBottom: 8, }, }, @@ -273,6 +271,18 @@ const styles = (theme: ThemeColors) => paddingVertical: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_INNER_PADDING, }, + h1: { + fontSize: variables.fontSizeLarge, + fontFamily: FontUtils.fontFamily.platform.EXP_NEUE_BOLD.fontFamily, + fontWeight: FontUtils.fontFamily.platform.EXP_NEUE_BOLD.fontWeight, + marginBottom: 8, + }, + + strong: { + fontFamily: FontUtils.fontFamily.platform.EXP_NEUE_BOLD.fontFamily, + fontWeight: FontUtils.fontFamily.platform.EXP_NEUE_BOLD.fontWeight, + }, + autoCompleteSuggestionContainer: { flexDirection: 'row', alignItems: 'center', @@ -4264,6 +4274,16 @@ const styles = (theme: ThemeColors) => ...writingDirection.ltr, ...headlineFont, fontSize: variables.fontSizeXLarge, + lineHeight: variables.lineHeightSizeh2, + maxWidth: '100%', + ...wordBreak.breakWord, + }, + + taskTitleMenuItemItalic: { + ...writingDirection.ltr, + ...headlineItalicFont, + fontSize: variables.fontSizeXLarge, + lineHeight: variables.lineHeightSizeh2, maxWidth: '100%', ...wordBreak.breakWord, }, diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 97cbc7b4a8e3..39d5e5ca959a 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -628,8 +628,14 @@ function getModalPaddingStyles({ /** * Returns the font size for the HTML code tag renderer. */ -function getCodeFontSize(isInsideH1: boolean) { - return isInsideH1 ? 15 : 13; +function getCodeFontSize(isInsideH1: boolean, isInsideTaskTitle?: boolean) { + if (isInsideH1 && !isInsideTaskTitle) { + return 15; + } + if (isInsideTaskTitle) { + return 19; + } + return 13; } /** diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 5a964cd7d6f0..5f99086aec56 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -179,9 +179,24 @@ describe('OptionsListUtils', () => { isOwnPolicyExpenseChat: true, type: CONST.REPORT.TYPE.CHAT, }, + '11': { + lastReadTime: '2021-01-14 11:25:39.200', + lastVisibleActionCreated: '2022-11-22 03:26:02.001', + reportID: '11', + isPinned: false, + participants: { + 10: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN}, + }, + reportName: '', + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + isOwnPolicyExpenseChat: true, + type: CONST.REPORT.TYPE.CHAT, + policyID, + policyName: POLICY.name, + }, // Thread report with notification preference = hidden - '11': { + '12': { lastReadTime: '2021-01-14 11:25:39.200', lastVisibleActionCreated: '2022-11-22 03:26:02.001', reportID: '11', @@ -195,6 +210,8 @@ describe('OptionsListUtils', () => { type: CONST.REPORT.TYPE.CHAT, policyID, policyName: POLICY.name, + parentReportActionID: '123', + parentReportID: '123', }, }; @@ -547,7 +564,7 @@ describe('OptionsListUtils', () => { // Filtering of personalDetails that have reports is done in filterOptions expect(results.personalDetails.length).toBe(9); - // Then all of the reports should be shown including the archived rooms, except for the report with notificationPreferences hidden. + // Then all of the reports should be shown including the archived rooms, except for the thread report with notificationPreferences hidden. expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length - 1); }); @@ -766,8 +783,8 @@ describe('OptionsListUtils', () => { // When we pass an empty search value let results = getShareDestinationOptions(filteredReports, OPTIONS.personalDetails, []); - // Then we should expect all the recent reports to show but exclude the archived rooms - expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length - 1); + // Then we should expect all the recent reports to show but exclude the archived rooms and the hidden thread + expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length - 2); // Filter current REPORTS_WITH_WORKSPACE_ROOMS as we do in the component, before getting share destination options const filteredReportsWithWorkspaceRooms = Object.values(OPTIONS_WITH_WORKSPACE_ROOM.reports).reduce((filtered, option) => { @@ -782,8 +799,8 @@ describe('OptionsListUtils', () => { // When we also have a policy to return rooms in the results results = getShareDestinationOptions(filteredReportsWithWorkspaceRooms, OPTIONS.personalDetails, []); // Then we should expect the DMS, the group chats and the workspace room to show - // We should expect all the recent reports to show, excluding the archived rooms - expect(results.recentReports.length).toBe(Object.values(OPTIONS_WITH_WORKSPACE_ROOM.reports).length - 1); + // We should expect all the recent reports to show, excluding the archived rooms and the hidden thread + expect(results.recentReports.length).toBe(Object.values(OPTIONS_WITH_WORKSPACE_ROOM.reports).length - 2); }); describe('getShareLogOptions', () => { @@ -834,7 +851,7 @@ describe('OptionsListUtils', () => { const options = getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); const filteredOptions = filterAndOrderOptions(options, ''); - expect(filteredOptions.recentReports.length + filteredOptions.personalDetails.length).toBe(12); + expect(filteredOptions.recentReports.length + filteredOptions.personalDetails.length).toBe(13); }); it('should return filtered options in correct order', () => { @@ -923,7 +940,7 @@ describe('OptionsListUtils', () => { const options = getSearchOptions(OPTIONS); const filteredOptions = filterAndOrderOptions(options, searchText); - expect(filteredOptions.recentReports.length).toBe(2); + expect(filteredOptions.recentReports.length).toBe(3); expect(filteredOptions.recentReports.at(0)?.text).toBe('Mister Fantastic'); expect(filteredOptions.recentReports.at(1)?.text).toBe('Mister Fantastic, Invisible Woman'); }); @@ -1151,7 +1168,7 @@ describe('OptionsListUtils', () => { const options = getSearchOptions(OPTIONS); const filteredOptions = filterAndOrderOptions(options, 'fantastic'); - expect(filteredOptions.recentReports.length).toBe(2); + expect(filteredOptions.recentReports.length).toBe(3); expect(filteredOptions.recentReports.at(0)?.text).toBe('Mister Fantastic'); expect(filteredOptions.recentReports.at(1)?.text).toBe('Mister Fantastic, Invisible Woman'); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index e25b9adc89f2..6151ac20ba30 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -3,6 +3,7 @@ import {addDays, format as formatDate} from 'date-fns'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import DateUtils from '@libs/DateUtils'; +import {translateLocal} from '@libs/Localize'; import { buildOptimisticChatReport, buildOptimisticCreatedReportAction, @@ -501,6 +502,20 @@ describe('ReportUtils', () => { expect(getReportName(threadOfRemovedRoomMemberAction, policy, removedParentReportAction)).toBe('removed ragnar@vikings.net'); }); }); + + describe('Task Report', () => { + const htmlTaskTitle = `

heading with link

`; + + it('Should return the text extracted from report name html', () => { + const report: Report = {...createRandomReport(1), type: 'task'}; + expect(getReportName({...report, reportName: htmlTaskTitle})).toEqual('heading with link'); + }); + + it('Should return deleted task translations when task is is deleted', () => { + const report: Report = {...createRandomReport(1), type: 'task', isDeletedParentAction: true}; + expect(getReportName({...report, reportName: htmlTaskTitle})).toEqual(translateLocal('parentReportAction.deletedTask')); + }); + }); }); describe('requiresAttentionFromCurrentUser', () => {