Skip to content

[HOLD BE] Show category visibility change logs in #admins - #105465

Open
MelvinBot wants to merge 5 commits into
mainfrom
claude-categoryVisibilityChangeLogs
Open

MelvinBot wants to merge 5 commits into
mainfrom
claude-categoryVisibilityChangeLogs

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

Auth will post POLICYCHANGELOG_SET_CATEGORY_VISIBILITY entries to #admins as data (categoryNames, accountIDs, hadRestriction, reason), not as text. The App had no handler for this action, so it would fall back to the raw message text. This PR builds the sentence when the entry renders, like the other category change logs, so it uses the viewer's language and members' current names.

  • Adds SET_CATEGORY_VISIBILITY to CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG, the new originalMessage fields, and CONST.POLICY.CATEGORY_VISIBILITY (the 10-item threshold and the reason values).
  • Adds the action to isCategoryModificationAction, so the LHN preview, thread title, and copy-to-clipboard show the same text. Also maps it to categoryResolver in PolicyChangeLogContent.
  • Adds the action to SYSTEM_POLICY_CHANGE_LOG_ACTION_TYPES, so it's treated as passive audit content and collapses with neighboring system messages like the other category change logs.
  • Adds getCategoryVisibilityUpdateMessage, called from getWorkspaceCategoryUpdateMessage:
    • Categories: the "Meals" category / the "Meals" and "Fuel" categories / "14 categories" (10 or more). An empty list uses "changed {members} to see all categories".
    • Members: display names joined with formatList / "everyone" (empty list) / "12 members" (10 or more).
    • Uses "set" or "changed" based on hadRestriction, and appends the reason sentence for categoriesDeleted and categoriesDisabled.
  • Full lists at the top of the thread: getWorkspaceCategoryUpdateMessage takes a new shouldShowFullList param. When it's true and a list has 10 or more items, the message is "Categories: …" and/or "Members: …" naming every item. Instead of adding a new flag in ReportActionItemParentAction, this reuses the existing isThreadReportParentAction prop, which AncestorReportActionItem already sets. It goes ReportActionItem → ActionContentRouter → PolicyChangeLogContent → categoryResolver.
  • Quotes: each name is wrapped by a new quotedCategoryName translation, so each locale uses its own quote marks instead of English quotes in shared code.
  • Translations: the keys are added to every locale, because the other locale files are typed against en and typecheck fails without them. The non-English copy is a first pass; please review or regenerate it.

Fixed Issues

$ #105085
PROPOSAL: #105085 (comment)

Tests

Precondition: the backend must post POLICYCHANGELOG_SET_CATEGORY_VISIBILITY entries to #admins (the Auth work in step 1.17). The App has no UI that creates this action, so these steps can't run in dev until that ships. Use a workspace with at least 12 members and 14 enabled categories, including "Meals" and "Fuel".

  1. As a workspace admin, open the workspace's #admins room.
  2. Restrict member A, who had no restriction, to "Meals".
  3. Verify #admins shows "set A to only see the "Meals" category".
  4. Change A to "Meals" and "Fuel".
  5. Verify it shows "changed A to only see the "Meals" and "Fuel" categories".
  6. Remove A's restriction.
  7. Verify it shows "changed A to see all categories".
  8. Restrict A to 14 categories.
  9. Verify it shows "… to only see 14 categories". Open the entry's thread and verify the top shows "Categories: " followed by all 14 names.
  10. Restrict 12 members to "Meals".
  11. Verify it shows "set 12 members to only see the "Meals" category". Open the thread and verify the top shows "Members: " followed by all 12 names.
  12. Restrict A to only "Fuel", then delete the "Fuel" category.
  13. Verify the new entry ends with ", since their only category was deleted. Update their category visibility if needed."
  14. Repeat step 12, but disable "Fuel" instead of deleting it. Verify the entry ends with ", since their only category was disabled. Update their category visibility if needed."
  15. Verify the #admins preview in the LHN matches the latest entry's text. Right-click (or long-press) an entry, select Copy to clipboard, and verify the pasted text matches.
  16. Verify that two or more consecutive category visibility entries collapse into a "show N updates" row with other system messages, and expand when tapped.
  17. Change your language to Spanish and verify the entries are shown in Spanish.
  18. Have member A change their display name, then verify the existing entries show A's new name.
  • Verify that no errors appear in the JS console

Offline tests

  1. Complete the Tests precondition and steps 1–9.
  2. Go offline.
  3. Open #admins and verify the category visibility entries still show the same text, including the full lists at the top of the thread.

QA Steps

Same as tests. This needs the backend to post POLICYCHANGELOG_SET_CATEGORY_VISIBILITY entries on staging first.

  • Verify that no errors appear in the JS console

AI Tests

  • npm run typecheck: passed
  • npm run lint-changed: passed
  • npm run spell-changed: passed
  • npm run fmt (oxfmt): no changes outside this PR
  • npm test -- tests/unit/ReportActionsUtilsTest.ts tests/unit/PolicyChangeLogContentTest.tsx tests/ui/ReportActionItemTest.tsx tests/ui/AncestorReportActionItemTest.tsx tests/unit/ReportNameUtilsTest.ts tests/unit/OptionsListUtilsTest.tsx: 1551 passed. New cases cover every row of the issue's example table.
  • npm test -- tests/unit/SystemMessageDisplayStateTest.ts tests/unit/ReportActionsUtilsTest.ts: 669 passed, including a new case that classifies SET_CATEGORY_VISIBILITY as a system message.
  • npm run react-compiler-compliance-check check-changed: could not run in this environment (the base ref couldn't be resolved). The TSX changes only pass a prop through.
  • No UI check: the backend doesn't send this action yet, so it can't be created in a dev environment.

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified any copy / text that was added to the app is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If the PR adds or modifies the UI:
    • I asked an AI agent to review the changes for accessibility issues and addressed its findings.
    • I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.
    • I verified all new/changed elements have meaningful accessible names and roles.
    • I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Render POLICYCHANGELOG_SET_CATEGORY_VISIBILITY entries as a translated sentence built from categoryNames, accountIDs, hadRestriction and reason. Long lists show a count, and the full lists show at the top of the entry's thread.

Co-authored-by: huult <huult@users.noreply.github.com>
@melvin-bot melvin-bot Bot added Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web labels Oct 30, 2026
@MelvinBot
MelvinBot requested a review from a team October 30, 2026 07:44
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I created this PR from an upstream branch since I don't have push access to your fork.

To take ownership of this branch and be able to push updates, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-categoryVisibilityChangeLogs
git checkout -b claude-categoryVisibilityChangeLogs upstream/claude-categoryVisibilityChangeLogs
git push -u origin claude-categoryVisibilityChangeLogs

Then you can close this PR and open a new one from your fork.

@OSBotify

Copy link
Copy Markdown
Contributor

🦜 Polyglot Parrot! 🦜

Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues:

View the translation diff
diff --git a/src/languages/de.ts b/src/languages/de.ts
index c0ab2100..82d85bd0 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -9973,12 +9973,12 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
             return `hat die Positionenbelege der Kategorie „${categoryName}“ auf ${newValue} geändert (zuvor ${oldValue})`;
         },
         setCategoryName: (oldName: string, newName: string) => `hat die Kategorie „${oldName}“ in „${newName}“ umbenannt`,
