diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index d802d33cbdf79..5031d6a57fcec 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -186,6 +186,7 @@ function AvatarWithDisplayName({ const {localeCompare, formatPhoneNumber} = useLocalize(); const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`, {canEvict: false}); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST) ?? CONST.EMPTY_OBJECT; + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -199,7 +200,7 @@ function AvatarWithDisplayName({ const title = getReportName(report, reportAttributes); const isParentReportArchived = useReportIsArchived(report?.parentReportID); const subtitle = getChatRoomSubtitle(report, true, isReportArchived); - const parentNavigationSubtitleData = getParentNavigationSubtitle(report, isParentReportArchived, reportAttributes); + const parentNavigationSubtitleData = getParentNavigationSubtitle(report, conciergeReportID, isParentReportArchived, reportAttributes); const isMoneyRequestOrReport = isMoneyRequestReport(report) || isMoneyRequest(report) || isTrackExpenseReport(report) || isInvoiceReport(report); const ownerPersonalDetails = getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); const displayNamesWithTooltips = getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails), false, localeCompare, formatPhoneNumber); diff --git a/src/components/SelectionListWithSections/Search/ExpenseReportListItemRow.tsx b/src/components/SelectionListWithSections/Search/ExpenseReportListItemRow.tsx index 36b1636294dc5..63a42d1d59513 100644 --- a/src/components/SelectionListWithSections/Search/ExpenseReportListItemRow.tsx +++ b/src/components/SelectionListWithSections/Search/ExpenseReportListItemRow.tsx @@ -9,6 +9,7 @@ import type {SearchColumnType} from '@components/Search/types'; import type {ExpenseReportListItemType} from '@components/SelectionListWithSections/types'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -19,6 +20,7 @@ import {getParentNavigationSubtitle, getReportStatusTranslation} from '@libs/Rep import {isCorrectSearchUserName} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportAction} from '@src/types/onyx'; import ActionCell from './ActionCell'; import DateCell from './DateCell'; @@ -67,6 +69,7 @@ function ExpenseReportListItemRow({ const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const {isLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const expensifyIcons = useMemoizedLazyExpensifyIcons(['ArrowRight']); @@ -232,7 +235,7 @@ function ExpenseReportListItemRow({ const isInMobileSelectionMode = shouldUseNarrowLayout && !!canSelectMultiple; // Compute accessible group label (user name, subtitle, report title, status, amount) - const parentNavigationSubtitleData = getParentNavigationSubtitle(item); + const parentNavigationSubtitleData = getParentNavigationSubtitle(item, conciergeReportID); const subtitleLabel = translate('threads.parentNavigationSummary', parentNavigationSubtitleData); const statusLabel = getReportStatusTranslation({stateNum: item.stateNum, statusNum: item.statusNum, translate}); const amountLabel = convertToDisplayString(totalDisplaySpend, currency); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 22eb7007262f0..bfdd327139959 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5655,7 +5655,7 @@ function getAdminRoomInvitedParticipants(translate: LocalizedTranslate, parentRe /** * Parse html of reportAction into text */ -function parseReportActionHtmlToText(reportAction: OnyxEntry, reportID: string | undefined, childReportID?: string): string { +function parseReportActionHtmlToText(reportAction: OnyxEntry, reportID: string | undefined, conciergeReportID: string | undefined, childReportID?: string): string { if (!reportAction) { return ''; } @@ -5679,7 +5679,7 @@ function parseReportActionHtmlToText(reportAction: OnyxEntry, repo if (match[1] !== childReportID) { // This will be fixed as follow up https://github.com/Expensify/App/pull/75357 // eslint-disable-next-line @typescript-eslint/no-use-before-define, @typescript-eslint/no-deprecated - reportIDToName[match[1]] = getReportName({report: getReportOrDraftReport(match[1])}) ?? ''; + reportIDToName[match[1]] = getReportName({report: getReportOrDraftReport(match[1]), conciergeReportID}) ?? ''; } } @@ -5711,6 +5711,7 @@ function getReportActionMessage({ childReportID, reports, personalDetails, + conciergeReportID, }: { reportAction: OnyxEntry; translate: LocalizedTranslate; @@ -5719,6 +5720,7 @@ function getReportActionMessage({ childReportID?: string; reports?: Report[]; personalDetails?: Partial; + conciergeReportID: string | undefined; }) { if (isEmptyObject(reportAction)) { return ''; @@ -5764,7 +5766,7 @@ function getReportActionMessage({ return getReimbursementDeQueuedOrCanceledActionMessage(translate, reportAction, getReportOrDraftReport(reportID, reports)); } - return parseReportActionHtmlToText(reportAction, reportID, childReportID); + return parseReportActionHtmlToText(reportAction, reportID, conciergeReportID, childReportID); } /** @@ -5858,6 +5860,7 @@ function getReportName(reportNameInformation: GetReportNameParams): string { childReportID: report?.reportID, reports, personalDetails, + conciergeReportID, }).replaceAll(/(\n+|\r\n|\n|\r)/gm, ' '); if (isAttachment && reportActionMessage) { // eslint-disable-next-line @typescript-eslint/no-deprecated @@ -6126,7 +6129,12 @@ function getPendingChatMembers(accountIDs: number[], previousPendingChatMembers: /** * Gets the parent navigation subtitle for the report */ -function getParentNavigationSubtitle(report: OnyxEntry, isParentReportArchived = false, reportAttributes?: ReportAttributesDerivedValue['reports']): ParentNavigationSummaryParams { +function getParentNavigationSubtitle( + report: OnyxEntry, + conciergeReportID: string | undefined, + isParentReportArchived = false, + reportAttributes?: ReportAttributesDerivedValue['reports'], +): ParentNavigationSummaryParams { const parentReport = getParentReport(report); if (isEmptyObject(parentReport)) { @@ -6165,7 +6173,7 @@ function getParentNavigationSubtitle(report: OnyxEntry, isParentReportAr return { // This will be fixed as follow up https://github.com/Expensify/App/pull/75357 // eslint-disable-next-line @typescript-eslint/no-deprecated - reportName: getReportName({report: parentReport, reportAttributes}), + reportName: getReportName({report: parentReport, reportAttributes, conciergeReportID}), workspaceName: getPolicyName({report: parentReport, returnEmptyIfNotFound: true}), }; } diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 30e15174e4aa2..d495fa645bfc9 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -1020,6 +1020,7 @@ function getShareDestination( reports: OnyxCollection, personalDetails: OnyxEntry, localeCompare: LocaleContextProps['localeCompare'], + conciergeReportID: string | undefined, ): ShareDestination { const report = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; @@ -1049,7 +1050,7 @@ function getShareDestination( icons: ReportUtils.getIcons(report, LocalePhoneNumber.formatPhoneNumber, personalDetails, Expensicons.FallbackAvatar), // Will be fixed in https://github.com/Expensify/App/issues/76852 // eslint-disable-next-line @typescript-eslint/no-deprecated - displayName: ReportUtils.getReportName({report}), + displayName: ReportUtils.getReportName({report, conciergeReportID}), subtitle, displayNamesWithTooltips, shouldUseFullTitleToDisplay: ReportUtils.shouldUseFullTitleToDisplay(report), diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index da4bb7a036755..d97a7baac0d63 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -223,7 +223,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail const isReportArchived = useReportIsArchived(report?.reportID); const isArchivedRoom = useMemo(() => isArchivedNonExpenseReport(report, isReportArchived), [report, isReportArchived]); const shouldDisableRename = useMemo(() => shouldDisableRenameUtil(report, isReportArchived), [report, isReportArchived]); - const parentNavigationSubtitleData = getParentNavigationSubtitle(report, isParentReportArchived); + const parentNavigationSubtitleData = getParentNavigationSubtitle(report, conciergeReportID, isParentReportArchived); const base62ReportID = getBase62ReportID(Number(report.reportID)); const ancestors = useAncestors(report); // eslint-disable-next-line react-hooks/exhaustive-deps -- policy is a dependency because `getChatRoomSubtitle` calls `getPolicyName` which in turn retrieves the value from the `policy` value stored in Onyx diff --git a/src/pages/ShareCodePage.tsx b/src/pages/ShareCodePage.tsx index de57baeefc4ab..5d179d3e21604 100644 --- a/src/pages/ShareCodePage.tsx +++ b/src/pages/ShareCodePage.tsx @@ -15,6 +15,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useEnvironment from '@hooks/useEnvironment'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useReportAttributes from '@hooks/useReportAttributes'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -38,6 +39,7 @@ import shouldAllowDownloadQRCode from '@libs/shouldAllowDownloadQRCode'; import addTrailingForwardSlash from '@libs/UrlUtils'; import {getAvatarURL} from '@libs/UserAvatarUtils'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, Report} from '@src/types/onyx'; @@ -77,6 +79,7 @@ function ShareCodePage({report, policy, backTo}: ShareCodePageProps) { const qrCodeRef = useRef(null); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const reportAttributes = useReportAttributes(); const isParentReportArchived = useReportIsArchived(report?.parentReportID); const isReportArchived = useReportIsArchived(report?.reportID); @@ -94,11 +97,11 @@ function ShareCodePage({report, policy, backTo}: ShareCodePageProps) { .join(' & '); } - return getParentNavigationSubtitle(report, isParentReportArchived).workspaceName ?? getChatRoomSubtitle(report, false, isReportArchived); + return getParentNavigationSubtitle(report, conciergeReportID, isParentReportArchived).workspaceName ?? getChatRoomSubtitle(report, false, isReportArchived); } return currentUserPersonalDetails.login; - }, [report, currentUserPersonalDetails.login, isReport, isReportArchived, isParentReportArchived, formatPhoneNumber]); + }, [report, currentUserPersonalDetails.login, isReport, isReportArchived, isParentReportArchived, formatPhoneNumber, conciergeReportID]); const reportForTitle = useMemo(() => getReportForHeader(report), [report]); diff --git a/src/pages/inbox/HeaderView.tsx b/src/pages/inbox/HeaderView.tsx index 17e7c324f6743..29ba83825749c 100644 --- a/src/pages/inbox/HeaderView.tsx +++ b/src/pages/inbox/HeaderView.tsx @@ -124,6 +124,7 @@ function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked, const isReportArchived = isArchivedReport(reportNameValuePairs); const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: reportsSelector}); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const {translate, localeCompare, formatPhoneNumber} = useLocalize(); const theme = useTheme(); @@ -160,7 +161,7 @@ function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked, : undefined; const statusColorForInvoiceReport = isParentInvoiceAndIsChatThread ? getReportStatusColorStyle(theme, reportHeaderData?.stateNum, reportHeaderData?.statusNum) : {}; const isParentReportHeaderDataArchived = useReportIsArchived(reportHeaderData?.parentReportID); - const parentNavigationSubtitleData = getParentNavigationSubtitle(parentNavigationReport, isParentReportHeaderDataArchived); + const parentNavigationSubtitleData = getParentNavigationSubtitle(parentNavigationReport, conciergeReportID, isParentReportHeaderDataArchived); const reportDescription = Parser.htmlToText(getReportDescription(report)); const policyName = getPolicyName({report, returnEmptyIfNotFound: true}); const policyDescription = getPolicyDescriptionText(policy); @@ -243,7 +244,6 @@ function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked, const isReportInRHP = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; const shouldDisplaySearchRouter = !isInSidePanel && (!isReportInRHP || isSmallScreenWidth); const [onboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED); - const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const isChatUsedForOnboarding = isChatUsedForOnboardingReportUtils(report, onboarding, conciergeReportID, onboardingPurposeSelected); const shouldShowRegisterForWebinar = introSelected?.companySize === CONST.ONBOARDING_COMPANY_SIZE.MICRO && (isChatUsedForOnboarding || (isAdminRoom(report) && !isChatThread)) && !isInSidePanel; diff --git a/src/pages/tasks/NewTaskPage.tsx b/src/pages/tasks/NewTaskPage.tsx index 62d6e7510abdb..265b7f607bd7d 100644 --- a/src/pages/tasks/NewTaskPage.tsx +++ b/src/pages/tasks/NewTaskPage.tsx @@ -35,6 +35,7 @@ function NewTaskPage({route}: NewTaskPageProps) { const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const styles = useThemeStyles(); const {translate, formatPhoneNumber, localeCompare} = useLocalize(); @@ -45,7 +46,7 @@ function NewTaskPage({route}: NewTaskPageProps) { localeCompare, formatPhoneNumber, ); - const shareDestination = task?.shareDestination ? getShareDestination(task.shareDestination, reports, personalDetails, localeCompare) : undefined; + const shareDestination = task?.shareDestination ? getShareDestination(task.shareDestination, reports, personalDetails, localeCompare, conciergeReportID) : undefined; const parentReport = task?.shareDestination ? reports?.[`${ONYXKEYS.COLLECTION.REPORT}${task.shareDestination}`] : undefined; const ancestors = useAncestors(parentReport); const taskKey = `${task?.assignee}|${task?.assigneeAccountID}|${task?.description}|${task?.parentReportID}|${task?.shareDestination}|${task?.title}`; diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 2924aad985fb0..e5271eb3bb85d 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -115,6 +115,7 @@ import { isRootGroupChat, isSelfDMOrSelfDMThread, isWorkspaceMemberLeavingWorkspaceRoom, + parseReportActionHtmlToText, parseReportRouteParams, prepareOnboardingOnyxData, pushTransactionViolationsOnyxData, @@ -2820,16 +2821,94 @@ describe('ReportUtils', () => { }); it('should return the correct parent navigation subtitle for the archived invoice report', () => { - const actual = getParentNavigationSubtitle(baseArchivedPolicyExpenseChat, true); + const actual = getParentNavigationSubtitle(baseArchivedPolicyExpenseChat, undefined, true); const normalizedActual = {...actual, reportName: actual.reportName?.replaceAll('\u00A0', ' ')}; expect(normalizedActual).toEqual({reportName: 'A workspace & Ragnar Lothbrok (archived)'}); }); it('should return the correct parent navigation subtitle for the non archived invoice report', () => { - const actual = getParentNavigationSubtitle(baseArchivedPolicyExpenseChat, false); + const actual = getParentNavigationSubtitle(baseArchivedPolicyExpenseChat, undefined, false); const normalizedActual = {...actual, reportName: actual.reportName?.replaceAll('\u00A0', ' ')}; expect(normalizedActual).toEqual({reportName: 'A workspace & Ragnar Lothbrok'}); }); + + it('should return empty object when report has no parent and is not expense or IOU', () => { + const chatReport: Report = { + reportID: '100', + lastReadTime: '2024-02-01 04:56:47.233', + reportName: 'Chat Report', + type: CONST.REPORT.TYPE.CHAT, + }; + const actual = getParentNavigationSubtitle(chatReport, undefined); + expect(actual).toEqual({}); + }); + + it('should pass conciergeReportID through to getReportName for parent report name resolution', () => { + const conciergeReportID = '999'; + const conciergeParentReport: Report = { + reportID: conciergeReportID, + lastReadTime: '2024-02-01 04:56:47.233', + reportName: 'Concierge', + type: CONST.REPORT.TYPE.CHAT, + }; + const childReport: Report = { + reportID: '1000', + lastReadTime: '2024-02-01 04:56:47.233', + parentReportID: conciergeReportID, + parentReportActionID: '1', + reportName: 'Child Thread', + type: CONST.REPORT.TYPE.CHAT, + }; + + const reportCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.REPORT, [conciergeParentReport, childReport], (report) => report.reportID); + return Onyx.multiSet({ + ...reportCollectionDataSet, + }) + .then(waitForBatchedUpdates) + .then(() => { + const actual = getParentNavigationSubtitle(childReport, conciergeReportID); + expect(actual.reportName).toBe('Concierge'); + }); + }); + + it('should return reportName and workspaceName when parent report exists and conciergeReportID is undefined', () => { + const actual = getParentNavigationSubtitle(baseArchivedPolicyExpenseChat, undefined, false); + expect(actual).toHaveProperty('reportName'); + }); + }); + + describe('parseReportActionHtmlToText', () => { + it('should return empty string for undefined reportAction', () => { + const result = parseReportActionHtmlToText(undefined, '123', undefined); + expect(result).toBe(''); + }); + + it('should return text from reportAction message when no html', () => { + const reportAction = { + ...createRandomReportAction(1), + message: [{type: 'COMMENT', text: 'Hello world'}], + } as unknown as ReportAction; + const result = parseReportActionHtmlToText(reportAction, '123', undefined); + expect(result).toBe('Hello world'); + }); + + it('should parse html to text with conciergeReportID', () => { + const reportAction = { + ...createRandomReportAction(1), + message: [{type: 'COMMENT', html: '

Hello world

', text: 'Hello world'}], + } as unknown as ReportAction; + const result = parseReportActionHtmlToText(reportAction, '123', '999'); + expect(result).toBe('Hello world'); + }); + + it('should handle conciergeReportID being undefined', () => { + const reportAction = { + ...createRandomReportAction(1), + message: [{type: 'COMMENT', html: '

Test message

', text: 'Test message'}], + } as unknown as ReportAction; + const result = parseReportActionHtmlToText(reportAction, '456', undefined); + expect(result).toBe('Test message'); + }); }); describe('requiresAttentionFromCurrentUser', () => {