diff --git a/src/CONST.ts b/src/CONST.ts index 122ab114d40e..112e7083f33e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3429,6 +3429,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/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/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 495b3dbd51fd..4f22d65cbea0 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -1,4 +1,3 @@ -import {Str} from 'expensify-common'; import React from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; @@ -12,7 +11,6 @@ import {usePersonalDetails} from '@components/OnyxProvider'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import RenderHTML from '@components/RenderHTML'; import {showContextMenuForReport} from '@components/ShowContextMenuContext'; -import Text from '@components/Text'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; @@ -27,7 +25,6 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import getButtonState from '@libs/getButtonState'; import Navigation from '@libs/Navigation/Navigation'; import {isCanceledTaskReport, isOpenTaskReport, isReportManager} from '@libs/ReportUtils'; -import {getTaskTitleFromReport} from '@libs/TaskUtils'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -74,7 +71,6 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che const isTaskCompleted = !isEmptyObject(taskReport) ? taskReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED : action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED; - const taskTitle = Str.htmlEncode(getTaskTitleFromReport(taskReport, action?.childReportName ?? '')); const taskAssigneeAccountID = getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; const taskOwnerAccountID = taskReport?.ownerAccountID ?? action?.actorAccountID ?? CONST.DEFAULT_NUMBER_ID; const hasAssignee = taskAssigneeAccountID > 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/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/ReportUtils.ts b/src/libs/ReportUtils.ts index 61ad34bf6c0a..6032740c392c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4441,6 +4441,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}); @@ -4499,15 +4507,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; } @@ -6350,7 +6354,7 @@ function buildOptimisticEditedTaskFieldReportAction({title, description}: Task): { type: CONST.REPORT.MESSAGE.TYPE.COMMENT, text: changelog, - html: description ? getParsedComment(changelog) : changelog, + html: getParsedComment(changelog), }, ], person: [ @@ -6702,7 +6706,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/Task.ts b/src/libs/actions/Task.ts index 7132e870ed70..644b588839e4 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -114,7 +114,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, @@ -123,6 +123,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; @@ -320,7 +323,7 @@ function createTaskAndNavigate( parentReportID, taskReportID: optimisticTaskReport.reportID, createdTaskReportActionID: optimisticTaskCreatedAction.reportActionID, - title: optimisticTaskReport.reportName, + htmlTitle: optimisticTaskReport.reportName, description: optimisticTaskReport.description, assignee: assigneeEmail, assigneeAccountID, @@ -541,8 +544,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; @@ -558,7 +563,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}), @@ -579,6 +584,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}), @@ -605,7 +612,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/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/styles/index.ts b/src/styles/index.ts index 2f052564303b..51eee5bed61d 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', @@ -4258,6 +4268,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/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 2653a33b0ea5..ea7bfb15cb99 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', () => {