From fd83371b5c21dc297cb68f38d62fc49779455a19 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Sun, 1 Mar 2026 16:54:38 +0500 Subject: [PATCH 01/10] add delay submissions msg in all locals --- src/languages/de.ts | 2 ++ src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ src/languages/fr.ts | 2 ++ src/languages/it.ts | 2 ++ src/languages/ja.ts | 2 ++ src/languages/nl.ts | 2 ++ src/languages/pl.ts | 2 ++ src/languages/pt-BR.ts | 2 ++ src/languages/zh-hans.ts | 2 ++ 10 files changed, 20 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index a41559c50c685..c124fe1fa6061 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1316,6 +1316,8 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `für ${comment}` : ''}`, submitted: (memo?: string) => `eingereicht${memo ? `, mit dem Vermerk ${memo}` : ''}`, automaticallySubmitted: `eingereicht über Einreichungen verzögern`, + submittedVia: 'eingereicht über ', + delaySubmissions: 'Einreichungen verzögern', queuedToSubmitViaDEW: 'zur Einreichung über benutzerdefinierten Genehmigungsworkflow eingereiht', queuedToApproveViaDEW: 'Zur Genehmigung über benutzerdefinierten Genehmigungsworkflow eingereiht', trackedAmount: (formattedAmount: string, comment?: string) => `Verfolgen ${formattedAmount}${comment ? `für ${comment}` : ''}`, diff --git a/src/languages/en.ts b/src/languages/en.ts index 6afd64b0f924d..63120d25ac662 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1338,6 +1338,8 @@ const translations = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? ` for ${comment}` : ''}`, submitted: (memo?: string) => `submitted${memo ? `, saying ${memo}` : ''}`, automaticallySubmitted: `submitted via delay submissions`, + submittedVia: 'submitted via ', + delaySubmissions: 'delay submissions', queuedToSubmitViaDEW: 'queued to submit via custom approval workflow', queuedToApproveViaDEW: 'queued to approve via custom approval workflow', trackedAmount: (formattedAmount: string, comment?: string) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 42f18a50bb3ea..674368fb31bca 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1168,6 +1168,8 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount, comment) => `${formattedAmount}${comment ? ` para ${comment}` : ''}`, submitted: (memo) => `enviado${memo ? `, dijo ${memo}` : ''}`, automaticallySubmitted: `envió mediante retrasar envíos`, + submittedVia: 'envió mediante ', + delaySubmissions: 'retrasar envíos', queuedToSubmitViaDEW: 'en cola para enviar a través del flujo de aprobación personalizado', queuedToApproveViaDEW: 'en cola para aprobar a través del flujo de aprobación personalizado', trackedAmount: (formattedAmount, comment) => `realizó un seguimiento de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 0be24b76c8f2a..e682906c020d0 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1320,6 +1320,8 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `pour ${comment}` : ''}`, submitted: (memo?: string) => `soumis${memo ? `, indiquant « ${memo} »` : ''}`, automaticallySubmitted: `soumis via soumissions différées`, + submittedVia: 'soumis via ', + delaySubmissions: 'soumissions différées', queuedToSubmitViaDEW: 'en file d’attente pour être soumis via le circuit d’approbation personnalisé', queuedToApproveViaDEW: 'mis en file d’attente pour approbation via un processus d’approbation personnalisé', trackedAmount: (formattedAmount: string, comment?: string) => `suivi de ${formattedAmount}${comment ? `pour ${comment}` : ''}`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 0049e5738d298..ce4d91136398a 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1314,6 +1314,8 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `per ${comment}` : ''}`, submitted: (memo?: string) => `inviato${memo ? `, con nota: ${memo}` : ''}`, automaticallySubmitted: `inviato tramite invio posticipato`, + submittedVia: 'inviato tramite ', + delaySubmissions: 'invio posticipato', queuedToSubmitViaDEW: 'in coda per l’invio tramite flusso di approvazione personalizzato', queuedToApproveViaDEW: 'in coda per l’approvazione tramite flusso di approvazione personalizzato', trackedAmount: (formattedAmount: string, comment?: string) => `monitoraggio di ${formattedAmount}${comment ? `per ${comment}` : ''}`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 2401f5ebbc63e..6c842030c7d2f 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1306,6 +1306,8 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `${comment}用` : ''}`, submitted: (memo?: string) => `送信済み${memo ? `、メモ: ${memo}` : ''}`, automaticallySubmitted: `提出の延期 経由で提出されました`, + submittedVia: ' 経由で提出されました', + delaySubmissions: '提出の延期', queuedToSubmitViaDEW: 'カスタム承認ワークフローで提出待ち', queuedToApproveViaDEW: 'カスタム承認ワークフローで承認待ちに設定されました', trackedAmount: (formattedAmount: string, comment?: string) => `追跡中 ${formattedAmount}${comment ? `${comment}用` : ''}`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index f9e531173a01b..e753d9f5b9fae 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1313,6 +1313,8 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `voor ${comment}` : ''}`, submitted: (memo?: string) => `ingediend${memo ? `, met de omschrijving ${memo}` : ''}`, automaticallySubmitted: `ingediend via uitgestelde indieningen`, + submittedVia: 'ingediend via ', + delaySubmissions: 'uitgestelde indieningen', queuedToSubmitViaDEW: 'in wachtrij om via aangepast goedkeuringsproces in te dienen', queuedToApproveViaDEW: 'in de wachtrij gezet voor goedkeuring via aangepaste goedkeuringsworkflow', trackedAmount: (formattedAmount: string, comment?: string) => `volgt ${formattedAmount}${comment ? `voor ${comment}` : ''}`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 6affa32730dd0..f6926edf7e9cc 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1313,6 +1313,8 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `za ${comment}` : ''}`, submitted: (memo?: string) => `przesłano${memo ? `, wpisując ${memo}` : ''}`, automaticallySubmitted: `przesłano przez opóźnianie wysyłania`, + submittedVia: 'przesłano przez ', + delaySubmissions: 'opóźnianie wysyłania', queuedToSubmitViaDEW: 'w kolejce do przesłania przez niestandardowy proces zatwierdzania', queuedToApproveViaDEW: 'oczekuje na zatwierdzenie w niestandardowym procesie akceptacji', trackedAmount: (formattedAmount: string, comment?: string) => `śledzenie ${formattedAmount}${comment ? `za ${comment}` : ''}`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index baea489063f58..c4e6bb84bf4ee 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1312,6 +1312,8 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `para ${comment}` : ''}`, submitted: (memo?: string) => `enviado${memo ? `, dizendo ${memo}` : ''}`, automaticallySubmitted: `enviado via atrasar envios`, + submittedVia: 'enviado via ', + delaySubmissions: 'atrasar envios', queuedToSubmitViaDEW: 'na fila para enviar via fluxo de aprovação personalizado', queuedToApproveViaDEW: 'na fila para aprovar via fluxo de aprovação personalizado', trackedAmount: (formattedAmount: string, comment?: string) => `rastreamento de ${formattedAmount}${comment ? `para ${comment}` : ''}`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 1355afe4f97d0..9996ced3dc76d 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1288,6 +1288,8 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `用于 ${comment}` : ''}`, submitted: (memo?: string) => `已提交${memo ? `,备注为 ${memo}` : ''}`, automaticallySubmitted: `通过延迟提交提交`, + submittedVia: '通过', + delaySubmissions: '延迟提交', queuedToSubmitViaDEW: '已排队,待通过自定义审批流程提交', queuedToApproveViaDEW: '已排队,等待通过自定义审批流程批准', trackedAmount: (formattedAmount: string, comment?: string) => `跟踪 ${formattedAmount}${comment ? `用于 ${comment}` : ''}`, From ad34bcbf4d7df94e7a8c67cdd56b1736d0f811b7 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Sun, 1 Mar 2026 16:55:37 +0500 Subject: [PATCH 02/10] refactor: ReportActionItemMessageWithExplain to handle submitted via harvesting --- .../inbox/report/PureReportActionItem.tsx | 1 + .../ReportActionItemMessageWithExplain.tsx | 59 ++++++++++++++----- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 44e5316191b0d..f5cb36ee2c7cf 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -1338,6 +1338,7 @@ function PureReportActionItem({ action={action} childReport={childReport} originalReport={originalReport} + wasSubmittedViaHarvesting /> ); } else if (hasPendingDEWSubmit(reportMetadata, isDEWPolicy) && isPendingAdd) { diff --git a/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx b/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx index 67325f1f2b840..c0f62d8168719 100644 --- a/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx +++ b/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx @@ -15,6 +15,7 @@ import {openLink} from '@libs/actions/Link'; import {explain} from '@libs/actions/Report'; import {hasReasoning} from '@libs/ReportActionsUtils'; import variables from '@styles/variables'; +import CONST from '@src/CONST'; import type {Report, ReportAction} from '@src/types/onyx'; import ReportActionItemBasicMessage from './ReportActionItemBasicMessage'; @@ -30,13 +31,16 @@ type ReportActionItemMessageWithExplainProps = { /** Original report from which the given reportAction is first created */ originalReport: OnyxEntry; + + /** Whether the report was submitted via delay submissions */ + wasSubmittedViaHarvesting?: boolean; }; /** * Wrapper component that renders a message and automatically appends the "Explain" link * if the action has reasoning. */ -function ReportActionItemMessageWithExplain({message, action, childReport, originalReport}: ReportActionItemMessageWithExplainProps) { +function ReportActionItemMessageWithExplain({message, action, childReport, originalReport, wasSubmittedViaHarvesting = false}: ReportActionItemMessageWithExplainProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -59,6 +63,45 @@ function ReportActionItemMessageWithExplain({message, action, childReport, origi ); } + const explainAndIconBlock = ( + + explain(childReport, originalReport, action, translate, personalDetail.accountID, personalDetail?.timezone)} + style={[styles.chatItemMessage, styles.link, styles.mrHalf]} + text={translate('common.explain')} + /> + + + ); + + if (wasSubmittedViaHarvesting) { + return ( + + + + openLink(CONST.SELECT_WORKFLOWS_HELP_URL, environmentURL)} + style={[styles.chatItemMessage, styles.link]} + text={translate('iou.delaySubmissions')} + /> + + {explainAndIconBlock} + + + ); + } + return ( @@ -66,19 +109,7 @@ function ReportActionItemMessageWithExplain({message, action, childReport, origi textStyles={[styles.chatItemMessage, styles.colorMuted]} text={`${message}. `} /> - - explain(childReport, originalReport, action, translate, personalDetail.accountID, personalDetail?.timezone)} - style={[styles.chatItemMessage, styles.link, styles.mrHalf]} - text={translate('common.explain')} - /> - - + {explainAndIconBlock} ); From 1e94be02e8e6b48c124d1c653105892b32a60401 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Mon, 2 Mar 2026 00:12:22 +0500 Subject: [PATCH 03/10] Use existing automaticallySubmitted and extract text and link --- src/CONST/index.ts | 3 ++ src/languages/de.ts | 2 - src/languages/en.ts | 2 - src/languages/es.ts | 2 - src/languages/fr.ts | 2 - src/languages/it.ts | 2 - src/languages/ja.ts | 2 - src/languages/nl.ts | 2 - src/languages/pl.ts | 2 - src/languages/pt-BR.ts | 2 - src/languages/zh-hans.ts | 2 - .../ReportActionItemMessageWithExplain.tsx | 31 ++++++++----- tests/ui/PureReportActionItemTest.tsx | 45 +++++++++++++++++++ 13 files changed, 69 insertions(+), 30 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 19ac23e1cd543..4a3bd3c4f0c18 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -388,6 +388,9 @@ const CONST = { // Regex to get link in href prop inside of component REGEX_LINK_IN_ANCHOR: /]*?\s+)?href="([^"]*)"/gi, + // Regex to parse a single anchor tag and extract text, href, and label + REGEX_ANCHOR_WITH_TEXT: /^(.*?)]*href="([^"]*)"[^>]*>(.*?)<\/a>(.*)$/s, + // Regex to read violation value from string given by backend VIOLATION_LIMIT_REGEX: /[^0-9]+/g, diff --git a/src/languages/de.ts b/src/languages/de.ts index c124fe1fa6061..a41559c50c685 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1316,8 +1316,6 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `für ${comment}` : ''}`, submitted: (memo?: string) => `eingereicht${memo ? `, mit dem Vermerk ${memo}` : ''}`, automaticallySubmitted: `eingereicht über Einreichungen verzögern`, - submittedVia: 'eingereicht über ', - delaySubmissions: 'Einreichungen verzögern', queuedToSubmitViaDEW: 'zur Einreichung über benutzerdefinierten Genehmigungsworkflow eingereiht', queuedToApproveViaDEW: 'Zur Genehmigung über benutzerdefinierten Genehmigungsworkflow eingereiht', trackedAmount: (formattedAmount: string, comment?: string) => `Verfolgen ${formattedAmount}${comment ? `für ${comment}` : ''}`, diff --git a/src/languages/en.ts b/src/languages/en.ts index 63120d25ac662..6afd64b0f924d 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1338,8 +1338,6 @@ const translations = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? ` for ${comment}` : ''}`, submitted: (memo?: string) => `submitted${memo ? `, saying ${memo}` : ''}`, automaticallySubmitted: `submitted via delay submissions`, - submittedVia: 'submitted via ', - delaySubmissions: 'delay submissions', queuedToSubmitViaDEW: 'queued to submit via custom approval workflow', queuedToApproveViaDEW: 'queued to approve via custom approval workflow', trackedAmount: (formattedAmount: string, comment?: string) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 674368fb31bca..42f18a50bb3ea 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1168,8 +1168,6 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount, comment) => `${formattedAmount}${comment ? ` para ${comment}` : ''}`, submitted: (memo) => `enviado${memo ? `, dijo ${memo}` : ''}`, automaticallySubmitted: `envió mediante retrasar envíos`, - submittedVia: 'envió mediante ', - delaySubmissions: 'retrasar envíos', queuedToSubmitViaDEW: 'en cola para enviar a través del flujo de aprobación personalizado', queuedToApproveViaDEW: 'en cola para aprobar a través del flujo de aprobación personalizado', trackedAmount: (formattedAmount, comment) => `realizó un seguimiento de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index e682906c020d0..0be24b76c8f2a 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1320,8 +1320,6 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `pour ${comment}` : ''}`, submitted: (memo?: string) => `soumis${memo ? `, indiquant « ${memo} »` : ''}`, automaticallySubmitted: `soumis via soumissions différées`, - submittedVia: 'soumis via ', - delaySubmissions: 'soumissions différées', queuedToSubmitViaDEW: 'en file d’attente pour être soumis via le circuit d’approbation personnalisé', queuedToApproveViaDEW: 'mis en file d’attente pour approbation via un processus d’approbation personnalisé', trackedAmount: (formattedAmount: string, comment?: string) => `suivi de ${formattedAmount}${comment ? `pour ${comment}` : ''}`, diff --git a/src/languages/it.ts b/src/languages/it.ts index ce4d91136398a..0049e5738d298 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1314,8 +1314,6 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `per ${comment}` : ''}`, submitted: (memo?: string) => `inviato${memo ? `, con nota: ${memo}` : ''}`, automaticallySubmitted: `inviato tramite invio posticipato`, - submittedVia: 'inviato tramite ', - delaySubmissions: 'invio posticipato', queuedToSubmitViaDEW: 'in coda per l’invio tramite flusso di approvazione personalizzato', queuedToApproveViaDEW: 'in coda per l’approvazione tramite flusso di approvazione personalizzato', trackedAmount: (formattedAmount: string, comment?: string) => `monitoraggio di ${formattedAmount}${comment ? `per ${comment}` : ''}`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 6c842030c7d2f..2401f5ebbc63e 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1306,8 +1306,6 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `${comment}用` : ''}`, submitted: (memo?: string) => `送信済み${memo ? `、メモ: ${memo}` : ''}`, automaticallySubmitted: `提出の延期 経由で提出されました`, - submittedVia: ' 経由で提出されました', - delaySubmissions: '提出の延期', queuedToSubmitViaDEW: 'カスタム承認ワークフローで提出待ち', queuedToApproveViaDEW: 'カスタム承認ワークフローで承認待ちに設定されました', trackedAmount: (formattedAmount: string, comment?: string) => `追跡中 ${formattedAmount}${comment ? `${comment}用` : ''}`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index e753d9f5b9fae..f9e531173a01b 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1313,8 +1313,6 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `voor ${comment}` : ''}`, submitted: (memo?: string) => `ingediend${memo ? `, met de omschrijving ${memo}` : ''}`, automaticallySubmitted: `ingediend via uitgestelde indieningen`, - submittedVia: 'ingediend via ', - delaySubmissions: 'uitgestelde indieningen', queuedToSubmitViaDEW: 'in wachtrij om via aangepast goedkeuringsproces in te dienen', queuedToApproveViaDEW: 'in de wachtrij gezet voor goedkeuring via aangepaste goedkeuringsworkflow', trackedAmount: (formattedAmount: string, comment?: string) => `volgt ${formattedAmount}${comment ? `voor ${comment}` : ''}`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index f6926edf7e9cc..6affa32730dd0 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1313,8 +1313,6 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `za ${comment}` : ''}`, submitted: (memo?: string) => `przesłano${memo ? `, wpisując ${memo}` : ''}`, automaticallySubmitted: `przesłano przez opóźnianie wysyłania`, - submittedVia: 'przesłano przez ', - delaySubmissions: 'opóźnianie wysyłania', queuedToSubmitViaDEW: 'w kolejce do przesłania przez niestandardowy proces zatwierdzania', queuedToApproveViaDEW: 'oczekuje na zatwierdzenie w niestandardowym procesie akceptacji', trackedAmount: (formattedAmount: string, comment?: string) => `śledzenie ${formattedAmount}${comment ? `za ${comment}` : ''}`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index c4e6bb84bf4ee..baea489063f58 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1312,8 +1312,6 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `para ${comment}` : ''}`, submitted: (memo?: string) => `enviado${memo ? `, dizendo ${memo}` : ''}`, automaticallySubmitted: `enviado via atrasar envios`, - submittedVia: 'enviado via ', - delaySubmissions: 'atrasar envios', queuedToSubmitViaDEW: 'na fila para enviar via fluxo de aprovação personalizado', queuedToApproveViaDEW: 'na fila para aprovar via fluxo de aprovação personalizado', trackedAmount: (formattedAmount: string, comment?: string) => `rastreamento de ${formattedAmount}${comment ? `para ${comment}` : ''}`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 9996ced3dc76d..1355afe4f97d0 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1288,8 +1288,6 @@ const translations: TranslationDeepObject = { expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `用于 ${comment}` : ''}`, submitted: (memo?: string) => `已提交${memo ? `,备注为 ${memo}` : ''}`, automaticallySubmitted: `通过延迟提交提交`, - submittedVia: '通过', - delaySubmissions: '延迟提交', queuedToSubmitViaDEW: '已排队,待通过自定义审批流程提交', queuedToApproveViaDEW: '已排队,等待通过自定义审批流程批准', trackedAmount: (formattedAmount: string, comment?: string) => `跟踪 ${formattedAmount}${comment ? `用于 ${comment}` : ''}`, diff --git a/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx b/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx index c0f62d8168719..58676a885ab74 100644 --- a/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx +++ b/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx @@ -80,21 +80,32 @@ function ReportActionItemMessageWithExplain({message, action, childReport, origi ); if (wasSubmittedViaHarvesting) { + // Split the translated string into inline parts to support languages with different word order + const messageSplitByAnchor = message.match(CONST.REGEX_ANCHOR_WITH_TEXT); + const messagePrefix = messageSplitByAnchor ? (messageSplitByAnchor.at(1) ?? '') : message; + const anchorHref = messageSplitByAnchor ? (messageSplitByAnchor.at(2) ?? '') : ''; + const anchorLabel = messageSplitByAnchor ? (messageSplitByAnchor.at(3) ?? '') : ''; + const messageSuffix = messageSplitByAnchor ? (messageSplitByAnchor.at(4) ?? '') : ''; + return ( + {!!messagePrefix && ( + + )} + {!!anchorLabel && ( + openLink(anchorHref, environmentURL)} + style={[styles.chatItemMessage, styles.link]} + text={anchorLabel} + /> + )} - openLink(CONST.SELECT_WORKFLOWS_HELP_URL, environmentURL)} - style={[styles.chatItemMessage, styles.link]} - text={translate('iou.delaySubmissions')} - /> - {explainAndIconBlock} diff --git a/tests/ui/PureReportActionItemTest.tsx b/tests/ui/PureReportActionItemTest.tsx index 40a8351e1996d..631cc1a6b6291 100644 --- a/tests/ui/PureReportActionItemTest.tsx +++ b/tests/ui/PureReportActionItemTest.tsx @@ -182,6 +182,51 @@ describe('PureReportActionItem', () => { }); }); + describe('Submitted via harvesting with AI Explain', () => { + afterEach(() => { + jest.restoreAllMocks(); + (openLink as jest.Mock).mockClear(); + }); + + it('renders message parts inline and shows Explain button when action has reasoning', async () => { + jest.spyOn(ReportActionUtils, 'hasReasoning').mockReturnValue(true); + + const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, {harvesting: true}); + renderItemWithAction(action); + await waitForBatchedUpdatesWithAct(); + + const translatedMessage = translateLocal('iou.automaticallySubmitted'); + const messageSplitByAnchor = translatedMessage.match(CONST.REGEX_ANCHOR_WITH_TEXT); + expect(messageSplitByAnchor).not.toBeNull(); + + const textBeforeLink = messageSplitByAnchor?.at(1) ?? ''; + const anchorText = messageSplitByAnchor?.at(3) ?? ''; + + if (textBeforeLink) { + const firstPrefixWord = textBeforeLink.trim().split(/\s+/).at(0) ?? ''; + expect(screen.getByText(new RegExp(firstPrefixWord))).toBeOnTheScreen(); + } + expect(screen.getByText(new RegExp(anchorText.split(/\s+/).at(0) ?? ''))).toBeOnTheScreen(); + expect(screen.getByText(translateLocal('common.explain'))).toBeOnTheScreen(); + }); + + it('clicking the delay submissions link opens SELECT_WORKFLOWS_HELP_URL', async () => { + jest.spyOn(ReportActionUtils, 'hasReasoning').mockReturnValue(true); + + const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, {harvesting: true}); + renderItemWithAction(action); + await waitForBatchedUpdatesWithAct(); + + const translatedMessage = translateLocal('iou.automaticallySubmitted'); + const messageSplitByAnchor = translatedMessage.match(CONST.REGEX_ANCHOR_WITH_TEXT); + const firstWord = messageSplitByAnchor?.at(3)?.split(/\s+/).at(0) ?? ''; + + fireEvent.press(screen.getByText(new RegExp(firstWord)), {preventDefault: jest.fn()}); + + expect(openLink).toHaveBeenCalledWith(CONST.SELECT_WORKFLOWS_HELP_URL, expect.any(String)); + }); + }); + describe('Manual actions', () => { it('APPROVED action', async () => { const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.APPROVED, {automaticAction: false}); From 5e37b8513198c2d240f25ab44c818460e5a6d29a Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Mon, 2 Mar 2026 03:46:04 +0500 Subject: [PATCH 04/10] update test mocks --- tests/ui/PureReportActionItemTest.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/ui/PureReportActionItemTest.tsx b/tests/ui/PureReportActionItemTest.tsx index 631cc1a6b6291..f923d042af2bf 100644 --- a/tests/ui/PureReportActionItemTest.tsx +++ b/tests/ui/PureReportActionItemTest.tsx @@ -182,16 +182,13 @@ describe('PureReportActionItem', () => { }); }); - describe('Submitted via harvesting with AI Explain', () => { + describe('Submitted via harvesting with Explain', () => { afterEach(() => { - jest.restoreAllMocks(); (openLink as jest.Mock).mockClear(); }); it('renders message parts inline and shows Explain button when action has reasoning', async () => { - jest.spyOn(ReportActionUtils, 'hasReasoning').mockReturnValue(true); - - const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, {harvesting: true}); + const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, {harvesting: true, reasoning: 'Test reasoning'}); renderItemWithAction(action); await waitForBatchedUpdatesWithAct(); @@ -211,9 +208,7 @@ describe('PureReportActionItem', () => { }); it('clicking the delay submissions link opens SELECT_WORKFLOWS_HELP_URL', async () => { - jest.spyOn(ReportActionUtils, 'hasReasoning').mockReturnValue(true); - - const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, {harvesting: true}); + const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, {harvesting: true, reasoning: 'Test reasoning'}); renderItemWithAction(action); await waitForBatchedUpdatesWithAct(); From 5fb3daab216b569e8d0273a1a62a6211d8538507 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Fri, 6 Mar 2026 04:10:04 +0500 Subject: [PATCH 05/10] remove strong from Explain --- src/languages/de.ts | 2 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/languages/fr.ts | 2 +- src/languages/it.ts | 2 +- src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 2 +- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 8a6be1581b8f4..bff7128f3b563 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1586,7 +1586,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'Der Gesamtbetrag ist zu hoch. Verringere die Stunden oder reduziere den Satz.', }, correctRateError: 'Beheben Sie den Kursfehler und versuchen Sie es erneut.', - AskToExplain: `. Erklären ✨`, + AskToExplain: `. Erklären`, duplicateNonDefaultWorkspacePerDiemError: 'Sie können Per-Diem-Ausgaben nicht über mehrere Workspaces hinweg duplizieren, da sich die Sätze zwischen den Workspaces unterscheiden können.', rulesModifiedFields: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 3f10e590d6ed3..4da1529ea3719 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1611,7 +1611,7 @@ const translations = { amountTooLargeError: 'The total amount is too large. Lower the hours or reduce the rate.', }, correctRateError: 'Fix the rate error and try again.', - AskToExplain: `. Explain ✨`, + AskToExplain: `. Explain`, rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'marked the expense as "reimbursable"' : 'marked the expense as "non-reimbursable"'), billable: (value: boolean) => (value ? 'marked the expense as "billable"' : 'marked the expense as "non-billable"'), diff --git a/src/languages/es.ts b/src/languages/es.ts index b415f6a0dd7fa..2c4a4890671bc 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1452,7 +1452,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'El importe total es demasiado alto. Reduce las horas o disminuye la tasa.', }, correctRateError: 'Corrige el error de la tasa y vuelve a intentarlo.', - AskToExplain: `. Explicar ✨`, + AskToExplain: `. Explicar`, rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'marcó el gasto como "reembolsable"' : 'marcó el gasto como "no reembolsable"'), billable: (value: boolean) => (value ? 'marcó el gasto como "facturable"' : 'marcó el gasto como "no facturable"'), diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 0b82ad790797b..66f4fb25871a2 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1591,7 +1591,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'Le montant total est trop élevé. Réduisez le nombre d’heures ou diminuez le taux.', }, correctRateError: 'Corrigez l’erreur de taux et réessayez.', - AskToExplain: `. Expliquer ✨`, + AskToExplain: `. Expliquer`, duplicateNonDefaultWorkspacePerDiemError: 'Vous ne pouvez pas dupliquer les indemnités journalières entre plusieurs espaces de travail, car les taux peuvent différer d’un espace de travail à l’autre.', rulesModifiedFields: { diff --git a/src/languages/it.ts b/src/languages/it.ts index d273792b023f0..4ccdd1f16d380 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1583,7 +1583,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'L’importo totale è troppo alto. Riduci le ore o abbassa la tariffa.', }, correctRateError: "Correggi l'errore di tariffa e riprova.", - AskToExplain: `. Spiega ✨`, + AskToExplain: `. Spiega`, duplicateNonDefaultWorkspacePerDiemError: 'Non puoi duplicare le spese di diaria tra diversi spazi di lavoro perché le tariffe potrebbero essere diverse tra gli spazi di lavoro.', rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'ha contrassegnato la spesa come "rimborsabile"' : 'ha contrassegnato la spesa come "non rimborsabile"'), diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 0a811ba9bb8c5..2de44dd20e28c 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1573,7 +1573,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: '合計金額が大きすぎます。時間を減らすか、レートを下げてください。', }, correctRateError: 'レートのエラーを修正して、もう一度お試しください。', - AskToExplain: `・説明 ✨`, + AskToExplain: `・説明`, duplicateNonDefaultWorkspacePerDiemError: 'ワークスペースごとに日当レートが異なる場合があるため、日当経費をワークスペース間で複製することはできません。', rulesModifiedFields: { reimbursable: (value: boolean) => (value ? '経費を「精算対象」に指定しました' : '経費を「精算対象外」にマークしました'), diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 6ffe1f3c1a213..6be3edbdc7566 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1581,7 +1581,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'Het totale bedrag is te hoog. Verlaag het aantal uren of verlaag het tarief.', }, correctRateError: 'Los de tarieffout op en probeer het opnieuw.', - AskToExplain: `. Uitleggen ✨`, + AskToExplain: `. Uitleggen`, duplicateNonDefaultWorkspacePerDiemError: 'Je kunt dagvergoedingen niet dupliceren tussen werkruimtes, omdat de tarieven per werkruimte kunnen verschillen.', rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'markeerde de uitgave als „terugbetaalbaar”' : 'heeft de uitgave als ‘niet-vergoedbaar’ gemarkeerd'), diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 5fac6256ad35a..c3193ce1c5138 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1580,7 +1580,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'Łączna kwota jest zbyt wysoka. Zmniejsz liczbę godzin lub obniż stawkę.', }, correctRateError: 'Napraw błąd stawki i spróbuj ponownie.', - AskToExplain: `. Wyjaśnij ✨`, + AskToExplain: `. Wyjaśnij`, duplicateNonDefaultWorkspacePerDiemError: 'Nie możesz duplikować wydatków z tytułu diet między przestrzeniami roboczymi, ponieważ stawki mogą się różnić między poszczególnymi przestrzeniami.', rulesModifiedFields: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index a441fe5c534ce..dec426f84cb71 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1578,7 +1578,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'O valor total é muito alto. Diminua as horas ou reduza a tarifa.', }, correctRateError: 'Corrija o erro de taxa e tente novamente.', - AskToExplain: `. Explicar ✨`, + AskToExplain: `. Explicar`, duplicateNonDefaultWorkspacePerDiemError: 'Você não pode duplicar despesas de diárias entre espaços de trabalho porque as tarifas podem variar entre eles.', rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'marcou a despesa como "reembolsável"' : 'marcou a despesa como “não reembolsável”'), diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 3f3090d975a42..eb290c19148ee 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1548,7 +1548,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: '总金额过大。请减少工时或降低费率。', }, correctRateError: '修复费率错误后请重试。', - AskToExplain: `。说明 ✨`, + AskToExplain: `。说明`, duplicateNonDefaultWorkspacePerDiemError: '您无法在不同工作区之间复制每日津贴报销,因为各工作区的补贴标准可能不同。', rulesModifiedFields: { reimbursable: (value: boolean) => (value ? '将该报销单标记为“可报销”' : '将该报销单标记为“不可报销”'), From 2524e30cdfc0be4f840b5113a1b237796ed463fc Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Fri, 6 Mar 2026 04:10:42 +0500 Subject: [PATCH 06/10] Add custom SparklesIconRenderer --- .../BaseHTMLEngineProvider.tsx | 4 +++ .../HTMLRenderers/SparklesIconRenderer.tsx | 30 +++++++++++++++++++ .../HTMLEngineProvider/HTMLRenderers/index.ts | 2 ++ src/components/RenderHTML.tsx | 2 ++ 4 files changed, 38 insertions(+) create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/SparklesIconRenderer.tsx diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 9e3f00124f84c..40207e0e3894d 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -186,6 +186,10 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim tagName: 'bullet-item', contentModel: HTMLContentModel.block, }), + 'sparkles-icon': HTMLElementModel.fromCustomModel({ + tagName: 'sparkles-icon', + contentModel: HTMLContentModel.mixed, + }), }), [ styles.taskTitleMenuItem, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/SparklesIconRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/SparklesIconRenderer.tsx new file mode 100644 index 0000000000000..b8c22956809df --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/SparklesIconRenderer.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; + +/** + * Renders a Sparkles SVG icon as an inline custom HTML element + */ +function SparklesIconRenderer() { + const icons = useMemoizedLazyExpensifyIcons(['Sparkles']); + const theme = useTheme(); + const styles = useThemeStyles(); + + return ( + + + + ); +} + +export default SparklesIconRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index f77dc115c8545..3d91670c38e75 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -16,6 +16,7 @@ import NextStepEmailRenderer from './NextStepEmailRenderer'; import PreRenderer from './PreRenderer'; import RBRRenderer from './RBRRenderer'; import ShortMentionRenderer from './ShortMentionRenderer'; +import SparklesIconRenderer from './SparklesIconRenderer'; import TaskTitleRenderer from './TaskTitleRenderer'; import TransactionHistoryLinkRenderer from './TransactionHistoryLinkRenderer'; import UserDetailsRenderer from './UserDetailsRenderer'; @@ -50,6 +51,7 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { 'concierge-link': ConciergeLinkRenderer, 'transaction-history-link': TransactionHistoryLinkRenderer, 'account-manager-link': AccountManagerLinkRenderer, + 'sparkles-icon': SparklesIconRenderer, /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/src/components/RenderHTML.tsx b/src/components/RenderHTML.tsx index a52ccf2c8d969..ababd16463b8e 100644 --- a/src/components/RenderHTML.tsx +++ b/src/components/RenderHTML.tsx @@ -5,6 +5,7 @@ import useHasTextAncestor from '@hooks/useHasTextAncestor'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Parser from '@libs/Parser'; import BulletItemRenderer from './HTMLEngineProvider/HTMLRenderers/BulletItemRenderer'; +import SparklesIconRenderer from './HTMLEngineProvider/HTMLRenderers/SparklesIconRenderer'; type LinkPressHandler = NonNullable['onPress']; @@ -53,6 +54,7 @@ function RenderHTML({html: htmlParam, onLinkPress, isSelectable}: RenderHTMLProp const renderers = { /* eslint-disable @typescript-eslint/naming-convention */ 'bullet-item': BulletItemRenderer, + 'sparkles-icon': SparklesIconRenderer, }; const htmlSource = ( From b89c9bb9c1aa0afc258872c29c217460d3aebfa9 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Fri, 6 Mar 2026 04:11:16 +0500 Subject: [PATCH 07/10] update ReportActionItemMessageWithExplain to use sparkles icon --- src/pages/inbox/report/PureReportActionItem.tsx | 1 - .../inbox/report/ReportActionItemMessageWithExplain.tsx | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index afd7e7d6d1e06..9b858c23d0418 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -1351,7 +1351,6 @@ function PureReportActionItem({ action={action} childReport={childReport} originalReport={originalReport} - wasSubmittedViaHarvesting /> ); } else if (hasPendingDEWSubmit(reportMetadata, isDEWPolicy) && isPendingAdd) { diff --git a/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx b/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx index 3b61f7c47333b..dad7a31beebc4 100644 --- a/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx +++ b/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx @@ -24,9 +24,6 @@ type ReportActionItemMessageWithExplainProps = { /** Original report from which the given reportAction is first created */ originalReport: OnyxEntry; - - /** Whether the report was submitted via delay submissions */ - wasSubmittedViaHarvesting?: boolean; }; /** @@ -39,7 +36,7 @@ function ReportActionItemMessageWithExplain({message, action, childReport, origi const {environmentURL} = useEnvironment(); const actionHasReasoning = hasReasoning(action); - const computedMessage = actionHasReasoning ? `${message}${translate('iou.AskToExplain')}` : message; + const computedMessage = actionHasReasoning ? `${message}${translate('iou.AskToExplain')}` : message; const handleLinkPress = (event: GestureResponderEvent | KeyboardEvent, href: string) => { // Handle the special "Explain" link From b3761aa8ec263ad0aa174a805830a95f8fd6c8e4 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Fri, 6 Mar 2026 20:20:42 +0500 Subject: [PATCH 08/10] remove unrelated test --- tests/ui/PureReportActionItemTest.tsx | 40 --------------------------- 1 file changed, 40 deletions(-) diff --git a/tests/ui/PureReportActionItemTest.tsx b/tests/ui/PureReportActionItemTest.tsx index f923d042af2bf..40a8351e1996d 100644 --- a/tests/ui/PureReportActionItemTest.tsx +++ b/tests/ui/PureReportActionItemTest.tsx @@ -182,46 +182,6 @@ describe('PureReportActionItem', () => { }); }); - describe('Submitted via harvesting with Explain', () => { - afterEach(() => { - (openLink as jest.Mock).mockClear(); - }); - - it('renders message parts inline and shows Explain button when action has reasoning', async () => { - const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, {harvesting: true, reasoning: 'Test reasoning'}); - renderItemWithAction(action); - await waitForBatchedUpdatesWithAct(); - - const translatedMessage = translateLocal('iou.automaticallySubmitted'); - const messageSplitByAnchor = translatedMessage.match(CONST.REGEX_ANCHOR_WITH_TEXT); - expect(messageSplitByAnchor).not.toBeNull(); - - const textBeforeLink = messageSplitByAnchor?.at(1) ?? ''; - const anchorText = messageSplitByAnchor?.at(3) ?? ''; - - if (textBeforeLink) { - const firstPrefixWord = textBeforeLink.trim().split(/\s+/).at(0) ?? ''; - expect(screen.getByText(new RegExp(firstPrefixWord))).toBeOnTheScreen(); - } - expect(screen.getByText(new RegExp(anchorText.split(/\s+/).at(0) ?? ''))).toBeOnTheScreen(); - expect(screen.getByText(translateLocal('common.explain'))).toBeOnTheScreen(); - }); - - it('clicking the delay submissions link opens SELECT_WORKFLOWS_HELP_URL', async () => { - const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, {harvesting: true, reasoning: 'Test reasoning'}); - renderItemWithAction(action); - await waitForBatchedUpdatesWithAct(); - - const translatedMessage = translateLocal('iou.automaticallySubmitted'); - const messageSplitByAnchor = translatedMessage.match(CONST.REGEX_ANCHOR_WITH_TEXT); - const firstWord = messageSplitByAnchor?.at(3)?.split(/\s+/).at(0) ?? ''; - - fireEvent.press(screen.getByText(new RegExp(firstWord)), {preventDefault: jest.fn()}); - - expect(openLink).toHaveBeenCalledWith(CONST.SELECT_WORKFLOWS_HELP_URL, expect.any(String)); - }); - }); - describe('Manual actions', () => { it('APPROVED action', async () => { const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.APPROVED, {automaticAction: false}); From bc322fa2613990a2f7c6fd06f2151129ee4d82b3 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Fri, 6 Mar 2026 20:27:14 +0500 Subject: [PATCH 09/10] remove regex --- src/CONST/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 35026844d78e0..a4327b299f78f 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -388,9 +388,6 @@ const CONST = { // Regex to get link in href prop inside of component REGEX_LINK_IN_ANCHOR: /]*?\s+)?href="([^"]*)"/gi, - // Regex to parse a single anchor tag and extract text, href, and label - REGEX_ANCHOR_WITH_TEXT: /^(.*?)]*href="([^"]*)"[^>]*>(.*?)<\/a>(.*)$/s, - // Regex to read violation value from string given by backend VIOLATION_LIMIT_REGEX: /[^0-9]+/g, From d0270b69ad3e411189675e5e3ae581e2e1b32b72 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Wed, 11 Mar 2026 07:45:12 +0500 Subject: [PATCH 10/10] move sparkles-icon in anchor --- src/languages/de.ts | 2 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/languages/fr.ts | 2 +- src/languages/it.ts | 2 +- src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 2 +- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index bff7128f3b563..60edcacd58d4d 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1586,7 +1586,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'Der Gesamtbetrag ist zu hoch. Verringere die Stunden oder reduziere den Satz.', }, correctRateError: 'Beheben Sie den Kursfehler und versuchen Sie es erneut.', - AskToExplain: `. Erklären`, + AskToExplain: `. Erklären`, duplicateNonDefaultWorkspacePerDiemError: 'Sie können Per-Diem-Ausgaben nicht über mehrere Workspaces hinweg duplizieren, da sich die Sätze zwischen den Workspaces unterscheiden können.', rulesModifiedFields: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 4da1529ea3719..70e48407f64e9 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1611,7 +1611,7 @@ const translations = { amountTooLargeError: 'The total amount is too large. Lower the hours or reduce the rate.', }, correctRateError: 'Fix the rate error and try again.', - AskToExplain: `. Explain`, + AskToExplain: `. Explain`, rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'marked the expense as "reimbursable"' : 'marked the expense as "non-reimbursable"'), billable: (value: boolean) => (value ? 'marked the expense as "billable"' : 'marked the expense as "non-billable"'), diff --git a/src/languages/es.ts b/src/languages/es.ts index 2c4a4890671bc..414775e1f9854 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1452,7 +1452,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'El importe total es demasiado alto. Reduce las horas o disminuye la tasa.', }, correctRateError: 'Corrige el error de la tasa y vuelve a intentarlo.', - AskToExplain: `. Explicar`, + AskToExplain: `. Explicar`, rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'marcó el gasto como "reembolsable"' : 'marcó el gasto como "no reembolsable"'), billable: (value: boolean) => (value ? 'marcó el gasto como "facturable"' : 'marcó el gasto como "no facturable"'), diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 66f4fb25871a2..746e79a43145f 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1591,7 +1591,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'Le montant total est trop élevé. Réduisez le nombre d’heures ou diminuez le taux.', }, correctRateError: 'Corrigez l’erreur de taux et réessayez.', - AskToExplain: `. Expliquer`, + AskToExplain: `. Expliquer`, duplicateNonDefaultWorkspacePerDiemError: 'Vous ne pouvez pas dupliquer les indemnités journalières entre plusieurs espaces de travail, car les taux peuvent différer d’un espace de travail à l’autre.', rulesModifiedFields: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 4ccdd1f16d380..89ed00830822c 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1583,7 +1583,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'L’importo totale è troppo alto. Riduci le ore o abbassa la tariffa.', }, correctRateError: "Correggi l'errore di tariffa e riprova.", - AskToExplain: `. Spiega`, + AskToExplain: `. Spiega`, duplicateNonDefaultWorkspacePerDiemError: 'Non puoi duplicare le spese di diaria tra diversi spazi di lavoro perché le tariffe potrebbero essere diverse tra gli spazi di lavoro.', rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'ha contrassegnato la spesa come "rimborsabile"' : 'ha contrassegnato la spesa come "non rimborsabile"'), diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 2de44dd20e28c..9cad80325ba19 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1573,7 +1573,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: '合計金額が大きすぎます。時間を減らすか、レートを下げてください。', }, correctRateError: 'レートのエラーを修正して、もう一度お試しください。', - AskToExplain: `・説明`, + AskToExplain: `・説明`, duplicateNonDefaultWorkspacePerDiemError: 'ワークスペースごとに日当レートが異なる場合があるため、日当経費をワークスペース間で複製することはできません。', rulesModifiedFields: { reimbursable: (value: boolean) => (value ? '経費を「精算対象」に指定しました' : '経費を「精算対象外」にマークしました'), diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 6be3edbdc7566..975839fbd2149 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1581,7 +1581,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'Het totale bedrag is te hoog. Verlaag het aantal uren of verlaag het tarief.', }, correctRateError: 'Los de tarieffout op en probeer het opnieuw.', - AskToExplain: `. Uitleggen`, + AskToExplain: `. Uitleggen`, duplicateNonDefaultWorkspacePerDiemError: 'Je kunt dagvergoedingen niet dupliceren tussen werkruimtes, omdat de tarieven per werkruimte kunnen verschillen.', rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'markeerde de uitgave als „terugbetaalbaar”' : 'heeft de uitgave als ‘niet-vergoedbaar’ gemarkeerd'), diff --git a/src/languages/pl.ts b/src/languages/pl.ts index c3193ce1c5138..4356113bae61d 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1580,7 +1580,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'Łączna kwota jest zbyt wysoka. Zmniejsz liczbę godzin lub obniż stawkę.', }, correctRateError: 'Napraw błąd stawki i spróbuj ponownie.', - AskToExplain: `. Wyjaśnij`, + AskToExplain: `. Wyjaśnij`, duplicateNonDefaultWorkspacePerDiemError: 'Nie możesz duplikować wydatków z tytułu diet między przestrzeniami roboczymi, ponieważ stawki mogą się różnić między poszczególnymi przestrzeniami.', rulesModifiedFields: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index dec426f84cb71..250ae762ee5f9 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1578,7 +1578,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: 'O valor total é muito alto. Diminua as horas ou reduza a tarifa.', }, correctRateError: 'Corrija o erro de taxa e tente novamente.', - AskToExplain: `. Explicar`, + AskToExplain: `. Explicar`, duplicateNonDefaultWorkspacePerDiemError: 'Você não pode duplicar despesas de diárias entre espaços de trabalho porque as tarifas podem variar entre eles.', rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'marcou a despesa como "reembolsável"' : 'marcou a despesa como “não reembolsável”'), diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index eb290c19148ee..bba0974a58e33 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1548,7 +1548,7 @@ const translations: TranslationDeepObject = { amountTooLargeError: '总金额过大。请减少工时或降低费率。', }, correctRateError: '修复费率错误后请重试。', - AskToExplain: `。说明`, + AskToExplain: `。说明`, duplicateNonDefaultWorkspacePerDiemError: '您无法在不同工作区之间复制每日津贴报销,因为各工作区的补贴标准可能不同。', rulesModifiedFields: { reimbursable: (value: boolean) => (value ? '将该报销单标记为“可报销”' : '将该报销单标记为“不可报销”'), diff --git a/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx b/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx index dad7a31beebc4..ad6629117fcc2 100644 --- a/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx +++ b/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx @@ -36,7 +36,7 @@ function ReportActionItemMessageWithExplain({message, action, childReport, origi const {environmentURL} = useEnvironment(); const actionHasReasoning = hasReasoning(action); - const computedMessage = actionHasReasoning ? `${message}${translate('iou.AskToExplain')}` : message; + const computedMessage = actionHasReasoning ? `${message}${translate('iou.AskToExplain')}` : message; const handleLinkPress = (event: GestureResponderEvent | KeyboardEvent, href: string) => { // Handle the special "Explain" link