From 19a65030f735afcaf9cb6c81e530d7b571f4d5e6 Mon Sep 17 00:00:00 2001 From: "daledah (via MelvinBot)" Date: Wed, 21 Oct 2026 10:49:08 +0000 Subject: [PATCH 1/3] Show "removed" instead of "set to \"\"" when a text report field is cleared Co-authored-by: daledah --- src/languages/de.ts | 1 + src/languages/el.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + src/libs/ReportActionsUtils.ts | 5 ++-- src/libs/ReportUtils.ts | 6 +++-- tests/unit/ReportActionsUtilsTest.ts | 16 +++++++++++ tests/unit/ReportUtilsTest.ts | 40 ++++++++++++++++++++++++++++ 15 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 5f8a2b4cd3c9..e32cb71c58c2 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -10552,6 +10552,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc changeField: (oldValue: string | undefined, newValue: string, fieldName: string) => `${fieldName} auf „${newValue}“ geändert (zuvor „${oldValue}“)`, changeFieldEmpty: (newValue: string, fieldName: string) => `setze ${fieldName} auf „${newValue}“`, changeFieldRemoved: (oldValue: string, fieldName: string) => `${fieldName} entfernt (zuvor „${oldValue}“)`, + changeFieldCleared: (fieldName: string) => `${fieldName} entfernt`, changeReportPolicy: (toPolicyName: string, fromPolicyName?: string) => { if (!toPolicyName) { return `hat den Arbeitsbereich${fromPolicyName ? `(zuvor ${fromPolicyName})` : ''} geändert`; diff --git a/src/languages/el.ts b/src/languages/el.ts index 05f8a99c394b..e5ce5b442c5f 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -10752,6 +10752,7 @@ ${reportName}`, changeField: (oldValue, newValue, fieldName) => `άλλαξε το ${fieldName} σε «${newValue}» (προηγουμένως «${oldValue}»)`, changeFieldEmpty: (newValue, fieldName) => `ορίστε το ${fieldName} σε «${newValue}»`, changeFieldRemoved: (oldValue, fieldName) => `αφαίρεσε το ${fieldName} (προηγουμένως «${oldValue}»)`, + changeFieldCleared: (fieldName) => `αφαίρεσε το ${fieldName}`, changeReportPolicy: (toPolicyName: string, fromPolicyName?: string) => { if (!toPolicyName) { return `άλλαξε τον χώρο εργασίας${fromPolicyName ? `(προηγουμένως ${fromPolicyName})` : ''}`; diff --git a/src/languages/en.ts b/src/languages/en.ts index 55a23bbfbd3d..372a100e3112 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -10789,6 +10789,7 @@ const translations = { changeField: (oldValue: string | undefined, newValue: string, fieldName: string) => `changed ${fieldName} to "${newValue}" (previously "${oldValue}")`, changeFieldEmpty: (newValue: string, fieldName: string) => `set ${fieldName} to "${newValue}"`, changeFieldRemoved: (oldValue: string, fieldName: string) => `removed ${fieldName} (previously "${oldValue}")`, + changeFieldCleared: (fieldName: string) => `removed ${fieldName}`, changeReportPolicy: (toPolicyName: string, fromPolicyName?: string) => { if (!toPolicyName) { return `changed the workspace${fromPolicyName ? ` (previously ${fromPolicyName})` : ''}`; diff --git a/src/languages/es.ts b/src/languages/es.ts index e491088e0ee7..4a7148d488f9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -10354,6 +10354,7 @@ ${reportName}`, changeField: (oldValue, newValue, fieldName) => `cambió ${fieldName} a "${newValue}" (previamente "${oldValue}")`, changeFieldEmpty: (newValue, fieldName) => `estableció ${fieldName} a ${newValue}`, changeFieldRemoved: (oldValue, fieldName) => `eliminó ${fieldName} (previamente "${oldValue}")`, + changeFieldCleared: (fieldName) => `eliminó ${fieldName}`, changeReportPolicy: (toPolicyName, fromPolicyName) => { if (!toPolicyName) { return `cambió el espacio de trabajo${fromPolicyName ? ` (previamente ${fromPolicyName})` : ''}`; diff --git a/src/languages/fr.ts b/src/languages/fr.ts index b48b6cab62bc..215fe4f837e6 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -10578,6 +10578,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e changeField: (oldValue: string | undefined, newValue: string, fieldName: string) => `a modifié ${fieldName} en « ${newValue} » (auparavant « ${oldValue} »)`, changeFieldEmpty: (newValue: string, fieldName: string) => `définir ${fieldName} sur « ${newValue} »`, changeFieldRemoved: (oldValue: string, fieldName: string) => `a supprimé ${fieldName} (auparavant « ${oldValue} »)`, + changeFieldCleared: (fieldName: string) => `a supprimé ${fieldName}`, changeReportPolicy: (toPolicyName: string, fromPolicyName?: string) => { if (!toPolicyName) { return `a modifié l’espace de travail${fromPolicyName ? `(précédemment ${fromPolicyName})` : ''}`; diff --git a/src/languages/it.ts b/src/languages/it.ts index 8669dd1065af..4dae6d7dd095 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -10500,6 +10500,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, changeField: (oldValue: string | undefined, newValue: string, fieldName: string) => `ha modificato ${fieldName} in "${newValue}" (in precedenza "${oldValue}")`, changeFieldEmpty: (newValue: string, fieldName: string) => `imposta ${fieldName} su "${newValue}"`, changeFieldRemoved: (oldValue: string, fieldName: string) => `ha rimosso ${fieldName} (in precedenza "${oldValue}")`, + changeFieldCleared: (fieldName: string) => `ha rimosso ${fieldName}`, changeReportPolicy: (toPolicyName: string, fromPolicyName?: string) => { if (!toPolicyName) { return `ha modificato il workspace${fromPolicyName ? `(precedentemente ${fromPolicyName})` : ''}`; diff --git a/src/languages/ja.ts b/src/languages/ja.ts index ed4b43097ac6..eb588fadee38 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -10346,6 +10346,7 @@ ${reportName}`, changeField: (oldValue: string | undefined, newValue: string, fieldName: string) => `${fieldName} を「${newValue}」(以前は「${oldValue}」)に変更しました`, changeFieldEmpty: (newValue: string, fieldName: string) => `${fieldName} を「${newValue}」に設定`, changeFieldRemoved: (oldValue: string, fieldName: string) => `${fieldName} を削除しました(以前は「${oldValue}」)`, + changeFieldCleared: (fieldName: string) => `${fieldName} を削除しました`, changeReportPolicy: (toPolicyName: string, fromPolicyName?: string) => { if (!toPolicyName) { return `ワークスペース${fromPolicyName ? `(以前は${fromPolicyName})` : ''}を変更しました`; diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 0543602f1d02..f8f7cd525944 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -10470,6 +10470,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, changeField: (oldValue: string | undefined, newValue: string, fieldName: string) => `heeft ${fieldName} gewijzigd in „${newValue}” (voorheen „${oldValue}”)`, changeFieldEmpty: (newValue: string, fieldName: string) => `stel ${fieldName} in op "${newValue}"`, changeFieldRemoved: (oldValue: string, fieldName: string) => `heeft ${fieldName} verwijderd (voorheen „${oldValue}”)`, + changeFieldCleared: (fieldName: string) => `heeft ${fieldName} verwijderd`, changeReportPolicy: (toPolicyName: string, fromPolicyName?: string) => { if (!toPolicyName) { return `heeft de werkruimte${fromPolicyName ? `(voorheen ${fromPolicyName})` : ''} gewijzigd`; diff --git a/src/languages/pl.ts b/src/languages/pl.ts index a79b25c541ae..2a2998ecebd2 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -10485,6 +10485,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, changeField: (oldValue: string | undefined, newValue: string, fieldName: string) => `zmieniono ${fieldName} na „${newValue}” (wcześniej „${oldValue}”)`, changeFieldEmpty: (newValue: string, fieldName: string) => `ustaw ${fieldName} na „${newValue}”`, changeFieldRemoved: (oldValue: string, fieldName: string) => `usunięto ${fieldName} (wcześniej „${oldValue}”)`, + changeFieldCleared: (fieldName: string) => `usunięto ${fieldName}`, changeReportPolicy: (toPolicyName: string, fromPolicyName?: string) => { if (!toPolicyName) { return `zmieniono przestrzeń roboczą${fromPolicyName ? `(wcześniej ${fromPolicyName})` : ''}`; diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 597f30dd1438..bfdc550ecd59 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -10467,6 +10467,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, changeField: (oldValue: string | undefined, newValue: string, fieldName: string) => `alterou ${fieldName} para "${newValue}" (antes "${oldValue}")`, changeFieldEmpty: (newValue: string, fieldName: string) => `definir ${fieldName} como "${newValue}"`, changeFieldRemoved: (oldValue: string, fieldName: string) => `removeu ${fieldName} (antes "${oldValue}")`, + changeFieldCleared: (fieldName: string) => `removeu ${fieldName}`, changeReportPolicy: (toPolicyName: string, fromPolicyName?: string) => { if (!toPolicyName) { return `alterou o espaço de trabalho${fromPolicyName ? `(antes ${fromPolicyName})` : ''}`; diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index bc038bbd09ab..6bf3c6abcde6 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -10065,6 +10065,7 @@ ${reportName}`, changeField: (oldValue: string | undefined, newValue: string, fieldName: string) => `将${fieldName}更改为“${newValue}”(先前为“${oldValue}”)`, changeFieldEmpty: (newValue: string, fieldName: string) => `将 ${fieldName} 设置为“${newValue}”`, changeFieldRemoved: (oldValue: string, fieldName: string) => `已移除${fieldName}(先前为“${oldValue}”)`, + changeFieldCleared: (fieldName: string) => `已移除${fieldName}`, changeReportPolicy: (toPolicyName: string, fromPolicyName?: string) => { if (!toPolicyName) { return `更改了工作区${fromPolicyName ? `(原为 ${fromPolicyName})` : ''}`; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index eceeea54af6f..c616b08fb604 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -2432,8 +2432,9 @@ function getMessageOfOldDotReportAction(translate: LocalizedTranslate, oldDotAct switch (actionName) { case CONST.REPORT.ACTIONS.TYPE.CHANGE_FIELD: { const {oldValue, newValue, fieldName} = originalMessage; - if (oldValue && !newValue) { - return translate('report.actions.type.changeFieldRemoved', oldValue, fieldName); + if (!newValue) { + // A cleared field has no new value to show, so it must never fall through to `set {field} to ""` + return oldValue ? translate('report.actions.type.changeFieldRemoved', oldValue, fieldName) : translate('report.actions.type.changeFieldCleared', fieldName); } if (!oldValue) { return translate('report.actions.type.changeFieldEmpty', newValue, fieldName); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 45400a160ceb..fe72fab0e72f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4859,6 +4859,8 @@ function getReimbursementDeQueuedOrCanceledActionMessage( * */ function buildOptimisticChangeFieldAction(reportField: PolicyReportField, previousReportField: PolicyReportField, currentUserAccountID: number): OptimisticChangeFieldAction { + // A field that was never saved on the report shows its workspace default, so that is the value the user changed + const previousValue = previousReportField.value ?? previousReportField.defaultValue; return { actionName: CONST.REPORT.ACTIONS.TYPE.CHANGE_FIELD, actorAccountID: currentUserAccountID, @@ -4881,7 +4883,7 @@ function buildOptimisticChangeFieldAction(reportField: PolicyReportField, previo { type: 'TEXT', style: 'normal', - text: ` (previously '${previousReportField.value}').`, + text: ` (previously '${previousValue}').`, }, ], originalMessage: { @@ -4889,7 +4891,7 @@ function buildOptimisticChangeFieldAction(reportField: PolicyReportField, previo newType: reportField.type, newValue: reportField.value, oldType: previousReportField.type, - oldValue: previousReportField.value, + oldValue: previousValue, }, person: [ { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 69d8a0df33a8..7a189d728050 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -1867,6 +1867,22 @@ describe('ReportActionsUtils', () => { expect(message).toBe('removed Project (previously "Apollo")'); }); + it('should return the cleared field message when a field without a saved value is cleared', () => { + // Given a field change where both values are empty, which is what clearing a field that only showed its default can send + const action: Parameters[1] = { + reportActionID: '1', + created: '2024-01-01 00:00:00.000', + actionName: CONST.REPORT.ACTIONS.TYPE.CHANGE_FIELD, + originalMessage: {fieldName: 'Project', oldValue: '', newValue: ''}, + }; + + // When the message is built + const message = ReportActionsUtils.getMessageOfOldDotReportAction(translateLocal, action); + + // Then it says the field was removed, instead of `set Project to ""` + expect(message).toBe('removed Project'); + }); + it('should return the set field message when a field without a value is filled in', () => { // Given a field change whose old value is empty const action: Parameters[1] = { diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index e3f610e18328..748a8c7e862f 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -35,6 +35,7 @@ import { buildOptimisticApprovedReportAction, buildOptimisticCancelPaymentReportAction, buildOptimisticCardAssignedReportAction, + buildOptimisticChangeFieldAction, buildOptimisticChatReport, buildOptimisticClosedReportAction, buildOptimisticCreatedReportAction, @@ -24701,6 +24702,45 @@ describe('ReportUtils', () => { }); }); + describe('buildOptimisticChangeFieldAction', () => { + const textField: PolicyReportField = { + name: 'text', + fieldID: 'text_field', + defaultValue: 'Initial', + orderWeight: 1, + type: 'text', + deletable: true, + target: CONST.REPORT.TYPE.EXPENSE, + values: [], + keys: [], + externalIDs: [], + disabledOptions: [], + isTax: false, + }; + + it('should use the default value as the old value when the field was never saved on the report', () => { + // Given a text field that has no saved value on the report, so the user sees its workspace default + const previousReportField = {...textField, value: undefined}; + + // When the user clears it + const action = buildOptimisticChangeFieldAction({...textField, value: ''}, previousReportField, 1); + + // Then the action records the default as the old value, so the message says what was removed + expect(action).toMatchObject({originalMessage: {oldValue: 'Initial', newValue: ''}}); + }); + + it('should use the saved value as the old value when the field has one', () => { + // Given a text field whose saved value differs from its default + const previousReportField = {...textField, value: 'Saved'}; + + // When the user clears it + const action = buildOptimisticChangeFieldAction({...textField, value: ''}, previousReportField, 1); + + // Then the action records the saved value, not the default + expect(action).toMatchObject({originalMessage: {oldValue: 'Saved', newValue: ''}}); + }); + }); + describe('hasVisibleReportFieldViolations', () => { const policyID = 'policy-field-violations'; From a172bc9a837df542efdeb81e64864c10bfdd0dc9 Mon Sep 17 00:00:00 2001 From: "daledah (via MelvinBot)" Date: Wed, 21 Oct 2026 11:40:25 +0000 Subject: [PATCH 2/3] Remove the defaultValue fallback from the optimistic CHANGEFIELD action The server reuses the client's reportActionID and overwrites originalMessage without the default as oldValue, so the fallback made the message flicker from 'removed text (previously ...)' to 'removed text'. Co-authored-by: daledah --- src/libs/ReportUtils.ts | 6 ++---- tests/unit/ReportUtilsTest.ts | 7 ++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fe72fab0e72f..45400a160ceb 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4859,8 +4859,6 @@ function getReimbursementDeQueuedOrCanceledActionMessage( * */ function buildOptimisticChangeFieldAction(reportField: PolicyReportField, previousReportField: PolicyReportField, currentUserAccountID: number): OptimisticChangeFieldAction { - // A field that was never saved on the report shows its workspace default, so that is the value the user changed - const previousValue = previousReportField.value ?? previousReportField.defaultValue; return { actionName: CONST.REPORT.ACTIONS.TYPE.CHANGE_FIELD, actorAccountID: currentUserAccountID, @@ -4883,7 +4881,7 @@ function buildOptimisticChangeFieldAction(reportField: PolicyReportField, previo { type: 'TEXT', style: 'normal', - text: ` (previously '${previousValue}').`, + text: ` (previously '${previousReportField.value}').`, }, ], originalMessage: { @@ -4891,7 +4889,7 @@ function buildOptimisticChangeFieldAction(reportField: PolicyReportField, previo newType: reportField.type, newValue: reportField.value, oldType: previousReportField.type, - oldValue: previousValue, + oldValue: previousReportField.value, }, person: [ { diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 748a8c7e862f..df1643fb6207 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -24718,15 +24718,16 @@ describe('ReportUtils', () => { isTax: false, }; - it('should use the default value as the old value when the field was never saved on the report', () => { + it('should not use the default value as the old value when the field was never saved on the report', () => { // Given a text field that has no saved value on the report, so the user sees its workspace default const previousReportField = {...textField, value: undefined}; // When the user clears it const action = buildOptimisticChangeFieldAction({...textField, value: ''}, previousReportField, 1); - // Then the action records the default as the old value, so the message says what was removed - expect(action).toMatchObject({originalMessage: {oldValue: 'Initial', newValue: ''}}); + // Then the action leaves the old value empty, matching the server's action so the message doesn't change when it arrives + expect(action.originalMessage?.oldValue).toBeUndefined(); + expect(action.originalMessage?.newValue).toBe(''); }); it('should use the saved value as the old value when the field has one', () => { From 52c32921c6edad1a509b991e928c00871f104808 Mon Sep 17 00:00:00 2001 From: "daledah (via MelvinBot)" Date: Wed, 21 Oct 2026 11:54:56 +0000 Subject: [PATCH 3/3] Use toMatchObject instead of the deprecated originalMessage property in the CHANGEFIELD test Co-authored-by: daledah --- tests/unit/ReportUtilsTest.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index df1643fb6207..da55b78517b4 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -24726,8 +24726,7 @@ describe('ReportUtils', () => { const action = buildOptimisticChangeFieldAction({...textField, value: ''}, previousReportField, 1); // Then the action leaves the old value empty, matching the server's action so the message doesn't change when it arrives - expect(action.originalMessage?.oldValue).toBeUndefined(); - expect(action.originalMessage?.newValue).toBe(''); + expect(action).toMatchObject({originalMessage: {oldValue: undefined, newValue: ''}}); }); it('should use the saved value as the old value when the field has one', () => {