-        setCategoryVisibility: (members: string, categories: string) => `hat ${members} so eingestellt, dass sie nur ${categories} sehen`,
-        changedCategoryVisibility: (members: string, categories: string) => `hat ${members} so geändert, dass sie nur ${categories} sehen`,
-        clearedCategoryVisibility: (members: string) => `hat ${members} so geändert, dass sie alle Kategorien sehen`,
+        setCategoryVisibility: (members: string, categories: string) => `Legen Sie fest, dass ${members} nur ${categories} sehen können`,
+        changedCategoryVisibility: (members: string, categories: string) => `hat ${members} so geändert, dass sie nur noch ${categories} sehen`,
+        clearedCategoryVisibility: (members: string) => `${members} können jetzt alle Kategorien sehen`,
         quotedCategoryName: (categoryName: string) => `„${categoryName}“`,
         oneCategory: (categoryName: string) => `die Kategorie ${categoryName}`,
-        someCategories: (categoryList: string) => `die Kategorien ${categoryList}`,
+        someCategories: (categoryList: string) => `die ${categoryList} Kategorien`,
         everyone: 'alle',
         categoryCount: ({count}: {count: number}) => ({
             one: '1 Kategorie',
@@ -9990,8 +9990,8 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
         }),
         categoryVisibilityCategoriesList: (categoryList: string) => `Kategorien: ${categoryList}`,
         categoryVisibilityMembersList: (memberList: string) => `Mitglieder: ${memberList}`,
-        categoryVisibilityReasonDeleted: ', da ihre einzige Kategorie gelöscht wurde. Aktualisiere bei Bedarf ihre Kategoriesichtbarkeit.',
-        categoryVisibilityReasonDisabled: ', da ihre einzige Kategorie deaktiviert wurde. Aktualisiere bei Bedarf ihre Kategoriesichtbarkeit.',
+        categoryVisibilityReasonDeleted: ', da ihre einzige Kategorie gelöscht wurde. Aktualisieren Sie bei Bedarf ihre Kategoriesichtbarkeit.',
+        categoryVisibilityReasonDisabled: ', da ihre einzige Kategorie deaktiviert war. Aktualisieren Sie bei Bedarf deren Kategoriesichtbarkeit.',
         updatedDescriptionHint: (categoryName: string, newValue?: string, oldValue?: string) => {
             if (!newValue) {
                 return `hat den Beschreibungshinweis „${oldValue}“ aus der Kategorie „${categoryName}“ entfernt`;
diff --git a/src/languages/el.ts b/src/languages/el.ts
index e4b63e77..ef11fae4 100644
--- a/src/languages/el.ts
+++ b/src/languages/el.ts
@@ -10132,12 +10132,12 @@ ${reportName}`,
             return `άλλαξε τις αναλυτικές αποδείξεις της κατηγορίας «${categoryName}» σε ${newValue} (προηγουμένως ${oldValue})`;
         },
         setCategoryName: (oldName: string, newName: string) => `μετονόμασε την κατηγορία «${oldName}» σε «${newName}»`,
-        setCategoryVisibility: (members: string, categories: string) => `όρισε ώστε ${members} να βλέπουν μόνο ${categories}`,
-        changedCategoryVisibility: (members: string, categories: string) => `άλλαξε ώστε ${members} να βλέπουν μόνο ${categories}`,
-        clearedCategoryVisibility: (members: string) => `άλλαξε ώστε ${members} να βλέπουν όλες τις κατηγορίες`,
+        setCategoryVisibility: (members: string, categories: string) => `ορίστε ώστε τα μέλη ${members} να βλέπουν μόνο τις κατηγορίες ${categories}`,
+        changedCategoryVisibility: (members: string, categories: string) => `άλλαξε τα ${members} ώστε να βλέπουν μόνο τις ${categories}`,
+        clearedCategoryVisibility: (members: string) => `άλλαξε τα δικαιώματα των ${members} ώστε να βλέπουν όλες τις κατηγορίες`,
         quotedCategoryName: (categoryName: string) => `«${categoryName}»`,
-        oneCategory: (categoryName: string) => `την κατηγορία ${categoryName}`,
-        someCategories: (categoryList: string) => `τις κατηγορίες ${categoryList}`,
+        oneCategory: (categoryName: string) => `η κατηγορία ${categoryName}`,
+        someCategories: (categoryList: string) => `οι κατηγορίες ${categoryList}`,
         everyone: 'όλοι',
         categoryCount: ({count}: {count: number}) => ({
             one: '1 κατηγορία',
@@ -10149,8 +10149,8 @@ ${reportName}`,
         }),
         categoryVisibilityCategoriesList: (categoryList: string) => `Κατηγορίες: ${categoryList}`,
         categoryVisibilityMembersList: (memberList: string) => `Μέλη: ${memberList}`,
