Skip to content
Merged
9 changes: 8 additions & 1 deletion src/components/Search/SearchRouter/useAskConcierge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import useSidePanelReportID from '@hooks/useSidePanelReportID';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';

import {addAttachmentWithComment, addComment} from '@userActions/Report';
import {createTaskFromMarkdown} from '@userActions/Task';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -25,7 +26,9 @@ function useAskConcierge({forceConcierge = false}: {forceConcierge?: boolean} =
const {openConciergeAnywhere, isInSidePanel} = useOpenConciergeAnywhere();
const targetReportID = !forceConcierge && isInSidePanel && sidePanelReportID ? sidePanelReportID : conciergeReportID;
const [targetReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(targetReportID)}`);
const {timezone, accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const {timezone, accountID: currentUserAccountID} = currentUserPersonalDetails;
const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE);
const delegateAccountID = useDelegateAccountID();
const shouldShowAskConcierge = !!targetReportID && !!targetReport;

Expand All @@ -35,6 +38,10 @@ function useAskConcierge({forceConcierge = false}: {forceConcierge?: boolean} =
return;
}
openConciergeAnywhere({forceConcierge});

if (createTaskFromMarkdown({text: trimmedQuery, parentReport: targetReport, currentUserPersonalDetails, quickAction})) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce the shorthand task-title limit before creating tasks

When the Home prompt contains a [] title longer than CONST.TITLE_CHARACTER_LIMIT, this new path calls createTaskFromMarkdown() and clears the input without any length validation. The report composer explicitly rejects the same shorthand in useDebouncedCommentMaxLengthValidation, but ConciergePromptBox only checks that the value is nonempty, so Home can now submit task titles that the established composer flow prevents; enforce the same limit before claiming and creating the task.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

From what I see it's true, but I'd handle it as a follow-up

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

nvm, I'll add a fix here

return;
}
addComment({
report: targetReport,
notifyReportID: targetReportID,
Expand Down
92 changes: 91 additions & 1 deletion src/libs/actions/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@ import type {CancelTaskParams, CompleteTaskParams, CreateTaskParams, EditTaskAss
import {WRITE_COMMANDS} from '@libs/API/types';
import DateUtils from '@libs/DateUtils';
import * as ErrorUtils from '@libs/ErrorUtils';
import {isEmailPublicDomain} from '@libs/LoginUtils';
import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
import Navigation from '@libs/Navigation/Navigation';
import {getDBTimeWithSkew} from '@libs/NetworkState';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import {addDomainToShortMention} from '@libs/ParsingUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import {deprecatedGetReportName} from '@libs/ReportNameUtils';
import * as ReportUtils from '@libs/ReportUtils';
import {buildOptimisticSnapshotData} from '@libs/SearchQueryUtils';
import {getAllPersonalDetailLogins} from '@libs/ShortMentionLogins';
import playSound, {SOUNDS} from '@libs/Sound';
import type {AvatarSource} from '@libs/UserAvatarUtils';
import {generateAccountID} from '@libs/UserUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -27,6 +31,7 @@ import type {Route} from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type {Icon} from '@src/types/onyx/OnyxCommon';
import type PersonalDetails from '@src/types/onyx/PersonalDetails';
import type {CurrentUserPersonalDetails} from '@src/types/onyx/PersonalDetails';
import type {ReportActions} from '@src/types/onyx/ReportAction';
import type ReportAction from '@src/types/onyx/ReportAction';
import type {OnyxData} from '@src/types/onyx/Request';
Expand All @@ -35,6 +40,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';

import type {NullishDeep, OnyxEntry, OnyxUpdate} from 'react-native-onyx';

import {Str} from 'expensify-common';
import Onyx from 'react-native-onyx';

import {getMostRecentReportID, navigateToConciergeChatAndDeleteReport, notifyNewAction, optimisticReportLastData} from './Report';
Expand Down Expand Up @@ -87,6 +93,19 @@ type CreateTaskAndNavigateParams = {
taskCreatorAndAssigneeDetails: OnyxEntry<OnyxTypes.PersonalDetailsList>;
};

type CreateTaskFromMarkdownParams = {
/** The already trimmed text the user is sending */
text: string;
/** The parent report to which the task belongs */
parentReport: OnyxEntry<OnyxTypes.Report>;
/** The current user's personal details */
currentUserPersonalDetails: CurrentUserPersonalDetails;
/** The quick action associated with the task */
quickAction: OnyxEntry<OnyxTypes.QuickAction>;
/** The ancestors of the task */
ancestors?: ReportUtils.Ancestor[];
};

type DeleteTaskOptions = {
ancestors?: ReportUtils.Ancestor[];
shouldNavigateBack?: boolean;
Expand Down Expand Up @@ -410,6 +429,77 @@ function createTaskAndNavigate(params: CreateTaskAndNavigateParams) {
notifyNewAction(parentReportID, optimisticAddCommentReport.reportAction, true);
}

/**
* Creates a task from the `[] title` markdown shorthand (with an optional `@mention` assignee).
*
* @returns true when a task was created, so the caller can skip sending the text as a plain comment.
*/
function createTaskFromMarkdown({text, parentReport, currentUserPersonalDetails, quickAction, ancestors = []}: CreateTaskFromMarkdownParams): boolean {
// A task cannot be created without a parent report, so let the caller fall back to sending the text as a comment.
if (!parentReport?.reportID) {
return false;
}

const taskMatch = text.match(CONST.REGEX.TASK_TITLE_WITH_OPTIONAL_SHORT_MENTION);
if (!taskMatch) {
return false;
}

let taskTitle = taskMatch[3] ? taskMatch[3].trim().replaceAll('\n', ' ') : undefined;
if (!taskTitle || taskTitle.length > CONST.TITLE_CHARACTER_LIMIT) {
return false;
}

const currentUserEmail = currentUserPersonalDetails.email ?? '';
const mention = taskMatch[1] ? taskMatch[1].trim() : '';
const currentUserPrivateDomain = isEmailPublicDomain(currentUserEmail) ? '' : Str.extractEmailDomain(currentUserEmail);
const mentionWithDomain = addDomainToShortMention(mention, getAllPersonalDetailLogins(), currentUserPrivateDomain) ?? mention;
const isValidMention = Str.isValidEmail(mentionWithDomain);

let assignee: OnyxEntry<OnyxTypes.PersonalDetails>;
let assigneeChatReport;
if (mentionWithDomain) {
if (isValidMention) {
assignee = PersonalDetailsUtils.getPersonalDetailByEmail(mentionWithDomain);
if (!assignee) {
const optimisticDataForNewAssignee = setNewOptimisticAssignee(currentUserPersonalDetails.accountID, {
accountID: generateAccountID(mentionWithDomain),
login: mentionWithDomain,
});
assignee = optimisticDataForNewAssignee.assignee;
assigneeChatReport = optimisticDataForNewAssignee.assigneeReport;
}
} else {
taskTitle = `@${mentionWithDomain} ${taskTitle}`;
}
}

const taskCreatorAndAssigneeDetails = {[currentUserPersonalDetails.accountID]: currentUserPersonalDetails};
if (assignee) {
taskCreatorAndAssigneeDetails[assignee.accountID] = assignee;
}

createTaskAndNavigate({
parentReport,
title: taskTitle,
description: '',
assigneeEmail: assignee?.login ?? '',
currentUserAccountID: currentUserPersonalDetails.accountID,
currentUserEmail,
currentUserDisplayName: currentUserPersonalDetails.displayName,
currentUserAvatar: currentUserPersonalDetails.avatar,
assigneeAccountID: assignee?.accountID,
assigneeChatReport,
policyID: parentReport?.policyID,
isCreatedUsingMarkdown: true,
quickAction,
ancestors,
taskCreatorAndAssigneeDetails,
});

return true;
}

function buildTaskData(
taskReport: OnyxEntry<OnyxTypes.Report>,
taskReportID: string,
Expand Down Expand Up @@ -1554,6 +1644,7 @@ function completeTestDriveTask(

export {
createTaskAndNavigate,
createTaskFromMarkdown,
editTask,
editTaskAssignee,
setTitleValue,
Expand All @@ -1574,7 +1665,6 @@ export {
getTaskAssigneeAccountID,
clearTaskErrors,
canModifyTask,
setNewOptimisticAssignee,
getNavigationUrlOnTaskDelete,
canActionTask,
getFinishOnboardingTaskOnyxData,
Expand Down
Loading
Loading