Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request : Upgrade task titles with markdown rendering #54165

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -119,6 +124,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
styles.mutedNormalTextLabel,
styles.onlyEmojisText,
styles.onlyEmojisTextLineHeight,
styles.taskTitleMenuItem,
],
);
/* eslint-enable @typescript-eslint/naming-convention */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import useEnvironment from '@hooks/useEnvironment';
import useThemeStyles from '@hooks/useThemeStyles';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import * as Link from '@userActions/Link';

Check failure on line 12 in src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Namespace imports from @userActions are not allowed. Use named imports instead. Example: import { action } from "@userActions/module"
import CONST from '@src/CONST';

type AnchorRendererProps = CustomRendererProps<TBlock> & {
Expand All @@ -33,6 +33,8 @@
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)) {
Expand All @@ -41,7 +43,7 @@
// TODO: We should use TextLink, but I'm leaving it as Text for now because TextLink breaks the alignment in Android.
return (
<Text
style={styles.link}
style={[styles.link, isChildOfTaskTitle && {fontFamily: undefined, fontSize: undefined}]}
onPress={() => Link.openLink(attrHref, environmentURL, isAttachment)}
suppressHighlighting
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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<TText | TPhrasing>) {
const styles = useThemeStyles();

return (
<Text style={[styles.taskTitleMenuItem]}>
<TNodeChildrenRenderer tnode={tnode} />
</Text>
);
}

TaskTitleRenderer.displayName = 'TaskTitleRenderer';

export default TaskTitleRenderer;
2 changes: 2 additions & 0 deletions src/components/HTMLEngineProvider/HTMLRenderers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import MentionReportRenderer from './MentionReportRenderer';
import MentionUserRenderer from './MentionUserRenderer';
import NextStepEmailRenderer from './NextStepEmailRenderer';
import PreRenderer from './PreRenderer';
import TaskTitleRenderer from './TaskTitleRendered';
import VideoRenderer from './VideoRenderer';

/**
Expand All @@ -26,6 +27,7 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = {
edited: EditedRenderer,
pre: PreRenderer,
/* eslint-disable @typescript-eslint/naming-convention */
'task-title': TaskTitleRenderer,
'mention-user': MentionUserRenderer,
'mention-report': MentionReportRenderer,
'mention-here': MentionHereRenderer,
Expand Down
6 changes: 5 additions & 1 deletion src/components/HTMLEngineProvider/htmlEngineUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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};
13 changes: 9 additions & 4 deletions src/components/ReportActionItem/TaskView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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';
Expand Down Expand Up @@ -41,13 +42,14 @@
useEffect(() => {
setTaskReport(report);
}, [report]);

const taskTitle = convertToLTR(report?.reportName ?? '');
const taskTitle = `<task-title>${convertToLTR(report.reportName ?? '')}<task-title>`;

Check failure on line 45 in src/components/ReportActionItem/TaskView.tsx

View workflow job for this annotation

GitHub Actions / typecheck

'report' is possibly 'undefined'.
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();
Expand Down Expand Up @@ -107,12 +109,15 @@
disabled={!canActionTask}
/>
<View style={[styles.flexRow, styles.flex1]}>
<Text
{/* <Text
numberOfLines={3}
style={styles.taskTitleMenuItem}
>
{taskTitle}
</Text>
</Text> */}
<View style={styles.renderHTMLTitle}>
<RenderHTML html={taskTitle} />
</View>
</View>
{!isDisableInteractive && (
<View style={styles.taskRightIconContainer}>
Expand Down
3 changes: 3 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4420,6 +4420,9 @@ function getReportNameInternal({
return getIOUUnapprovedMessage(parentReportAction);
}

if (isTaskReport(report)) {
return Parser.htmlToText(report?.reportName ?? '');
}
if (isChatThread(report)) {
if (!isEmptyObject(parentReportAction) && isTransactionThread(parentReportAction)) {
formattedName = getTransactionReportName({reportAction: parentReportAction, transactions, reports});
Expand Down
4 changes: 3 additions & 1 deletion src/libs/TaskUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Report> = {};
Expand Down Expand Up @@ -55,7 +56,8 @@ function getTaskTitleFromReport(taskReport: OnyxEntry<Report>, fallbackTitle = '
// 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',
// 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 {
Expand Down
7 changes: 5 additions & 2 deletions src/libs/actions/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task

// 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();
const parsedTitle = ReportUtils.getParsedComment(reportName ?? '');

// Description can be unset, so we default to an empty string if so
const newDescription = typeof description === 'string' ? ReportUtils.getParsedComment(description) : report.description;
Expand All @@ -558,7 +559,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}),
Expand All @@ -579,6 +580,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}),
Expand All @@ -605,7 +608,7 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task

const parameters: EditTaskParams = {
taskReportID: report.reportID,
title: reportName,
title: parsedTitle,
description: reportDescription,
editedTaskReportActionID: editTaskReportAction.reportActionID,
};
Expand Down
11 changes: 9 additions & 2 deletions src/pages/tasks/TaskTitlePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';

Check failure on line 16 in src/pages/tasks/TaskTitlePage.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Namespace imports from @libs are not allowed. Use named imports instead. Example: import { method } from "@libs/module"
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {TaskDetailsNavigatorParamList} from '@libs/Navigation/types';
import Parser from '@libs/Parser';
import * as ReportUtils from '@libs/ReportUtils';

Check failure on line 21 in src/pages/tasks/TaskTitlePage.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Namespace imports from @libs are not allowed. Use named imports instead. Example: import { method } from "@libs/module"
import withReportOrNotFound from '@pages/home/report/withReportOrNotFound';
import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound';
import variables from '@styles/variables';
import * as Task from '@userActions/Task';

Check failure on line 25 in src/pages/tasks/TaskTitlePage.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Namespace imports from @userActions are not allowed. Use named imports instead. Example: import { action } from "@userActions/module"
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
Expand All @@ -40,7 +42,7 @@

if (!title) {
ErrorUtils.addErrorMessage(errors, INPUT_IDS.TITLE, translate('newTaskPage.pleaseEnterTaskName'));
} else if (title.length > CONST.TITLE_CHARACTER_LIMIT) {
} else if (title.length > CONST.DESCRIPTION_LIMIT) {
ErrorUtils.addErrorMessage(errors, INPUT_IDS.TITLE, translate('common.error.characterLimitExceedCounter', {length: title.length, limit: CONST.TITLE_CHARACTER_LIMIT}));
}

Expand Down Expand Up @@ -104,7 +106,8 @@
name={INPUT_IDS.TITLE}
label={translate('task.title')}
accessibilityLabel={translate('task.title')}
defaultValue={report?.reportName ?? ''}
// defaultValue={report?.reportName ?? ''}
defaultValue={Parser.htmlToMarkdown(report?.reportName ?? '')}
ref={(element: AnimatedTextInputRef) => {
if (!element) {
return;
Expand All @@ -114,6 +117,10 @@
}
inputRef.current = element;
}}
autoGrowHeight
maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight}
shouldSubmitForm
isMarkdownEnabled

Check failure on line 123 in src/pages/tasks/TaskTitlePage.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Type '{ InputComponent: (props: CustomBaseTextInputProps & TextInputProps & RefAttributes<BaseTextInputRef>) => ReactElement<...> | null; ... 10 more ...; isMarkdownEnabled: true; }' is not assignable to type 'IntrinsicAttributes & Omit<CustomBaseTextInputProps & TextInputProps & RefAttributes<BaseTextInputRef>, "ref"> & InputComponentValueProps<...> & { ...; } & RefAttributes<...>'.
/>
</View>
</FormProvider>
Expand Down
Loading