-        categoryVisibilityReasonDeleted: ', επειδή η μοναδική τους κατηγορία διαγράφηκε. Ενημερώστε την ορατότητα κατηγοριών τους αν χρειάζεται.',
-        categoryVisibilityReasonDisabled: ', επειδή η μοναδική τους κατηγορία απενεργοποιήθηκε. Ενημερώστε την ορατότητα κατηγοριών τους αν χρειάζεται.',
+        categoryVisibilityReasonDeleted: ', επειδή η μοναδική τους κατηγορία διαγράφηκε. Ενημερώστε, αν χρειάζεται, την ορατότητα της κατηγορίας τους.',
+        categoryVisibilityReasonDisabled: ', καθώς η μοναδική τους κατηγορία ήταν απενεργοποιημένη. Ενημερώστε την ορατότητα της κατηγορίας τους, αν χρειάζεται.',
         updatedDescriptionHint: (categoryName: string, newValue?: string, oldValue?: string) => {
             if (!newValue) {
                 return `αφαίρεσε την υπόδειξη περιγραφής «${oldValue}» από την κατηγορία «${categoryName}»`;
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 6e7e6011..f9496ad8 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -9793,12 +9793,12 @@ ${reportName}`,
             return `cambió los Recibos detallados de la categoría "${categoryName}" a ${newValue} (previamente ${oldValue})`;
         },
         setCategoryName: (oldName, newName) => `renombró la categoría "${oldName}" a "${newName}"`,
-        setCategoryVisibility: (members, categories) => `configuró a ${members} para que solo vean ${categories}`,
-        changedCategoryVisibility: (members, categories) => `cambió a ${members} para que solo vean ${categories}`,
-        clearedCategoryVisibility: (members) => `cambió a ${members} para que vean todas las categorías`,
+        setCategoryVisibility: (members: string, categories: string) => `configura ${members} para que solo vean ${categories}`,
+        changedCategoryVisibility: (members: string, categories: string) => `cambió ${members} para que solo vean ${categories}`,
+        clearedCategoryVisibility: (members: string) => `cambió ${members} para que vean todas las categorías`,
         quotedCategoryName: (categoryName) => `"${categoryName}"`,
-        oneCategory: (categoryName) => `la categoría ${categoryName}`,
-        someCategories: (categoryList) => `las categorías ${categoryList}`,
+        oneCategory: (categoryName: string) => `la categoría ${categoryName}`,
+        someCategories: (categoryList: string) => `las ${categoryList} categorías`,
         everyone: 'todos',
         categoryCount: ({count}: {count: number}) => ({
             one: '1 categoría',
@@ -9808,10 +9808,10 @@ ${reportName}`,
             one: '1 miembro',
             other: `${count} miembros`,
         }),
-        categoryVisibilityCategoriesList: (categoryList) => `Categorías: ${categoryList}`,
-        categoryVisibilityMembersList: (memberList) => `Miembros: ${memberList}`,
-        categoryVisibilityReasonDeleted: ', ya que su única categoría fue eliminada. Actualiza su visibilidad de categorías si es necesario.',
-        categoryVisibilityReasonDisabled: ', ya que su única categoría fue deshabilitada. Actualiza su visibilidad de categorías si es necesario.',
+        categoryVisibilityCategoriesList: (categoryList: string) => `Categorías: ${categoryList}`,
+        categoryVisibilityMembersList: (memberList: string) => `Miembros: ${memberList}`,
+        categoryVisibilityReasonDeleted: ', ya que su única categoría se eliminó. Actualiza la visibilidad de su categoría si es necesario.',
+        categoryVisibilityReasonDisabled: ', ya que su única categoría estaba deshabilitada. Actualiza la visibilidad de su categoría si es necesario.',
         updateCategories: (count) => `actualizó ${count} categorías`,
         updateTagListName: (oldName, newName) => `cambió el nombre de la lista de etiquetas a "${newName}" (previamente "${oldName}")`,
         updateTagList: (tagListName) => `actualizó las etiquetas de la lista "${tagListName}"`,
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index ce238e21..c269cec1 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -9997,25 +9997,25 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
             return `a modifié les reçus détaillés de la catégorie « ${categoryName} » en ${newValue} (auparavant ${oldValue})`;
         },
         setCategoryName: (oldName: string, newName: string) => `a renommé la catégorie « ${oldName} » en « ${newName} »`,
-        setCategoryVisibility: (members: string, categories: string) => `a configuré ${members} pour ne voir que ${categories}`,
-        changedCategoryVisibility: (members: string, categories: string) => `a modifié ${members} pour ne voir que ${categories}`,
+        setCategoryVisibility: (members: string, categories: string) => `définir ${members} pour ne voir que ${categories}`,
+        changedCategoryVisibility: (members: string, categories: string) => `a modifié ${members} pour qu'ils ne voient que ${categories}`,
         clearedCategoryVisibility: (members: string) => `a modifié ${members} pour voir toutes les catégories`,
         quotedCategoryName: (categoryName: string) => `« ${categoryName} »`,
         oneCategory: (categoryName: string) => `la catégorie ${categoryName}`,
-        someCategories: (categoryList: string) => `les catégories ${categoryList}`,
+        someCategories: (categoryList: string) => `les ${categoryList} catégories`,
         everyone: 'tout le monde',
         categoryCount: ({count}: {count: number}) => ({
-            one: `${count} catégorie`,
+            one: '1 catégorie',
             other: `${count} catégories`,
         }),
         memberCount: ({count}: {count: number}) => ({
-            one: `${count} membre`,
+            one: '1 membre',
             other: `${count} membres`,
         }),
-        categoryVisibilityCategoriesList: (categoryList: string) => `Catégories : ${categoryList}`,
-        categoryVisibilityMembersList: (memberList: string) => `Membres : ${memberList}`,
-        categoryVisibilityReasonDeleted: ', car leur seule catégorie a été supprimée. Mettez à jour leur visibilité des catégories si nécessaire.',
-        categoryVisibilityReasonDisabled: ', car leur seule catégorie a été désactivée. Mettez à jour leur visibilité des catégories si nécessaire.',
+        categoryVisibilityCategoriesList: (categoryList: string) => `Catégories : ${categoryList}`,
+        categoryVisibilityMembersList: (memberList: string) => `Membres : ${memberList}`,
+        categoryVisibilityReasonDeleted: ', car leur seule catégorie a été supprimée. Mettez à jour la visibilité de leur catégorie si nécessaire.',
+        categoryVisibilityReasonDisabled: ', car leur seule catégorie a été désactivée. Mettez à jour la visibilité de leurs catégories si nécessaire.',
         updatedDescriptionHint: (categoryName: string, newValue?: string, oldValue?: string) => {
             if (!newValue) {
                 return `a supprimé l’indication de description « ${oldValue} » de la catégorie « ${categoryName} »`;
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 9e01a1e1..ff8b5106 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -9917,8 +9917,8 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
             return `ha modificato le ricevute con voce di dettaglio della categoria "${categoryName}" in ${newValue} (in precedenza ${oldValue})`;
         },
         setCategoryName: (oldName: string, newName: string) => `ha rinominato la categoria "${oldName}" in "${newName}"`,
-        setCategoryVisibility: (members: string, categories: string) => `ha impostato ${members} per vedere solo ${categories}`,
-        changedCategoryVisibility: (members: string, categories: string) => `ha modificato ${members} per vedere solo ${categories}`,
+        setCategoryVisibility: (members: string, categories: string) => `imposta ${members} in modo che vedano solo ${categories}`,
+        changedCategoryVisibility: (members: string, categories: string) => `ha modificato ${members} in modo che possano vedere solo ${categories}`,
         clearedCategoryVisibility: (members: string) => `ha modificato ${members} per vedere tutte le categorie`,
         quotedCategoryName: (categoryName: string) => `"${categoryName}"`,
         oneCategory: (categoryName: string) => `la categoria ${categoryName}`,
@@ -9934,8 +9934,8 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
         }),
         categoryVisibilityCategoriesList: (categoryList: string) => `Categorie: ${categoryList}`,
         categoryVisibilityMembersList: (memberList: string) => `Membri: ${memberList}`,
