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

Allow HTML for task titles #55967

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
cd226e9
Allow HTML for task titles
johnmlee101 Jan 29, 2025
72fb9d6
Don't encode task titles
johnmlee101 Jan 29, 2025
7f727d7
Support both html and non-html task titles
johnmlee101 Jan 29, 2025
7077d24
Eslint
johnmlee101 Jan 29, 2025
2cebebc
Additional html and text docs
johnmlee101 Jan 29, 2025
9834057
prettier
johnmlee101 Jan 29, 2025
96676ea
Safe access of task titles
johnmlee101 Jan 29, 2025
aa8fd5b
Fix name
johnmlee101 Jan 29, 2025
77f560d
prettier
johnmlee101 Jan 29, 2025
ea4fa84
MAke work with useOnyx
johnmlee101 Jan 29, 2025
d281ba1
Generalize getting report nmae
johnmlee101 Jan 29, 2025
36d8edb
Use new formatter for reportName to correctly access report names
johnmlee101 Jan 29, 2025
487f610
Fix imports
johnmlee101 Jan 29, 2025
7407194
Prettier
johnmlee101 Jan 29, 2025
e10dddc
Get working in a more simplified way
johnmlee101 Jan 29, 2025
905a9b6
Remove str
johnmlee101 Jan 29, 2025
b8feffa
Prettier
johnmlee101 Jan 29, 2025
fd505e6
Add task-title renderer
johnmlee101 Jan 30, 2025
31bae0f
Render HTML from task title
johnmlee101 Jan 30, 2025
0d17784
Grow height with taskttile page editing
johnmlee101 Jan 30, 2025
2c0adc0
Allow for createTask flows to render html when editing tasks
johnmlee101 Jan 30, 2025
8f937a2
Prettier
johnmlee101 Jan 30, 2025
b285f42
TaskUtils
johnmlee101 Jan 30, 2025
7221db7
Prettier
johnmlee101 Jan 30, 2025
d9bd484
Remove getParsedComment
johnmlee101 Jan 30, 2025
0bead0f
Default values
johnmlee101 Jan 30, 2025
d36bc52
Missing dependency
johnmlee101 Jan 30, 2025
d8b14fd
Merge branch 'main' into john-title-task-html
johnmlee101 Jan 31, 2025
23b8c93
Merge branch 'main' into john-title-task-html
johnmlee101 Feb 3, 2025
a45b1a0
Fix new schema
johnmlee101 Feb 3, 2025
4fe45d6
Rename file
johnmlee101 Feb 4, 2025
e0ce0f5
Fix reference
johnmlee101 Feb 4, 2025
344ba8d
Remove unnecessary formatting for getReportName
johnmlee101 Feb 4, 2025
4a6559c
Change defaults
johnmlee101 Feb 4, 2025
dbc7563
Fix default values for string values
johnmlee101 Feb 4, 2025
6e6981e
prettier
johnmlee101 Feb 4, 2025
75e1863
Merge branch 'main' into john-title-task-html
johnmlee101 Feb 4, 2025
70f63cc
Merge branch 'main' into john-title-task-html
johnmlee101 Feb 6, 2025
88c4526
Don't parse comment if we're getting original report title
johnmlee101 Feb 6, 2025
5d1a576
Multi-line support
johnmlee101 Feb 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3343,6 +3343,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
'mention-user': HTMLElementModel.fromCustomModel({tagName: 'mention-user', contentModel: HTMLContentModel.textual}),
'mention-report': HTMLElementModel.fromCustomModel({tagName: 'mention-report', contentModel: HTMLContentModel.textual}),
'mention-here': HTMLElementModel.fromCustomModel({tagName: 'mention-here', contentModel: HTMLContentModel.textual}),
'task-title': HTMLElementModel.fromCustomModel({
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you see a need for changing this to a markdown-title or a generic renderer so that we can reuse the logic, instead of tagging it to task title?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For now I think its fine, since tasks are the big focus at the moment

tagName: 'task-title',
contentModel: HTMLContentModel.textual,
mixedUAStyles: {...styles.taskTitleMenuItem},
}),
'next-step': HTMLElementModel.fromCustomModel({
tagName: 'next-step',
mixedUAStyles: {...styles.textLabelSupporting, ...styles.lh16},
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
@@ -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 './TaskTitleRenderer';
import VideoRenderer from './VideoRenderer';

/**
Expand All @@ -32,6 +33,7 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = {
emoji: EmojiRenderer,
'next-step-email': NextStepEmailRenderer,
'deleted-action': DeletedActionRenderer,
'task-title': TaskTitleRenderer,
/* eslint-enable @typescript-eslint/naming-convention */
};

Expand Down
4 changes: 2 additions & 2 deletions src/components/ReportActionItem/TaskPreview.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -26,6 +25,7 @@ import ControlSelection from '@libs/ControlSelection';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import getButtonState from '@libs/getButtonState';
import Navigation from '@libs/Navigation/Navigation';
import Parser from '@libs/Parser';
import {isCanceledTaskReport, isOpenTaskReport, isReportManager} from '@libs/ReportUtils';
import {getTaskTitleFromReport} from '@libs/TaskUtils';
import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
Expand Down Expand Up @@ -74,7 +74,7 @@ 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 taskTitle = Parser.htmlToText(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;
Expand Down
4 changes: 2 additions & 2 deletions src/components/ReportActionItem/TaskView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -41,7 +42,6 @@ function TaskView({report}: TaskViewProps) {
useEffect(() => {
setTaskReport(report);
}, [report]);

const taskTitle = convertToLTR(report?.reportName ?? '');
const assigneeTooltipDetails = getDisplayNamesWithTooltips(getPersonalDetailsForAccountIDs(report?.managerID ? [report?.managerID] : [], personalDetails), false);
const isOpen = isOpenTaskReport(report);
Expand Down Expand Up @@ -111,7 +111,7 @@ function TaskView({report}: TaskViewProps) {
numberOfLines={3}
style={styles.taskTitleMenuItem}
>
{taskTitle}
<RenderHTML html={`<task-title>${taskTitle}</task-title>`} />
Copy link
Contributor

Choose a reason for hiding this comment

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

Haven't found anything broken with this but react-native-render-html warns against using RenderHTML as a child inside Text node.

</Text>
</View>
{!isDisableInteractive && (
Expand Down
2 changes: 1 addition & 1 deletion src/libs/API/parameters/CreateTaskParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/libs/API/parameters/EditTaskParams.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
type EditTaskParams = {
taskReportID?: string;
title?: string;
htmlTitle?: string;
description?: string;
editedTaskReportActionID?: string;
};
Expand Down
2 changes: 1 addition & 1 deletion src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6662,7 +6662,7 @@ function buildOptimisticTaskReport(

return {
reportID: generateReportID(),
reportName: title,
reportName: getParsedComment(title ?? ''),
description: getParsedComment(description ?? ''),
ownerAccountID,
participants,
Expand Down
5 changes: 3 additions & 2 deletions 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 @@ -53,9 +54,9 @@ function getTaskReportActionMessage(action: OnyxEntry<ReportAction>): Pick<Messa

function getTaskTitleFromReport(taskReport: OnyxEntry<Report>, 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;
return taskReport?.reportID && taskReport.reportName ? Parser.htmlToText(taskReport.reportName) : fallbackTitle;
}

function getTaskTitle(taskReportID: string | undefined, fallbackTitle = ''): string {
Expand Down
11 changes: 7 additions & 4 deletions src/libs/actions/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -542,7 +545,7 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.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();
const reportName = title ? ReportUtils.getParsedComment(title)?.trim() : report?.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 Down Expand Up @@ -605,7 +608,7 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task

const parameters: EditTaskParams = {
taskReportID: report.reportID,
title: reportName,
htmlTitle: reportName,
description: reportDescription,
editedTaskReportActionID: editTaskReportAction.reportActionID,
};
Expand Down
33 changes: 13 additions & 20 deletions src/pages/tasks/NewTaskDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, {useEffect, 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';
Expand All @@ -25,16 +24,11 @@ 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<Task>;
};
type NewTaskDetailsPageProps = PlatformStackScreenProps<NewTaskNavigatorParamList, typeof SCREENS.NEW_TASK.DETAILS>;

type NewTaskDetailsPageProps = NewTaskDetailsPageOnyxProps & PlatformStackScreenProps<NewTaskNavigatorParamList, typeof SCREENS.NEW_TASK.DETAILS>;

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 ?? '');
Expand All @@ -47,7 +41,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]);

Expand All @@ -57,8 +51,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) {
Expand All @@ -76,7 +70,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));
}
Expand All @@ -100,6 +94,7 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) {
validate={validate}
onSubmit={onSubmit}
enabledWhenOffline
allowHTML
>
<View style={styles.mb5}>
<InputWrapper
Expand All @@ -110,9 +105,11 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) {
inputID={INPUT_IDS.TASK_TITLE}
label={translate('task.title')}
accessibilityLabel={translate('task.title')}
value={taskTitle}
value={Parser.htmlToMarkdown(Parser.replace(taskTitle))}
onValueChange={setTaskTitle}
autoCorrect={false}
autoGrowHeight
type="markdown"
/>
</View>
<View style={styles.mb5}>
Expand All @@ -139,8 +136,4 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) {

NewTaskDetailsPage.displayName = 'NewTaskDetailsPage';

export default withOnyx<NewTaskDetailsPageProps, NewTaskDetailsPageOnyxProps>({
task: {
key: ONYXKEYS.TASK,
},
})(NewTaskDetailsPage);
export default NewTaskDetailsPage;
Loading
Loading