-        categoryVisibilityReasonDeleted: ', poiché la loro unica categoria è stata eliminata. Aggiorna la loro visibilità delle categorie se necessario.',
-        categoryVisibilityReasonDisabled: ', poiché la loro unica categoria è stata disattivata. Aggiorna la loro visibilità delle categorie se necessario.',
+        categoryVisibilityReasonDeleted: ', perché la loro unica categoria è stata eliminata. Aggiorna la visibilità della loro categoria se necessario.',
+        categoryVisibilityReasonDisabled: ', perché la loro unica categoria è stata disattivata. Aggiorna la visibilità della loro categoria se necessario.',
         updatedDescriptionHint: (categoryName: string, newValue?: string, oldValue?: string) => {
             if (!newValue) {
                 return `ha rimosso il suggerimento di descrizione "${oldValue}" dalla categoria "${categoryName}"`;
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index e102450d..ab69fad1 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -7454,7 +7454,6 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO
                 title: 'メンバーをコピー',
                 prompt: ({count, targetWorkspaceName}: {count: number; targetWorkspaceName: string}) => ({
                     one: `${targetWorkspaceName}のメンバーをこのワークスペースのメンバーで置き換えてもよろしいですか?`,
-                    // Japanese has no "one" plural category, so handle the single-workspace case here
                     other:
                         count === 1
                             ? `${targetWorkspaceName}のメンバーをこのワークスペースのメンバーで置き換えてもよろしいですか?`
@@ -9778,25 +9777,25 @@ ${reportName}`,
             return `「${categoryName}」カテゴリの明細付きレシートを${newValue}に変更しました(以前は${oldValue})`;
         },
         setCategoryName: (oldName: string, newName: string) => `カテゴリ名を「${oldName}」から「${newName}」に変更しました`,
-        setCategoryVisibility: (members: string, categories: string) => `${members}が${categories}のみを表示できるように設定しました`,
-        changedCategoryVisibility: (members: string, categories: string) => `${members}が${categories}のみを表示できるように変更しました`,
-        clearedCategoryVisibility: (members: string) => `${members}がすべてのカテゴリを表示できるように変更しました`,
+        setCategoryVisibility: (members: string, categories: string) => `${members} が ${categories} だけを表示できるように設定します`,
+        changedCategoryVisibility: (members: string, categories: string) => `${members} が ${categories} のみを表示するように変更しました`,
+        clearedCategoryVisibility: (members: string) => `${members} がすべてのカテゴリを表示できるように変更しました`,
         quotedCategoryName: (categoryName: string) => `「${categoryName}」`,
-        oneCategory: (categoryName: string) => `カテゴリ${categoryName}`,
-        someCategories: (categoryList: string) => `カテゴリ${categoryList}`,
+        oneCategory: (categoryName: string) => `${categoryName} カテゴリ`,
+        someCategories: (categoryList: string) => `${categoryList} 件のカテゴリー`,
         everyone: '全員',
         categoryCount: ({count}: {count: number}) => ({
             one: '1 件のカテゴリ',
-            other: `${count} 件のカテゴリ`,
+            other: `${count} 個のカテゴリ`,
         }),
         memberCount: ({count}: {count: number}) => ({
-            one: '1 人のメンバー',
-            other: `${count} 人のメンバー`,
+            one: 'メンバー 1 人',
+            other: `${count} 名のメンバー`,
         }),
-        categoryVisibilityCategoriesList: (categoryList: string) => `カテゴリ: ${categoryList}`,
-        categoryVisibilityMembersList: (memberList: string) => `メンバー: ${memberList}`,
-        categoryVisibilityReasonDeleted: '。唯一のカテゴリが削除されたためです。必要に応じてカテゴリの表示設定を更新してください。',
-        categoryVisibilityReasonDisabled: '。唯一のカテゴリが無効化されたためです。必要に応じてカテゴリの表示設定を更新してください。',
+        categoryVisibilityCategoriesList: (categoryList: string) => `カテゴリ:${categoryList}`,
+        categoryVisibilityMembersList: (memberList: string) => `メンバー:${memberList}`,
+        categoryVisibilityReasonDeleted: 'は、そのユーザーの唯一のカテゴリが削除されたためです。必要に応じて、そのユーザーのカテゴリの表示設定を更新してください。',
+        categoryVisibilityReasonDisabled: '、そのユーザーの唯一のカテゴリが無効になっているためです。必要に応じてカテゴリの表示設定を更新してください。',
         updatedDescriptionHint: (categoryName: string, newValue?: string, oldValue?: string) => {
             if (!newValue) {
                 return `カテゴリ「${categoryName}」から説明のヒント「${oldValue}」を削除しました`;
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 2aba237f..4a8a8760 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -9894,12 +9894,12 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
             return `heeft de categorie „${categoryName}” Itemized receipts gewijzigd naar ${newValue} (voorheen ${oldValue})`;
         },
         setCategoryName: (oldName: string, newName: string) => `heeft de categorie "${oldName}" hernoemd naar "${newName}"`,
-        setCategoryVisibility: (members: string, categories: string) => `heeft ${members} zo ingesteld dat ze alleen ${categories} zien`,
-        changedCategoryVisibility: (members: string, categories: string) => `heeft ${members} zo gewijzigd dat ze alleen ${categories} zien`,
-        clearedCategoryVisibility: (members: string) => `heeft ${members} zo gewijzigd dat ze alle categorieën zien`,
+        setCategoryVisibility: (members: string, categories: string) => `stel ${members} in om alleen ${categories} te zien`,
+        changedCategoryVisibility: (members: string, categories: string) => `heeft ${members} gewijzigd zodat ze alleen ${categories} zien`,
+        clearedCategoryVisibility: (members: string) => `heeft ${members} gewijzigd zodat ze alle categorieën kunnen zien`,
         quotedCategoryName: (categoryName: string) => `"${categoryName}"`,
         oneCategory: (categoryName: string) => `de categorie ${categoryName}`,
-        someCategories: (categoryList: string) => `de categorieën ${categoryList}`,
+        someCategories: (categoryList: string) => `de ${categoryList} categorieën`,
         everyone: 'iedereen',
         categoryCount: ({count}: {count: number}) => ({
             one: '1 categorie',
@@ -9912,7 +9912,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
         categoryVisibilityCategoriesList: (categoryList: string) => `Categorieën: ${categoryList}`,
         categoryVisibilityMembersList: (memberList: string) => `Leden: ${memberList}`,
         categoryVisibilityReasonDeleted: ', omdat hun enige categorie is verwijderd. Werk hun categoriezichtbaarheid bij indien nodig.',
-        categoryVisibilityReasonDisabled: ', omdat hun enige categorie is uitgeschakeld. Werk hun categoriezichtbaarheid bij indien nodig.',
+        categoryVisibilityReasonDisabled: ', omdat hun enige categorie was uitgeschakeld. Werk indien nodig hun categoriezichtbaarheid bij.',
         updatedDescriptionHint: (categoryName: string, newValue?: string, oldValue?: string) => {
             if (!newValue) {
                 return `heeft de beschrijvingshint "${oldValue}" verwijderd uit de categorie "${categoryName}"`;
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 133b6905..56131965 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -9915,29 +9915,25 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
             return `zmienił(a) pozycjonowanie paragonów dla kategorii „${categoryName}” na ${newValue} (wcześniej ${oldValue})`;
         },
         setCategoryName: (oldName: string, newName: string) => `zmieniono nazwę kategorii „${oldName}” na „${newName}”`,
-        setCategoryVisibility: (members: string, categories: string) => `ustawiono, aby ${members} widzieli tylko ${categories}`,
-        changedCategoryVisibility: (members: string, categories: string) => `zmieniono, aby ${members} widzieli tylko ${categories}`,
-        clearedCategoryVisibility: (members: string) => `zmieniono, aby ${members} widzieli wszystkie kategorie`,
+        setCategoryVisibility: (members: string, categories: string) => `ustaw, by ${members} widzieli tylko ${categories}`,
+        changedCategoryVisibility: (members: string, categories: string) => `zmienił(a) ustawienia, aby ${members} widzieli tylko ${categories}`,
+        clearedCategoryVisibility: (members: string) => `zmieniono uprawnienia ${members}, aby widzieli wszystkie kategorie`,
         quotedCategoryName: (categoryName: string) => `„${categoryName}”`,
-        oneCategory: (categoryName: string) => `kategorię ${categoryName}`,
-        someCategories: (categoryList: string) => `kategorie ${categoryList}`,
+        oneCategory: (categoryName: string) => `kategoria ${categoryName}`,
+        someCategories: (categoryList: string) => `${categoryList} kategorie`,
         everyone: 'wszyscy',
         categoryCount: ({count}: {count: number}) => ({
-            one: '1 kategorię',
-            few: `${count} kategorie`,
-            many: `${count} kategorii`,
-            other: `${count} kategorii`,
+            one: '1 kategoria',
+            other: `${count} kategorie`,
         }),
         memberCount: ({count}: {count: number}) => ({
             one: '1 członek',
-            few: `${count} członków`,
-            many: `${count} członków`,
             other: `${count} członków`,
         }),
         categoryVisibilityCategoriesList: (categoryList: string) => `Kategorie: ${categoryList}`,
         categoryVisibilityMembersList: (memberList: string) => `Członkowie: ${memberList}`,
-        categoryVisibilityReasonDeleted: ', ponieważ ich jedyna kategoria została usunięta. W razie potrzeby zaktualizuj ich widoczność kategorii.',
-        categoryVisibilityReasonDisabled: ', ponieważ ich jedyna kategoria została wyłączona. W razie potrzeby zaktualizuj ich widoczność kategorii.',
+        categoryVisibilityReasonDeleted: ', ponieważ ich jedyna kategoria została usunięta. Zaktualizuj w razie potrzeby widoczność ich kategorii.',
+        categoryVisibilityReasonDisabled: ', ponieważ ich jedyna kategoria została wyłączona. Zaktualizuj widoczność ich kategorii, jeśli to konieczne.',
         updatedDescriptionHint: (categoryName: string, newValue?: string, oldValue?: string) => {
             if (!newValue) {
                 return `usunięto podpowiedź opisu „${oldValue}” z kategorii „${categoryName}”`;
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 4b3239d9..bef9748c 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -9902,25 +9902,25 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
             return `alterou a categoria "${categoryName}" Itens detalhados de recibos para ${newValue} (antes ${oldValue})`;
         },
         setCategoryName: (oldName: string, newName: string) => `renomeou a categoria "${oldName}" para "${newName}"`,
-        setCategoryVisibility: (members: string, categories: string) => `configurou ${members} para ver apenas ${categories}`,
-        changedCategoryVisibility: (members: string, categories: string) => `alterou ${members} para ver apenas ${categories}`,
+        setCategoryVisibility: (members: string, categories: string) => `definir ${members} para ver apenas ${categories}`,
+        changedCategoryVisibility: (members: string, categories: string) => `alterou ${members} para verem apenas ${categories}`,
         clearedCategoryVisibility: (members: string) => `alterou ${members} para ver todas as categorias`,
         quotedCategoryName: (categoryName: string) => `"${categoryName}"`,
         oneCategory: (categoryName: string) => `a categoria ${categoryName}`,
         someCategories: (categoryList: string) => `as categorias ${categoryList}`,
         everyone: 'todos',
         categoryCount: ({count}: {count: number}) => ({
-            one: `${count} categoria`,
+            one: '1 categoria',
             other: `${count} categorias`,
         }),
         memberCount: ({count}: {count: number}) => ({
-            one: `${count} membro`,
+            one: '1 membro',
             other: `${count} membros`,
         }),
         categoryVisibilityCategoriesList: (categoryList: string) => `Categorias: ${categoryList}`,
         categoryVisibilityMembersList: (memberList: string) => `Membros: ${memberList}`,
-        categoryVisibilityReasonDeleted: ', pois a única categoria deles foi excluída. Atualize a visibilidade de categorias deles, se necessário.',
-        categoryVisibilityReasonDisabled: ', pois a única categoria deles foi desativada. Atualize a visibilidade de categorias deles, se necessário.',
+        categoryVisibilityReasonDeleted: ', já que a única categoria deles foi excluída. Atualize a visibilidade da categoria deles, se necessário.',
+        categoryVisibilityReasonDisabled: ', já que a única categoria deles foi desativada. Atualize a visibilidade da categoria, se necessário.',
         updatedDescriptionHint: (categoryName: string, newValue?: string, oldValue?: string) => {
             if (!newValue) {
                 return `removeu a dica de descrição "${oldValue}" da categoria "${categoryName}"`;
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index de530f89..a5383b12 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -7281,7 +7281,6 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM
                 title: '复制成员',
                 prompt: ({count, targetWorkspaceName}: {count: number; targetWorkspaceName: string}) => ({
                     one: `确定要用此工作区的成员替换 ${targetWorkspaceName} 的成员吗?`,
-                    // Chinese has no "one" plural category, so handle the single-workspace case here
                     other: count === 1 ? `确定要用此工作区的成员替换 ${targetWorkspaceName} 的成员吗?` : `确定要用此工作区的成员替换 ${count} 个工作区的成员吗?`,
                 }),
             },
@@ -9533,12 +9532,12 @@ ${reportName}`,
             return `将“${categoryName}”类别的分项收据更改为 ${newValue}(之前为 ${oldValue})`;
         },
         setCategoryName: (oldName: string, newName: string) => `已将类别“${oldName}”重命名为“${newName}”`,
-        setCategoryVisibility: (members: string, categories: string) => `已将${members}设置为仅可查看${categories}`,
-        changedCategoryVisibility: (members: string, categories: string) => `已将${members}更改为仅可查看${categories}`,
-        clearedCategoryVisibility: (members: string) => `已将${members}更改为可查看所有类别`,
+        setCategoryVisibility: (members: string, categories: string) => `将 ${members} 设置为只能看到(使用)${categories}`,
+        changedCategoryVisibility: (members: string, categories: string) => `已更改${members}为仅能看到${categories}`,
+        clearedCategoryVisibility: (members: string) => `已更改${members}为可查看所有类别`,
         quotedCategoryName: (categoryName: string) => `“${categoryName}”`,
-        oneCategory: (categoryName: string) => `类别${categoryName}`,
-        someCategories: (categoryList: string) => `类别${categoryList}`,
+        oneCategory: (categoryName: string) => `${categoryName} 类别`,
+        someCategories: (categoryList: string) => `${categoryList} 个类别`,
         everyone: '所有人',
         categoryCount: ({count}: {count: number}) => ({
             one: '1 个类别',
@@ -9550,8 +9549,8 @@ ${reportName}`,
         }),
         categoryVisibilityCategoriesList: (categoryList: string) => `类别:${categoryList}`,
         categoryVisibilityMembersList: (memberList: string) => `成员:${memberList}`,
-        categoryVisibilityReasonDeleted: ',因为其唯一的类别已被删除。如有需要,请更新其类别可见性。',
-        categoryVisibilityReasonDisabled: ',因为其唯一的类别已被禁用。如有需要,请更新其类别可见性。',
+        categoryVisibilityReasonDeleted: ',因为他们唯一的类别已被删除。如有需要,请更新其类别可见性。',
+        categoryVisibilityReasonDisabled: ',因为他们唯一的类别已被禁用。如有需要,请更新其类别可见性。',
         updatedDescriptionHint: (categoryName: string, newValue?: string, oldValue?: string) => {
             if (!newValue) {
                 return `已从类别“${categoryName}”中移除描述提示“${oldValue}”`;

Note

You can apply these changes to your branch by copying the patch to your clipboard, then running pbpaste | git apply 😉

View workflow run

@MelvinBot

MelvinBot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Web test steps verification results: ⚠️ partial

The #admins room and its LHN entry rendered on dev web with no visible errors. I couldn't test the new POLICYCHANGELOG_SET_CATEGORY_VISIBILITY message itself: the backend doesn't send it yet, and the web test tool can't inject a mock action into Onyx. The PR's Tests section lists only the JS console check, so the rendering of the new message is still untested.
#admins room open with LHN entry visible, no errors shown

Step Status
Open the workspace #admins room and its LHN entry; confirm no errors appear in the JS console (documented step)Finished onboarding, created a workspace, and opened #admins from the LHN. The room showed the standard welcome message with no error banners or blank panes. The test tool can't read the browser console directly. As a proxy, every network call in the flow (OpenReport, CreateWorkspace, GetMissingOnyxMessages, CompleteGuidedSetup, and others) returned 200.
#admins room open with LHN entry visible, no errors shown
✅
Extra: inject a local mock category visibility action (categories "Meals" and "Fuel", reason categoriesDeleted) and confirm the sentence rendersNot verifiable here. The web test tool has no way to run scripts in the page, so it can't write a mock report action to Onyx. No result was faked.
❌

view run · view recording

@MelvinBot

MelvinBot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Android test steps verification results: ✅ pass

The workspace #admins room and its LHN entry render normally on Android with no error banners or crash screens. The new category visibility message itself wasn't tested, because the backend doesn't send that action yet.
Opened #admins room rendering normally with welcome message and composer

Step Status
Open the workspace #admins room from the LHN; confirm it renders with no errors, including in the JS console (documented step)Signed in, finished onboarding, and opened #admins from the auto-created workspace. The LHN entry showed normal preview text with no error icon. The room showed its header, welcome message, and a working composer, with no red box or error banner. JS console logs weren't captured on this run, so this check is based on what showed on screen.
LHN showing the pinned #admins chat entry with no error indicators
Opened #admins room rendering normally with welcome message and composer
✅

view run · view recording

@huult

huult commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot ready for review

@MelvinBot
MelvinBot marked this pull request as ready for review October 30, 2026 08:35
@MelvinBot
MelvinBot requested a review from a team as a code owner October 30, 2026 08:35
@melvin-bot
melvin-bot Bot requested a review from huult October 30, 2026 08:35
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Done. This PR is now marked as ready for review.

The Tests and QA Steps sections still have the // TODO placeholders, and the PR Author Checklist is mostly unchecked. Reviewers will expect you to fill these in.


view run

@melvin-bot
melvin-bot Bot removed the request for review from a team October 30, 2026 08:35
@melvin-bot

melvin-bot Bot commented Oct 30, 2026

Copy link
Copy Markdown

@huult Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot requested review from JmillsExpensify and removed request for a team October 30, 2026 08:35
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T08:41:26.968902Z 76a0e4a Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@huult

huult commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified any copy / text that was added to the app is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If the PR adds or modifies the UI:
    • I asked an AI agent to review the changes for accessibility issues and addressed its findings.
    • I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.
    • I verified all new/changed elements have meaningful accessible names and roles.
    • I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
iOS: mWeb Safari
MacOS: Chrome / Safari

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 76a0e4a973

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/libs/ReportActionsUtils.ts Outdated
const getQuotedCategoryNames = () => categoryNames.map((name) => translate('workspaceActions.quotedCategoryName', getDecodedCategoryName(name)));
const getMemberNames = () => {
const hiddenTranslation = translate('common.hidden');
return accountIDs.map((accountID) => getDisplayNameForParticipant({accountID, formatPhoneNumber, hiddenTranslation}));

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 Subscribe to the affected members' personal details

When a visibility action renders before the target members’ personal details are populated, or when their display names later change, getDisplayNameForParticipant() reads the module-scoped PersonalDetailsStore cache, but PolicyChangeLogContent never subscribes to those account IDs. The row can therefore continue showing Hidden or stale names until an unrelated render occurs; subscribe with usePersonalDetailsByIDs(accountIDs) and pass the selected details into this formatter.

AGENTS.md reference: AGENTS.md:L47-L49

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@MelvinBot review this

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.

Valid finding, fixed in fd5ef92.

PolicyChangeLogContent now subscribes to the members in the action with usePersonalDetailsByIDs(accountIDs) (PolicyChangeLogContent.tsx:334-335). This matches how MemberChangeContent handles targetAccountIDs. The details go through getWorkspaceCategoryUpdateMessage to getDisplayNameForParticipant as personalDetailsData (ReportActionsUtils.ts:3846). The row now re-renders when a member's details load or change, instead of staying on "Hidden" or an old name.

Details
  • The new personalDetails param is optional. Non-React callers (LHN preview, thread title, copy to clipboard) don't pass it and keep using the cache, as before.
  • For other policy change log actions, accountIDs is undefined, so the selector returns an empty map and those rows don't re-render.
  • Added a unit test showing that subscribed details take precedence over the cache.
  • Checks: npm run typecheck passed. ESLint on the changed files passed. The React Compiler check on PolicyChangeLogContent.tsx passed. ReportActionsUtilsTest, PolicyChangeLogContentTest, and ReportActionItemTest passed (1078 tests). The App's inline code review found no rule violations.

view run

@huult

huult commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot Please add SET_CATEGORY_VISIBILITY to SYSTEM_POLICY_CHANGE_LOG_ACTION_TYPES in ReportActionsUtils.ts (the other category actions are listed there, and its comment says new policy actions must opt in), and fill in the Tests/QA sections.

Co-authored-by: huult <huult@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team as a code owner October 30, 2026 08:54
@melvin-bot
melvin-bot Bot requested review from chiragsalian and removed request for a team October 30, 2026 08:54
@melvin-bot

melvin-bot Bot commented Oct 30, 2026

Copy link
Copy Markdown

@chiragsalian Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@huult

huult commented Oct 30, 2026

Copy link
Copy Markdown
Contributor
Screenshot 2026-10-30 at 16 01 56
   await Onyx.merge('personalDetailsList', {
     900001: {accountID: 900001, displayName: 'Bob Sterling', login: 'bob@example.com'},
     900002: {accountID: 900002, displayName: 'Cathy Langford', login: 'cathy@example.com'},
   });
   const mk = (n, om) => ({[`91000000000000${n}`]: {reportActionID: `91000000000000${n}`, actionName: 'POLICYCHANGELOG_SET_CATEGORY_VISIBILITY', actorAccountID: <your accountID>, created: `2026-10-30 09:0${n}:00.000`, shouldShow: true, avatar: '', person: [{type: 'TEXT', style: 'strong', text: 'Admin'}], message: [{type: 'COMMENT', html: '', text: ''}], originalMessage: om}});
   await Onyx.merge('reportActions_REPORT_ID', {
     ...mk(1, {categoryNames: ['Meals', 'Fuel'], accountIDs: [900001], hadRestriction: false, reason: 'setByAdmin'}),
     ...mk(2, {categoryNames: ['Meals'], accountIDs: [900001], hadRestriction: true, reason: 'setByAdmin'}),
     ...mk(3, {categoryNames: [], accountIDs: [900001], hadRestriction: true, reason: 'setByAdmin'}),
     ...mk(4, {categoryNames: ['Meals', 'Fuel'], accountIDs: [], hadRestriction: false, reason: 'setByAdmin'}),
     ...mk(5, {categoryNames: [], accountIDs: [900001], hadRestriction: true, reason: 'categoriesDeleted'}),
     ...mk(6, {categoryNames: [], accountIDs: [900001], hadRestriction: true, reason: 'categoriesDisabled'}),
   });

Result using mock data

Co-authored-by: huult <huult@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done. I added SET_CATEGORY_VISIBILITY to SYSTEM_POLICY_CHANGE_LOG_ACTION_TYPES and filled in the Tests, Offline tests, and QA sections.

  • Code: src/libs/ReportActionsUtils.ts:268. I also added the action to the passive audit cases in tests/unit/SystemMessageDisplayStateTest.ts:81.
  • Checks: typecheck and lint passed. SystemMessageDisplayStateTest and ReportActionsUtilsTest passed (669 tests).
  • Tests/QA: the steps cover every message variant, the full lists in the thread, the LHN preview, copy to clipboard, collapsing, translation, and offline.

Nobody has run these steps yet. They need the backend to post this action, so please run them and add screenshots once it does.


view run

@codecov

codecov Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/CONST/index.ts 95.96% <ø> (ø)
src/libs/ReportActionsUtils.ts 85.59% <100.00%> (+0.23%) ⬆️
src/pages/inbox/report/ReportActionItem.tsx 91.11% <ø> (ø)
...nbox/report/actionContents/ActionContentRouter.tsx 87.25% <100.00%> (+0.10%) ⬆️
...x/report/actionContents/PolicyChangeLogContent.tsx 100.00% <100.00%> (ø)
... and 7 files with indirect coverage changes

@MelvinBot

Copy link
Copy Markdown
Contributor Author

The failing check is unrelated to this PR.

Analysis: The only real failure is validate-perf-tests, and it is flaky. The [ReportActionsList] should re-render when only a heartbeat field of the report changes measurement went over the zero-deviation limit by 0.6 renders, which is an average across runs. Please rerun the failed job. I wasn't able to trigger the rerun from here.

Check independent approval will pass once the PR gets an independent approval.

Evidence
  • Latest commit: 61ee4c56 only adds SET_CATEGORY_VISIBILITY to SYSTEM_POLICY_CHANGE_LOG_ACTION_TYPES in src/libs/ReportActionsUtils.ts, plus a unit test. That change doesn't touch how ReportActionsList renders.
  • Previous commit: Reassure passed on the previous head, fd5ef92f.
  • Other PRs: In the last hour, this same [ReportActionsList] validation failed with fractional render-count differences (0.6–0.8) on several unrelated PRs. Examples include krishna2323/issue-101117, krishna2323/issue/105288-columns-search, fix/no-cycle-part21-small-leaf-cuts and fix/103234.
  • Other checks: All Jest unit test jobs, ESLint, typecheck, Oxfmt and spellcheck pass on 61ee4c56.
  • Failed run: Reassure Performance Tests run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

The failing validate-perf-tests check is a known flaky test, not a problem with this PR. I didn't push any changes.

What to do: Re-run all jobs in the Reassure Performance Tests run. Re-running only the failed job reuses the same results and fails again. You can also wait for [No QA] Mock fetch in ReportActionsListRowWrites perf test to merge. That PR fixes the flaky test suite.

Check independent approval is also failing. It will pass once the PR has an independent approval.

Evidence
  • This PR: [ReportActionsList] should re-render when only a heartbeat field of the report changes failed by 0.6 renders. The allowed difference is 0.
  • Other unrelated PRs today: different tests in the same suite failed the same way. For example, "report name changes" was off by 0.7, "unrelated report is written" by 0.8 and "unrelated transaction is written" by 0.6.
  • Why it's noise: the differences are fractional averages, which points to nondeterministic renders rather than a real extra render. A different test in the suite fails each time.
  • Root cause: the ReportActionsListRowWrites perf test doesn't mock fetch, so stray API calls make render counts unstable. The PR linked above fixes this.

view run

@JmillsExpensify JmillsExpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All the cases LGTM so going to approve

@MelvinBot

Copy link
Copy Markdown
Contributor Author

The failing check is unrelated to this PR, so I didn't push a fix.

Analysis: Reassure Performance Tests / validate-perf-tests failed because [ReportActionsList] should re-render when only a heartbeat field of the report changes averaged 27.6 renders against a baseline of 27. The check allows a deviation of 0. This test group is flaky:

Evidence: This PR only changes how POLICYCHANGELOG_SET_CATEGORY_VISIBILITY actions are rendered (PolicyChangeLogContent, ActionContentRouter, and one prop in ReportActionItem). It doesn't touch ReportActionsList or its Onyx subscriptions. Re-running the Reassure workflow should clear the failure.

The other failing check, PR Reviewer Checklist / checklist, passes once a reviewer completes the checklist.


view run

@chiragsalian
chiragsalian requested a review from daledah October 30, 2026 17:56
@daledah

daledah commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

The code looks good overall, but it seems the translations are still MelvinBot's first pass and the Polyglot Parrot translations haven't been applied yet. @MelvinBot please update the translations to match the diff in this comment: #105465 (comment)

@MelvinBot

Copy link
Copy Markdown
Contributor Author

You're right: the branch still has my first-pass translations. For example, src/languages/de.ts line 9976 still has the old setCategoryVisibility string. The Polyglot Parrot diff covers 10 locale files, and none of it is applied yet.

Before applying the diff as-is, note that a few Parrot strings look worse than the current ones:

  • de setCategoryVisibility: "Legen Sie fest, dass…" is an imperative ("Set…"), not a past-tense log entry.
  • zh-hans someCategories: ${categoryList} 个类别 treats the list as a count, so it reads "Meals, Fuel (number of) categories".

I can only make code changes when directed by the PR author, an assignee, or an existing co-author. To have me implement this, the PR assignee or author should reply with a request tagging me.

@huult, should I apply the Polyglot Parrot translations?


view run

@daledah

daledah commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

@huult could you please check this comment?

@huult

huult commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot, please apply the suggested changes above.

Co-authored-by: huult <huult@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I applied the Polyglot Parrot translations in d3244a8. I kept the current string only where the Parrot version breaks the sentence, as listed below.

Strings I kept unchanged, and why
  • setCategoryVisibility, all 10 locales: Parrot turned the past-tense log entry into a command or infinitive, for example de "Legen Sie fest…", fr "définir…", and pt-BR "definir…".
  • someCategories, zh-hans and ja: 个类别 and 件のカテゴリー are counters, so the category list reads as a number.
  • someCategories, fr: "les « A » et « B » catégories" puts the names in an unnatural position. I kept "les catégories …".
  • el oneCategory, someCategories, and changedCategoryVisibility: Parrot uses the nominative case where the object case is needed. It also adds a second article, which would read "τις την κατηγορία…".
  • pl oneCategory and categoryCount/memberCount: Parrot uses the nominative "kategoria" where "kategorię" is needed. It also drops the few/many plural forms, so 14 would read "14 kategorie" instead of "14 kategorii".
  • ja categoryVisibilityReasonDeleted: the string starts with "は、", which is ungrammatical after "…しました".
  • es: I applied only the two reason strings. The other es changes added : string types, which es.ts doesn't use. Parrot also dropped the personal "a" before ${members}.
  • ja/zh-hans "Copy members" comments: Parrot deleted two code comments that this PR doesn't touch, so I left them in place.

I also switched fr qu'ils to the curly qu’ils, which fr.ts uses in most places.

Checks: typecheck passed, and TranslateTest, LocalizeTests, and ReportActionsUtilsTest passed (563 tests). ESLint doesn't apply because its config ignores the locale files. I couldn't run the oxfmt check here, but the edits only change string contents.


view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

The failing check is unrelated to this PR. No code fix is needed. Please re-run the validate-perf-tests job.

Analysis: validate-perf-tests failed on [ReportActionsList] should re-render when an unrelated report is written. The render count difference was 0.6 against an allowed deviation of 0. This test suite is flaky right now:

  • In the last day, several unrelated PRs failed tests in this same file with fractional render-count differences of 0.6–1. Examples include rushat/concierge-ask-fixes, rory-singleton-mini-context-menu, carlos-billpay-existing-bills, krishna2323/105322-105503-submission-followups, and claude-createCategoryFromSearch.
  • A fractional average difference means only some measurement runs rendered once more. A deterministic regression would shift every run.

Evidence: The test writes only to an unrelated report_ key. This PR adds a personal details lookup and passes a boolean prop into PolicyChangeLogContent. Neither change reacts to writes on other reports.

The other failing checks (checklist and Check independent approval) are review checklists, not code failures.

@huult

huult commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot could you sync with main?

…lityChangeLogs

Co-authored-by: huult <huult@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Synced with main. The merge had no conflicts and is pushed as 20e752d.


view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

⚠️ MelvinBot CI Fix Loop Detected

MelvinBot has made 5 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR.

Next steps: A human should review the recent commits and resolve the conflicting CI errors manually.

@daledah

daledah commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

@MelvinBot @huult Please add [HOLD BE] to the title since we need the BE to be deployed before we can test this properly

@MelvinBot MelvinBot changed the title Show category visibility change logs in #admins [HOLD BE] Show category visibility change logs in #admins Nov 3, 2026
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Done. The title is now [HOLD BE] Show category visibility change logs in #admins.


view run

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants