From d41b16267715ba5c97d27b44b7c84f6a6873768d Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:45:01 +0530 Subject: [PATCH 01/41] feat: add Guest workspace role --- src/CONST/index.ts | 2 ++ src/components/RoomHeaderAvatars.tsx | 6 ++++- src/components/WorkspaceMemberRoleList.tsx | 7 ++++++ src/languages/de.ts | 7 ++++++ src/languages/en.ts | 7 ++++++ src/languages/es.ts | 7 ++++++ src/languages/fr.ts | 7 ++++++ src/languages/it.ts | 7 ++++++ src/languages/ja.ts | 7 ++++++ src/languages/nl.ts | 7 ++++++ src/languages/pl.ts | 7 ++++++ src/languages/pt-BR.ts | 7 ++++++ src/languages/zh-hans.ts | 7 ++++++ src/libs/Fullstory/types.ts | 2 +- src/libs/Fullstory/utils.ts | 4 ++- src/libs/PolicyUtils.ts | 15 ++++++++--- src/libs/telemetry/TelemetrySynchronizer.ts | 4 ++- src/pages/DynamicReportDetailsPage.tsx | 10 ++++++-- src/pages/DynamicReportParticipantsPage.tsx | 8 ++++-- src/pages/workspace/MemberRightIcon.tsx | 1 + src/pages/workspace/WorkspaceInitialPage.tsx | 4 ++- src/pages/workspace/WorkspaceMembersPage.tsx | 17 +++++++++++++ .../workspace/members/ImportedMembersPage.tsx | 2 +- .../workspace/rooms/WorkspaceRoomsPage.tsx | 7 ++++-- tests/actions/PolicyMemberTest.ts | 25 +++++++++++++++++++ tests/ui/WorkspaceMembersTest.tsx | 5 ++++ tests/unit/FullstoryUtilsTest.ts | 19 ++++++++++++++ tests/unit/PolicyUtilsTest.ts | 20 ++++++++++++++- 28 files changed, 212 insertions(+), 16 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 23bf9c07d228..6c9df906a940 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3996,6 +3996,7 @@ const CONST = { OWNER: 'owner', ADMIN: 'admin', AUDITOR: 'auditor', + GUEST: 'guest', USER: 'user', EDITOR: 'editor', CARD_ADMIN: 'cardAdmin', @@ -4111,6 +4112,7 @@ const CONST = { MAKE_MEMBER: 'makeMember', MAKE_ADMIN: 'makeAdmin', MAKE_AUDITOR: 'makeAuditor', + MAKE_GUEST: 'makeGuest', MAKE_CARD_ADMIN: 'makeCardAdmin', MAKE_PEOPLE_ADMIN: 'makePeopleAdmin', MAKE_PAYMENTS_ADMIN: 'makePaymentsAdmin', diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index a1854b66487b..2235697a19b1 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -5,7 +5,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {clearAvatarErrors, updatePolicyRoomAvatar} from '@libs/actions/Report'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; -import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; +import {isAnnounceRoom, isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {isDefaultAvatar} from '@libs/UserAvatarUtils'; import variables from '@styles/variables'; @@ -50,6 +50,10 @@ function RoomHeaderAvatars({icons, report, policy, participants, currentUserAcco const StyleUtils = useStyleUtils(); const canEditRoomAvatar = isUserCreatedPolicyRoom(report) && participants.includes(currentUserAccountID) && !!policy && policy.role !== CONST.POLICY.ROLE.AUDITOR; + if (policy?.role === CONST.POLICY.ROLE.GUEST && isAnnounceRoom(report)) { + return null; + } + if (!icons.length) { return null; } diff --git a/src/components/WorkspaceMemberRoleList.tsx b/src/components/WorkspaceMemberRoleList.tsx index c27b37694980..de250b33f5ff 100644 --- a/src/components/WorkspaceMemberRoleList.tsx +++ b/src/components/WorkspaceMemberRoleList.tsx @@ -56,6 +56,13 @@ function WorkspaceMemberRoleList({role, policy, navigateBackTo = undefined, isLo isSelected: role === CONST.POLICY.ROLE.AUDITOR, keyForList: CONST.POLICY.ROLE.AUDITOR, }, + { + value: CONST.POLICY.ROLE.GUEST, + text: translate('workspace.common.roleName', CONST.POLICY.ROLE.GUEST), + alternateText: translate('workspace.common.guestAlternateText'), + isSelected: role === CONST.POLICY.ROLE.GUEST, + keyForList: CONST.POLICY.ROLE.GUEST, + }, { value: CONST.POLICY.ROLE.CARD_ADMIN, text: translate('workspace.common.roleName', CONST.POLICY.ROLE.CARD_ADMIN), diff --git a/src/languages/de.ts b/src/languages/de.ts index 7985462fac08..fb79634c0bc7 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4632,6 +4632,7 @@ ${amount} für ${merchant} – ${date}`, memberAlternateText: 'Berichte einreichen und freigeben.', adminAlternateText: 'Berichte und Arbeitsbereichseinstellungen verwalten.', auditorAlternateText: 'Berichte anzeigen und kommentieren.', + guestAlternateText: 'Berichte einreichen und genehmigen, mit Chats nur auf Einladung.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4640,6 +4641,8 @@ ${amount} für ${merchant} – ${date}`, return 'Workspace-Administrator'; case CONST.POLICY.ROLE.AUDITOR: return 'Prüfer'; + case CONST.POLICY.ROLE.GUEST: + return 'Gast'; case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: @@ -6645,6 +6648,10 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, one: 'Zum Prüfer machen', other: 'Prüfende hinzufügen', }), + makeGuest: () => ({ + one: 'Zum Gast machen', + other: 'Zu Gästen machen', + }), makePeopleAdmin: () => ({ one: 'Zum Personaladministrator machen', other: 'Personaladministratoren festlegen', diff --git a/src/languages/en.ts b/src/languages/en.ts index 076f253dd641..e69ea362d0b8 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4745,6 +4745,7 @@ const translations = { memberAlternateText: 'Submit and approve reports.', adminAlternateText: 'Manage reports and workspace settings.', auditorAlternateText: 'View and comment on reports.', + guestAlternateText: 'Submit and approve reports, with invite-only chats.', cardAdminAlternateText: 'Manage workspace cards.', peopleAdminAlternateText: 'Manage members and approval workflows.', paymentsAdminAlternateText: 'Manage workflow payments.', @@ -4761,6 +4762,8 @@ const translations = { return 'Workspace admin'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; + case CONST.POLICY.ROLE.GUEST: + return 'Guest'; case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: @@ -6743,6 +6746,10 @@ const translations = { one: 'Make auditor', other: 'Make auditors', }), + makeGuest: () => ({ + one: 'Make guest', + other: 'Make guests', + }), makeCardAdmin: () => ({ one: 'Make card admin', other: 'Make card admins', diff --git a/src/languages/es.ts b/src/languages/es.ts index 8aafd38e5a39..68bd8bba5f83 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4518,6 +4518,7 @@ ${amount} para ${merchant} - ${date}`, memberAlternateText: 'Presentar y aprobar informes.', adminAlternateText: 'Gestionar informes y configuración del área de trabajo.', auditorAlternateText: 'Ver y comentar los informes.', + guestAlternateText: 'Enviar y aprobar informes, con chats solo por invitación.', reimbursementChoice: { [CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES]: 'Directo', [CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO]: 'Ninguno', @@ -4531,6 +4532,8 @@ ${amount} para ${merchant} - ${date}`, return 'Administrador del espacio de trabajo'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; + case CONST.POLICY.ROLE.GUEST: + return 'Invitado'; case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: @@ -6484,6 +6487,10 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, one: 'Convertir en auditor', other: 'Convertir en auditores', }), + makeGuest: () => ({ + one: 'Convertir en invitado', + other: 'Convertir en invitados', + }), makePeopleAdmin: () => ({ one: 'Hacer administrador de personas', other: 'Hacer administradores de personas', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 03fc902ca84c..5fedfc12d0bb 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4641,6 +4641,7 @@ ${amount} pour ${merchant} - ${date}`, memberAlternateText: 'Soumettre et approuver des notes de frais.', adminAlternateText: 'Gérer les notes de frais et les paramètres de l’espace de travail.', auditorAlternateText: 'Afficher et commenter les notes de frais.', + guestAlternateText: 'Envoyer et approuver des rapports, avec des discussions sur invitation uniquement.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4649,6 +4650,8 @@ ${amount} pour ${merchant} - ${date}`, return 'Administrateur de l’espace de travail'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditeur'; + case CONST.POLICY.ROLE.GUEST: + return 'Invité'; case CONST.POLICY.ROLE.EDITOR: return 'Éditeur'; case CONST.POLICY.ROLE.CARD_ADMIN: @@ -6670,6 +6673,10 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, one: 'Nommer auditeur', other: 'Créer des auditeurs', }), + makeGuest: () => ({ + one: 'Nommer invité', + other: 'Nommer invités', + }), makePeopleAdmin: () => ({ one: 'Nommer administrateur des personnes', other: 'Nommer des administrateurs des personnes', diff --git a/src/languages/it.ts b/src/languages/it.ts index d5d143f1be5f..3cfdeca61844 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4607,6 +4607,7 @@ ${amount} per ${merchant} - ${date}`, memberAlternateText: 'Invia e approva i report.', adminAlternateText: 'Gestisci i report e le impostazioni dello spazio di lavoro.', auditorAlternateText: 'Visualizza e commenta i report.', + guestAlternateText: 'Invia e approva i report, con chat solo su invito.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4615,6 +4616,8 @@ ${amount} per ${merchant} - ${date}`, return 'Amministratore spazio di lavoro'; case CONST.POLICY.ROLE.AUDITOR: return 'Revisore'; + case CONST.POLICY.ROLE.GUEST: + return 'Ospite'; case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: @@ -6622,6 +6625,10 @@ Il piano Control parte da 9 $ al mese per ogni membro attivo.`, one: 'Rendi revisore', other: 'Crea revisori', }), + makeGuest: () => ({ + one: 'Rendi ospite', + other: 'Rendi ospiti', + }), makePeopleAdmin: () => ({ one: 'Rendi amministratore persone', other: 'Rendi amministratori persone', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 22bdedf0bf98..943a63122639 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4571,6 +4571,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' memberAlternateText: 'レポートを提出して承認します。', adminAlternateText: 'レポートとワークスペースの設定を管理します。', auditorAlternateText: 'レポートを表示してコメントします。', + guestAlternateText: 'レポートを提出および承認し、招待されたチャットのみを使用します。', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4579,6 +4580,8 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' return 'ワークスペース管理者'; case CONST.POLICY.ROLE.AUDITOR: return '監査担当者'; + case CONST.POLICY.ROLE.GUEST: + return 'ゲスト'; case CONST.POLICY.ROLE.EDITOR: return '編集者'; case CONST.POLICY.ROLE.CARD_ADMIN: @@ -6540,6 +6543,10 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から one: '監査担当者に設定', other: '監査担当者を作成', }), + makeGuest: () => ({ + one: 'ゲストにする', + other: 'ゲストにする', + }), makePeopleAdmin: () => ({ one: 'People 管理者にする', other: 'People 管理者にする', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 2b7e77d15664..411c4a29153c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4606,6 +4606,7 @@ ${amount} voor ${merchant} - ${date}`, memberAlternateText: 'Dien rapporten in en keur ze goed.', adminAlternateText: 'Beheer rapporten en werkruimte-instellingen.', auditorAlternateText: 'Rapporten bekijken en erop reageren.', + guestAlternateText: 'Rapporten indienen en goedkeuren, met chats alleen op uitnodiging.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4614,6 +4615,8 @@ ${amount} voor ${merchant} - ${date}`, return 'Beheerder van werkruimte'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; + case CONST.POLICY.ROLE.GUEST: + return 'Gast'; case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: @@ -6608,6 +6611,10 @@ Het Control-abonnement begint bij $9 per actieve deelnemer per maand.`, one: 'Auditeur maken', other: 'Auditors aanmaken', }), + makeGuest: () => ({ + one: 'Gast maken', + other: 'Gasten maken', + }), makePeopleAdmin: () => ({ one: 'People-beheerder maken', other: 'People-beheerders maken', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index bb4db669b8d7..5c3e99af908a 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4591,6 +4591,7 @@ ${amount} dla ${merchant} - ${date}`, memberAlternateText: 'Przesyłaj i zatwierdzaj raporty.', adminAlternateText: 'Zarządzaj raportami i ustawieniami przestrzeni roboczej.', auditorAlternateText: 'Przeglądaj i komentuj raporty.', + guestAlternateText: 'Przesyłaj i zatwierdzaj raporty, korzystając tylko z czatów na zaproszenie.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4599,6 +4600,8 @@ ${amount} dla ${merchant} - ${date}`, return 'Administrator przestrzeni roboczej'; case CONST.POLICY.ROLE.AUDITOR: return 'Audytor'; + case CONST.POLICY.ROLE.GUEST: + return 'Gość'; case CONST.POLICY.ROLE.EDITOR: return 'Edytor'; case CONST.POLICY.ROLE.CARD_ADMIN: @@ -6588,6 +6591,10 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, one: 'Ustaw jako audytora', other: 'Utwórz audytorów', }), + makeGuest: () => ({ + one: 'Ustaw jako gościa', + other: 'Ustaw jako gości', + }), makePeopleAdmin: () => ({ one: 'Ustaw jako administratora osób', other: 'Ustaw administratorów osób', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 550ac8da144d..c7170083b32b 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4600,6 +4600,7 @@ ${amount} para ${merchant} - ${date}`, memberAlternateText: 'Enviar e aprovar relatórios.', adminAlternateText: 'Gerencie relatórios e configurações do espaço de trabalho.', auditorAlternateText: 'Visualize e comente relatórios.', + guestAlternateText: 'Envie e aprove relatórios, com chats somente por convite.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4608,6 +4609,8 @@ ${amount} para ${merchant} - ${date}`, return 'Administrador da área de trabalho'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; + case CONST.POLICY.ROLE.GUEST: + return 'Convidado'; case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: @@ -6604,6 +6607,10 @@ O plano Control começa em US$ 9 por membro ativo por mês.`, one: 'Tornar auditor', other: 'Tornar auditores', }), + makeGuest: () => ({ + one: 'Tornar convidado', + other: 'Tornar convidados', + }), makePeopleAdmin: () => ({ one: 'Tornar administrador de pessoas', other: 'Tornar administradores de pessoas', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index bd140d2ad548..3384aad31176 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4469,6 +4469,7 @@ ${amount},商户:${merchant} - 日期:${date}`, memberAlternateText: '提交并审批报销报告。', adminAlternateText: '管理报表和工作区设置。', auditorAlternateText: '查看并评论报表。', + guestAlternateText: '提交和批准报表,仅使用受邀聊天。', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4477,6 +4478,8 @@ ${amount},商户:${merchant} - 日期:${date}`, return '工作区管理员'; case CONST.POLICY.ROLE.AUDITOR: return '审计员'; + case CONST.POLICY.ROLE.GUEST: + return '访客'; case CONST.POLICY.ROLE.EDITOR: return '编辑者'; case CONST.POLICY.ROLE.CARD_ADMIN: @@ -6392,6 +6395,10 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM one: '设为审计员', other: '创建审计员', }), + makeGuest: () => ({ + one: '设为访客', + other: '设为访客', + }), makePeopleAdmin: () => ({ one: '设为人员管理员', other: '设为人员管理员', diff --git a/src/libs/Fullstory/types.ts b/src/libs/Fullstory/types.ts index b003f93cae57..489e98045a9d 100644 --- a/src/libs/Fullstory/types.ts +++ b/src/libs/Fullstory/types.ts @@ -15,7 +15,7 @@ type FullstoryUserVars = { user_status?: 'new' | 'returning'; has_completed_onboarding?: boolean; onb_step?: 'registration' | 'accounting' | 'completed'; - user_role?: 'admin' | 'auditor' | 'member'; + user_role?: 'admin' | 'auditor' | 'member' | 'guest'; workspace_state?: 'has_workspaces' | 'no_workspaces'; workspace_count?: number; workspace_member_count?: number; diff --git a/src/libs/Fullstory/utils.ts b/src/libs/Fullstory/utils.ts index bb58c016f19c..92f4e72ffd68 100644 --- a/src/libs/Fullstory/utils.ts +++ b/src/libs/Fullstory/utils.ts @@ -142,7 +142,7 @@ function getOnboardingStep(onboardingPath: string | undefined, hasCompletedOnboa } function getUserRole(activePolicies: Policy[]): FullstoryUserVars['user_role'] { - let userRole: FullstoryUserVars['user_role'] = 'member'; + let userRole: FullstoryUserVars['user_role'] = activePolicies.some((policy) => policy?.role === CONST.POLICY.ROLE.GUEST) ? 'guest' : 'member'; for (const policy of activePolicies) { if (policy?.role === CONST.POLICY.ROLE.ADMIN) { @@ -150,6 +150,8 @@ function getUserRole(activePolicies: Policy[]): FullstoryUserVars['user_role'] { } if (policy?.role === CONST.POLICY.ROLE.AUDITOR) { userRole = 'auditor'; + } else if (userRole !== 'auditor' && policy?.role !== CONST.POLICY.ROLE.GUEST) { + userRole = 'member'; } } diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index df02377f9f98..8a8d15bf9bf3 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -155,6 +155,9 @@ const ROLE_PERMISSION_BUNDLES: Record controlPolicyOnlyRole === role); @@ -218,10 +221,10 @@ function canMemberAssignRole(policy: OnyxInputOrEntry, login: string, ro return true; } - // Reaching here: USER always, plus AUDITOR only on corporate policies (control-only roles are + // Reaching here: USER always, plus GUEST/AUDITOR only on corporate policies (control-only roles are // already filtered out on non-corporate policies above). Assigning USER/AUDITOR needs the // MEMBERS permission, and only on corporate policies. - const isNonElevatedRole = role === CONST.POLICY.ROLE.USER || role === CONST.POLICY.ROLE.AUDITOR; + const isNonElevatedRole = role === CONST.POLICY.ROLE.USER || role === CONST.POLICY.ROLE.GUEST || role === CONST.POLICY.ROLE.AUDITOR; return isCorporatePolicy && canMemberWrite(policy, login, CONST.POLICY.POLICY_FEATURE.MEMBERS) && isNonElevatedRole; } @@ -782,6 +785,11 @@ function createInvoiceConfigurationTextSelector(translate: LocaleContextProps['t */ const isPolicyUser = (policy: OnyxInputOrEntry, currentUserLogin?: string): boolean => getPolicyRole(policy, currentUserLogin) === CONST.POLICY.ROLE.USER; +/** + * Checks if the current user is a guest of the policy. + */ +const isPolicyGuest = (policy: OnyxInputOrEntry, currentUserLogin?: string): boolean => getPolicyRole(policy, currentUserLogin) === CONST.POLICY.ROLE.GUEST; + /** * Checks if the current user is an auditor of the policy */ @@ -3049,6 +3057,7 @@ export { isPendingDeletePolicy, isPolicyAdmin, isPolicyUser, + isPolicyGuest, isPolicyAuditor, hasEligibleBankAccountShareRecipient, isPolicyEmployee, diff --git a/src/libs/telemetry/TelemetrySynchronizer.ts b/src/libs/telemetry/TelemetrySynchronizer.ts index 04f226ef6a47..160bdbc7811a 100644 --- a/src/libs/telemetry/TelemetrySynchronizer.ts +++ b/src/libs/telemetry/TelemetrySynchronizer.ts @@ -145,7 +145,7 @@ function sendPoliciesContext() { } const activePolicies = getActivePolicies(policies, session.email).map((policy) => policy.id); - let userRole: string = CONST.POLICY.ROLE.USER; + let userRole: string = Object.values(policies).some((policy) => policy?.role === CONST.POLICY.ROLE.GUEST) ? CONST.POLICY.ROLE.GUEST : CONST.POLICY.ROLE.USER; for (const policy of Object.values(policies)) { if (policy?.role === CONST.POLICY.ROLE.ADMIN) { userRole = CONST.POLICY.ROLE.ADMIN; @@ -153,6 +153,8 @@ function sendPoliciesContext() { } if (policy?.role === CONST.POLICY.ROLE.AUDITOR) { userRole = CONST.POLICY.ROLE.AUDITOR; + } else if (userRole !== CONST.POLICY.ROLE.AUDITOR && policy?.role !== CONST.POLICY.ROLE.GUEST) { + userRole = CONST.POLICY.ROLE.USER; } } diff --git a/src/pages/DynamicReportDetailsPage.tsx b/src/pages/DynamicReportDetailsPage.tsx index 2bb450172516..de09adfaac68 100644 --- a/src/pages/DynamicReportDetailsPage.tsx +++ b/src/pages/DynamicReportDetailsPage.tsx @@ -50,7 +50,7 @@ import TransitionTracker from '@libs/Navigation/TransitionTracker'; import type {ReportDetailsNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types'; import Parser from '@libs/Parser'; import Permissions from '@libs/Permissions'; -import {isPolicyAdmin as isPolicyAdminUtil, isPolicyEmployee as isPolicyEmployeeUtil, shouldShowPolicy} from '@libs/PolicyUtils'; +import {isPolicyAdmin as isPolicyAdminUtil, isPolicyEmployee as isPolicyEmployeeUtil, isPolicyGuest, shouldShowPolicy} from '@libs/PolicyUtils'; import {getOneTransactionThreadReportID, getOriginalMessage, getTrackExpenseActionableWhisper, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; import {deprecatedGetReportName} from '@libs/ReportNameUtils'; import { @@ -73,6 +73,7 @@ import { getReportDescription, getReportFieldKey, getReportForHeader, + isAnnounceRoom, isArchivedNonExpenseReport, isCanceledTaskReport as isCanceledTaskReportUtil, isChatRoom as isChatRoomUtil, @@ -241,6 +242,7 @@ function DynamicReportDetailsPage({policy, report, route, reportMetadata, report const isChatRoom = useMemo(() => isChatRoomUtil(report), [report]); const isUserCreatedPolicyRoom = useMemo(() => isUserCreatedPolicyRoomUtil(report), [report]); const isDefaultRoom = useMemo(() => isDefaultRoomUtil(report), [report]); + const isGuestAnnounceRoom = isPolicyGuest(policy) && isAnnounceRoom(report); const isChatThread = useMemo(() => isChatThreadUtil(report), [report]); const isMoneyRequestReport = useMemo(() => isMoneyRequestReportUtil(report), [report]); const isMoneyRequest = useMemo(() => isMoneyRequestUtil(report), [report]); @@ -465,6 +467,7 @@ function DynamicReportDetailsPage({policy, report, route, reportMetadata, report // - The report is not a user created room with participants to show i.e. DM, Group Chat, etc // - The report is a user created room and the room and the current user is a workspace member i.e. non-workspace members should not see this option. if ( + !isGuestAnnounceRoom && (isGroupChat || (isDefaultRoom && isChatThread && isPolicyEmployee) || (!isUserCreatedPolicyRoom && participants.length) || @@ -489,7 +492,10 @@ function DynamicReportDetailsPage({policy, report, route, reportMetadata, report } }, }); - } else if ((isUserCreatedPolicyRoom && (!participants.length || !isPolicyEmployee)) || ((isDefaultRoom || isPolicyExpenseChat) && isChatThread && !isPolicyEmployee)) { + } else if ( + !isGuestAnnounceRoom && + ((isUserCreatedPolicyRoom && (!participants.length || !isPolicyEmployee)) || ((isDefaultRoom || isPolicyExpenseChat) && isChatThread && !isPolicyEmployee)) + ) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.INVITE, translationKey: 'common.invite', diff --git a/src/pages/DynamicReportParticipantsPage.tsx b/src/pages/DynamicReportParticipantsPage.tsx index 9571f5a15b9f..179faf43ac3b 100755 --- a/src/pages/DynamicReportParticipantsPage.tsx +++ b/src/pages/DynamicReportParticipantsPage.tsx @@ -17,6 +17,7 @@ import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; import useReportAttributes from '@hooks/useReportAttributes'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -30,6 +31,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ParticipantsNavigatorParamList} from '@libs/Navigation/types'; import {temporaryGetDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; +import {isPolicyGuest} from '@libs/PolicyUtils'; import {deprecatedGetReportName} from '@libs/ReportNameUtils'; import { getReportPersonalDetailsParticipants, @@ -72,6 +74,8 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr const {translate, formatPhoneNumber} = useLocalize(); const {showConfirmModal} = useConfirmModal(); const styles = useThemeStyles(); + const policy = usePolicy(report?.policyID); + const isGuestAnnounceRoom = isAnnounceRoom(report) && isPolicyGuest(policy); // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use the selection mode only on small screens // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -114,7 +118,7 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr const activeParticipants = participantsForDisplay.filter((participant) => isOffline || !participant.isPendingDelete); useEffect(() => { - if (!isAnnounceRoom(report)) { + if (!isAnnounceRoom(report) || isGuestAnnounceRoom) { return; } openRoomMembersPage(report.reportID); @@ -233,7 +237,7 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr style={[styles.defaultModalContainer]} testID="DynamicReportParticipantsPage" > - + { diff --git a/src/pages/workspace/MemberRightIcon.tsx b/src/pages/workspace/MemberRightIcon.tsx index e68025c139d1..2b213d65259f 100644 --- a/src/pages/workspace/MemberRightIcon.tsx +++ b/src/pages/workspace/MemberRightIcon.tsx @@ -28,6 +28,7 @@ export default function MemberRightIcon({role, owner, login, badgeStyles}: Membe } else if ( role === CONST.POLICY.ROLE.ADMIN || role === CONST.POLICY.ROLE.AUDITOR || + role === CONST.POLICY.ROLE.GUEST || role === CONST.POLICY.ROLE.CARD_ADMIN || role === CONST.POLICY.ROLE.PEOPLE_ADMIN || role === CONST.POLICY.ROLE.PAYMENTS_ADMIN diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index bedad351f855..19955377d0f6 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -47,6 +47,7 @@ import { isPendingDeletePolicy, isPerDiemEnabled, isPolicyAdmin, + isPolicyGuest, isTimeTrackingEnabled, shouldShowEmployeeListError, shouldShowSyncError, @@ -125,6 +126,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector}); const policy = policyDraft?.id ? policyDraft : policyProp; const policyID = policy?.id; + const isGuest = isPolicyGuest(policy, currentUserLogin ?? ''); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params?.policyID}`); const workspaceAccountID = useWorkspaceAccountID(policyID); @@ -291,7 +293,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac screenName: SCREENS.WORKSPACE.ROOMS, sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.ROOMS, }, - ]; + ].filter((item) => !isGuest || item.screenName === SCREENS.WORKSPACE.PROFILE); if (isGroupPolicy(policy) && shouldShowProtectedItems) { if (canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.REPORT_FIELDS)) { diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index cafa5b984e40..7dad55ba5ddf 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -559,6 +559,12 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers icon: icons.UserEye, onSelected: () => changeUserRole(CONST.POLICY.ROLE.AUDITOR), }; + const guestOption = { + text: translate('workspace.people.makeGuest', {count: selectedEmployees.length}), + value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_GUEST, + icon: icons.User, + onSelected: () => changeUserRole(CONST.POLICY.ROLE.GUEST), + }; const cardAdminOption = { text: translate('workspace.people.makeCardAdmin', {count: selectedEmployees.length}), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_CARD_ADMIN, @@ -579,6 +585,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers }; const hasAtLeastOneNonAuditorRole = selectedEmployeesRoles.some((role) => role !== CONST.POLICY.ROLE.AUDITOR); + const hasAtLeastOneNonGuestRole = selectedEmployeesRoles.some((role) => role !== CONST.POLICY.ROLE.GUEST); const hasAtLeastOneNonCardAdminRole = selectedEmployeesRoles.some((role) => role !== CONST.POLICY.ROLE.CARD_ADMIN); const hasAtLeastOneNonPeopleAdminRole = selectedEmployeesRoles.some((role) => role !== CONST.POLICY.ROLE.PEOPLE_ADMIN); const hasAtLeastOneNonPaymentsAdminRole = selectedEmployeesRoles.some((role) => role !== CONST.POLICY.ROLE.PAYMENTS_ADMIN); @@ -605,6 +612,16 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers options.push(auditorOption); } + if ( + hasAtLeastOneNonGuestRole && + isControlPolicy(policy) && + !hasAtLeastOnePayer && + canManageSelectedEmployees && + canMemberAssignRole(policy, currentUserLogin ?? '', CONST.POLICY.ROLE.GUEST) + ) { + options.push(guestOption); + } + if (hasAtLeastOneNonCardAdminRole && isControlPolicy(policy) && !hasAtLeastOnePayer && canAssignElevatedRoles) { options.push(cardAdminOption); } diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index b4a98aaa5394..3073c445f2dc 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -112,7 +112,7 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { const membersRolesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ROLE); - const controlPolicyOnlyRoles = [CONST.POLICY.ROLE.AUDITOR, CONST.POLICY.ROLE.CARD_ADMIN, CONST.POLICY.ROLE.PEOPLE_ADMIN, CONST.POLICY.ROLE.PAYMENTS_ADMIN]; + const controlPolicyOnlyRoles = [CONST.POLICY.ROLE.AUDITOR, CONST.POLICY.ROLE.GUEST, CONST.POLICY.ROLE.CARD_ADMIN, CONST.POLICY.ROLE.PEOPLE_ADMIN, CONST.POLICY.ROLE.PAYMENTS_ADMIN]; const hasControlPolicyOnlyRole = membersRolesColumn !== -1 && spreadsheet?.data?.at(membersRolesColumn)?.some((role, index) => { diff --git a/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx b/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx index f02ce9d76a92..177f1da12fdd 100644 --- a/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx +++ b/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx @@ -19,7 +19,7 @@ import {openReport} from '@libs/actions/Report'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import {isPolicyAdmin} from '@libs/PolicyUtils'; +import {isPolicyAdmin, isPolicyGuest} from '@libs/PolicyUtils'; import {deprecatedGetReportName} from '@libs/ReportNameUtils'; import {getParticipantsAccountIDsForDisplay} from '@libs/ReportUtils'; @@ -96,7 +96,10 @@ function WorkspaceRoomsPage({route}: WorkspaceRoomsPageProps) { }); return ( - + { }); }); describe('updateWorkspaceMembersRole', () => { + it('updates a member to Guest optimistically', async () => { + const member = createPersonalDetails(2); + const memberLogin = member.login ?? ''; + const policy: PolicyType = { + ...createRandomPolicy(0, CONST.POLICY.TYPE.CORPORATE), + employeeList: { + [memberLogin]: { + email: memberLogin, + role: CONST.POLICY.ROLE.USER, + }, + }, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + mockFetch?.pause?.(); + Member.updateWorkspaceMembersRole(policy, [memberLogin], [member.accountID], CONST.POLICY.ROLE.GUEST); + await waitForBatchedUpdates(); + + const optimisticPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`); + expect(optimisticPolicy?.employeeList?.[memberLogin]?.role).toBe(CONST.POLICY.ROLE.GUEST); + expect(optimisticPolicy?.employeeList?.[memberLogin]?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + + await mockFetch?.resume?.(); + }); + it('Update member to admin role', async () => { const fakeUser2 = createPersonalDetails(2); const fakePolicy: PolicyType = { diff --git a/tests/ui/WorkspaceMembersTest.tsx b/tests/ui/WorkspaceMembersTest.tsx index 2876f57b72bc..32fc30840b79 100644 --- a/tests/ui/WorkspaceMembersTest.tsx +++ b/tests/ui/WorkspaceMembersTest.tsx @@ -169,6 +169,11 @@ describe('WorkspaceMembers', () => { const makeAuditorMenuItem = screen.getByTestId(`PopoverMenuItem-${makeAuditorText}`); expect(makeAuditorMenuItem).toBeOnTheScreen(); + // Find and verify "Make guest" dropdown menu item + const makeGuestText = TestHelper.translateLocal('workspace.people.makeGuest', {count: 1}); + const makeGuestMenuItem = screen.getByTestId(`PopoverMenuItem-${makeGuestText}`); + expect(makeGuestMenuItem).toBeOnTheScreen(); + // Find and verify "Make card admin" dropdown menu item const makeCardAdminText = TestHelper.translateLocal('workspace.people.makeCardAdmin', {count: 1}); const makeCardAdminMenuItem = screen.getByTestId(`PopoverMenuItem-${makeCardAdminText}`); diff --git a/tests/unit/FullstoryUtilsTest.ts b/tests/unit/FullstoryUtilsTest.ts index e58635454a1c..4d50be1a8e94 100644 --- a/tests/unit/FullstoryUtilsTest.ts +++ b/tests/unit/FullstoryUtilsTest.ts @@ -100,4 +100,23 @@ describe('FullstoryUtils', () => { expect(role.user_role).toBe('member'); }); + + it('reports Guest as the lowest workspace role', () => { + const guestPolicy = {...createRandomPolicy(1, CONST.POLICY.TYPE.CORPORATE), role: CONST.POLICY.ROLE.GUEST}; + const auditorPolicy = {...createRandomPolicy(2, CONST.POLICY.TYPE.CORPORATE), role: CONST.POLICY.ROLE.AUDITOR}; + const commonInput = { + account: {isFromPublicDomain: true}, + activePolicy: guestPolicy, + introSelected: undefined, + onboarding: undefined, + onboardingCompanySize: undefined, + onboardingLastVisitedPath: undefined, + onboardingPurposeSelected: undefined, + session: {email: 'test@test.com'}, + userMetadata: {}, + }; + + expect(buildFullstoryUserVars({...commonInput, policies: {policy_1: guestPolicy}}).user_role).toBe('guest'); + expect(buildFullstoryUserVars({...commonInput, policies: {policy_1: guestPolicy, policy_2: auditorPolicy}}).user_role).toBe('auditor'); + }); }); diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index d992c12a32e2..71b1f2621fbc 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -334,6 +334,15 @@ describe('PolicyUtils', () => { } }); + it('allows guests to read only the workspace overview', () => { + const policy = buildPolicy(CONST.POLICY.ROLE.GUEST); + + for (const feature of Object.values(CONST.POLICY.POLICY_FEATURE)) { + expect(canMemberRead(policy, memberLogin, feature)).toBe(feature === CONST.POLICY.POLICY_FEATURE.OVERVIEW); + expect(canMemberWrite(policy, memberLogin, feature)).toBe(false); + } + }); + it('limits scoped admins to their assigned write features', () => { expect(canMemberWrite(buildPolicy(CONST.POLICY.ROLE.CARD_ADMIN), memberLogin, CONST.POLICY.POLICY_FEATURE.COMPANY_CARDS)).toBe(true); expect(canMemberWrite(buildPolicy(CONST.POLICY.ROLE.CARD_ADMIN), memberLogin, CONST.POLICY.POLICY_FEATURE.WORKFLOWS_PAYMENTS)).toBe(false); @@ -341,10 +350,11 @@ describe('PolicyUtils', () => { expect(canMemberWrite(buildPolicy(CONST.POLICY.ROLE.PAYMENTS_ADMIN), memberLogin, CONST.POLICY.POLICY_FEATURE.WORKFLOWS_PAYMENTS)).toBe(true); }); - it('limits People Admin member role management to members and auditors', () => { + it('limits People Admin member role management to guests, members, and auditors', () => { const policy = buildPolicy(CONST.POLICY.ROLE.PEOPLE_ADMIN); expect(canMemberAssignRole(policy, memberLogin, CONST.POLICY.ROLE.USER)).toBe(true); + expect(canMemberAssignRole(policy, memberLogin, CONST.POLICY.ROLE.GUEST)).toBe(true); expect(canMemberAssignRole(policy, memberLogin, CONST.POLICY.ROLE.AUDITOR)).toBe(true); expect(canMemberAssignRole(policy, memberLogin, CONST.POLICY.ROLE.ADMIN)).toBe(false); expect(canMemberAssignRole(policy, memberLogin, CONST.POLICY.ROLE.CARD_ADMIN)).toBe(false); @@ -352,6 +362,14 @@ describe('PolicyUtils', () => { expect(canMemberAssignRole(policy, memberLogin, CONST.POLICY.ROLE.PAYMENTS_ADMIN)).toBe(false); }); + it('allows Guest assignment only on Control workspaces', () => { + const controlPolicy = buildPolicy(CONST.POLICY.ROLE.ADMIN); + const collectPolicy = {...controlPolicy, type: CONST.POLICY.TYPE.TEAM}; + + expect(canMemberAssignRole(controlPolicy, memberLogin, CONST.POLICY.ROLE.GUEST)).toBe(true); + expect(canMemberAssignRole(collectPolicy, memberLogin, CONST.POLICY.ROLE.GUEST)).toBe(false); + }); + it('allows Submit workspace editors to manage editor memberships without assigning roles', () => { const policy = { ...buildPolicy(CONST.POLICY.ROLE.EDITOR), From 4dd3681f8111787762320d428d42700d3e417235 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:49:58 +0530 Subject: [PATCH 02/41] Fix duplicate policy declaration, add Greek guest translations, apply formatting --- src/components/CategoryPicker/index.tsx | 18 +++++----- src/components/ImageWithLoading.tsx | 25 ++++++------- src/languages/el.ts | 5 +++ .../AppNavigator/AuthScreensInitHandler.tsx | 16 ++------- src/libs/TransactionUtils/index.ts | 14 ++++---- src/libs/actions/Policy/ReportField.ts | 36 ++++++++++--------- src/pages/DynamicReportParticipantsPage.tsx | 3 +- tests/ui/IOURequestStepDistanceTest.tsx | 5 +-- .../ui/components/ButtonKeyboardShortcut.tsx | 13 ++++--- 9 files changed, 66 insertions(+), 69 deletions(-) diff --git a/src/components/CategoryPicker/index.tsx b/src/components/CategoryPicker/index.tsx index 92462b1038a8..5028a6acc93b 100644 --- a/src/components/CategoryPicker/index.tsx +++ b/src/components/CategoryPicker/index.tsx @@ -85,14 +85,16 @@ function CategoryPicker({selectedCategory, policyID, onSubmit, shouldShowNoneOpt }); const noneOption: OptionTree[] = shouldShowNoneOption - ? getNoneOption(debouncedSearchValue, !selectedCategory, translate).map((option): OptionTree => ({ - text: option.text, - keyForList: option.keyForList, - searchText: '', - tooltipText: option.text, - isDisabled: false, - isSelected: option.isSelected, - })) + ? getNoneOption(debouncedSearchValue, !selectedCategory, translate).map( + (option): OptionTree => ({ + text: option.text, + keyForList: option.keyForList, + searchText: '', + tooltipText: option.text, + isDisabled: false, + isSelected: option.isSelected, + }), + ) : []; const noneOptionSection = { title: '', diff --git a/src/components/ImageWithLoading.tsx b/src/components/ImageWithLoading.tsx index 643c90ac0cf0..6e88b97139cf 100644 --- a/src/components/ImageWithLoading.tsx +++ b/src/components/ImageWithLoading.tsx @@ -87,18 +87,19 @@ function ImageWithLoading({ style={[styles.w100, styles.h100, containerStyles]} onLayout={onLayout} > - {isLoading && !!previewUri && ( - // Preview is a placeholder; parent onLoad should fire only when the full image is ready. - // eslint-disable-next-line react-native-a11y/has-valid-accessibility-ignores-invert-colors -- Custom Image wrapper does not support this prop. - - )} + {isLoading && + !!previewUri && ( + // Preview is a placeholder; parent onLoad should fire only when the full image is ready. + // eslint-disable-next-line react-native-a11y/has-valid-accessibility-ignores-invert-colors -- Custom Image wrapper does not support this prop. + + )} {/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-ignores-invert-colors -- Custom Image wrapper does not support this prop. */} ({ + one: 'Ορισμός ως επισκέπτη', + other: 'Ορισμός ως επισκέπτες', + }), makeCardAdmin: () => ({ one: 'Ορισμός διαχειριστή κάρτας', other: 'Ορίστε διαχειριστές κάρτας', diff --git a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx index 1448ad658145..38959065c908 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx @@ -160,13 +160,7 @@ function AuthScreensInitHandler() { return; } // This means sign in in RHP was successful, so we can subscribe to user events - initializePusher( - session?.accountID, - session?.email, - () => topmostOneTransactionThreadReportIDRef.current, - formatPhoneNumber, - () => reportAttributesRef.current, - ); + initializePusher(session?.accountID, session?.email, () => topmostOneTransactionThreadReportIDRef.current, formatPhoneNumber, () => reportAttributesRef.current); // eslint-disable-next-line react-hooks/exhaustive-deps -- This handler should only be re-registered when the session changes. }, [session?.accountID, session?.email]); @@ -195,13 +189,7 @@ function AuthScreensInitHandler() { }); PusherConnectionManager.init(); - initializePusher( - session?.accountID, - session?.email, - () => topmostOneTransactionThreadReportIDRef.current, - formatPhoneNumber, - () => reportAttributesRef.current, - ).finally(() => { + initializePusher(session?.accountID, session?.email, () => topmostOneTransactionThreadReportIDRef.current, formatPhoneNumber, () => reportAttributesRef.current).finally(() => { endSpan(CONST.TELEMETRY.SPAN_NAVIGATION.PUSHER_INIT); }); diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 0f34501c3bfd..58d58a42ced8 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -2718,12 +2718,14 @@ function getChildTransactions(transactions: OnyxCollection, origina function createUnreportedExpenses(transactions: Array | undefined>): UnreportedExpenseListItemType[] { return transactions .filter((t): t is Transaction => t !== undefined) - .map((transaction): UnreportedExpenseListItemType => ({ - ...transaction, - isDisabled: isTransactionPendingDelete(transaction), - keyForList: transaction.transactionID, - errors: transaction.errors as Errors | undefined, - })); + .map( + (transaction): UnreportedExpenseListItemType => ({ + ...transaction, + isDisabled: isTransactionPendingDelete(transaction), + keyForList: transaction.transactionID, + errors: transaction.errors as Errors | undefined, + }), + ); } type GetEligibleTransactionsToAddParams = { diff --git a/src/libs/actions/Policy/ReportField.ts b/src/libs/actions/Policy/ReportField.ts index bf46d3edb344..01cd2864f232 100644 --- a/src/libs/actions/Policy/ReportField.ts +++ b/src/libs/actions/Policy/ReportField.ts @@ -240,15 +240,17 @@ function createReportField({name, type, initialValue, listValues, disabledListVa errorFields: null, }, }, - ...(policyReportIDs ?? []).map((reportID): OnyxUpdate => ({ - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - onyxMethod: Onyx.METHOD.MERGE, - value: { - fieldList: { - [fieldKey]: {...optimisticReportFieldDataForPolicy, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, + ...(policyReportIDs ?? []).map( + (reportID): OnyxUpdate => ({ + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + onyxMethod: Onyx.METHOD.MERGE, + value: { + fieldList: { + [fieldKey]: {...optimisticReportFieldDataForPolicy, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, + }, }, - }, - })), + }), + ), ]; const failureData: Array> = [ @@ -264,15 +266,17 @@ function createReportField({name, type, initialValue, listValues, disabledListVa }, }, }, - ...(policyReportIDs ?? []).map((reportID): OnyxUpdate => ({ - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - onyxMethod: Onyx.METHOD.MERGE, - value: { - fieldList: { - [fieldKey]: null, + ...(policyReportIDs ?? []).map( + (reportID): OnyxUpdate => ({ + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + onyxMethod: Onyx.METHOD.MERGE, + value: { + fieldList: { + [fieldKey]: null, + }, }, - }, - })), + }), + ), ]; const onyxData: OnyxData = { diff --git a/src/pages/DynamicReportParticipantsPage.tsx b/src/pages/DynamicReportParticipantsPage.tsx index 5e6a3ae510bb..57c58cc49d97 100755 --- a/src/pages/DynamicReportParticipantsPage.tsx +++ b/src/pages/DynamicReportParticipantsPage.tsx @@ -76,8 +76,6 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr const {translate, formatPhoneNumber} = useLocalize(); const {showConfirmModal} = useConfirmModal(); const styles = useThemeStyles(); - const policy = usePolicy(report?.policyID); - const isGuestAnnounceRoom = isAnnounceRoom(report) && isPolicyGuest(policy); // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use the selection mode only on small screens // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -93,6 +91,7 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr const isGroupChat = isGroupChatUtils(report); const isCurrentUserGroupChatAdmin = isGroupChat && isCurrentUserAdmin; const policy = usePolicy(report?.policyID); + const isGuestAnnounceRoom = isAnnounceRoom(report) && isPolicyGuest(policy); const shouldShowInviteButton = canInviteMembersToReport(report, policy, isReportArchived, currentUserAccountID); const {isOffline} = useNetwork(); const canSelectMultiple = isGroupChat && isCurrentUserAdmin && (isSmallScreenWidth ? isMobileSelectionModeEnabled : true); diff --git a/tests/ui/IOURequestStepDistanceTest.tsx b/tests/ui/IOURequestStepDistanceTest.tsx index 006cdf20bca2..63a32db96c26 100644 --- a/tests/ui/IOURequestStepDistanceTest.tsx +++ b/tests/ui/IOURequestStepDistanceTest.tsx @@ -907,10 +907,7 @@ describe('IOURequestStepDistance - editing the waypoints of an expense with an a await waitForBatchedUpdatesWithAct(); // `getAllByLabelText` matches both the field label and the underlying ; pick the input. - fireEvent.changeText( - screen.getAllByLabelText(/common\.distance/).find((element) => 'value' in element.props)!, - '150', - ); + fireEvent.changeText(screen.getAllByLabelText(/common\.distance/).find((element) => 'value' in element.props)!, '150'); // The Manual-tab Save button is the second "common.save" → submitManualDistance fireEvent.press(screen.getAllByText('common.save').at(1)!); diff --git a/tests/ui/components/ButtonKeyboardShortcut.tsx b/tests/ui/components/ButtonKeyboardShortcut.tsx index 1cae88415acc..24d01bfd76a9 100644 --- a/tests/ui/components/ButtonKeyboardShortcut.tsx +++ b/tests/ui/components/ButtonKeyboardShortcut.tsx @@ -21,13 +21,12 @@ let capturedShortcutConfig: Record | undefined; jest.mock('@hooks/useKeyboardShortcut', () => // eslint-disable-next-line @typescript-eslint/no-explicit-any (shortcut: {shortcutKey: string}, callback: (e?: KeyboardEvent) => void, config: Record = {}) => { - if (shortcut.shortcutKey !== 'Enter' || !config.isActive) { - return; - } - enterKeyCallback = callback; - capturedShortcutConfig = config; - }, -); + if (shortcut.shortcutKey !== 'Enter' || !config.isActive) { + return; + } + enterKeyCallback = callback; + capturedShortcutConfig = config; +}); // ────────────────────────────────────────────────────────────────────────────── /** From e5fd6a44bc36797e20283e73bbe79f2595f61763 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:09:10 +0530 Subject: [PATCH 03/41] Updates --- .../isDeployChecklistLocked/index.js | 157 ++++-------------- .../javascript/proposalPoliceComment/index.js | 157 ++++-------------- .github/libs/GithubUtils.ts | 14 +- scripts/lint/eslint/ESLintLinter.ts | 26 +-- 4 files changed, 80 insertions(+), 274 deletions(-) diff --git a/.github/actions/javascript/isDeployChecklistLocked/index.js b/.github/actions/javascript/isDeployChecklistLocked/index.js index ebdf2bf6961c..d228861485f1 100644 --- a/.github/actions/javascript/isDeployChecklistLocked/index.js +++ b/.github/actions/javascript/isDeployChecklistLocked/index.js @@ -3433,11 +3433,7 @@ var require_CONST = __commonJS({ SHIP_CARD: "ship_card", REPORT_CARD_FRAUD: "report_card_fraud", ISSUE_CARD: "issue_card", - UPDATE_CARD: "update_card", - UPDATE_PERSONAL_DETAILS: "update_personal_details", - ADD_DELEGATE: "add_delegate", - UPDATE_DELEGATE: "update_delegate", - CHANGE_PRIMARY_LOGIN: "change_primary_login" + UPDATE_CARD: "update_card" }, EXPENSIFY_CARD: { FEED_NAME: "Expensify Card", @@ -23112,7 +23108,6 @@ var require_ExpensiMark = __commonJS({ var Constants = __importStar(require_CONST()); var UrlPatterns = __importStar(require_Url()); var Logger_1 = __importDefault(require_Logger()); - var tlds_1 = __importDefault(require_tlds()); var Utils = __importStar(require_utils()); var EXTRAS_DEFAULT = {}; var ASCII_DIGIT_START = "0".charCodeAt(0); @@ -23125,9 +23120,6 @@ var require_ExpensiMark = __commonJS({ var NON_BREAKING_SPACE_CODE = 160; var URL_PROTOCOLS = ["https://", "http://", "ftps://", "ftp://"]; var URL_CANDIDATE_PREFIX_CHARACTERS = "@_*~"; - var URL_TLD_LIST = tlds_1.default.toLowerCase().split("|"); - var URL_TLDS = new Set(URL_TLD_LIST); - var MAX_URL_TLD_LENGTH = Math.max(...URL_TLD_LIST.map((tld) => tld.length)); var PROTECTED_TAG_NAMES = /* @__PURE__ */ new Set(["a", "code", "pre", "video"]); var MARKDOWN_LINK_REGEX = new RegExp(`\\[((?:[^\\[\\]\\r\\n]*(?:\\[[^\\[\\]\\r\\n]*][^\\[\\]\\r\\n]*)*))]\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, "gi"); var MARKDOWN_IMAGE_REGEX = new RegExp(`\\!(?:\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)])?\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, "gi"); @@ -23173,9 +23165,6 @@ var require_ExpensiMark = __commonJS({ } return text.replace(regexp, replacement); } - function canUseCandidateScanning(text, shouldEscapeText) { - return shouldEscapeText || !text.includes("<") && !text.includes(">"); - } function isAsciiAlphaNumeric(character) { if (!character) { return false; @@ -23247,59 +23236,15 @@ var require_ExpensiMark = __commonJS({ } return URL_PROTOCOLS.find((protocol) => text.slice(position, position + protocol.length).toLowerCase() === protocol); } - function isValidHostnameLabel(text, start, end) { - if (start >= end || !isAsciiAlphaNumeric(text[start]) || !isAsciiAlphaNumeric(text[end - 1])) { - return false; - } - for (let index = start + 1; index < end - 1; index++) { - if (!isAsciiAlphaNumeric(text[index]) && text[index] !== "-") { - return false; - } - } - return true; - } - function findHostnameStart(text, dotPosition) { - let hostnameStart = dotPosition; - let labelEnd = dotPosition; - while (labelEnd > 0) { - let rawLabelStart = labelEnd - 1; - while (rawLabelStart >= 0 && text[rawLabelStart] !== "." && isHostnameCharacter(text[rawLabelStart])) { - rawLabelStart--; - } - rawLabelStart++; - let labelStart = rawLabelStart; - while (labelStart < labelEnd && text[labelStart] === "-") { - labelStart++; - } - if (!isValidHostnameLabel(text, labelStart, labelEnd)) { - break; - } - hostnameStart = labelStart; - if (labelStart !== rawLabelStart) { - break; - } - const separatorPosition = labelStart - 1; - if (separatorPosition < 0 || text[separatorPosition] !== ".") { - break; - } - labelEnd = separatorPosition; + function findHostnameEnd(text, hostnameStart, dotPosition) { + let hostnameEnd = dotPosition + 1; + while (hostnameEnd < text.length && (isAsciiAlphaNumeric(text[hostnameEnd]) || text[hostnameEnd] === "-")) { + hostnameEnd++; } - return hostnameStart === dotPosition ? void 0 : hostnameStart; - } - function findKnownTldEnd(text, dotPosition) { - const maximumEnd = Math.min(text.length, dotPosition + 1 + MAX_URL_TLD_LENGTH); - for (let end = dotPosition + 2; end <= maximumEnd; end++) { - const currentCharacter = text[end - 1]; - if (!isAsciiAlphaNumeric(currentCharacter) && currentCharacter !== "-") { - break; - } - const nextCharacter = text[end]; - const hasValidBoundary = !nextCharacter || nextCharacter === ":" || nextCharacter === "_" || !isWordCharacter(nextCharacter); - if (hasValidBoundary && URL_TLDS.has(text.slice(dotPosition + 1, end).toLowerCase())) { - return end; - } + if (hostnameStart === dotPosition || hostnameEnd === dotPosition + 1) { + return void 0; } - return void 0; + return hostnameEnd; } function extendUrlCandidateBoundaries(text, start, end) { let candidateStart = start; @@ -23312,65 +23257,24 @@ var require_ExpensiMark = __commonJS({ } return { start: candidateStart, end: candidateEnd }; } - function startsWithIgnoreCase(text, expected, position) { - return text.slice(position, position + expected.length).toLowerCase() === expected; - } - function filterUrlCandidatesBlockedByFollowingHtml(text, candidates) { - var _a; - if (candidates.length === 0 || !text.includes("<") && !text.includes(">")) { - return candidates; - } - const validCandidates = []; - let candidateIndex = candidates.length - 1; - let nextLessThan = text.length; - let nextGreaterThan = text.length; - let nextOpeningAnchor = text.length; - let nextClosingAnchor = text.length; - for (let index = text.length; index >= 0 && candidateIndex >= 0; index--) { - if (text[index] === "<") { - nextLessThan = index; - if (((_a = text[index + 1]) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === "a") { - nextOpeningAnchor = index; - } else if (startsWithIgnoreCase(text, "", index)) { - nextClosingAnchor = index; - } - } else if (text[index] === ">") { - nextGreaterThan = index; - } - while (candidateIndex >= 0 && candidates[candidateIndex].end === index) { - const firstHtmlBoundaryIsClosingTag = nextLessThan < nextGreaterThan && text.startsWith("", nextLessThan); - const firstTagIsProtectedClosingTag = startsWithIgnoreCase(text, "", nextLessThan) || startsWithIgnoreCase(text, "", nextLessThan); - const isBlockedByFollowingHtml = ( - // Mirrors `(?![^<]*>)`: reject when `>` appears before the next `<`. - nextGreaterThan < nextLessThan || // Mirrors `[^<>]*<\/(?!h1>)`: reject a later closing tag other than ``. - firstHtmlBoundaryIsClosingTag || // Mirrors `((?:(?!`: reject `` unless another `` appears first. - nextClosingAnchor < nextOpeningAnchor || // Mirrors `[^<]*(<\/pre>|<\/code>)`: reject a later protected closing tag. - firstTagIsProtectedClosingTag - ); - if (!isBlockedByFollowingHtml) { - validCandidates.push(candidates[candidateIndex]); - } - candidateIndex--; - } - } - return validCandidates.reverse(); - } function findUrlCandidates(text) { const candidates = []; const protectedTags = []; let index = 0; + let hostnameRunStart = 0; while (index < text.length) { if (text[index] === "<") { const nextIndex = updateProtectedTagStack(text, index, protectedTags); if (nextIndex === void 0) { - index++; - continue; + break; } index = nextIndex; + hostnameRunStart = index; continue; } if (protectedTags.length > 0) { index++; + hostnameRunStart = index; continue; } const matchedProtocol = getProtocolAt(text, index); @@ -23378,23 +23282,29 @@ var require_ExpensiMark = __commonJS({ const candidate2 = extendUrlCandidateBoundaries(text, index, index + matchedProtocol.length); candidates.push(candidate2); index = candidate2.end; + hostnameRunStart = candidate2.end; + continue; + } + if (!isHostnameCharacter(text[index])) { + hostnameRunStart = index + 1; + index++; continue; } if (text[index] !== ".") { index++; continue; } - const tldEnd = findKnownTldEnd(text, index); - const hostnameStart = tldEnd === void 0 ? void 0 : findHostnameStart(text, index); - if (tldEnd === void 0 || hostnameStart === void 0) { + const hostnameEnd = findHostnameEnd(text, hostnameRunStart, index); + if (hostnameEnd === void 0) { index++; continue; } - const candidate = extendUrlCandidateBoundaries(text, hostnameStart, tldEnd); + const candidate = extendUrlCandidateBoundaries(text, hostnameRunStart, hostnameEnd); candidates.push(candidate); index = candidate.end; + hostnameRunStart = candidate.end; } - return filterUrlCandidatesBlockedByFollowingHtml(text, candidates); + return candidates; } function replaceMarkdownCandidates(text, regexp, replacement, marker, canOpen) { if (!text.includes(marker)) { @@ -23467,14 +23377,6 @@ var require_ExpensiMark = __commonJS({ output.push(text.slice(outputStart)); return output.join(""); } - function processMarkdownRule(regex2, marker, canOpen) { - return (textToProcess, replacement, _shouldKeepRawInput, shouldEscapeText) => { - if (canUseCandidateScanning(textToProcess, shouldEscapeText)) { - return replaceMarkdownCandidates(textToProcess, regex2, replacement, marker, canOpen); - } - return replaceTextWithExtras(textToProcess, regex2, EXTRAS_DEFAULT, replacement); - }; - } function replaceBlockElementWithNewLine(htmlString) { let splitText = htmlString.replaceAll(/
> (|<\/div>||\n<\/comment>|<\/comment>|

|<\/h1>|

|<\/h2>|

|<\/h3>|

|<\/h4>|

|<\/h5>|
|<\/h6>|

|<\/p>|

  • |<\/li>)/gi, "
    > ").split(/|<\/div>||\n<\/comment>|<\/comment>|

    |<\/h1>|

    |<\/h2>|

    |<\/h3>|

    |<\/h4>|

    |<\/h5>|
    |<\/h6>|

    |<\/p>|

  • |<\/li>|
    |<\/blockquote>/); const stripHTML = (text) => str_1.default.stripHTML(text); @@ -23893,9 +23795,9 @@ var require_ExpensiMark = __commonJS({ */ { name: "autolink", - process: (textToProcess, replacement, _shouldKeepRawInput, shouldEscapeText) => { + process: (textToProcess, replacement) => { const regex2 = new RegExp(`(?![^<]*>|[^<>]*<\\/(?!h1>))([_*~]*?)${UrlPatterns.MARKDOWN_URL_REGEX}\\1(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`, "gi"); - return this.modifyTextForUrlLinks(regex2, textToProcess, replacement, canUseCandidateScanning(textToProcess, shouldEscapeText)); + return this.modifyTextForUrlLinks(regex2, textToProcess, replacement, true); }, replacement: (_extras, _match, g1, g2) => { const href = str_1.default.sanitizeURL(g2); @@ -23964,8 +23866,7 @@ ${"
    ".repeat(i)}`, "\n"); name: "autoEmail", regex: new RegExp(`([^\\w'#%+-]|^)${Constants.CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<>]*<\\/(?!em|h1|blockquote))`, "gim"), replacement: '$1$2', - rawInputReplacement: '$1$2', - shouldSkipProcessing: (textToCheck) => !textToCheck.includes("@") + rawInputReplacement: '$1$2' }, /** * This regex matches a short user mention in a string. @@ -24001,7 +23902,7 @@ ${"
    ".repeat(i)}`, "\n"); // \B will match everything that \b doesn't, so it works // for * and ~: https://www.rexegg.com/regex-boundaries.html#notb name: "bold", - process: processMarkdownRule(BOLD_MARKDOWN_REGEX, "*", canOpenBoldMarkdown), + process: (textToProcess, replacement) => replaceMarkdownCandidates(textToProcess, BOLD_MARKDOWN_REGEX, replacement, "*", canOpenBoldMarkdown), replacement: (_extras, match, g1, g2) => { if (g1.includes("_")) { return `${g1}${g2}`; @@ -24011,7 +23912,7 @@ ${"
    ".repeat(i)}`, "\n"); }, { name: "strikethrough", - process: processMarkdownRule(STRIKETHROUGH_MARKDOWN_REGEX, "~", canOpenStrikethroughMarkdown), + process: (textToProcess, replacement) => replaceMarkdownCandidates(textToProcess, STRIKETHROUGH_MARKDOWN_REGEX, replacement, "~", canOpenStrikethroughMarkdown), replacement: (_extras, match, g1) => g1.includes("") || containsNonPairTag(g1) ? match : `${g1}` }, { @@ -24380,7 +24281,7 @@ ${g2} } const replacement = shouldKeepRawInput && rule.rawInputReplacement ? rule.rawInputReplacement : rule.replacement; if ("process" in rule) { - replacedText = rule.process(replacedText, replacement, shouldKeepRawInput, shouldEscapeText); + replacedText = rule.process(replacedText, replacement, shouldKeepRawInput); } else { replacedText = replaceTextWithExtras(replacedText, rule.regex, extras, replacement); } diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js index 126b34462df4..0d6eafcdd4f6 100644 --- a/.github/actions/javascript/proposalPoliceComment/index.js +++ b/.github/actions/javascript/proposalPoliceComment/index.js @@ -24247,11 +24247,7 @@ var require_CONST = __commonJS({ SHIP_CARD: "ship_card", REPORT_CARD_FRAUD: "report_card_fraud", ISSUE_CARD: "issue_card", - UPDATE_CARD: "update_card", - UPDATE_PERSONAL_DETAILS: "update_personal_details", - ADD_DELEGATE: "add_delegate", - UPDATE_DELEGATE: "update_delegate", - CHANGE_PRIMARY_LOGIN: "change_primary_login" + UPDATE_CARD: "update_card" }, EXPENSIFY_CARD: { FEED_NAME: "Expensify Card", @@ -43926,7 +43922,6 @@ var require_ExpensiMark = __commonJS({ var Constants = __importStar(require_CONST()); var UrlPatterns = __importStar(require_Url()); var Logger_1 = __importDefault(require_Logger()); - var tlds_1 = __importDefault(require_tlds()); var Utils = __importStar(require_utils2()); var EXTRAS_DEFAULT = {}; var ASCII_DIGIT_START = "0".charCodeAt(0); @@ -43939,9 +43934,6 @@ var require_ExpensiMark = __commonJS({ var NON_BREAKING_SPACE_CODE = 160; var URL_PROTOCOLS = ["https://", "http://", "ftps://", "ftp://"]; var URL_CANDIDATE_PREFIX_CHARACTERS = "@_*~"; - var URL_TLD_LIST = tlds_1.default.toLowerCase().split("|"); - var URL_TLDS = new Set(URL_TLD_LIST); - var MAX_URL_TLD_LENGTH = Math.max(...URL_TLD_LIST.map((tld) => tld.length)); var PROTECTED_TAG_NAMES = /* @__PURE__ */ new Set(["a", "code", "pre", "video"]); var MARKDOWN_LINK_REGEX = new RegExp(`\\[((?:[^\\[\\]\\r\\n]*(?:\\[[^\\[\\]\\r\\n]*][^\\[\\]\\r\\n]*)*))]\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, "gi"); var MARKDOWN_IMAGE_REGEX = new RegExp(`\\!(?:\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)])?\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, "gi"); @@ -43987,9 +43979,6 @@ var require_ExpensiMark = __commonJS({ } return text.replace(regexp, replacement); } - function canUseCandidateScanning(text, shouldEscapeText) { - return shouldEscapeText || !text.includes("<") && !text.includes(">"); - } function isAsciiAlphaNumeric(character) { if (!character) { return false; @@ -44061,59 +44050,15 @@ var require_ExpensiMark = __commonJS({ } return URL_PROTOCOLS.find((protocol) => text.slice(position, position + protocol.length).toLowerCase() === protocol); } - function isValidHostnameLabel(text, start, end) { - if (start >= end || !isAsciiAlphaNumeric(text[start]) || !isAsciiAlphaNumeric(text[end - 1])) { - return false; - } - for (let index = start + 1; index < end - 1; index++) { - if (!isAsciiAlphaNumeric(text[index]) && text[index] !== "-") { - return false; - } - } - return true; - } - function findHostnameStart(text, dotPosition) { - let hostnameStart = dotPosition; - let labelEnd = dotPosition; - while (labelEnd > 0) { - let rawLabelStart = labelEnd - 1; - while (rawLabelStart >= 0 && text[rawLabelStart] !== "." && isHostnameCharacter(text[rawLabelStart])) { - rawLabelStart--; - } - rawLabelStart++; - let labelStart = rawLabelStart; - while (labelStart < labelEnd && text[labelStart] === "-") { - labelStart++; - } - if (!isValidHostnameLabel(text, labelStart, labelEnd)) { - break; - } - hostnameStart = labelStart; - if (labelStart !== rawLabelStart) { - break; - } - const separatorPosition = labelStart - 1; - if (separatorPosition < 0 || text[separatorPosition] !== ".") { - break; - } - labelEnd = separatorPosition; + function findHostnameEnd(text, hostnameStart, dotPosition) { + let hostnameEnd = dotPosition + 1; + while (hostnameEnd < text.length && (isAsciiAlphaNumeric(text[hostnameEnd]) || text[hostnameEnd] === "-")) { + hostnameEnd++; } - return hostnameStart === dotPosition ? void 0 : hostnameStart; - } - function findKnownTldEnd(text, dotPosition) { - const maximumEnd = Math.min(text.length, dotPosition + 1 + MAX_URL_TLD_LENGTH); - for (let end = dotPosition + 2; end <= maximumEnd; end++) { - const currentCharacter = text[end - 1]; - if (!isAsciiAlphaNumeric(currentCharacter) && currentCharacter !== "-") { - break; - } - const nextCharacter = text[end]; - const hasValidBoundary = !nextCharacter || nextCharacter === ":" || nextCharacter === "_" || !isWordCharacter(nextCharacter); - if (hasValidBoundary && URL_TLDS.has(text.slice(dotPosition + 1, end).toLowerCase())) { - return end; - } + if (hostnameStart === dotPosition || hostnameEnd === dotPosition + 1) { + return void 0; } - return void 0; + return hostnameEnd; } function extendUrlCandidateBoundaries(text, start, end) { let candidateStart = start; @@ -44126,65 +44071,24 @@ var require_ExpensiMark = __commonJS({ } return { start: candidateStart, end: candidateEnd }; } - function startsWithIgnoreCase(text, expected, position) { - return text.slice(position, position + expected.length).toLowerCase() === expected; - } - function filterUrlCandidatesBlockedByFollowingHtml(text, candidates) { - var _a3; - if (candidates.length === 0 || !text.includes("<") && !text.includes(">")) { - return candidates; - } - const validCandidates = []; - let candidateIndex = candidates.length - 1; - let nextLessThan = text.length; - let nextGreaterThan = text.length; - let nextOpeningAnchor = text.length; - let nextClosingAnchor = text.length; - for (let index = text.length; index >= 0 && candidateIndex >= 0; index--) { - if (text[index] === "<") { - nextLessThan = index; - if (((_a3 = text[index + 1]) === null || _a3 === void 0 ? void 0 : _a3.toLowerCase()) === "a") { - nextOpeningAnchor = index; - } else if (startsWithIgnoreCase(text, "", index)) { - nextClosingAnchor = index; - } - } else if (text[index] === ">") { - nextGreaterThan = index; - } - while (candidateIndex >= 0 && candidates[candidateIndex].end === index) { - const firstHtmlBoundaryIsClosingTag = nextLessThan < nextGreaterThan && text.startsWith("", nextLessThan); - const firstTagIsProtectedClosingTag = startsWithIgnoreCase(text, "", nextLessThan) || startsWithIgnoreCase(text, "", nextLessThan); - const isBlockedByFollowingHtml = ( - // Mirrors `(?![^<]*>)`: reject when `>` appears before the next `<`. - nextGreaterThan < nextLessThan || // Mirrors `[^<>]*<\/(?!h1>)`: reject a later closing tag other than `
  • `. - firstHtmlBoundaryIsClosingTag || // Mirrors `((?:(?!`: reject `` unless another `` appears first. - nextClosingAnchor < nextOpeningAnchor || // Mirrors `[^<]*(<\/pre>|<\/code>)`: reject a later protected closing tag. - firstTagIsProtectedClosingTag - ); - if (!isBlockedByFollowingHtml) { - validCandidates.push(candidates[candidateIndex]); - } - candidateIndex--; - } - } - return validCandidates.reverse(); - } function findUrlCandidates(text) { const candidates = []; const protectedTags = []; let index = 0; + let hostnameRunStart = 0; while (index < text.length) { if (text[index] === "<") { const nextIndex = updateProtectedTagStack(text, index, protectedTags); if (nextIndex === void 0) { - index++; - continue; + break; } index = nextIndex; + hostnameRunStart = index; continue; } if (protectedTags.length > 0) { index++; + hostnameRunStart = index; continue; } const matchedProtocol = getProtocolAt(text, index); @@ -44192,23 +44096,29 @@ var require_ExpensiMark = __commonJS({ const candidate2 = extendUrlCandidateBoundaries(text, index, index + matchedProtocol.length); candidates.push(candidate2); index = candidate2.end; + hostnameRunStart = candidate2.end; + continue; + } + if (!isHostnameCharacter(text[index])) { + hostnameRunStart = index + 1; + index++; continue; } if (text[index] !== ".") { index++; continue; } - const tldEnd = findKnownTldEnd(text, index); - const hostnameStart = tldEnd === void 0 ? void 0 : findHostnameStart(text, index); - if (tldEnd === void 0 || hostnameStart === void 0) { + const hostnameEnd = findHostnameEnd(text, hostnameRunStart, index); + if (hostnameEnd === void 0) { index++; continue; } - const candidate = extendUrlCandidateBoundaries(text, hostnameStart, tldEnd); + const candidate = extendUrlCandidateBoundaries(text, hostnameRunStart, hostnameEnd); candidates.push(candidate); index = candidate.end; + hostnameRunStart = candidate.end; } - return filterUrlCandidatesBlockedByFollowingHtml(text, candidates); + return candidates; } function replaceMarkdownCandidates(text, regexp, replacement, marker, canOpen) { if (!text.includes(marker)) { @@ -44281,14 +44191,6 @@ var require_ExpensiMark = __commonJS({ output.push(text.slice(outputStart)); return output.join(""); } - function processMarkdownRule(regex2, marker, canOpen) { - return (textToProcess, replacement, _shouldKeepRawInput, shouldEscapeText) => { - if (canUseCandidateScanning(textToProcess, shouldEscapeText)) { - return replaceMarkdownCandidates(textToProcess, regex2, replacement, marker, canOpen); - } - return replaceTextWithExtras(textToProcess, regex2, EXTRAS_DEFAULT, replacement); - }; - } function replaceBlockElementWithNewLine(htmlString) { let splitText = htmlString.replaceAll(/
    > (|<\/div>||\n<\/comment>|<\/comment>|

    |<\/h1>|

    |<\/h2>|

    |<\/h3>|

    |<\/h4>|

    |<\/h5>|
    |<\/h6>|

    |<\/p>|

  • |<\/li>)/gi, "
    > ").split(/|<\/div>||\n<\/comment>|<\/comment>|

    |<\/h1>|

    |<\/h2>|

    |<\/h3>|

    |<\/h4>|

    |<\/h5>|
    |<\/h6>|

    |<\/p>|

  • |<\/li>|
    |<\/blockquote>/); const stripHTML = (text) => str_1.default.stripHTML(text); @@ -44707,9 +44609,9 @@ var require_ExpensiMark = __commonJS({ */ { name: "autolink", - process: (textToProcess, replacement, _shouldKeepRawInput, shouldEscapeText) => { + process: (textToProcess, replacement) => { const regex2 = new RegExp(`(?![^<]*>|[^<>]*<\\/(?!h1>))([_*~]*?)${UrlPatterns.MARKDOWN_URL_REGEX}\\1(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`, "gi"); - return this.modifyTextForUrlLinks(regex2, textToProcess, replacement, canUseCandidateScanning(textToProcess, shouldEscapeText)); + return this.modifyTextForUrlLinks(regex2, textToProcess, replacement, true); }, replacement: (_extras, _match, g1, g2) => { const href = str_1.default.sanitizeURL(g2); @@ -44778,8 +44680,7 @@ ${"
    ".repeat(i)}`, "\n"); name: "autoEmail", regex: new RegExp(`([^\\w'#%+-]|^)${Constants.CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<>]*<\\/(?!em|h1|blockquote))`, "gim"), replacement: '$1$2', - rawInputReplacement: '$1$2', - shouldSkipProcessing: (textToCheck) => !textToCheck.includes("@") + rawInputReplacement: '$1$2' }, /** * This regex matches a short user mention in a string. @@ -44815,7 +44716,7 @@ ${"
    ".repeat(i)}`, "\n"); // \B will match everything that \b doesn't, so it works // for * and ~: https://www.rexegg.com/regex-boundaries.html#notb name: "bold", - process: processMarkdownRule(BOLD_MARKDOWN_REGEX, "*", canOpenBoldMarkdown), + process: (textToProcess, replacement) => replaceMarkdownCandidates(textToProcess, BOLD_MARKDOWN_REGEX, replacement, "*", canOpenBoldMarkdown), replacement: (_extras, match2, g1, g2) => { if (g1.includes("_")) { return `${g1}${g2}`; @@ -44825,7 +44726,7 @@ ${"
    ".repeat(i)}`, "\n"); }, { name: "strikethrough", - process: processMarkdownRule(STRIKETHROUGH_MARKDOWN_REGEX, "~", canOpenStrikethroughMarkdown), + process: (textToProcess, replacement) => replaceMarkdownCandidates(textToProcess, STRIKETHROUGH_MARKDOWN_REGEX, replacement, "~", canOpenStrikethroughMarkdown), replacement: (_extras, match2, g1) => g1.includes("") || containsNonPairTag(g1) ? match2 : `${g1}` }, { @@ -45194,7 +45095,7 @@ ${g2} } const replacement = shouldKeepRawInput && rule.rawInputReplacement ? rule.rawInputReplacement : rule.replacement; if ("process" in rule) { - replacedText = rule.process(replacedText, replacement, shouldKeepRawInput, shouldEscapeText); + replacedText = rule.process(replacedText, replacement, shouldKeepRawInput); } else { replacedText = replaceTextWithExtras(replacedText, rule.regex, extras, replacement); } diff --git a/.github/libs/GithubUtils.ts b/.github/libs/GithubUtils.ts index e3a6868ebe93..e76eab58af06 100644 --- a/.github/libs/GithubUtils.ts +++ b/.github/libs/GithubUtils.ts @@ -503,12 +503,14 @@ class GithubUtils { core.info(`🎉 Successfully fetched ${allCommits.length} total commits`); core.endGroup(); console.log(''); - return allCommits.map((commit): CommitType => ({ - commit: commit.sha, - subject: commit.commit.message, - authorName: commit.commit.author?.name ?? 'Unknown', - date: commit.commit.committer?.date ?? '', - })); + return allCommits.map( + (commit): CommitType => ({ + commit: commit.sha, + subject: commit.commit.message, + authorName: commit.commit.author?.name ?? 'Unknown', + date: commit.commit.committer?.date ?? '', + }), + ); } catch (error) { if (error instanceof RequestError && error.status === 404) { core.error( diff --git a/scripts/lint/eslint/ESLintLinter.ts b/scripts/lint/eslint/ESLintLinter.ts index 0f9276c0c1dc..a95b86780322 100644 --- a/scripts/lint/eslint/ESLintLinter.ts +++ b/scripts/lint/eslint/ESLintLinter.ts @@ -49,18 +49,20 @@ function normalizeESLintResults(results: ESLintJSONResult[]): LintFileResult[] { return results.map((result) => ({ filePath: result.filePath, source: result.source, - messages: result.messages.map((message): LintMessage => ({ - filePath: result.filePath, - ruleID: message[ESLINT_RULE_ID_KEY], - severity: normalizeSeverity(message.severity), - message: message.message, - line: message.line ?? 0, - column: message.column ?? 0, - endLine: message.endLine, - endColumn: message.endColumn, - suggestions: message.suggestions, - fix: message.fix, - })), + messages: result.messages.map( + (message): LintMessage => ({ + filePath: result.filePath, + ruleID: message[ESLINT_RULE_ID_KEY], + severity: normalizeSeverity(message.severity), + message: message.message, + line: message.line ?? 0, + column: message.column ?? 0, + endLine: message.endLine, + endColumn: message.endColumn, + suggestions: message.suggestions, + fix: message.fix, + }), + ), })); } From 6031a089d26a38e364ce569b89e6e61aadb30871 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:12:11 +0530 Subject: [PATCH 04/41] Fix WorkspaceMenuItem type widening in guest filter --- .../isDeployChecklistLocked/index.js | 157 ++++++++++++++---- .../javascript/proposalPoliceComment/index.js | 157 ++++++++++++++---- .github/libs/GithubUtils.ts | 14 +- scripts/lint/eslint/ESLintLinter.ts | 26 ++- src/pages/workspace/getWorkspaceMenuItems.ts | 5 +- 5 files changed, 277 insertions(+), 82 deletions(-) diff --git a/.github/actions/javascript/isDeployChecklistLocked/index.js b/.github/actions/javascript/isDeployChecklistLocked/index.js index d228861485f1..ebdf2bf6961c 100644 --- a/.github/actions/javascript/isDeployChecklistLocked/index.js +++ b/.github/actions/javascript/isDeployChecklistLocked/index.js @@ -3433,7 +3433,11 @@ var require_CONST = __commonJS({ SHIP_CARD: "ship_card", REPORT_CARD_FRAUD: "report_card_fraud", ISSUE_CARD: "issue_card", - UPDATE_CARD: "update_card" + UPDATE_CARD: "update_card", + UPDATE_PERSONAL_DETAILS: "update_personal_details", + ADD_DELEGATE: "add_delegate", + UPDATE_DELEGATE: "update_delegate", + CHANGE_PRIMARY_LOGIN: "change_primary_login" }, EXPENSIFY_CARD: { FEED_NAME: "Expensify Card", @@ -23108,6 +23112,7 @@ var require_ExpensiMark = __commonJS({ var Constants = __importStar(require_CONST()); var UrlPatterns = __importStar(require_Url()); var Logger_1 = __importDefault(require_Logger()); + var tlds_1 = __importDefault(require_tlds()); var Utils = __importStar(require_utils()); var EXTRAS_DEFAULT = {}; var ASCII_DIGIT_START = "0".charCodeAt(0); @@ -23120,6 +23125,9 @@ var require_ExpensiMark = __commonJS({ var NON_BREAKING_SPACE_CODE = 160; var URL_PROTOCOLS = ["https://", "http://", "ftps://", "ftp://"]; var URL_CANDIDATE_PREFIX_CHARACTERS = "@_*~"; + var URL_TLD_LIST = tlds_1.default.toLowerCase().split("|"); + var URL_TLDS = new Set(URL_TLD_LIST); + var MAX_URL_TLD_LENGTH = Math.max(...URL_TLD_LIST.map((tld) => tld.length)); var PROTECTED_TAG_NAMES = /* @__PURE__ */ new Set(["a", "code", "pre", "video"]); var MARKDOWN_LINK_REGEX = new RegExp(`\\[((?:[^\\[\\]\\r\\n]*(?:\\[[^\\[\\]\\r\\n]*][^\\[\\]\\r\\n]*)*))]\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, "gi"); var MARKDOWN_IMAGE_REGEX = new RegExp(`\\!(?:\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)])?\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, "gi"); @@ -23165,6 +23173,9 @@ var require_ExpensiMark = __commonJS({ } return text.replace(regexp, replacement); } + function canUseCandidateScanning(text, shouldEscapeText) { + return shouldEscapeText || !text.includes("<") && !text.includes(">"); + } function isAsciiAlphaNumeric(character) { if (!character) { return false; @@ -23236,15 +23247,59 @@ var require_ExpensiMark = __commonJS({ } return URL_PROTOCOLS.find((protocol) => text.slice(position, position + protocol.length).toLowerCase() === protocol); } - function findHostnameEnd(text, hostnameStart, dotPosition) { - let hostnameEnd = dotPosition + 1; - while (hostnameEnd < text.length && (isAsciiAlphaNumeric(text[hostnameEnd]) || text[hostnameEnd] === "-")) { - hostnameEnd++; + function isValidHostnameLabel(text, start, end) { + if (start >= end || !isAsciiAlphaNumeric(text[start]) || !isAsciiAlphaNumeric(text[end - 1])) { + return false; } - if (hostnameStart === dotPosition || hostnameEnd === dotPosition + 1) { - return void 0; + for (let index = start + 1; index < end - 1; index++) { + if (!isAsciiAlphaNumeric(text[index]) && text[index] !== "-") { + return false; + } + } + return true; + } + function findHostnameStart(text, dotPosition) { + let hostnameStart = dotPosition; + let labelEnd = dotPosition; + while (labelEnd > 0) { + let rawLabelStart = labelEnd - 1; + while (rawLabelStart >= 0 && text[rawLabelStart] !== "." && isHostnameCharacter(text[rawLabelStart])) { + rawLabelStart--; + } + rawLabelStart++; + let labelStart = rawLabelStart; + while (labelStart < labelEnd && text[labelStart] === "-") { + labelStart++; + } + if (!isValidHostnameLabel(text, labelStart, labelEnd)) { + break; + } + hostnameStart = labelStart; + if (labelStart !== rawLabelStart) { + break; + } + const separatorPosition = labelStart - 1; + if (separatorPosition < 0 || text[separatorPosition] !== ".") { + break; + } + labelEnd = separatorPosition; } - return hostnameEnd; + return hostnameStart === dotPosition ? void 0 : hostnameStart; + } + function findKnownTldEnd(text, dotPosition) { + const maximumEnd = Math.min(text.length, dotPosition + 1 + MAX_URL_TLD_LENGTH); + for (let end = dotPosition + 2; end <= maximumEnd; end++) { + const currentCharacter = text[end - 1]; + if (!isAsciiAlphaNumeric(currentCharacter) && currentCharacter !== "-") { + break; + } + const nextCharacter = text[end]; + const hasValidBoundary = !nextCharacter || nextCharacter === ":" || nextCharacter === "_" || !isWordCharacter(nextCharacter); + if (hasValidBoundary && URL_TLDS.has(text.slice(dotPosition + 1, end).toLowerCase())) { + return end; + } + } + return void 0; } function extendUrlCandidateBoundaries(text, start, end) { let candidateStart = start; @@ -23257,24 +23312,65 @@ var require_ExpensiMark = __commonJS({ } return { start: candidateStart, end: candidateEnd }; } + function startsWithIgnoreCase(text, expected, position) { + return text.slice(position, position + expected.length).toLowerCase() === expected; + } + function filterUrlCandidatesBlockedByFollowingHtml(text, candidates) { + var _a; + if (candidates.length === 0 || !text.includes("<") && !text.includes(">")) { + return candidates; + } + const validCandidates = []; + let candidateIndex = candidates.length - 1; + let nextLessThan = text.length; + let nextGreaterThan = text.length; + let nextOpeningAnchor = text.length; + let nextClosingAnchor = text.length; + for (let index = text.length; index >= 0 && candidateIndex >= 0; index--) { + if (text[index] === "<") { + nextLessThan = index; + if (((_a = text[index + 1]) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === "a") { + nextOpeningAnchor = index; + } else if (startsWithIgnoreCase(text, "", index)) { + nextClosingAnchor = index; + } + } else if (text[index] === ">") { + nextGreaterThan = index; + } + while (candidateIndex >= 0 && candidates[candidateIndex].end === index) { + const firstHtmlBoundaryIsClosingTag = nextLessThan < nextGreaterThan && text.startsWith("", nextLessThan); + const firstTagIsProtectedClosingTag = startsWithIgnoreCase(text, "", nextLessThan) || startsWithIgnoreCase(text, "", nextLessThan); + const isBlockedByFollowingHtml = ( + // Mirrors `(?![^<]*>)`: reject when `>` appears before the next `<`. + nextGreaterThan < nextLessThan || // Mirrors `[^<>]*<\/(?!h1>)`: reject a later closing tag other than `
  • `. + firstHtmlBoundaryIsClosingTag || // Mirrors `((?:(?!`: reject `` unless another `` appears first. + nextClosingAnchor < nextOpeningAnchor || // Mirrors `[^<]*(<\/pre>|<\/code>)`: reject a later protected closing tag. + firstTagIsProtectedClosingTag + ); + if (!isBlockedByFollowingHtml) { + validCandidates.push(candidates[candidateIndex]); + } + candidateIndex--; + } + } + return validCandidates.reverse(); + } function findUrlCandidates(text) { const candidates = []; const protectedTags = []; let index = 0; - let hostnameRunStart = 0; while (index < text.length) { if (text[index] === "<") { const nextIndex = updateProtectedTagStack(text, index, protectedTags); if (nextIndex === void 0) { - break; + index++; + continue; } index = nextIndex; - hostnameRunStart = index; continue; } if (protectedTags.length > 0) { index++; - hostnameRunStart = index; continue; } const matchedProtocol = getProtocolAt(text, index); @@ -23282,29 +23378,23 @@ var require_ExpensiMark = __commonJS({ const candidate2 = extendUrlCandidateBoundaries(text, index, index + matchedProtocol.length); candidates.push(candidate2); index = candidate2.end; - hostnameRunStart = candidate2.end; - continue; - } - if (!isHostnameCharacter(text[index])) { - hostnameRunStart = index + 1; - index++; continue; } if (text[index] !== ".") { index++; continue; } - const hostnameEnd = findHostnameEnd(text, hostnameRunStart, index); - if (hostnameEnd === void 0) { + const tldEnd = findKnownTldEnd(text, index); + const hostnameStart = tldEnd === void 0 ? void 0 : findHostnameStart(text, index); + if (tldEnd === void 0 || hostnameStart === void 0) { index++; continue; } - const candidate = extendUrlCandidateBoundaries(text, hostnameRunStart, hostnameEnd); + const candidate = extendUrlCandidateBoundaries(text, hostnameStart, tldEnd); candidates.push(candidate); index = candidate.end; - hostnameRunStart = candidate.end; } - return candidates; + return filterUrlCandidatesBlockedByFollowingHtml(text, candidates); } function replaceMarkdownCandidates(text, regexp, replacement, marker, canOpen) { if (!text.includes(marker)) { @@ -23377,6 +23467,14 @@ var require_ExpensiMark = __commonJS({ output.push(text.slice(outputStart)); return output.join(""); } + function processMarkdownRule(regex2, marker, canOpen) { + return (textToProcess, replacement, _shouldKeepRawInput, shouldEscapeText) => { + if (canUseCandidateScanning(textToProcess, shouldEscapeText)) { + return replaceMarkdownCandidates(textToProcess, regex2, replacement, marker, canOpen); + } + return replaceTextWithExtras(textToProcess, regex2, EXTRAS_DEFAULT, replacement); + }; + } function replaceBlockElementWithNewLine(htmlString) { let splitText = htmlString.replaceAll(/
    > (|<\/div>||\n<\/comment>|<\/comment>|

    |<\/h1>|

    |<\/h2>|

    |<\/h3>|

    |<\/h4>|

    |<\/h5>|
    |<\/h6>|

    |<\/p>|

  • |<\/li>)/gi, "
    > ").split(/|<\/div>||\n<\/comment>|<\/comment>|

    |<\/h1>|

    |<\/h2>|

    |<\/h3>|

    |<\/h4>|

    |<\/h5>|
    |<\/h6>|

    |<\/p>|

  • |<\/li>|
    |<\/blockquote>/); const stripHTML = (text) => str_1.default.stripHTML(text); @@ -23795,9 +23893,9 @@ var require_ExpensiMark = __commonJS({ */ { name: "autolink", - process: (textToProcess, replacement) => { + process: (textToProcess, replacement, _shouldKeepRawInput, shouldEscapeText) => { const regex2 = new RegExp(`(?![^<]*>|[^<>]*<\\/(?!h1>))([_*~]*?)${UrlPatterns.MARKDOWN_URL_REGEX}\\1(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`, "gi"); - return this.modifyTextForUrlLinks(regex2, textToProcess, replacement, true); + return this.modifyTextForUrlLinks(regex2, textToProcess, replacement, canUseCandidateScanning(textToProcess, shouldEscapeText)); }, replacement: (_extras, _match, g1, g2) => { const href = str_1.default.sanitizeURL(g2); @@ -23866,7 +23964,8 @@ ${"
    ".repeat(i)}`, "\n"); name: "autoEmail", regex: new RegExp(`([^\\w'#%+-]|^)${Constants.CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<>]*<\\/(?!em|h1|blockquote))`, "gim"), replacement: '$1$2', - rawInputReplacement: '$1$2' + rawInputReplacement: '$1$2', + shouldSkipProcessing: (textToCheck) => !textToCheck.includes("@") }, /** * This regex matches a short user mention in a string. @@ -23902,7 +24001,7 @@ ${"
    ".repeat(i)}`, "\n"); // \B will match everything that \b doesn't, so it works // for * and ~: https://www.rexegg.com/regex-boundaries.html#notb name: "bold", - process: (textToProcess, replacement) => replaceMarkdownCandidates(textToProcess, BOLD_MARKDOWN_REGEX, replacement, "*", canOpenBoldMarkdown), + process: processMarkdownRule(BOLD_MARKDOWN_REGEX, "*", canOpenBoldMarkdown), replacement: (_extras, match, g1, g2) => { if (g1.includes("_")) { return `${g1}${g2}`; @@ -23912,7 +24011,7 @@ ${"
    ".repeat(i)}`, "\n"); }, { name: "strikethrough", - process: (textToProcess, replacement) => replaceMarkdownCandidates(textToProcess, STRIKETHROUGH_MARKDOWN_REGEX, replacement, "~", canOpenStrikethroughMarkdown), + process: processMarkdownRule(STRIKETHROUGH_MARKDOWN_REGEX, "~", canOpenStrikethroughMarkdown), replacement: (_extras, match, g1) => g1.includes("") || containsNonPairTag(g1) ? match : `${g1}` }, { @@ -24281,7 +24380,7 @@ ${g2} } const replacement = shouldKeepRawInput && rule.rawInputReplacement ? rule.rawInputReplacement : rule.replacement; if ("process" in rule) { - replacedText = rule.process(replacedText, replacement, shouldKeepRawInput); + replacedText = rule.process(replacedText, replacement, shouldKeepRawInput, shouldEscapeText); } else { replacedText = replaceTextWithExtras(replacedText, rule.regex, extras, replacement); } diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js index 0d6eafcdd4f6..126b34462df4 100644 --- a/.github/actions/javascript/proposalPoliceComment/index.js +++ b/.github/actions/javascript/proposalPoliceComment/index.js @@ -24247,7 +24247,11 @@ var require_CONST = __commonJS({ SHIP_CARD: "ship_card", REPORT_CARD_FRAUD: "report_card_fraud", ISSUE_CARD: "issue_card", - UPDATE_CARD: "update_card" + UPDATE_CARD: "update_card", + UPDATE_PERSONAL_DETAILS: "update_personal_details", + ADD_DELEGATE: "add_delegate", + UPDATE_DELEGATE: "update_delegate", + CHANGE_PRIMARY_LOGIN: "change_primary_login" }, EXPENSIFY_CARD: { FEED_NAME: "Expensify Card", @@ -43922,6 +43926,7 @@ var require_ExpensiMark = __commonJS({ var Constants = __importStar(require_CONST()); var UrlPatterns = __importStar(require_Url()); var Logger_1 = __importDefault(require_Logger()); + var tlds_1 = __importDefault(require_tlds()); var Utils = __importStar(require_utils2()); var EXTRAS_DEFAULT = {}; var ASCII_DIGIT_START = "0".charCodeAt(0); @@ -43934,6 +43939,9 @@ var require_ExpensiMark = __commonJS({ var NON_BREAKING_SPACE_CODE = 160; var URL_PROTOCOLS = ["https://", "http://", "ftps://", "ftp://"]; var URL_CANDIDATE_PREFIX_CHARACTERS = "@_*~"; + var URL_TLD_LIST = tlds_1.default.toLowerCase().split("|"); + var URL_TLDS = new Set(URL_TLD_LIST); + var MAX_URL_TLD_LENGTH = Math.max(...URL_TLD_LIST.map((tld) => tld.length)); var PROTECTED_TAG_NAMES = /* @__PURE__ */ new Set(["a", "code", "pre", "video"]); var MARKDOWN_LINK_REGEX = new RegExp(`\\[((?:[^\\[\\]\\r\\n]*(?:\\[[^\\[\\]\\r\\n]*][^\\[\\]\\r\\n]*)*))]\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, "gi"); var MARKDOWN_IMAGE_REGEX = new RegExp(`\\!(?:\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)])?\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, "gi"); @@ -43979,6 +43987,9 @@ var require_ExpensiMark = __commonJS({ } return text.replace(regexp, replacement); } + function canUseCandidateScanning(text, shouldEscapeText) { + return shouldEscapeText || !text.includes("<") && !text.includes(">"); + } function isAsciiAlphaNumeric(character) { if (!character) { return false; @@ -44050,15 +44061,59 @@ var require_ExpensiMark = __commonJS({ } return URL_PROTOCOLS.find((protocol) => text.slice(position, position + protocol.length).toLowerCase() === protocol); } - function findHostnameEnd(text, hostnameStart, dotPosition) { - let hostnameEnd = dotPosition + 1; - while (hostnameEnd < text.length && (isAsciiAlphaNumeric(text[hostnameEnd]) || text[hostnameEnd] === "-")) { - hostnameEnd++; + function isValidHostnameLabel(text, start, end) { + if (start >= end || !isAsciiAlphaNumeric(text[start]) || !isAsciiAlphaNumeric(text[end - 1])) { + return false; } - if (hostnameStart === dotPosition || hostnameEnd === dotPosition + 1) { - return void 0; + for (let index = start + 1; index < end - 1; index++) { + if (!isAsciiAlphaNumeric(text[index]) && text[index] !== "-") { + return false; + } + } + return true; + } + function findHostnameStart(text, dotPosition) { + let hostnameStart = dotPosition; + let labelEnd = dotPosition; + while (labelEnd > 0) { + let rawLabelStart = labelEnd - 1; + while (rawLabelStart >= 0 && text[rawLabelStart] !== "." && isHostnameCharacter(text[rawLabelStart])) { + rawLabelStart--; + } + rawLabelStart++; + let labelStart = rawLabelStart; + while (labelStart < labelEnd && text[labelStart] === "-") { + labelStart++; + } + if (!isValidHostnameLabel(text, labelStart, labelEnd)) { + break; + } + hostnameStart = labelStart; + if (labelStart !== rawLabelStart) { + break; + } + const separatorPosition = labelStart - 1; + if (separatorPosition < 0 || text[separatorPosition] !== ".") { + break; + } + labelEnd = separatorPosition; } - return hostnameEnd; + return hostnameStart === dotPosition ? void 0 : hostnameStart; + } + function findKnownTldEnd(text, dotPosition) { + const maximumEnd = Math.min(text.length, dotPosition + 1 + MAX_URL_TLD_LENGTH); + for (let end = dotPosition + 2; end <= maximumEnd; end++) { + const currentCharacter = text[end - 1]; + if (!isAsciiAlphaNumeric(currentCharacter) && currentCharacter !== "-") { + break; + } + const nextCharacter = text[end]; + const hasValidBoundary = !nextCharacter || nextCharacter === ":" || nextCharacter === "_" || !isWordCharacter(nextCharacter); + if (hasValidBoundary && URL_TLDS.has(text.slice(dotPosition + 1, end).toLowerCase())) { + return end; + } + } + return void 0; } function extendUrlCandidateBoundaries(text, start, end) { let candidateStart = start; @@ -44071,24 +44126,65 @@ var require_ExpensiMark = __commonJS({ } return { start: candidateStart, end: candidateEnd }; } + function startsWithIgnoreCase(text, expected, position) { + return text.slice(position, position + expected.length).toLowerCase() === expected; + } + function filterUrlCandidatesBlockedByFollowingHtml(text, candidates) { + var _a3; + if (candidates.length === 0 || !text.includes("<") && !text.includes(">")) { + return candidates; + } + const validCandidates = []; + let candidateIndex = candidates.length - 1; + let nextLessThan = text.length; + let nextGreaterThan = text.length; + let nextOpeningAnchor = text.length; + let nextClosingAnchor = text.length; + for (let index = text.length; index >= 0 && candidateIndex >= 0; index--) { + if (text[index] === "<") { + nextLessThan = index; + if (((_a3 = text[index + 1]) === null || _a3 === void 0 ? void 0 : _a3.toLowerCase()) === "a") { + nextOpeningAnchor = index; + } else if (startsWithIgnoreCase(text, "", index)) { + nextClosingAnchor = index; + } + } else if (text[index] === ">") { + nextGreaterThan = index; + } + while (candidateIndex >= 0 && candidates[candidateIndex].end === index) { + const firstHtmlBoundaryIsClosingTag = nextLessThan < nextGreaterThan && text.startsWith("", nextLessThan); + const firstTagIsProtectedClosingTag = startsWithIgnoreCase(text, "", nextLessThan) || startsWithIgnoreCase(text, "", nextLessThan); + const isBlockedByFollowingHtml = ( + // Mirrors `(?![^<]*>)`: reject when `>` appears before the next `<`. + nextGreaterThan < nextLessThan || // Mirrors `[^<>]*<\/(?!h1>)`: reject a later closing tag other than `
  • `. + firstHtmlBoundaryIsClosingTag || // Mirrors `((?:(?!`: reject `` unless another `` appears first. + nextClosingAnchor < nextOpeningAnchor || // Mirrors `[^<]*(<\/pre>|<\/code>)`: reject a later protected closing tag. + firstTagIsProtectedClosingTag + ); + if (!isBlockedByFollowingHtml) { + validCandidates.push(candidates[candidateIndex]); + } + candidateIndex--; + } + } + return validCandidates.reverse(); + } function findUrlCandidates(text) { const candidates = []; const protectedTags = []; let index = 0; - let hostnameRunStart = 0; while (index < text.length) { if (text[index] === "<") { const nextIndex = updateProtectedTagStack(text, index, protectedTags); if (nextIndex === void 0) { - break; + index++; + continue; } index = nextIndex; - hostnameRunStart = index; continue; } if (protectedTags.length > 0) { index++; - hostnameRunStart = index; continue; } const matchedProtocol = getProtocolAt(text, index); @@ -44096,29 +44192,23 @@ var require_ExpensiMark = __commonJS({ const candidate2 = extendUrlCandidateBoundaries(text, index, index + matchedProtocol.length); candidates.push(candidate2); index = candidate2.end; - hostnameRunStart = candidate2.end; - continue; - } - if (!isHostnameCharacter(text[index])) { - hostnameRunStart = index + 1; - index++; continue; } if (text[index] !== ".") { index++; continue; } - const hostnameEnd = findHostnameEnd(text, hostnameRunStart, index); - if (hostnameEnd === void 0) { + const tldEnd = findKnownTldEnd(text, index); + const hostnameStart = tldEnd === void 0 ? void 0 : findHostnameStart(text, index); + if (tldEnd === void 0 || hostnameStart === void 0) { index++; continue; } - const candidate = extendUrlCandidateBoundaries(text, hostnameRunStart, hostnameEnd); + const candidate = extendUrlCandidateBoundaries(text, hostnameStart, tldEnd); candidates.push(candidate); index = candidate.end; - hostnameRunStart = candidate.end; } - return candidates; + return filterUrlCandidatesBlockedByFollowingHtml(text, candidates); } function replaceMarkdownCandidates(text, regexp, replacement, marker, canOpen) { if (!text.includes(marker)) { @@ -44191,6 +44281,14 @@ var require_ExpensiMark = __commonJS({ output.push(text.slice(outputStart)); return output.join(""); } + function processMarkdownRule(regex2, marker, canOpen) { + return (textToProcess, replacement, _shouldKeepRawInput, shouldEscapeText) => { + if (canUseCandidateScanning(textToProcess, shouldEscapeText)) { + return replaceMarkdownCandidates(textToProcess, regex2, replacement, marker, canOpen); + } + return replaceTextWithExtras(textToProcess, regex2, EXTRAS_DEFAULT, replacement); + }; + } function replaceBlockElementWithNewLine(htmlString) { let splitText = htmlString.replaceAll(/
    > (|<\/div>||\n<\/comment>|<\/comment>|

    |<\/h1>|

    |<\/h2>|

    |<\/h3>|

    |<\/h4>|

    |<\/h5>|
    |<\/h6>|

    |<\/p>|

  • |<\/li>)/gi, "
    > ").split(/|<\/div>||\n<\/comment>|<\/comment>|

    |<\/h1>|

    |<\/h2>|

    |<\/h3>|

    |<\/h4>|

    |<\/h5>|
    |<\/h6>|

    |<\/p>|

  • |<\/li>|
    |<\/blockquote>/); const stripHTML = (text) => str_1.default.stripHTML(text); @@ -44609,9 +44707,9 @@ var require_ExpensiMark = __commonJS({ */ { name: "autolink", - process: (textToProcess, replacement) => { + process: (textToProcess, replacement, _shouldKeepRawInput, shouldEscapeText) => { const regex2 = new RegExp(`(?![^<]*>|[^<>]*<\\/(?!h1>))([_*~]*?)${UrlPatterns.MARKDOWN_URL_REGEX}\\1(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`, "gi"); - return this.modifyTextForUrlLinks(regex2, textToProcess, replacement, true); + return this.modifyTextForUrlLinks(regex2, textToProcess, replacement, canUseCandidateScanning(textToProcess, shouldEscapeText)); }, replacement: (_extras, _match, g1, g2) => { const href = str_1.default.sanitizeURL(g2); @@ -44680,7 +44778,8 @@ ${"
    ".repeat(i)}`, "\n"); name: "autoEmail", regex: new RegExp(`([^\\w'#%+-]|^)${Constants.CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<>]*<\\/(?!em|h1|blockquote))`, "gim"), replacement: '$1$2', - rawInputReplacement: '$1$2' + rawInputReplacement: '$1$2', + shouldSkipProcessing: (textToCheck) => !textToCheck.includes("@") }, /** * This regex matches a short user mention in a string. @@ -44716,7 +44815,7 @@ ${"
    ".repeat(i)}`, "\n"); // \B will match everything that \b doesn't, so it works // for * and ~: https://www.rexegg.com/regex-boundaries.html#notb name: "bold", - process: (textToProcess, replacement) => replaceMarkdownCandidates(textToProcess, BOLD_MARKDOWN_REGEX, replacement, "*", canOpenBoldMarkdown), + process: processMarkdownRule(BOLD_MARKDOWN_REGEX, "*", canOpenBoldMarkdown), replacement: (_extras, match2, g1, g2) => { if (g1.includes("_")) { return `${g1}${g2}`; @@ -44726,7 +44825,7 @@ ${"
    ".repeat(i)}`, "\n"); }, { name: "strikethrough", - process: (textToProcess, replacement) => replaceMarkdownCandidates(textToProcess, STRIKETHROUGH_MARKDOWN_REGEX, replacement, "~", canOpenStrikethroughMarkdown), + process: processMarkdownRule(STRIKETHROUGH_MARKDOWN_REGEX, "~", canOpenStrikethroughMarkdown), replacement: (_extras, match2, g1) => g1.includes("") || containsNonPairTag(g1) ? match2 : `${g1}` }, { @@ -45095,7 +45194,7 @@ ${g2} } const replacement = shouldKeepRawInput && rule.rawInputReplacement ? rule.rawInputReplacement : rule.replacement; if ("process" in rule) { - replacedText = rule.process(replacedText, replacement, shouldKeepRawInput); + replacedText = rule.process(replacedText, replacement, shouldKeepRawInput, shouldEscapeText); } else { replacedText = replaceTextWithExtras(replacedText, rule.regex, extras, replacement); } diff --git a/.github/libs/GithubUtils.ts b/.github/libs/GithubUtils.ts index e76eab58af06..e3a6868ebe93 100644 --- a/.github/libs/GithubUtils.ts +++ b/.github/libs/GithubUtils.ts @@ -503,14 +503,12 @@ class GithubUtils { core.info(`🎉 Successfully fetched ${allCommits.length} total commits`); core.endGroup(); console.log(''); - return allCommits.map( - (commit): CommitType => ({ - commit: commit.sha, - subject: commit.commit.message, - authorName: commit.commit.author?.name ?? 'Unknown', - date: commit.commit.committer?.date ?? '', - }), - ); + return allCommits.map((commit): CommitType => ({ + commit: commit.sha, + subject: commit.commit.message, + authorName: commit.commit.author?.name ?? 'Unknown', + date: commit.commit.committer?.date ?? '', + })); } catch (error) { if (error instanceof RequestError && error.status === 404) { core.error( diff --git a/scripts/lint/eslint/ESLintLinter.ts b/scripts/lint/eslint/ESLintLinter.ts index a95b86780322..0f9276c0c1dc 100644 --- a/scripts/lint/eslint/ESLintLinter.ts +++ b/scripts/lint/eslint/ESLintLinter.ts @@ -49,20 +49,18 @@ function normalizeESLintResults(results: ESLintJSONResult[]): LintFileResult[] { return results.map((result) => ({ filePath: result.filePath, source: result.source, - messages: result.messages.map( - (message): LintMessage => ({ - filePath: result.filePath, - ruleID: message[ESLINT_RULE_ID_KEY], - severity: normalizeSeverity(message.severity), - message: message.message, - line: message.line ?? 0, - column: message.column ?? 0, - endLine: message.endLine, - endColumn: message.endColumn, - suggestions: message.suggestions, - fix: message.fix, - }), - ), + messages: result.messages.map((message): LintMessage => ({ + filePath: result.filePath, + ruleID: message[ESLINT_RULE_ID_KEY], + severity: normalizeSeverity(message.severity), + message: message.message, + line: message.line ?? 0, + column: message.column ?? 0, + endLine: message.endLine, + endColumn: message.endColumn, + suggestions: message.suggestions, + fix: message.fix, + })), })); } diff --git a/src/pages/workspace/getWorkspaceMenuItems.ts b/src/pages/workspace/getWorkspaceMenuItems.ts index 74e608fcd91f..d7c386d86769 100644 --- a/src/pages/workspace/getWorkspaceMenuItems.ts +++ b/src/pages/workspace/getWorkspaceMenuItems.ts @@ -191,7 +191,7 @@ function getWorkspaceMenuItems({ }; const highlightedPolicyFeature = getObjectKeys(policyFeatureStates).find((key) => policyFeatureStates[key] && !previousPendingFields?.[key] && policy?.pendingFields?.[key]); - const items: WorkspaceMenuItem[] = [ + const defaultItems: WorkspaceMenuItem[] = [ { translationKey: 'workspace.common.profile', icon: icons.Building, @@ -215,7 +215,8 @@ function getWorkspaceMenuItems({ screenName: SCREENS.WORKSPACE.ROOMS, sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.ROOMS, }, - ].filter((item) => !isGuest || item.screenName === SCREENS.WORKSPACE.PROFILE); + ]; + const items = defaultItems.filter((item) => !isGuest || item.screenName === SCREENS.WORKSPACE.PROFILE); if (isGroupPolicy(policy) && shouldShowProtectedItems) { if (canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.REPORT_FIELDS)) { From 204e8c0ddc9c59afed8458dbda0dd1a03d2ef144 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:12:55 +0530 Subject: [PATCH 05/41] Restore main formatting on unrelated workflow files --- .github/libs/GithubUtils.ts | 14 ++++++++------ scripts/lint/eslint/ESLintLinter.ts | 26 ++++++++++++++------------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.github/libs/GithubUtils.ts b/.github/libs/GithubUtils.ts index e3a6868ebe93..e76eab58af06 100644 --- a/.github/libs/GithubUtils.ts +++ b/.github/libs/GithubUtils.ts @@ -503,12 +503,14 @@ class GithubUtils { core.info(`🎉 Successfully fetched ${allCommits.length} total commits`); core.endGroup(); console.log(''); - return allCommits.map((commit): CommitType => ({ - commit: commit.sha, - subject: commit.commit.message, - authorName: commit.commit.author?.name ?? 'Unknown', - date: commit.commit.committer?.date ?? '', - })); + return allCommits.map( + (commit): CommitType => ({ + commit: commit.sha, + subject: commit.commit.message, + authorName: commit.commit.author?.name ?? 'Unknown', + date: commit.commit.committer?.date ?? '', + }), + ); } catch (error) { if (error instanceof RequestError && error.status === 404) { core.error( diff --git a/scripts/lint/eslint/ESLintLinter.ts b/scripts/lint/eslint/ESLintLinter.ts index 0f9276c0c1dc..a95b86780322 100644 --- a/scripts/lint/eslint/ESLintLinter.ts +++ b/scripts/lint/eslint/ESLintLinter.ts @@ -49,18 +49,20 @@ function normalizeESLintResults(results: ESLintJSONResult[]): LintFileResult[] { return results.map((result) => ({ filePath: result.filePath, source: result.source, - messages: result.messages.map((message): LintMessage => ({ - filePath: result.filePath, - ruleID: message[ESLINT_RULE_ID_KEY], - severity: normalizeSeverity(message.severity), - message: message.message, - line: message.line ?? 0, - column: message.column ?? 0, - endLine: message.endLine, - endColumn: message.endColumn, - suggestions: message.suggestions, - fix: message.fix, - })), + messages: result.messages.map( + (message): LintMessage => ({ + filePath: result.filePath, + ruleID: message[ESLINT_RULE_ID_KEY], + severity: normalizeSeverity(message.severity), + message: message.message, + line: message.line ?? 0, + column: message.column ?? 0, + endLine: message.endLine, + endColumn: message.endColumn, + suggestions: message.suggestions, + fix: message.fix, + }), + ), })); } From 396eb71b4907063644faebe6037772f1f331e6c3 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:34:10 +0530 Subject: [PATCH 06/41] Hide room description editing from guests --- src/libs/ReportUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 724b001d8d03..0ecc832a301a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -163,6 +163,7 @@ import { isPerDiemEnabled, isPolicyAdmin as isPolicyAdminPolicyUtils, isPolicyAuditor, + isPolicyGuest, isPolicyOwner, isSubmitAndClose, isSubmitterApproveBlockedOnSubmitWorkspace, @@ -11784,7 +11785,8 @@ function canEditReportDescription(report: OnyxEntry, policy: OnyxEntry

    Date: Tue, 29 Sep 2026 21:15:39 +0530 Subject: [PATCH 07/41] Extract shared getHighestPolicyRole for telemetry role tagging --- src/libs/Fullstory/utils.ts | 18 ++++++------------ src/libs/PolicyUtils.ts | 21 +++++++++++++++++++++ src/libs/telemetry/TelemetrySynchronizer.ts | 16 ++-------------- tests/unit/FullstoryUtilsTest.ts | 2 ++ 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/libs/Fullstory/utils.ts b/src/libs/Fullstory/utils.ts index 92f4e72ffd68..1ffe106b7268 100644 --- a/src/libs/Fullstory/utils.ts +++ b/src/libs/Fullstory/utils.ts @@ -1,5 +1,5 @@ import {filterObject} from '@libs/ObjectUtils'; -import {getActivePolicies, isControlPolicy} from '@libs/PolicyUtils'; +import {getActivePolicies, getHighestPolicyRole, isControlPolicy} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -142,20 +142,14 @@ function getOnboardingStep(onboardingPath: string | undefined, hasCompletedOnboa } function getUserRole(activePolicies: Policy[]): FullstoryUserVars['user_role'] { - let userRole: FullstoryUserVars['user_role'] = activePolicies.some((policy) => policy?.role === CONST.POLICY.ROLE.GUEST) ? 'guest' : 'member'; + const highestRole = getHighestPolicyRole(activePolicies); - for (const policy of activePolicies) { - if (policy?.role === CONST.POLICY.ROLE.ADMIN) { - return 'admin'; - } - if (policy?.role === CONST.POLICY.ROLE.AUDITOR) { - userRole = 'auditor'; - } else if (userRole !== 'auditor' && policy?.role !== CONST.POLICY.ROLE.GUEST) { - userRole = 'member'; - } + // Fullstory calls a workspace member 'member' rather than 'user' + if (highestRole === CONST.POLICY.ROLE.USER) { + return 'member'; } - return userRole; + return highestRole; } function getPlanType(policies: Policy[]): FullstoryUserVars['plan_type'] { diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index bad71c004fb4..997a892c9450 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -866,6 +866,26 @@ const isPolicyUser = (policy: OnyxInputOrEntry, currentUserLogin?: strin */ const isPolicyGuest = (policy: OnyxInputOrEntry, currentUserLogin?: string): boolean => getPolicyRole(policy, currentUserLogin) === CONST.POLICY.ROLE.GUEST; +/** + * Returns the highest-privilege role held across the given policies. + * Ranking is admin > auditor > user > guest, so a member on any workspace is not reported as a guest. + */ +function getHighestPolicyRole( + policyList: Array>>, +): typeof CONST.POLICY.ROLE.ADMIN | typeof CONST.POLICY.ROLE.AUDITOR | typeof CONST.POLICY.ROLE.USER | typeof CONST.POLICY.ROLE.GUEST { + const roles = policyList.map((policy) => policy?.role); + if (roles.includes(CONST.POLICY.ROLE.ADMIN)) { + return CONST.POLICY.ROLE.ADMIN; + } + if (roles.includes(CONST.POLICY.ROLE.AUDITOR)) { + return CONST.POLICY.ROLE.AUDITOR; + } + if (roles.length > 0 && roles.every((role) => role === CONST.POLICY.ROLE.GUEST)) { + return CONST.POLICY.ROLE.GUEST; + } + return CONST.POLICY.ROLE.USER; +} + /** * Checks if the current user is an auditor of the policy */ @@ -3956,6 +3976,7 @@ export { getCustomUnitsForDuplication, getCountOfRequiredTagLists, getActiveEmployeeWorkspaces, + getHighestPolicyRole, getPolicyRole, hasIndependentTags, hasPerTagListRequired, diff --git a/src/libs/telemetry/TelemetrySynchronizer.ts b/src/libs/telemetry/TelemetrySynchronizer.ts index 973af3763218..a529a3fa911a 100644 --- a/src/libs/telemetry/TelemetrySynchronizer.ts +++ b/src/libs/telemetry/TelemetrySynchronizer.ts @@ -1,4 +1,4 @@ -import {getActivePolicies} from '@libs/PolicyUtils'; +import {getActivePolicies, getHighestPolicyRole} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -177,19 +177,7 @@ function sendPoliciesContext() { return; } const activePolicies = getActivePolicies(policies, session.email).map((policy) => policy.id); - - let userRole: string = Object.values(policies).some((policy) => policy?.role === CONST.POLICY.ROLE.GUEST) ? CONST.POLICY.ROLE.GUEST : CONST.POLICY.ROLE.USER; - for (const policy of Object.values(policies)) { - if (policy?.role === CONST.POLICY.ROLE.ADMIN) { - userRole = CONST.POLICY.ROLE.ADMIN; - break; - } - if (policy?.role === CONST.POLICY.ROLE.AUDITOR) { - userRole = CONST.POLICY.ROLE.AUDITOR; - } else if (userRole !== CONST.POLICY.ROLE.AUDITOR && policy?.role !== CONST.POLICY.ROLE.GUEST) { - userRole = CONST.POLICY.ROLE.USER; - } - } + const userRole = getHighestPolicyRole(Object.values(policies)); const policiesCountBucket = bucketPolicyCount(activePolicies.length); Sentry.setTag(CONST.TELEMETRY.TAGS.ACTIVE_POLICY, activePolicyID); diff --git a/tests/unit/FullstoryUtilsTest.ts b/tests/unit/FullstoryUtilsTest.ts index 4d50be1a8e94..c7b2122667c3 100644 --- a/tests/unit/FullstoryUtilsTest.ts +++ b/tests/unit/FullstoryUtilsTest.ts @@ -104,6 +104,7 @@ describe('FullstoryUtils', () => { it('reports Guest as the lowest workspace role', () => { const guestPolicy = {...createRandomPolicy(1, CONST.POLICY.TYPE.CORPORATE), role: CONST.POLICY.ROLE.GUEST}; const auditorPolicy = {...createRandomPolicy(2, CONST.POLICY.TYPE.CORPORATE), role: CONST.POLICY.ROLE.AUDITOR}; + const memberPolicy = {...createRandomPolicy(3, CONST.POLICY.TYPE.CORPORATE), role: CONST.POLICY.ROLE.USER}; const commonInput = { account: {isFromPublicDomain: true}, activePolicy: guestPolicy, @@ -118,5 +119,6 @@ describe('FullstoryUtils', () => { expect(buildFullstoryUserVars({...commonInput, policies: {policy_1: guestPolicy}}).user_role).toBe('guest'); expect(buildFullstoryUserVars({...commonInput, policies: {policy_1: guestPolicy, policy_2: auditorPolicy}}).user_role).toBe('auditor'); + expect(buildFullstoryUserVars({...commonInput, policies: {policy_1: guestPolicy, policy_3: memberPolicy}}).user_role).toBe('member'); }); }); From 0bd07211f040d532e8e80aa004f73b26e3944817 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 29 Sep 2026 21:18:09 +0530 Subject: [PATCH 08/41] Update PolicyUtils.ts --- src/libs/PolicyUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 997a892c9450..5c37bb03864b 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -868,7 +868,7 @@ const isPolicyGuest = (policy: OnyxInputOrEntry, currentUserLogin?: stri /** * Returns the highest-privilege role held across the given policies. - * Ranking is admin > auditor > user > guest, so a member on any workspace is not reported as a guest. + * Ranking is admin > auditor > user > guest. */ function getHighestPolicyRole( policyList: Array>>, From 7cfd2d91182db64cbde3491c2ecebe68c0011462 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 29 Sep 2026 21:57:18 +0530 Subject: [PATCH 09/41] Add translations --- src/languages/de.ts | 10 +++++----- src/languages/el.ts | 8 +++++--- src/languages/es.ts | 8 ++++---- src/languages/fr.ts | 8 ++++---- src/languages/ja.ts | 4 ++-- src/languages/nl.ts | 6 +++--- src/languages/pl.ts | 8 ++++---- src/languages/pt-BR.ts | 8 ++++---- src/languages/zh-hans.ts | 6 +++--- 9 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index d2990230211a..ddc3b32ded58 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4826,9 +4826,9 @@ ${amount} für ${merchant} – ${date}`, roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: - return 'Eigentümer'; + return 'Inhaber'; case CONST.POLICY.ROLE.ADMIN: - return 'Workspace-Administrator'; + return 'Workspace-Admin'; case CONST.POLICY.ROLE.AUDITOR: return 'Prüfer'; case CONST.POLICY.ROLE.GUEST: @@ -4838,7 +4838,7 @@ ${amount} für ${merchant} – ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Kartenverwaltung'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Personalverwaltung'; + return 'Personenverwaltung'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Zahlungsadministrator'; case CONST.POLICY.ROLE.USER: @@ -7095,8 +7095,8 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, other: 'Prüfende hinzufügen', }), makeGuest: () => ({ - one: 'Zum Gast machen', - other: 'Zu Gästen machen', + one: 'Als Gast hinzufügen', + other: 'Gäste erstellen', }), makePeopleAdmin: () => ({ one: 'Zum Personaladministrator machen', diff --git a/src/languages/el.ts b/src/languages/el.ts index 7d893ba69b66..d67937ea79ef 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -4909,6 +4909,8 @@ ${amount} για ${merchant} - ${date}`, return 'Διαχειριστής χώρου εργασίας'; case CONST.POLICY.ROLE.AUDITOR: return 'Ελεγκτής'; + case CONST.POLICY.ROLE.GUEST: + return 'Επισκέπτης'; case CONST.POLICY.ROLE.EDITOR: return 'Επεξεργαστής'; case CONST.POLICY.ROLE.CARD_ADMIN: @@ -4916,7 +4918,7 @@ ${amount} για ${merchant} - ${date}`, case CONST.POLICY.ROLE.PEOPLE_ADMIN: return 'Διαχείριση προσώπων'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: - return 'διαχείριση πληρωμών'; + return 'Διαχείριση πληρωμών'; case CONST.POLICY.ROLE.USER: return 'Μέλος'; default: @@ -7221,8 +7223,8 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ other: 'Δημιουργία ελεγκτών', }), makeGuest: () => ({ - one: 'Ορισμός ως επισκέπτη', - other: 'Ορισμός ως επισκέπτες', + one: 'Κάντε επισκέπτη', + other: 'Κάντε επισκέπτες', }), makeCardAdmin: () => ({ one: 'Ορισμός διαχειριστή κάρτας', diff --git a/src/languages/es.ts b/src/languages/es.ts index 730544fcad00..4e04f4b3b46e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4748,7 +4748,7 @@ ${amount} para ${merchant} - ${date}`, memberAlternateText: 'Presentar y aprobar informes.', adminAlternateText: 'Gestionar informes y configuración del área de trabajo.', auditorAlternateText: 'Ver y comentar los informes.', - guestAlternateText: 'Enviar y aprobar informes, con chats solo por invitación.', + guestAlternateText: 'Envía y aprueba informes, con chats solo por invitación.', reimbursementChoice: { [CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES]: 'Directo', [CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO]: 'Ninguno', @@ -4759,7 +4759,7 @@ ${amount} para ${merchant} - ${date}`, case CONST.POLICY.ROLE.OWNER: return 'Propietario'; case CONST.POLICY.ROLE.ADMIN: - return 'Administrador del espacio de trabajo'; + return 'Administrador de espacio de trabajo'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; case CONST.POLICY.ROLE.GUEST: @@ -4769,7 +4769,7 @@ ${amount} para ${merchant} - ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Administrador de tarjeta'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Administrador de personas'; + return 'Administración de personas'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Administrador de pagos'; case CONST.POLICY.ROLE.USER: @@ -6962,7 +6962,7 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, other: 'Convertir en auditores', }), makeGuest: () => ({ - one: 'Convertir en invitado', + one: 'Hacer invitado', other: 'Convertir en invitados', }), makePeopleAdmin: () => ({ diff --git a/src/languages/fr.ts b/src/languages/fr.ts index eae118a07ac7..a2476c1332cc 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4829,7 +4829,7 @@ ${amount} pour ${merchant} - ${date}`, memberAlternateText: 'Soumettre et approuver des notes de frais.', adminAlternateText: 'Gérer les notes de frais et les paramètres de l’espace de travail.', auditorAlternateText: 'Afficher et commenter les notes de frais.', - guestAlternateText: 'Envoyer et approuver des rapports, avec des discussions sur invitation uniquement.', + guestAlternateText: 'Soumettre et approuver des notes de frais, avec des discussions sur invitation uniquement.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4843,7 +4843,7 @@ ${amount} pour ${merchant} - ${date}`, case CONST.POLICY.ROLE.EDITOR: return 'Éditeur'; case CONST.POLICY.ROLE.CARD_ADMIN: - return 'Administrateur de carte'; + return 'Administrateur de la carte'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: return 'Administration des personnes'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: @@ -7114,8 +7114,8 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, other: 'Créer des auditeurs', }), makeGuest: () => ({ - one: 'Nommer invité', - other: 'Nommer invités', + one: 'Rendre invité', + other: 'Rendre invités', }), makePeopleAdmin: () => ({ one: 'Nommer administrateur des personnes', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 6248b89583af..74fd7d1e8a08 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4754,7 +4754,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' memberAlternateText: 'レポートを提出して承認します。', adminAlternateText: 'レポートとワークスペースの設定を管理します。', auditorAlternateText: 'レポートを表示してコメントします。', - guestAlternateText: 'レポートを提出および承認し、招待されたチャットのみを使用します。', + guestAlternateText: 'レポートの提出と承認ができ、招待制チャットも利用できます。', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -6965,7 +6965,7 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から }), makeGuest: () => ({ one: 'ゲストにする', - other: 'ゲストにする', + other: 'ゲストを作成', }), makePeopleAdmin: () => ({ one: 'People 管理者にする', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index c567a693fc50..7fdf6dc21a78 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4793,13 +4793,13 @@ ${amount} voor ${merchant} - ${date}`, memberAlternateText: 'Dien rapporten in en keur ze goed.', adminAlternateText: 'Beheer rapporten en werkruimte-instellingen.', auditorAlternateText: 'Rapporten bekijken en erop reageren.', - guestAlternateText: 'Rapporten indienen en goedkeuren, met chats alleen op uitnodiging.', + guestAlternateText: 'Dien rapporten in en keur ze goed, met chats op uitnodiging.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: return 'Eigenaar'; case CONST.POLICY.ROLE.ADMIN: - return 'Beheerder van werkruimte'; + return 'Werkruimtebeheerder'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; case CONST.POLICY.ROLE.GUEST: @@ -4809,7 +4809,7 @@ ${amount} voor ${merchant} - ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Kaartbeheer'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Beheer personen'; + return 'Personenbeheer'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Beheerder betalingen'; case CONST.POLICY.ROLE.USER: diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 47f6750c0401..eddbe0a57baf 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4821,7 +4821,7 @@ ${amount} dla ${merchant} - ${date}`, memberAlternateText: 'Przesyłaj i zatwierdzaj raporty.', adminAlternateText: 'Zarządzaj raportami i ustawieniami przestrzeni roboczej.', auditorAlternateText: 'Przeglądaj i komentuj raporty.', - guestAlternateText: 'Przesyłaj i zatwierdzaj raporty, korzystając tylko z czatów na zaproszenie.', + guestAlternateText: 'Wysyłaj i zatwierdzaj raporty, z czatami tylko na zaproszenie.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4837,7 +4837,7 @@ ${amount} dla ${merchant} - ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Administrator karty'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Administrator osób'; + return 'Zarządzanie personelem'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Administrator płatności'; case CONST.POLICY.ROLE.USER: @@ -7063,8 +7063,8 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, other: 'Utwórz audytorów', }), makeGuest: () => ({ - one: 'Ustaw jako gościa', - other: 'Ustaw jako gości', + one: 'Uczyń gościem', + other: 'Utwórz gości', }), makePeopleAdmin: () => ({ one: 'Ustaw jako administratora osób', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 84f6d83a7515..ab9a27f8d453 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4784,13 +4784,13 @@ ${amount} para ${merchant} - ${date}`, memberAlternateText: 'Enviar e aprovar relatórios.', adminAlternateText: 'Gerencie relatórios e configurações do espaço de trabalho.', auditorAlternateText: 'Visualize e comente relatórios.', - guestAlternateText: 'Envie e aprove relatórios, com chats somente por convite.', + guestAlternateText: 'Envie e aprove relatórios, com chats apenas por convite.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: return 'Proprietário'; case CONST.POLICY.ROLE.ADMIN: - return 'Administrador da área de trabalho'; + return 'Admin do workspace'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; case CONST.POLICY.ROLE.GUEST: @@ -4798,7 +4798,7 @@ ${amount} para ${merchant} - ${date}`, case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: - return 'Admin do cartão'; + return 'Administração de cartão'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: return 'Administração de pessoas'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: @@ -7041,7 +7041,7 @@ O plano Control começa em US$ 9 por membro ativo por mês.`, }), makeGuest: () => ({ one: 'Tornar convidado', - other: 'Tornar convidados', + other: 'Transformar em convidados', }), makePeopleAdmin: () => ({ one: 'Tornar administrador de pessoas', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 93527ee01637..cb1cf6ba91ac 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4639,7 +4639,7 @@ ${amount},商户:${merchant} - 日期:${date}`, memberAlternateText: '提交并审批报销报告。', adminAlternateText: '管理报表和工作区设置。', auditorAlternateText: '查看并评论报表。', - guestAlternateText: '提交和批准报表,仅使用受邀聊天。', + guestAlternateText: '提交和审批报销单,并使用仅限受邀的聊天。', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4651,11 +4651,11 @@ ${amount},商户:${merchant} - 日期:${date}`, case CONST.POLICY.ROLE.GUEST: return '访客'; case CONST.POLICY.ROLE.EDITOR: - return '编辑者'; + return '编辑器'; case CONST.POLICY.ROLE.CARD_ADMIN: return '卡片管理员'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return '人员管理员'; + return '人员管理'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return '付款管理员'; case CONST.POLICY.ROLE.USER: From f51dd0c1f7f71360509ecc5d0f8e8998fd868db5 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:54:42 +0530 Subject: [PATCH 10/41] Include all workspace roles in telemetry role ranking --- src/libs/Fullstory/types.ts | 2 +- src/libs/PolicyUtils.ts | 34 +++++++++++++-------- src/pages/DynamicReportDetailsPage.tsx | 7 ++--- src/pages/DynamicReportParticipantsPage.tsx | 3 +- tests/unit/FullstoryUtilsTest.ts | 23 ++++++++++++++ 5 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/libs/Fullstory/types.ts b/src/libs/Fullstory/types.ts index d759eaf6a346..d8f1b7c4620c 100644 --- a/src/libs/Fullstory/types.ts +++ b/src/libs/Fullstory/types.ts @@ -15,7 +15,7 @@ type FullstoryUserVars = { user_status?: 'new' | 'returning'; has_completed_onboarding?: boolean; onb_step?: 'registration' | 'accounting' | 'completed'; - user_role?: 'admin' | 'auditor' | 'member' | 'guest'; + user_role?: 'admin' | 'paymentsAdmin' | 'cardAdmin' | 'peopleAdmin' | 'auditor' | 'editor' | 'member' | 'guest'; workspace_state?: 'has_workspaces' | 'no_workspaces'; workspace_count?: number; workspace_member_count?: number; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 5c37bb03864b..6bbf624f991f 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -277,7 +277,7 @@ function canMemberAssignRole(policy: OnyxInputOrEntry, login: string, ro } // Reaching here: USER always, plus GUEST/AUDITOR only on corporate policies (control-only roles are - // already filtered out on non-corporate policies above). Assigning USER/AUDITOR needs the + // already filtered out on non-corporate policies above). Assigning USER/GUEST/AUDITOR needs the // MEMBERS permission, and only on corporate policies. const isNonElevatedRole = role === CONST.POLICY.ROLE.USER || role === CONST.POLICY.ROLE.GUEST || role === CONST.POLICY.ROLE.AUDITOR; return isCorporatePolicy && canMemberWrite(policy, login, CONST.POLICY.POLICY_FEATURE.MEMBERS) && isNonElevatedRole; @@ -866,22 +866,30 @@ const isPolicyUser = (policy: OnyxInputOrEntry, currentUserLogin?: strin */ const isPolicyGuest = (policy: OnyxInputOrEntry, currentUserLogin?: string): boolean => getPolicyRole(policy, currentUserLogin) === CONST.POLICY.ROLE.GUEST; +/** + * Workspace roles from highest to lowest privilege, matching the role hierarchy in Auth. + */ +const POLICY_ROLE_RANKING = [ + CONST.POLICY.ROLE.ADMIN, + CONST.POLICY.ROLE.PAYMENTS_ADMIN, + CONST.POLICY.ROLE.CARD_ADMIN, + CONST.POLICY.ROLE.PEOPLE_ADMIN, + CONST.POLICY.ROLE.AUDITOR, + CONST.POLICY.ROLE.EDITOR, + CONST.POLICY.ROLE.USER, + CONST.POLICY.ROLE.GUEST, +] as const; + /** * Returns the highest-privilege role held across the given policies. - * Ranking is admin > auditor > user > guest. + * Defaults to user when no ranked role is found, so a member on any workspace is not reported as a guest. */ -function getHighestPolicyRole( - policyList: Array>>, -): typeof CONST.POLICY.ROLE.ADMIN | typeof CONST.POLICY.ROLE.AUDITOR | typeof CONST.POLICY.ROLE.USER | typeof CONST.POLICY.ROLE.GUEST { +function getHighestPolicyRole(policyList: Array>>): (typeof POLICY_ROLE_RANKING)[number] { const roles = policyList.map((policy) => policy?.role); - if (roles.includes(CONST.POLICY.ROLE.ADMIN)) { - return CONST.POLICY.ROLE.ADMIN; - } - if (roles.includes(CONST.POLICY.ROLE.AUDITOR)) { - return CONST.POLICY.ROLE.AUDITOR; - } - if (roles.length > 0 && roles.every((role) => role === CONST.POLICY.ROLE.GUEST)) { - return CONST.POLICY.ROLE.GUEST; + for (const rankedRole of POLICY_ROLE_RANKING) { + if (roles.includes(rankedRole)) { + return rankedRole; + } } return CONST.POLICY.ROLE.USER; } diff --git a/src/pages/DynamicReportDetailsPage.tsx b/src/pages/DynamicReportDetailsPage.tsx index 206cf7389b56..ad816eff1acb 100644 --- a/src/pages/DynamicReportDetailsPage.tsx +++ b/src/pages/DynamicReportDetailsPage.tsx @@ -301,6 +301,8 @@ function DynamicReportDetailsPage({policy, report, route, reportMetadata, report const isThread = isThreadUtil(report); const shouldOpenRoomMembersPage = isUserCreatedPolicyRoom || isChatThread || (isPolicyExpenseChat && isPolicyAdmin); const participants = getParticipantsList(report, personalDetails, shouldOpenRoomMembersPage); + const shouldShowInviteMenuItem = + !isGuestAnnounceRoom && ((isUserCreatedPolicyRoom && (!participants.length || !isPolicyEmployee)) || ((isDefaultRoom || isPolicyExpenseChat) && isChatThread && !isPolicyEmployee)); let caseID: CaseID; if (isMoneyRequestReport || isInvoiceReport) { @@ -496,10 +498,7 @@ function DynamicReportDetailsPage({policy, report, route, reportMetadata, report } }, }); - } else if ( - !isGuestAnnounceRoom && - ((isUserCreatedPolicyRoom && (!participants.length || !isPolicyEmployee)) || ((isDefaultRoom || isPolicyExpenseChat) && isChatThread && !isPolicyEmployee)) - ) { + } else if (shouldShowInviteMenuItem) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.INVITE, translationKey: 'common.invite', diff --git a/src/pages/DynamicReportParticipantsPage.tsx b/src/pages/DynamicReportParticipantsPage.tsx index 57c58cc49d97..384146812119 100755 --- a/src/pages/DynamicReportParticipantsPage.tsx +++ b/src/pages/DynamicReportParticipantsPage.tsx @@ -92,6 +92,7 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr const isCurrentUserGroupChatAdmin = isGroupChat && isCurrentUserAdmin; const policy = usePolicy(report?.policyID); const isGuestAnnounceRoom = isAnnounceRoom(report) && isPolicyGuest(policy); + const shouldShowNotFoundView = !report || isArchivedNonExpenseReport(report, isReportArchived) || isSelfDM(report) || isGuestAnnounceRoom; const shouldShowInviteButton = canInviteMembersToReport(report, policy, isReportArchived, currentUserAccountID); const {isOffline} = useNetwork(); const canSelectMultiple = isGroupChat && isCurrentUserAdmin && (isSmallScreenWidth ? isMobileSelectionModeEnabled : true); @@ -271,7 +272,7 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr style={[styles.defaultModalContainer]} testID="DynamicReportParticipantsPage" > - + { diff --git a/tests/unit/FullstoryUtilsTest.ts b/tests/unit/FullstoryUtilsTest.ts index c7b2122667c3..8c8942176b4a 100644 --- a/tests/unit/FullstoryUtilsTest.ts +++ b/tests/unit/FullstoryUtilsTest.ts @@ -121,4 +121,27 @@ describe('FullstoryUtils', () => { expect(buildFullstoryUserVars({...commonInput, policies: {policy_1: guestPolicy, policy_2: auditorPolicy}}).user_role).toBe('auditor'); expect(buildFullstoryUserVars({...commonInput, policies: {policy_1: guestPolicy, policy_3: memberPolicy}}).user_role).toBe('member'); }); + + it('ranks scoped admin roles above auditor and member', () => { + const paymentsAdminPolicy = {...createRandomPolicy(1, CONST.POLICY.TYPE.CORPORATE), role: CONST.POLICY.ROLE.PAYMENTS_ADMIN}; + const cardAdminPolicy = {...createRandomPolicy(2, CONST.POLICY.TYPE.CORPORATE), role: CONST.POLICY.ROLE.CARD_ADMIN}; + const peopleAdminPolicy = {...createRandomPolicy(3, CONST.POLICY.TYPE.CORPORATE), role: CONST.POLICY.ROLE.PEOPLE_ADMIN}; + const auditorPolicy = {...createRandomPolicy(4, CONST.POLICY.TYPE.CORPORATE), role: CONST.POLICY.ROLE.AUDITOR}; + const memberPolicy = {...createRandomPolicy(5, CONST.POLICY.TYPE.CORPORATE), role: CONST.POLICY.ROLE.USER}; + const commonInput = { + account: {isFromPublicDomain: true}, + activePolicy: memberPolicy, + introSelected: undefined, + onboarding: undefined, + onboardingCompanySize: undefined, + onboardingLastVisitedPath: undefined, + onboardingPurposeSelected: undefined, + session: {email: 'test@test.com'}, + userMetadata: {}, + }; + + expect(buildFullstoryUserVars({...commonInput, policies: {policy_5: memberPolicy, policy_4: auditorPolicy, policy_3: peopleAdminPolicy}}).user_role).toBe('peopleAdmin'); + expect(buildFullstoryUserVars({...commonInput, policies: {policy_5: memberPolicy, policy_2: cardAdminPolicy, policy_3: peopleAdminPolicy}}).user_role).toBe('cardAdmin'); + expect(buildFullstoryUserVars({...commonInput, policies: {policy_5: memberPolicy, policy_1: paymentsAdminPolicy, policy_2: cardAdminPolicy}}).user_role).toBe('paymentsAdmin'); + }); }); From 6776ad8dba6bd169c3096ca902f1bdf512be2edc Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:01:30 +0530 Subject: [PATCH 11/41] Mock getHighestPolicyRole in TelemetrySynchronizer tests --- tests/unit/TelemetrySynchronizerTest.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/TelemetrySynchronizerTest.ts b/tests/unit/TelemetrySynchronizerTest.ts index 80be57b0114c..944e0626cd90 100644 --- a/tests/unit/TelemetrySynchronizerTest.ts +++ b/tests/unit/TelemetrySynchronizerTest.ts @@ -22,6 +22,7 @@ jest.mock('@sentry/react-native', () => ({ jest.mock('@libs/PolicyUtils', () => ({ getActivePolicies: jest.fn(), + getHighestPolicyRole: jest.fn(), })); jest.mock('@libs/telemetry/sendMemoryContext', () => ({ From 18c5176f972b57328b0e2708a9c0504586d7272c Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:15:38 +0530 Subject: [PATCH 12/41] Move getHighestPolicyRole to its own module and fix lint issues --- src/languages/de.ts | 10 +++--- src/languages/el.ts | 8 ++--- src/languages/es.ts | 8 ++--- src/languages/fr.ts | 8 ++--- src/languages/ja.ts | 4 +-- src/languages/nl.ts | 6 ++-- src/languages/pl.ts | 8 ++--- src/languages/pt-BR.ts | 8 ++--- src/languages/zh-hans.ts | 6 ++-- src/libs/Fullstory/utils.ts | 3 +- src/libs/PolicyUtils.ts | 29 ---------------- src/libs/getHighestPolicyRole.ts | 37 +++++++++++++++++++++ src/libs/telemetry/TelemetrySynchronizer.ts | 3 +- 13 files changed, 73 insertions(+), 65 deletions(-) create mode 100644 src/libs/getHighestPolicyRole.ts diff --git a/src/languages/de.ts b/src/languages/de.ts index ddc3b32ded58..d2990230211a 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4826,9 +4826,9 @@ ${amount} für ${merchant} – ${date}`, roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: - return 'Inhaber'; + return 'Eigentümer'; case CONST.POLICY.ROLE.ADMIN: - return 'Workspace-Admin'; + return 'Workspace-Administrator'; case CONST.POLICY.ROLE.AUDITOR: return 'Prüfer'; case CONST.POLICY.ROLE.GUEST: @@ -4838,7 +4838,7 @@ ${amount} für ${merchant} – ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Kartenverwaltung'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Personenverwaltung'; + return 'Personalverwaltung'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Zahlungsadministrator'; case CONST.POLICY.ROLE.USER: @@ -7095,8 +7095,8 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, other: 'Prüfende hinzufügen', }), makeGuest: () => ({ - one: 'Als Gast hinzufügen', - other: 'Gäste erstellen', + one: 'Zum Gast machen', + other: 'Zu Gästen machen', }), makePeopleAdmin: () => ({ one: 'Zum Personaladministrator machen', diff --git a/src/languages/el.ts b/src/languages/el.ts index d67937ea79ef..7d893ba69b66 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -4909,8 +4909,6 @@ ${amount} για ${merchant} - ${date}`, return 'Διαχειριστής χώρου εργασίας'; case CONST.POLICY.ROLE.AUDITOR: return 'Ελεγκτής'; - case CONST.POLICY.ROLE.GUEST: - return 'Επισκέπτης'; case CONST.POLICY.ROLE.EDITOR: return 'Επεξεργαστής'; case CONST.POLICY.ROLE.CARD_ADMIN: @@ -4918,7 +4916,7 @@ ${amount} για ${merchant} - ${date}`, case CONST.POLICY.ROLE.PEOPLE_ADMIN: return 'Διαχείριση προσώπων'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: - return 'Διαχείριση πληρωμών'; + return 'διαχείριση πληρωμών'; case CONST.POLICY.ROLE.USER: return 'Μέλος'; default: @@ -7223,8 +7221,8 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ other: 'Δημιουργία ελεγκτών', }), makeGuest: () => ({ - one: 'Κάντε επισκέπτη', - other: 'Κάντε επισκέπτες', + one: 'Ορισμός ως επισκέπτη', + other: 'Ορισμός ως επισκέπτες', }), makeCardAdmin: () => ({ one: 'Ορισμός διαχειριστή κάρτας', diff --git a/src/languages/es.ts b/src/languages/es.ts index 4e04f4b3b46e..730544fcad00 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4748,7 +4748,7 @@ ${amount} para ${merchant} - ${date}`, memberAlternateText: 'Presentar y aprobar informes.', adminAlternateText: 'Gestionar informes y configuración del área de trabajo.', auditorAlternateText: 'Ver y comentar los informes.', - guestAlternateText: 'Envía y aprueba informes, con chats solo por invitación.', + guestAlternateText: 'Enviar y aprobar informes, con chats solo por invitación.', reimbursementChoice: { [CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES]: 'Directo', [CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO]: 'Ninguno', @@ -4759,7 +4759,7 @@ ${amount} para ${merchant} - ${date}`, case CONST.POLICY.ROLE.OWNER: return 'Propietario'; case CONST.POLICY.ROLE.ADMIN: - return 'Administrador de espacio de trabajo'; + return 'Administrador del espacio de trabajo'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; case CONST.POLICY.ROLE.GUEST: @@ -4769,7 +4769,7 @@ ${amount} para ${merchant} - ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Administrador de tarjeta'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Administración de personas'; + return 'Administrador de personas'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Administrador de pagos'; case CONST.POLICY.ROLE.USER: @@ -6962,7 +6962,7 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, other: 'Convertir en auditores', }), makeGuest: () => ({ - one: 'Hacer invitado', + one: 'Convertir en invitado', other: 'Convertir en invitados', }), makePeopleAdmin: () => ({ diff --git a/src/languages/fr.ts b/src/languages/fr.ts index a2476c1332cc..eae118a07ac7 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4829,7 +4829,7 @@ ${amount} pour ${merchant} - ${date}`, memberAlternateText: 'Soumettre et approuver des notes de frais.', adminAlternateText: 'Gérer les notes de frais et les paramètres de l’espace de travail.', auditorAlternateText: 'Afficher et commenter les notes de frais.', - guestAlternateText: 'Soumettre et approuver des notes de frais, avec des discussions sur invitation uniquement.', + guestAlternateText: 'Envoyer et approuver des rapports, avec des discussions sur invitation uniquement.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4843,7 +4843,7 @@ ${amount} pour ${merchant} - ${date}`, case CONST.POLICY.ROLE.EDITOR: return 'Éditeur'; case CONST.POLICY.ROLE.CARD_ADMIN: - return 'Administrateur de la carte'; + return 'Administrateur de carte'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: return 'Administration des personnes'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: @@ -7114,8 +7114,8 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, other: 'Créer des auditeurs', }), makeGuest: () => ({ - one: 'Rendre invité', - other: 'Rendre invités', + one: 'Nommer invité', + other: 'Nommer invités', }), makePeopleAdmin: () => ({ one: 'Nommer administrateur des personnes', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 74fd7d1e8a08..6248b89583af 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4754,7 +4754,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' memberAlternateText: 'レポートを提出して承認します。', adminAlternateText: 'レポートとワークスペースの設定を管理します。', auditorAlternateText: 'レポートを表示してコメントします。', - guestAlternateText: 'レポートの提出と承認ができ、招待制チャットも利用できます。', + guestAlternateText: 'レポートを提出および承認し、招待されたチャットのみを使用します。', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -6965,7 +6965,7 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から }), makeGuest: () => ({ one: 'ゲストにする', - other: 'ゲストを作成', + other: 'ゲストにする', }), makePeopleAdmin: () => ({ one: 'People 管理者にする', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 7fdf6dc21a78..c567a693fc50 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4793,13 +4793,13 @@ ${amount} voor ${merchant} - ${date}`, memberAlternateText: 'Dien rapporten in en keur ze goed.', adminAlternateText: 'Beheer rapporten en werkruimte-instellingen.', auditorAlternateText: 'Rapporten bekijken en erop reageren.', - guestAlternateText: 'Dien rapporten in en keur ze goed, met chats op uitnodiging.', + guestAlternateText: 'Rapporten indienen en goedkeuren, met chats alleen op uitnodiging.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: return 'Eigenaar'; case CONST.POLICY.ROLE.ADMIN: - return 'Werkruimtebeheerder'; + return 'Beheerder van werkruimte'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; case CONST.POLICY.ROLE.GUEST: @@ -4809,7 +4809,7 @@ ${amount} voor ${merchant} - ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Kaartbeheer'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Personenbeheer'; + return 'Beheer personen'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Beheerder betalingen'; case CONST.POLICY.ROLE.USER: diff --git a/src/languages/pl.ts b/src/languages/pl.ts index eddbe0a57baf..47f6750c0401 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4821,7 +4821,7 @@ ${amount} dla ${merchant} - ${date}`, memberAlternateText: 'Przesyłaj i zatwierdzaj raporty.', adminAlternateText: 'Zarządzaj raportami i ustawieniami przestrzeni roboczej.', auditorAlternateText: 'Przeglądaj i komentuj raporty.', - guestAlternateText: 'Wysyłaj i zatwierdzaj raporty, z czatami tylko na zaproszenie.', + guestAlternateText: 'Przesyłaj i zatwierdzaj raporty, korzystając tylko z czatów na zaproszenie.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4837,7 +4837,7 @@ ${amount} dla ${merchant} - ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Administrator karty'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Zarządzanie personelem'; + return 'Administrator osób'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Administrator płatności'; case CONST.POLICY.ROLE.USER: @@ -7063,8 +7063,8 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, other: 'Utwórz audytorów', }), makeGuest: () => ({ - one: 'Uczyń gościem', - other: 'Utwórz gości', + one: 'Ustaw jako gościa', + other: 'Ustaw jako gości', }), makePeopleAdmin: () => ({ one: 'Ustaw jako administratora osób', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index ab9a27f8d453..84f6d83a7515 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4784,13 +4784,13 @@ ${amount} para ${merchant} - ${date}`, memberAlternateText: 'Enviar e aprovar relatórios.', adminAlternateText: 'Gerencie relatórios e configurações do espaço de trabalho.', auditorAlternateText: 'Visualize e comente relatórios.', - guestAlternateText: 'Envie e aprove relatórios, com chats apenas por convite.', + guestAlternateText: 'Envie e aprove relatórios, com chats somente por convite.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: return 'Proprietário'; case CONST.POLICY.ROLE.ADMIN: - return 'Admin do workspace'; + return 'Administrador da área de trabalho'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; case CONST.POLICY.ROLE.GUEST: @@ -4798,7 +4798,7 @@ ${amount} para ${merchant} - ${date}`, case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: - return 'Administração de cartão'; + return 'Admin do cartão'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: return 'Administração de pessoas'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: @@ -7041,7 +7041,7 @@ O plano Control começa em US$ 9 por membro ativo por mês.`, }), makeGuest: () => ({ one: 'Tornar convidado', - other: 'Transformar em convidados', + other: 'Tornar convidados', }), makePeopleAdmin: () => ({ one: 'Tornar administrador de pessoas', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index cb1cf6ba91ac..93527ee01637 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4639,7 +4639,7 @@ ${amount},商户:${merchant} - 日期:${date}`, memberAlternateText: '提交并审批报销报告。', adminAlternateText: '管理报表和工作区设置。', auditorAlternateText: '查看并评论报表。', - guestAlternateText: '提交和审批报销单,并使用仅限受邀的聊天。', + guestAlternateText: '提交和批准报表,仅使用受邀聊天。', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4651,11 +4651,11 @@ ${amount},商户:${merchant} - 日期:${date}`, case CONST.POLICY.ROLE.GUEST: return '访客'; case CONST.POLICY.ROLE.EDITOR: - return '编辑器'; + return '编辑者'; case CONST.POLICY.ROLE.CARD_ADMIN: return '卡片管理员'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return '人员管理'; + return '人员管理员'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return '付款管理员'; case CONST.POLICY.ROLE.USER: diff --git a/src/libs/Fullstory/utils.ts b/src/libs/Fullstory/utils.ts index 1ffe106b7268..b5f12498484b 100644 --- a/src/libs/Fullstory/utils.ts +++ b/src/libs/Fullstory/utils.ts @@ -1,5 +1,6 @@ +import getHighestPolicyRole from '@libs/getHighestPolicyRole'; import {filterObject} from '@libs/ObjectUtils'; -import {getActivePolicies, getHighestPolicyRole, isControlPolicy} from '@libs/PolicyUtils'; +import {getActivePolicies, isControlPolicy} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 6bbf624f991f..ca4968a62bf4 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -866,34 +866,6 @@ const isPolicyUser = (policy: OnyxInputOrEntry, currentUserLogin?: strin */ const isPolicyGuest = (policy: OnyxInputOrEntry, currentUserLogin?: string): boolean => getPolicyRole(policy, currentUserLogin) === CONST.POLICY.ROLE.GUEST; -/** - * Workspace roles from highest to lowest privilege, matching the role hierarchy in Auth. - */ -const POLICY_ROLE_RANKING = [ - CONST.POLICY.ROLE.ADMIN, - CONST.POLICY.ROLE.PAYMENTS_ADMIN, - CONST.POLICY.ROLE.CARD_ADMIN, - CONST.POLICY.ROLE.PEOPLE_ADMIN, - CONST.POLICY.ROLE.AUDITOR, - CONST.POLICY.ROLE.EDITOR, - CONST.POLICY.ROLE.USER, - CONST.POLICY.ROLE.GUEST, -] as const; - -/** - * Returns the highest-privilege role held across the given policies. - * Defaults to user when no ranked role is found, so a member on any workspace is not reported as a guest. - */ -function getHighestPolicyRole(policyList: Array>>): (typeof POLICY_ROLE_RANKING)[number] { - const roles = policyList.map((policy) => policy?.role); - for (const rankedRole of POLICY_ROLE_RANKING) { - if (roles.includes(rankedRole)) { - return rankedRole; - } - } - return CONST.POLICY.ROLE.USER; -} - /** * Checks if the current user is an auditor of the policy */ @@ -3984,7 +3956,6 @@ export { getCustomUnitsForDuplication, getCountOfRequiredTagLists, getActiveEmployeeWorkspaces, - getHighestPolicyRole, getPolicyRole, hasIndependentTags, hasPerTagListRequired, diff --git a/src/libs/getHighestPolicyRole.ts b/src/libs/getHighestPolicyRole.ts new file mode 100644 index 000000000000..1d8742c32f95 --- /dev/null +++ b/src/libs/getHighestPolicyRole.ts @@ -0,0 +1,37 @@ +import CONST from '@src/CONST'; +import type {Policy} from '@src/types/onyx'; + +import type {OnyxInputOrEntry} from 'react-native-onyx'; +import type {TupleToUnion} from 'type-fest'; + +/** + * Workspace roles from highest to lowest privilege, matching the role hierarchy in Auth. + */ +const POLICY_ROLE_RANKING = [ + CONST.POLICY.ROLE.ADMIN, + CONST.POLICY.ROLE.PAYMENTS_ADMIN, + CONST.POLICY.ROLE.CARD_ADMIN, + CONST.POLICY.ROLE.PEOPLE_ADMIN, + CONST.POLICY.ROLE.AUDITOR, + CONST.POLICY.ROLE.EDITOR, + CONST.POLICY.ROLE.USER, + CONST.POLICY.ROLE.GUEST, +] as const; + +/** + * Returns the highest-privilege role held across the given policies. + * Defaults to user when no ranked role is found, so a member on any workspace is not reported as a guest. + */ +function getHighestPolicyRole(policyList: Array>>): TupleToUnion { + const roles = new Set(policyList.map((policy) => policy?.role)); + for (const rankedRole of POLICY_ROLE_RANKING) { + if (roles.has(rankedRole)) { + return rankedRole; + } + } + return CONST.POLICY.ROLE.USER; +} + +export type HighestPolicyRole = TupleToUnion; + +export default getHighestPolicyRole; diff --git a/src/libs/telemetry/TelemetrySynchronizer.ts b/src/libs/telemetry/TelemetrySynchronizer.ts index a529a3fa911a..a73691283257 100644 --- a/src/libs/telemetry/TelemetrySynchronizer.ts +++ b/src/libs/telemetry/TelemetrySynchronizer.ts @@ -1,4 +1,5 @@ -import {getActivePolicies, getHighestPolicyRole} from '@libs/PolicyUtils'; +import getHighestPolicyRole from '@libs/getHighestPolicyRole'; +import {getActivePolicies} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; From 321138ea49642612799104a27d7907821fb77b68 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 30 Sep 2026 02:57:46 +0530 Subject: [PATCH 13/41] Fix type import and export style in getHighestPolicyRole --- src/libs/getHighestPolicyRole.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/libs/getHighestPolicyRole.ts b/src/libs/getHighestPolicyRole.ts index 1d8742c32f95..4eff62d6b694 100644 --- a/src/libs/getHighestPolicyRole.ts +++ b/src/libs/getHighestPolicyRole.ts @@ -1,9 +1,8 @@ -import CONST from '@src/CONST'; -import type {Policy} from '@src/types/onyx'; - -import type {OnyxInputOrEntry} from 'react-native-onyx'; import type {TupleToUnion} from 'type-fest'; +import CONST from '@src/CONST'; +import type {OnyxInputOrEntry, Policy} from '@src/types/onyx'; + /** * Workspace roles from highest to lowest privilege, matching the role hierarchy in Auth. */ @@ -32,6 +31,4 @@ function getHighestPolicyRole(policyList: Array; - export default getHighestPolicyRole; From 2e72e10ed70a17e0889af4446c194bb39d5c6394 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 30 Sep 2026 04:21:17 +0530 Subject: [PATCH 14/41] Use type imports and safe types in policyRolePermissions --- src/libs/policyRolePermissions.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/libs/policyRolePermissions.ts b/src/libs/policyRolePermissions.ts index 6edf64076f2c..c51dcdcfc336 100644 --- a/src/libs/policyRolePermissions.ts +++ b/src/libs/policyRolePermissions.ts @@ -1,19 +1,17 @@ import CONST from '@src/CONST'; -import {ValueOf} from 'type-fest'; +import type {ValueOf} from 'type-fest'; type PolicyFeature = ValueOf; type PolicyFeatureAccess = ValueOf; -const WRITE_ALL_POLICY_FEATURES = Object.fromEntries(Object.values(CONST.POLICY.POLICY_FEATURE).map((feature) => [feature, CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE])) as Record< - PolicyFeature, - PolicyFeatureAccess ->; +const WRITE_ALL_POLICY_FEATURES: Record = Object.fromEntries( + Object.values(CONST.POLICY.POLICY_FEATURE).map((feature) => [feature, CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE]), +); -const READ_ALL_POLICY_FEATURES = Object.fromEntries(Object.values(CONST.POLICY.POLICY_FEATURE).map((feature) => [feature, CONST.POLICY.POLICY_FEATURE_ACCESS.READ])) as Record< - PolicyFeature, - PolicyFeatureAccess ->; +const READ_ALL_POLICY_FEATURES: Record = Object.fromEntries( + Object.values(CONST.POLICY.POLICY_FEATURE).map((feature) => [feature, CONST.POLICY.POLICY_FEATURE_ACCESS.READ]), +); const EDITOR_POLICY_FEATURES = Object.fromEntries( Object.values(CONST.POLICY.POLICY_FEATURE) From 3dee4dedab06dff1c37d2ad381b3446c4cbcca2c Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 30 Sep 2026 19:20:24 +0530 Subject: [PATCH 15/41] Hide description edit affordance when user cannot edit --- src/pages/DynamicReportDetailsPage.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/DynamicReportDetailsPage.tsx b/src/pages/DynamicReportDetailsPage.tsx index ce6a980b8124..1c9857403a1c 100644 --- a/src/pages/DynamicReportDetailsPage.tsx +++ b/src/pages/DynamicReportDetailsPage.tsx @@ -1091,14 +1091,12 @@ function DynamicReportDetailsPage({policy, report, route, reportMetadata, report {shouldShowReportDescription && ( - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.REPORT_DESCRIPTION.path))}> + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.REPORT_DESCRIPTION.path)) : undefined}> {!!getReportDescription(report) && {getReportDescription(report)}} - - - + {canEditReportDescription && } From 6c1ac06d478b849cfd332a025582c89303605ae0 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 30 Sep 2026 19:24:43 +0530 Subject: [PATCH 16/41] Update WorkspaceInitialPage.tsx --- src/pages/workspace/WorkspaceInitialPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index ac61367edadc..5beb127fe3d2 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -84,6 +84,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const policy = policyDraft?.id ? policyDraft : policyProp; const policyID = policy?.id; const routePolicyID = route.params?.policyID; + const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${routePolicyID}`); const workspaceAccountID = useWorkspaceAccountID(policyID); From 79ed6b469a1cf9acd6df776ada39abf5a24e0c1b Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:50:30 +0530 Subject: [PATCH 17/41] Show room avatar to guests in report details --- src/components/RoomHeaderAvatars.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index 9b6ac8b60169..f1212c4d2359 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -5,7 +5,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {clearAvatarErrors, updatePolicyRoomAvatar} from '@libs/actions/Report'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; -import {isAnnounceRoom, isUserCreatedPolicyRoom} from '@libs/ReportUtils'; +import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {getAccountIDFromAvatarID, isDefaultAvatar} from '@libs/UserAvatarUtils'; import variables from '@styles/variables'; @@ -52,10 +52,6 @@ function RoomHeaderAvatars({icons, report, policy, participants, currentUserAcco const StyleUtils = useStyleUtils(); const canEditRoomAvatar = isUserCreatedPolicyRoom(report) && participants.includes(currentUserAccountID) && !!policy && policy.role !== CONST.POLICY.ROLE.AUDITOR; - if (policy?.role === CONST.POLICY.ROLE.GUEST && isAnnounceRoom(report)) { - return null; - } - if (!icons.length) { return null; } From c7d5df2408dee5cad4603689ab3270d7bad6c3a9 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:41:53 +0530 Subject: [PATCH 18/41] Address guest role review feedback --- src/components/RoomHeaderAvatars.tsx | 3 ++- src/components/Tables/WorkspaceMembersTable/index.tsx | 11 +++++++++++ src/languages/en.ts | 1 + .../ReportDetailsDescription.tsx | 2 +- .../Workspace/WorkspaceRestrictedActionPage.tsx | 7 ++++++- src/pages/workspace/WorkspaceMembersPage.tsx | 1 + src/pages/workspace/members/ImportedMembersPage.tsx | 4 ++-- src/pages/workspace/rooms/WorkspaceRoomsPage.tsx | 9 ++++++--- 8 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index f1212c4d2359..5c48c66d9d22 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -50,7 +50,8 @@ function RoomHeaderAvatars({icons, report, policy, participants, currentUserAcco const expensifyIcons = useMemoizedLazyExpensifyIcons(['Camera', 'FallbackAvatar', 'ImageCropSquareMask']); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const canEditRoomAvatar = isUserCreatedPolicyRoom(report) && participants.includes(currentUserAccountID) && !!policy && policy.role !== CONST.POLICY.ROLE.AUDITOR; + const canEditRoomAvatar = + isUserCreatedPolicyRoom(report) && participants.includes(currentUserAccountID) && !!policy && policy.role !== CONST.POLICY.ROLE.AUDITOR && policy.role !== CONST.POLICY.ROLE.GUEST; if (!icons.length) { return null; diff --git a/src/components/Tables/WorkspaceMembersTable/index.tsx b/src/components/Tables/WorkspaceMembersTable/index.tsx index 2b88b77ac975..358316a63c35 100644 --- a/src/components/Tables/WorkspaceMembersTable/index.tsx +++ b/src/components/Tables/WorkspaceMembersTable/index.tsx @@ -60,6 +60,7 @@ const WORKSPACE_MEMBER_FILTER_VALUES = { AUDITORS: 'auditors', CARD_ADMINS: 'cardAdmins', EDITORS: 'editors', + GUESTS: 'guests', MEMBERS: 'members', PAYMENTS_ADMINS: 'paymentsAdmins', PEOPLE_ADMINS: 'peopleAdmins', @@ -203,6 +204,11 @@ export default function WorkspaceMembersTable({ return true; } + const isGuest = item.role === CONST.POLICY.ROLE.GUEST; + if (filterValues.includes(WORKSPACE_MEMBER_FILTER_VALUES.GUESTS) && isGuest) { + return true; + } + const isEditor = item.role === CONST.POLICY.ROLE.EDITOR; if (filterValues.includes(WORKSPACE_MEMBER_FILTER_VALUES.EDITORS) && isEditor) { return true; @@ -253,6 +259,11 @@ export default function WorkspaceMembersTable({ label: translate('workspace.people.auditors'), value: WORKSPACE_MEMBER_FILTER_VALUES.AUDITORS, }); + + filterConfig.role.options.push({ + label: translate('workspace.people.guests'), + value: WORKSPACE_MEMBER_FILTER_VALUES.GUESTS, + }); } if (isSubmitPolicy(policy)) { diff --git a/src/languages/en.ts b/src/languages/en.ts index bdc897445903..7ebc2f06cbb0 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7436,6 +7436,7 @@ const translations = { paymentsAdmins: 'Payments Admins', approvers: 'Approvers', auditors: 'Auditors', + guests: 'Guests', editors: 'Editors', members: 'Members', emptyRoleFilter: { diff --git a/src/pages/DynamicReportDetailsPage/ReportDetailsDescription.tsx b/src/pages/DynamicReportDetailsPage/ReportDetailsDescription.tsx index f8ac2a4b4ce9..fb417c20ac8c 100644 --- a/src/pages/DynamicReportDetailsPage/ReportDetailsDescription.tsx +++ b/src/pages/DynamicReportDetailsPage/ReportDetailsDescription.tsx @@ -51,7 +51,7 @@ function ReportDetailsDescription({reportID}: ReportDetailsDescriptionProps) { return ( - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.REPORT_DESCRIPTION.path)) : undefined}> + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.REPORT_DESCRIPTION.path))}> {!!getReportDescription(report) && {getReportDescription(report)}} diff --git a/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx b/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx index 27d71ebef5dc..e8fc7dc09d56 100644 --- a/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx +++ b/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx @@ -10,7 +10,7 @@ import {openSubscriptionPage} from '@libs/actions/Subscription'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {RestrictedActionParamList} from '@libs/Navigation/types'; -import {isPolicyAdmin, isPolicyAuditor, isPolicyOwner, isPolicyUser} from '@libs/PolicyUtils'; +import {isPolicyAdmin, isPolicyAuditor, isPolicyGuest, isPolicyOwner, isPolicyUser} from '@libs/PolicyUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -104,6 +104,11 @@ function WorkspaceRestrictedActionPage({ return ; } + // Guests have no restricted actions to show, since they cannot perform any workspace action in the first place + if (isPolicyGuest(policy)) { + return ; + } + // Workspace User or Auditor if (isPolicyUser(policy, email) || isPolicyAuditor(policy, email)) { return ; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index bba108bbbaac..9c1c32af91ba 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -580,6 +580,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers text: translate('workspace.people.makeGuest', {count: selectedEmployees.length}), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_GUEST, icon: icons.User, + shouldSkipFocusRestore: hasSelectedRuleBot, onSelected: () => changeUserRole(CONST.POLICY.ROLE.GUEST), }; const cardAdminOption = { diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index 7ea6fa191f15..f2cd6351693e 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -16,6 +16,7 @@ import {findDuplicate, generateColumnNames} from '@libs/importSpreadsheetUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {isControlPolicyOnlyRole} from '@libs/policyRolePermissions'; import {canMemberAssignElevatedRole, canMemberAssignRole, canMemberManageMemberWithRole, isControlPolicy as isControlPolicyUtil, isPolicyMemberWithoutPendingDelete} from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -118,12 +119,11 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { const membersRolesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ROLE); - const controlPolicyOnlyRoles = [CONST.POLICY.ROLE.AUDITOR, CONST.POLICY.ROLE.GUEST, CONST.POLICY.ROLE.CARD_ADMIN, CONST.POLICY.ROLE.PEOPLE_ADMIN, CONST.POLICY.ROLE.PAYMENTS_ADMIN]; const hasControlPolicyOnlyRole = membersRolesColumn !== -1 && spreadsheet?.data?.at(membersRolesColumn)?.some((role, index) => { const memberRole = containsHeader ? spreadsheet?.data?.at(membersRolesColumn)?.at(index + 1) : (role ?? ''); - return controlPolicyOnlyRoles.some((controlPolicyOnlyRole) => controlPolicyOnlyRole === memberRole); + return isControlPolicyOnlyRole(memberRole); }); const controlPolicyColumns = [ CONST.CSV_IMPORT_COLUMNS.SUBMIT_TO, diff --git a/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx b/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx index 344b24e0b94d..c4731bb1d346 100644 --- a/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx +++ b/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx @@ -56,6 +56,7 @@ function WorkspaceRoomsPage({route}: WorkspaceRoomsPageProps) { const policyID = route.params.policyID; const policy = usePolicy(policyID); const isAdmin = isPolicyAdmin(policy); + const isGuest = isPolicyGuest(policy); const isArchived = isArchivedPolicy(policy); useWorkspaceDocumentTitle(policy?.name, 'workspace.common.rooms'); @@ -138,13 +139,15 @@ function WorkspaceRoomsPage({route}: WorkspaceRoomsPageProps) { // The fetch is driven by the requested page: loading more only bumps `pageNumber` and this effect issues the // request, the same way Search drives its own pagination from `offset`. Refocusing and coming back online refetch // the page that is currently displayed. + // Guests are blocked from this page, so they must not fetch room data either. Otherwise the rooms they + // should not browse would end up in Onyx. useEffect(() => { - if (!isFocused || isOffline) { + if (!isFocused || isOffline || isGuest) { return; } openPolicyRoomsPage(policyID, pageNumber, sortBy, roomSort.order, searchValue); - }, [isFocused, isOffline, pageNumber, policyID, roomSort.order, searchValue, sortBy]); + }, [isFocused, isOffline, isGuest, pageNumber, policyID, roomSort.order, searchValue, sortBy]); const loadMoreRooms = () => { // The requested page is only bumped once the previous one has landed, so repeated end-reached events while @@ -174,7 +177,7 @@ function WorkspaceRoomsPage({route}: WorkspaceRoomsPageProps) { return ( Date: Fri, 2 Oct 2026 12:20:13 +0530 Subject: [PATCH 19/41] Apply review fixes --- src/components/RoomHeaderAvatars.tsx | 4 ++-- .../DynamicReportDetailsPage/ReportDetailsDescription.tsx | 4 +++- .../Workspace/WorkspaceRestrictedActionPage.tsx | 7 +------ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index 5c48c66d9d22..1b9c0824ac20 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -5,6 +5,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {clearAvatarErrors, updatePolicyRoomAvatar} from '@libs/actions/Report'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; +import {isPolicyAuditor, isPolicyGuest} from '@libs/PolicyUtils'; import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {getAccountIDFromAvatarID, isDefaultAvatar} from '@libs/UserAvatarUtils'; @@ -50,8 +51,7 @@ function RoomHeaderAvatars({icons, report, policy, participants, currentUserAcco const expensifyIcons = useMemoizedLazyExpensifyIcons(['Camera', 'FallbackAvatar', 'ImageCropSquareMask']); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const canEditRoomAvatar = - isUserCreatedPolicyRoom(report) && participants.includes(currentUserAccountID) && !!policy && policy.role !== CONST.POLICY.ROLE.AUDITOR && policy.role !== CONST.POLICY.ROLE.GUEST; + const canEditRoomAvatar = isUserCreatedPolicyRoom(report) && participants.includes(currentUserAccountID) && !!policy && !isPolicyAuditor(policy) && !isPolicyGuest(policy); if (!icons.length) { return null; diff --git a/src/pages/DynamicReportDetailsPage/ReportDetailsDescription.tsx b/src/pages/DynamicReportDetailsPage/ReportDetailsDescription.tsx index fb417c20ac8c..38a7712154c8 100644 --- a/src/pages/DynamicReportDetailsPage/ReportDetailsDescription.tsx +++ b/src/pages/DynamicReportDetailsPage/ReportDetailsDescription.tsx @@ -56,7 +56,9 @@ function ReportDetailsDescription({reportID}: ReportDetailsDescriptionProps) { {!!getReportDescription(report) && {getReportDescription(report)}} - {canEditReportDescription && } + + + diff --git a/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx b/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx index e8fc7dc09d56..27d71ebef5dc 100644 --- a/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx +++ b/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx @@ -10,7 +10,7 @@ import {openSubscriptionPage} from '@libs/actions/Subscription'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {RestrictedActionParamList} from '@libs/Navigation/types'; -import {isPolicyAdmin, isPolicyAuditor, isPolicyGuest, isPolicyOwner, isPolicyUser} from '@libs/PolicyUtils'; +import {isPolicyAdmin, isPolicyAuditor, isPolicyOwner, isPolicyUser} from '@libs/PolicyUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -104,11 +104,6 @@ function WorkspaceRestrictedActionPage({ return ; } - // Guests have no restricted actions to show, since they cannot perform any workspace action in the first place - if (isPolicyGuest(policy)) { - return ; - } - // Workspace User or Auditor if (isPolicyUser(policy, email) || isPolicyAuditor(policy, email)) { return ; From c30b49135fd4cb2ba21508c572dc2750944c1648 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:41:45 +0530 Subject: [PATCH 20/41] Add guests translation key to all languages --- src/languages/de.ts | 1 + src/languages/el.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 + 10 files changed, 10 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index c8cafbf615f0..f096f5644d78 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7143,6 +7143,7 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, admins: 'Workspace-Administratoren', approvers: 'Genehmigende', auditors: 'Prüfer', + guests: 'Gäste', editors: 'Bearbeiter', emptyRoleFilter: {title: 'Keine Mitglieder entsprechen diesem Filter', subtitle: 'Laden Sie ein Mitglied ein oder ändern Sie den Filter oben.'}, configureHRSync: (providerName: string) => `Synchronisierung mit ${providerName} einrichten.`, diff --git a/src/languages/el.ts b/src/languages/el.ts index 336868f6f673..c4a83bf711e4 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -7265,6 +7265,7 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ paymentsAdmins: 'Διαχειριστές πληρωμών', approvers: 'Εγκρίνοντες', auditors: 'Ελεγκτές', + guests: 'Επισκέπτες', editors: 'Συντάκτες', members: 'Μέλη', emptyRoleFilter: { diff --git a/src/languages/es.ts b/src/languages/es.ts index b6ea4f7ac48d..3796a88464af 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6999,6 +6999,7 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, admins: 'Administradores del espacio de trabajo', approvers: 'Aprobadores', auditors: 'Auditores', + guests: 'Invitados', editors: 'Editores', emptyRoleFilter: { title: 'Ningún miembro coincide con este filtro', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 44b58f69e3c7..295bae6adff6 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7161,6 +7161,7 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, admins: 'Administrateurs de l’espace de travail', approvers: 'Approbateurs', auditors: 'Auditeurs', + guests: 'Invités', editors: 'Éditeurs', emptyRoleFilter: {title: 'Aucun membre ne correspond à ce filtre', subtitle: 'Invitez un membre ou modifiez le filtre ci-dessus.'}, configureHRSync: (providerName: string) => `Configurer la synchronisation ${providerName}.`, diff --git a/src/languages/it.ts b/src/languages/it.ts index bd08cd1c0e08..7e80a5b4227d 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7103,6 +7103,7 @@ Il piano Control parte da 9 $ al mese per ogni membro attivo.`, admins: 'Amministratori dello spazio di lavoro', approvers: 'Approvatori', auditors: 'Revisori', + guests: 'Ospiti', editors: 'Editori', emptyRoleFilter: {title: 'Nessun membro corrisponde a questo filtro', subtitle: 'Invita un membro o modifica il filtro qui sopra.'}, configureHRSync: (providerName: string) => `Configura la sincronizzazione di ${providerName}.`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index e0471e68d789..b7e02901dcc5 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7014,6 +7014,7 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から admins: 'ワークスペース管理者', approvers: '承認者', auditors: '監査担当者', + guests: 'ゲスト', editors: '編集者', emptyRoleFilter: {title: 'このフィルターに一致するメンバーはいません', subtitle: 'メンバーを招待するか、上のフィルターを変更してください。'}, configureHRSync: (providerName: string) => `${providerName} の同期を設定します。`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index e3cc43789249..2bcc0be2c2a9 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7088,6 +7088,7 @@ Het Control-abonnement begint bij $9 per actieve deelnemer per maand.`, admins: 'Workspace-beheerders', approvers: 'Fiatteurs', auditors: 'Accountants', + guests: 'Gasten', editors: 'Bewerkers', emptyRoleFilter: {title: 'Geen leden komen overeen met dit filter', subtitle: 'Nodig een lid uit of wijzig het filter hierboven.'}, configureHRSync: (providerName: string) => `Stel ${providerName}-synchronisatie in.`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index fb5f2d37c085..721de2bffa05 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7113,6 +7113,7 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, admins: 'Administratorzy przestrzeni roboczej', approvers: 'Osoby zatwierdzające', auditors: 'Audytorzy', + guests: 'Goście', editors: 'Edytorzy', emptyRoleFilter: {title: 'Żadni członkowie nie pasują do tego filtra', subtitle: 'Zaproś członka lub zmień filtr powyżej.'}, configureHRSync: (providerName: string) => `Skonfiguruj synchronizację ${providerName}.`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index fc2d410cff37..46a893b58655 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7090,6 +7090,7 @@ O plano Control começa em US$ 9 por membro ativo por mês.`, admins: 'Admins do workspace', approvers: 'Aprovadores', auditors: 'Auditores', + guests: 'Convidados', editors: 'Editores', emptyRoleFilter: {title: 'Nenhum membro corresponde a este filtro', subtitle: 'Convide um membro ou altere o filtro acima.'}, configureHRSync: (providerName: string) => `Configurar a sincronização do ${providerName}.`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 4668beb55518..59f052d606bd 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6839,6 +6839,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM admins: '工作区管理员', approvers: '审批人', auditors: '审计员', + guests: '访客', editors: '编辑者', emptyRoleFilter: {title: '没有成员符合此筛选条件', subtitle: '邀请成员或更改上方的筛选条件。'}, configureHRSync: (providerName: string) => `配置 ${providerName} 同步。`, From 64bfa7777bf26e507fe8f50ea68d3e5a97541df6 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 2 Oct 2026 14:10:05 +0530 Subject: [PATCH 21/41] Update translations --- src/languages/de.ts | 4 ++-- src/languages/el.ts | 16 +++++++++------- src/languages/es.ts | 4 ++-- src/languages/fr.ts | 6 +++--- src/languages/it.ts | 4 ++-- src/languages/ja.ts | 4 ++-- src/languages/nl.ts | 8 ++++---- src/languages/pl.ts | 4 ++-- src/languages/pt-BR.ts | 8 ++++---- src/languages/zh-hans.ts | 12 ++++++------ 10 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index f096f5644d78..80bb86e0f9c4 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4851,7 +4851,7 @@ ${amount} für ${merchant} – ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Kartenverwaltung'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Personalverwaltung'; + return 'Personenverwaltung'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Zahlungsadministrator'; case CONST.POLICY.ROLE.USER: @@ -7107,7 +7107,7 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, }), makeGuest: () => ({ one: 'Zum Gast machen', - other: 'Zu Gästen machen', + other: 'Gäste erstellen', }), makePeopleAdmin: () => ({ one: 'Zum Personaladministrator machen', diff --git a/src/languages/el.ts b/src/languages/el.ts index c4a83bf711e4..5811aad063f6 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -4905,7 +4905,7 @@ ${amount} για ${merchant} - ${date}`, memberAlternateText: 'Υποβάλετε και εγκρίνετε αναφορές.', adminAlternateText: 'Διαχειριστείτε αναφορές και ρυθμίσεις χώρου εργασίας.', auditorAlternateText: 'Προβολή και σχολιασμός αναφορών.', - guestAlternateText: 'Υποβάλετε και εγκρίνετε αναφορές, με συνομιλίες μόνο κατόπιν πρόσκλησης.', + guestAlternateText: 'Υποβολή και έγκριση αναφορών, με συνομιλίες μόνο με πρόσκληση.', cardAdminAlternateText: 'Διαχειριστείτε τις κάρτες χώρου εργασίας.', peopleAdminAlternateText: 'Διαχειριστείτε μέλη και ροές έγκρισης.', paymentsAdminAlternateText: 'Διαχειριστείτε τις πληρωμές ροής εργασιών.', @@ -4917,19 +4917,21 @@ ${amount} για ${merchant} - ${date}`, roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: - return 'Κάτοχος'; + return 'Ιδιοκτήτης'; case CONST.POLICY.ROLE.ADMIN: return 'Διαχειριστής χώρου εργασίας'; case CONST.POLICY.ROLE.AUDITOR: return 'Ελεγκτής'; + case CONST.POLICY.ROLE.GUEST: + return 'Επισκέπτης'; case CONST.POLICY.ROLE.EDITOR: return 'Επεξεργαστής'; case CONST.POLICY.ROLE.CARD_ADMIN: return 'Διαχείριση κάρτας'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Διαχείριση προσώπων'; + return 'Διαχείριση ατόμων'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: - return 'διαχείριση πληρωμών'; + return 'Διαχειριστής πληρωμών'; case CONST.POLICY.ROLE.USER: return 'Μέλος'; default: @@ -7232,8 +7234,8 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ other: 'Δημιουργία ελεγκτών', }), makeGuest: () => ({ - one: 'Ορισμός ως επισκέπτη', - other: 'Ορισμός ως επισκέπτες', + one: 'Κάντε επισκέπτη', + other: 'Προσθήκη επισκεπτών', }), makeCardAdmin: () => ({ one: 'Ορισμός διαχειριστή κάρτας', @@ -7265,7 +7267,7 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ paymentsAdmins: 'Διαχειριστές πληρωμών', approvers: 'Εγκρίνοντες', auditors: 'Ελεγκτές', - guests: 'Επισκέπτες', + guests: 'Καλεσμένοι', editors: 'Συντάκτες', members: 'Μέλη', emptyRoleFilter: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 3796a88464af..af6a3409fb39 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4761,7 +4761,7 @@ ${amount} para ${merchant} - ${date}`, memberAlternateText: 'Presentar y aprobar informes.', adminAlternateText: 'Gestionar informes y configuración del área de trabajo.', auditorAlternateText: 'Ver y comentar los informes.', - guestAlternateText: 'Enviar y aprobar informes, con chats solo por invitación.', + guestAlternateText: 'Envía y aprueba informes, con chats solo por invitación.', reimbursementChoice: { [CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES]: 'Directo', [CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO]: 'Ninguno', @@ -4782,7 +4782,7 @@ ${amount} para ${merchant} - ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Administrador de tarjeta'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Administrador de personas'; + return 'Administración de personas'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Administrador de pagos'; case CONST.POLICY.ROLE.USER: diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 295bae6adff6..42c6f9c3cacf 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4841,7 +4841,7 @@ ${amount} pour ${merchant} - ${date}`, memberAlternateText: 'Soumettre et approuver des notes de frais.', adminAlternateText: 'Gérer les notes de frais et les paramètres de l’espace de travail.', auditorAlternateText: 'Afficher et commenter les notes de frais.', - guestAlternateText: 'Envoyer et approuver des rapports, avec des discussions sur invitation uniquement.', + guestAlternateText: 'Soumettre et approuver des notes de frais, avec des discussions sur invitation uniquement.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -7124,8 +7124,8 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, other: 'Créer des auditeurs', }), makeGuest: () => ({ - one: 'Nommer invité', - other: 'Nommer invités', + one: 'Rendre invité', + other: 'Rendre invités', }), makePeopleAdmin: () => ({ one: 'Nommer administrateur des personnes', diff --git a/src/languages/it.ts b/src/languages/it.ts index 7e80a5b4227d..45d53149687b 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4818,11 +4818,11 @@ ${amount} per ${merchant} - ${date}`, case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: - return 'Amministrazione carte'; + return 'Amministrazione carta'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: return 'Amministrazione persone'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: - return 'Amministrazione pagamenti'; + return 'Amministratore pagamenti'; case CONST.POLICY.ROLE.USER: return 'Membro'; default: diff --git a/src/languages/ja.ts b/src/languages/ja.ts index b7e02901dcc5..345063e84f1d 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4767,7 +4767,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' memberAlternateText: 'レポートを提出して承認します。', adminAlternateText: 'レポートとワークスペースの設定を管理します。', auditorAlternateText: 'レポートを表示してコメントします。', - guestAlternateText: 'レポートを提出および承認し、招待されたチャットのみを使用します。', + guestAlternateText: 'レポートの提出と承認、招待制チャットができます。', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4775,7 +4775,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' case CONST.POLICY.ROLE.ADMIN: return 'ワークスペース管理者'; case CONST.POLICY.ROLE.AUDITOR: - return '監査担当者'; + return '監査人'; case CONST.POLICY.ROLE.GUEST: return 'ゲスト'; case CONST.POLICY.ROLE.EDITOR: diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 2bcc0be2c2a9..ad096837cd74 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4806,13 +4806,13 @@ ${amount} voor ${merchant} - ${date}`, memberAlternateText: 'Dien rapporten in en keur ze goed.', adminAlternateText: 'Beheer rapporten en werkruimte-instellingen.', auditorAlternateText: 'Rapporten bekijken en erop reageren.', - guestAlternateText: 'Rapporten indienen en goedkeuren, met chats alleen op uitnodiging.', + guestAlternateText: 'Dien rapporten in en keur ze goed, met chats op uitnodiging.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: return 'Eigenaar'; case CONST.POLICY.ROLE.ADMIN: - return 'Beheerder van werkruimte'; + return 'Beheerder van workspace'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; case CONST.POLICY.ROLE.GUEST: @@ -4820,9 +4820,9 @@ ${amount} voor ${merchant} - ${date}`, case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: - return 'Kaartbeheer'; + return 'Kaartbeheerder'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Beheer personen'; + return 'Personenbeheer'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Beheerder betalingen'; case CONST.POLICY.ROLE.USER: diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 721de2bffa05..25bfa27c09b2 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4834,7 +4834,7 @@ ${amount} dla ${merchant} - ${date}`, memberAlternateText: 'Przesyłaj i zatwierdzaj raporty.', adminAlternateText: 'Zarządzaj raportami i ustawieniami przestrzeni roboczej.', auditorAlternateText: 'Przeglądaj i komentuj raporty.', - guestAlternateText: 'Przesyłaj i zatwierdzaj raporty, korzystając tylko z czatów na zaproszenie.', + guestAlternateText: 'Wysyłaj i zatwierdzaj raporty oraz korzystaj z czatów tylko na zaproszenie.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -7077,7 +7077,7 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, }), makeGuest: () => ({ one: 'Ustaw jako gościa', - other: 'Ustaw jako gości', + other: 'Utwórz gości', }), makePeopleAdmin: () => ({ one: 'Ustaw jako administratora osób', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 46a893b58655..70b5d1cf0cd6 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4797,13 +4797,13 @@ ${amount} para ${merchant} - ${date}`, memberAlternateText: 'Enviar e aprovar relatórios.', adminAlternateText: 'Gerencie relatórios e configurações do espaço de trabalho.', auditorAlternateText: 'Visualize e comente relatórios.', - guestAlternateText: 'Envie e aprove relatórios, com chats somente por convite.', + guestAlternateText: 'Envie e aprove relatórios, com chats somente para convidados.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: return 'Proprietário'; case CONST.POLICY.ROLE.ADMIN: - return 'Administrador da área de trabalho'; + return 'Admin da área de trabalho'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; case CONST.POLICY.ROLE.GUEST: @@ -4811,9 +4811,9 @@ ${amount} para ${merchant} - ${date}`, case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: - return 'Admin do cartão'; + return 'Administrador do cartão'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Administração de pessoas'; + return 'Admin de pessoas'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Admin de pagamentos'; case CONST.POLICY.ROLE.USER: diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 59f052d606bd..08160ce37439 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4655,7 +4655,7 @@ ${amount},商户:${merchant} - 日期:${date}`, memberAlternateText: '提交并审批报销报告。', adminAlternateText: '管理报表和工作区设置。', auditorAlternateText: '查看并评论报表。', - guestAlternateText: '提交和批准报表,仅使用受邀聊天。', + guestAlternateText: '提交和审批报表,并使用仅限受邀的聊天。', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4665,13 +4665,13 @@ ${amount},商户:${merchant} - 日期:${date}`, case CONST.POLICY.ROLE.AUDITOR: return '审计员'; case CONST.POLICY.ROLE.GUEST: - return '访客'; + return '来宾'; case CONST.POLICY.ROLE.EDITOR: - return '编辑者'; + return '编辑器'; case CONST.POLICY.ROLE.CARD_ADMIN: return '卡片管理员'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return '人员管理员'; + return '人员管理'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return '付款管理员'; case CONST.POLICY.ROLE.USER: @@ -6807,7 +6807,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM }), makeGuest: () => ({ one: '设为访客', - other: '设为访客', + other: '设为来宾', }), makePeopleAdmin: () => ({ one: '设为人员管理员', @@ -6839,7 +6839,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM admins: '工作区管理员', approvers: '审批人', auditors: '审计员', - guests: '访客', + guests: '来宾', editors: '编辑者', emptyRoleFilter: {title: '没有成员符合此筛选条件', subtitle: '邀请成员或更改上方的筛选条件。'}, configureHRSync: (providerName: string) => `配置 ${providerName} 同步。`, From 4a4c4fdfc65198af8fa40093a4bad74702457ca3 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:45:53 +0530 Subject: [PATCH 22/41] Address review on guest permissions and rename --- src/languages/de.ts | 4 ++-- src/languages/el.ts | 16 +++++++------- src/languages/es.ts | 4 ++-- src/languages/fr.ts | 6 +++--- src/languages/it.ts | 4 ++-- src/languages/ja.ts | 4 ++-- src/languages/nl.ts | 8 +++---- src/languages/pl.ts | 4 ++-- src/languages/pt-BR.ts | 8 +++---- src/languages/zh-hans.ts | 12 +++++------ src/libs/PolicyUtils.ts | 4 ++-- src/libs/ReportUtils.ts | 6 +++++- src/libs/policyRolePermissions.ts | 21 +++++++++++-------- src/pages/DynamicReportDetailsPage/index.tsx | 2 +- .../WorkspaceRestrictedActionPage.tsx | 6 +++--- src/pages/settings/Report/RoomNamePage.tsx | 4 +++- src/pages/workspace/WorkspaceMembersPage.tsx | 4 ++++ src/pages/workspace/getWorkspaceMenuItems.ts | 11 +++++++++- tests/unit/TelemetrySynchronizerTest.ts | 1 - 19 files changed, 74 insertions(+), 55 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 80bb86e0f9c4..f096f5644d78 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4851,7 +4851,7 @@ ${amount} für ${merchant} – ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Kartenverwaltung'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Personenverwaltung'; + return 'Personalverwaltung'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Zahlungsadministrator'; case CONST.POLICY.ROLE.USER: @@ -7107,7 +7107,7 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, }), makeGuest: () => ({ one: 'Zum Gast machen', - other: 'Gäste erstellen', + other: 'Zu Gästen machen', }), makePeopleAdmin: () => ({ one: 'Zum Personaladministrator machen', diff --git a/src/languages/el.ts b/src/languages/el.ts index 5811aad063f6..c4a83bf711e4 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -4905,7 +4905,7 @@ ${amount} για ${merchant} - ${date}`, memberAlternateText: 'Υποβάλετε και εγκρίνετε αναφορές.', adminAlternateText: 'Διαχειριστείτε αναφορές και ρυθμίσεις χώρου εργασίας.', auditorAlternateText: 'Προβολή και σχολιασμός αναφορών.', - guestAlternateText: 'Υποβολή και έγκριση αναφορών, με συνομιλίες μόνο με πρόσκληση.', + guestAlternateText: 'Υποβάλετε και εγκρίνετε αναφορές, με συνομιλίες μόνο κατόπιν πρόσκλησης.', cardAdminAlternateText: 'Διαχειριστείτε τις κάρτες χώρου εργασίας.', peopleAdminAlternateText: 'Διαχειριστείτε μέλη και ροές έγκρισης.', paymentsAdminAlternateText: 'Διαχειριστείτε τις πληρωμές ροής εργασιών.', @@ -4917,21 +4917,19 @@ ${amount} για ${merchant} - ${date}`, roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: - return 'Ιδιοκτήτης'; + return 'Κάτοχος'; case CONST.POLICY.ROLE.ADMIN: return 'Διαχειριστής χώρου εργασίας'; case CONST.POLICY.ROLE.AUDITOR: return 'Ελεγκτής'; - case CONST.POLICY.ROLE.GUEST: - return 'Επισκέπτης'; case CONST.POLICY.ROLE.EDITOR: return 'Επεξεργαστής'; case CONST.POLICY.ROLE.CARD_ADMIN: return 'Διαχείριση κάρτας'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Διαχείριση ατόμων'; + return 'Διαχείριση προσώπων'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: - return 'Διαχειριστής πληρωμών'; + return 'διαχείριση πληρωμών'; case CONST.POLICY.ROLE.USER: return 'Μέλος'; default: @@ -7234,8 +7232,8 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ other: 'Δημιουργία ελεγκτών', }), makeGuest: () => ({ - one: 'Κάντε επισκέπτη', - other: 'Προσθήκη επισκεπτών', + one: 'Ορισμός ως επισκέπτη', + other: 'Ορισμός ως επισκέπτες', }), makeCardAdmin: () => ({ one: 'Ορισμός διαχειριστή κάρτας', @@ -7267,7 +7265,7 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ paymentsAdmins: 'Διαχειριστές πληρωμών', approvers: 'Εγκρίνοντες', auditors: 'Ελεγκτές', - guests: 'Καλεσμένοι', + guests: 'Επισκέπτες', editors: 'Συντάκτες', members: 'Μέλη', emptyRoleFilter: { diff --git a/src/languages/es.ts b/src/languages/es.ts index af6a3409fb39..3796a88464af 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4761,7 +4761,7 @@ ${amount} para ${merchant} - ${date}`, memberAlternateText: 'Presentar y aprobar informes.', adminAlternateText: 'Gestionar informes y configuración del área de trabajo.', auditorAlternateText: 'Ver y comentar los informes.', - guestAlternateText: 'Envía y aprueba informes, con chats solo por invitación.', + guestAlternateText: 'Enviar y aprobar informes, con chats solo por invitación.', reimbursementChoice: { [CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES]: 'Directo', [CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO]: 'Ninguno', @@ -4782,7 +4782,7 @@ ${amount} para ${merchant} - ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Administrador de tarjeta'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Administración de personas'; + return 'Administrador de personas'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Administrador de pagos'; case CONST.POLICY.ROLE.USER: diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 42c6f9c3cacf..295bae6adff6 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4841,7 +4841,7 @@ ${amount} pour ${merchant} - ${date}`, memberAlternateText: 'Soumettre et approuver des notes de frais.', adminAlternateText: 'Gérer les notes de frais et les paramètres de l’espace de travail.', auditorAlternateText: 'Afficher et commenter les notes de frais.', - guestAlternateText: 'Soumettre et approuver des notes de frais, avec des discussions sur invitation uniquement.', + guestAlternateText: 'Envoyer et approuver des rapports, avec des discussions sur invitation uniquement.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -7124,8 +7124,8 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, other: 'Créer des auditeurs', }), makeGuest: () => ({ - one: 'Rendre invité', - other: 'Rendre invités', + one: 'Nommer invité', + other: 'Nommer invités', }), makePeopleAdmin: () => ({ one: 'Nommer administrateur des personnes', diff --git a/src/languages/it.ts b/src/languages/it.ts index 45d53149687b..7e80a5b4227d 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4818,11 +4818,11 @@ ${amount} per ${merchant} - ${date}`, case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: - return 'Amministrazione carta'; + return 'Amministrazione carte'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: return 'Amministrazione persone'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: - return 'Amministratore pagamenti'; + return 'Amministrazione pagamenti'; case CONST.POLICY.ROLE.USER: return 'Membro'; default: diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 345063e84f1d..b7e02901dcc5 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4767,7 +4767,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' memberAlternateText: 'レポートを提出して承認します。', adminAlternateText: 'レポートとワークスペースの設定を管理します。', auditorAlternateText: 'レポートを表示してコメントします。', - guestAlternateText: 'レポートの提出と承認、招待制チャットができます。', + guestAlternateText: 'レポートを提出および承認し、招待されたチャットのみを使用します。', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4775,7 +4775,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' case CONST.POLICY.ROLE.ADMIN: return 'ワークスペース管理者'; case CONST.POLICY.ROLE.AUDITOR: - return '監査人'; + return '監査担当者'; case CONST.POLICY.ROLE.GUEST: return 'ゲスト'; case CONST.POLICY.ROLE.EDITOR: diff --git a/src/languages/nl.ts b/src/languages/nl.ts index ad096837cd74..2bcc0be2c2a9 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4806,13 +4806,13 @@ ${amount} voor ${merchant} - ${date}`, memberAlternateText: 'Dien rapporten in en keur ze goed.', adminAlternateText: 'Beheer rapporten en werkruimte-instellingen.', auditorAlternateText: 'Rapporten bekijken en erop reageren.', - guestAlternateText: 'Dien rapporten in en keur ze goed, met chats op uitnodiging.', + guestAlternateText: 'Rapporten indienen en goedkeuren, met chats alleen op uitnodiging.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: return 'Eigenaar'; case CONST.POLICY.ROLE.ADMIN: - return 'Beheerder van workspace'; + return 'Beheerder van werkruimte'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; case CONST.POLICY.ROLE.GUEST: @@ -4820,9 +4820,9 @@ ${amount} voor ${merchant} - ${date}`, case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: - return 'Kaartbeheerder'; + return 'Kaartbeheer'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Personenbeheer'; + return 'Beheer personen'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Beheerder betalingen'; case CONST.POLICY.ROLE.USER: diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 25bfa27c09b2..721de2bffa05 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4834,7 +4834,7 @@ ${amount} dla ${merchant} - ${date}`, memberAlternateText: 'Przesyłaj i zatwierdzaj raporty.', adminAlternateText: 'Zarządzaj raportami i ustawieniami przestrzeni roboczej.', auditorAlternateText: 'Przeglądaj i komentuj raporty.', - guestAlternateText: 'Wysyłaj i zatwierdzaj raporty oraz korzystaj z czatów tylko na zaproszenie.', + guestAlternateText: 'Przesyłaj i zatwierdzaj raporty, korzystając tylko z czatów na zaproszenie.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -7077,7 +7077,7 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, }), makeGuest: () => ({ one: 'Ustaw jako gościa', - other: 'Utwórz gości', + other: 'Ustaw jako gości', }), makePeopleAdmin: () => ({ one: 'Ustaw jako administratora osób', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 70b5d1cf0cd6..46a893b58655 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4797,13 +4797,13 @@ ${amount} para ${merchant} - ${date}`, memberAlternateText: 'Enviar e aprovar relatórios.', adminAlternateText: 'Gerencie relatórios e configurações do espaço de trabalho.', auditorAlternateText: 'Visualize e comente relatórios.', - guestAlternateText: 'Envie e aprove relatórios, com chats somente para convidados.', + guestAlternateText: 'Envie e aprove relatórios, com chats somente por convite.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: return 'Proprietário'; case CONST.POLICY.ROLE.ADMIN: - return 'Admin da área de trabalho'; + return 'Administrador da área de trabalho'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; case CONST.POLICY.ROLE.GUEST: @@ -4811,9 +4811,9 @@ ${amount} para ${merchant} - ${date}`, case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: - return 'Administrador do cartão'; + return 'Admin do cartão'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Admin de pessoas'; + return 'Administração de pessoas'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Admin de pagamentos'; case CONST.POLICY.ROLE.USER: diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 08160ce37439..59f052d606bd 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4655,7 +4655,7 @@ ${amount},商户:${merchant} - 日期:${date}`, memberAlternateText: '提交并审批报销报告。', adminAlternateText: '管理报表和工作区设置。', auditorAlternateText: '查看并评论报表。', - guestAlternateText: '提交和审批报表,并使用仅限受邀的聊天。', + guestAlternateText: '提交和批准报表,仅使用受邀聊天。', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4665,13 +4665,13 @@ ${amount},商户:${merchant} - 日期:${date}`, case CONST.POLICY.ROLE.AUDITOR: return '审计员'; case CONST.POLICY.ROLE.GUEST: - return '来宾'; + return '访客'; case CONST.POLICY.ROLE.EDITOR: - return '编辑器'; + return '编辑者'; case CONST.POLICY.ROLE.CARD_ADMIN: return '卡片管理员'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return '人员管理'; + return '人员管理员'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return '付款管理员'; case CONST.POLICY.ROLE.USER: @@ -6807,7 +6807,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM }), makeGuest: () => ({ one: '设为访客', - other: '设为来宾', + other: '设为访客', }), makePeopleAdmin: () => ({ one: '设为人员管理员', @@ -6839,7 +6839,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM admins: '工作区管理员', approvers: '审批人', auditors: '审计员', - guests: '来宾', + guests: '访客', editors: '编辑者', emptyRoleFilter: {title: '没有成员符合此筛选条件', subtitle: '邀请成员或更改上方的筛选条件。'}, configureHRSync: (providerName: string) => `配置 ${providerName} 同步。`, diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 3012bd211c85..c420c076a8c9 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -48,6 +48,8 @@ import type {TupleToUnion, ValueOf} from 'type-fest'; import {Str} from 'expensify-common'; +import type {PolicyFeature, PolicyFeatureAccess} from './policyRolePermissions'; + import {getQuickbooksOnlineIntegrationName, isQBORefreshTokenExpiringSoon} from './AccountingUtils'; import {getBankAccountFromID} from './actions/BankAccounts'; import {hasSynchronizationErrorMessage, isConnectionUnverified} from './actions/connections'; @@ -77,8 +79,6 @@ type PolicyWithTaxRates = { }; type TravelStep = ValueOf; -type PolicyFeature = ValueOf; -type PolicyFeatureAccess = ValueOf; type AccountingConnectionName = TupleToUnion; type WorkspaceDetails = { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 171ccc2dfb54..75f4bcf76764 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -11419,7 +11419,7 @@ function getAllWorkspaceReports(policyID?: string): Array> { /** * @param policy - the workspace the report is on, null if the user isn't a member of the workspace */ -function shouldDisableRename(report: OnyxEntry, isReportArchived = false): boolean { +function shouldDisableRename(report: OnyxEntry, isReportArchived = false, policy?: OnyxEntry): boolean { if ( isDefaultRoom(report) || isReportArchived || @@ -11443,6 +11443,10 @@ function shouldDisableRename(report: OnyxEntry, isReportArchived = false return true; } + if (policy?.role === CONST.POLICY.ROLE.GUEST) { + return true; + } + return false; } diff --git a/src/libs/policyRolePermissions.ts b/src/libs/policyRolePermissions.ts index c51dcdcfc336..0c8b6b3533af 100644 --- a/src/libs/policyRolePermissions.ts +++ b/src/libs/policyRolePermissions.ts @@ -5,19 +5,21 @@ import type {ValueOf} from 'type-fest'; type PolicyFeature = ValueOf; type PolicyFeatureAccess = ValueOf; -const WRITE_ALL_POLICY_FEATURES: Record = Object.fromEntries( - Object.values(CONST.POLICY.POLICY_FEATURE).map((feature) => [feature, CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE]), +const ALL_POLICY_FEATURES = Object.values(CONST.POLICY.POLICY_FEATURE); + +const WRITE_ALL_POLICY_FEATURES = ALL_POLICY_FEATURES.reduce>>( + (features, feature) => ({...features, [feature]: CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE}), + {}, ); -const READ_ALL_POLICY_FEATURES: Record = Object.fromEntries( - Object.values(CONST.POLICY.POLICY_FEATURE).map((feature) => [feature, CONST.POLICY.POLICY_FEATURE_ACCESS.READ]), +const READ_ALL_POLICY_FEATURES = ALL_POLICY_FEATURES.reduce>>( + (features, feature) => ({...features, [feature]: CONST.POLICY.POLICY_FEATURE_ACCESS.READ}), + {}, ); -const EDITOR_POLICY_FEATURES = Object.fromEntries( - Object.values(CONST.POLICY.POLICY_FEATURE) - .filter((feature) => feature !== CONST.POLICY.POLICY_FEATURE.ASSIGN_ELEVATED_ROLES) - .map((feature) => [feature, CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE]), -) as Partial>; +const EDITOR_POLICY_FEATURES = ALL_POLICY_FEATURES.filter((feature) => feature !== CONST.POLICY.POLICY_FEATURE.ASSIGN_ELEVATED_ROLES).reduce< + Partial> +>((features, feature) => ({...features, [feature]: CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE}), {}); const ROLE_PERMISSION_BUNDLES: Record>> = { [CONST.POLICY.ROLE.ADMIN]: WRITE_ALL_POLICY_FEATURES, @@ -56,4 +58,5 @@ function isControlPolicyOnlyRole(role: string | undefined): boolean { return CONTROL_POLICY_ONLY_ROLES.some((controlPolicyOnlyRole) => controlPolicyOnlyRole === role); } +export type {PolicyFeature, PolicyFeatureAccess}; export {ROLE_PERMISSION_BUNDLES, isControlPolicyOnlyRole}; diff --git a/src/pages/DynamicReportDetailsPage/index.tsx b/src/pages/DynamicReportDetailsPage/index.tsx index 5b3976617cb1..ddba68b04ed3 100644 --- a/src/pages/DynamicReportDetailsPage/index.tsx +++ b/src/pages/DynamicReportDetailsPage/index.tsx @@ -244,7 +244,7 @@ function DynamicReportDetailsPage({policy, report, route, reportMetadata, report const isSelfDMTrackExpenseReport = isTrackExpenseReport && isSelfDMUtil(parentReport); const isReportArchived = useReportIsArchived(report?.reportID); const isArchivedRoom = isArchivedNonExpenseReport(report, isReportArchived); - const shouldDisableRename = shouldDisableRenameUtil(report, isReportArchived); + const shouldDisableRename = shouldDisableRenameUtil(report, isReportArchived, policy); const parentNavigationSubtitleData = getParentNavigationSubtitle(report, policy, conciergeReportID, translate, derivedParentReportName, isParentReportArchived); const base62ReportID = getBase62ReportID(Number(report.reportID)); const ancestors = useAncestors(report); diff --git a/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx b/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx index 27d71ebef5dc..60fa65883f91 100644 --- a/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx +++ b/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx @@ -10,7 +10,7 @@ import {openSubscriptionPage} from '@libs/actions/Subscription'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {RestrictedActionParamList} from '@libs/Navigation/types'; -import {isPolicyAdmin, isPolicyAuditor, isPolicyOwner, isPolicyUser} from '@libs/PolicyUtils'; +import {isPolicyAdmin, isPolicyAuditor, isPolicyGuest, isPolicyOwner, isPolicyUser} from '@libs/PolicyUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -104,8 +104,8 @@ function WorkspaceRestrictedActionPage({ return ; } - // Workspace User or Auditor - if (isPolicyUser(policy, email) || isPolicyAuditor(policy, email)) { + // Workspace User, Auditor or Guest + if (isPolicyUser(policy, email) || isPolicyAuditor(policy, email) || isPolicyGuest(policy)) { return ; } diff --git a/src/pages/settings/Report/RoomNamePage.tsx b/src/pages/settings/Report/RoomNamePage.tsx index 4810afcd9b6f..b40245dc35b1 100644 --- a/src/pages/settings/Report/RoomNamePage.tsx +++ b/src/pages/settings/Report/RoomNamePage.tsx @@ -9,6 +9,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -41,6 +42,7 @@ function RoomNamePage({report, navigateBackTo}: RoomNamePageProps) { const {translate} = useLocalize(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const isReportArchived = useReportIsArchived(report?.reportID); + const policy = usePolicy(report?.policyID); const goBack = useCallback(() => { Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(navigateBackTo)); @@ -89,7 +91,7 @@ function RoomNamePage({report, navigateBackTo}: RoomNamePageProps) { includeSafeAreaPaddingBottom testID="RoomNamePage" > - + { + if (!canMemberRead(policy, currentUserPersonalDetails.login ?? '', CONST.POLICY.POLICY_FEATURE.MEMBERS)) { + return; + } const clientMemberEmails = Object.keys(getMemberAccountIDsForWorkspace(policy?.employeeList, employeePersonalDetails)); openWorkspaceMembersPage(route.params.policyID, clientMemberEmails); }; diff --git a/src/pages/workspace/getWorkspaceMenuItems.ts b/src/pages/workspace/getWorkspaceMenuItems.ts index 50687ec9f8dc..46cce7936864 100644 --- a/src/pages/workspace/getWorkspaceMenuItems.ts +++ b/src/pages/workspace/getWorkspaceMenuItems.ts @@ -220,7 +220,16 @@ function getWorkspaceMenuItems({ sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.ROOMS, }, ]; - const items = defaultItems.filter((item) => !isGuest || item.screenName === SCREENS.WORKSPACE.PROFILE); + const items = defaultItems.filter((item) => { + if (item.screenName === SCREENS.WORKSPACE.PROFILE) { + return true; + } + // POLICY_FEATURE has no ROOMS entry yet, so guests are hidden from rooms by role + if (item.screenName === SCREENS.WORKSPACE.ROOMS) { + return !isGuest; + } + return canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.MEMBERS); + }); if (isGroupPolicy(policy) && shouldShowProtectedItems) { if (canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.REPORT_FIELDS)) { diff --git a/tests/unit/TelemetrySynchronizerTest.ts b/tests/unit/TelemetrySynchronizerTest.ts index 944e0626cd90..80be57b0114c 100644 --- a/tests/unit/TelemetrySynchronizerTest.ts +++ b/tests/unit/TelemetrySynchronizerTest.ts @@ -22,7 +22,6 @@ jest.mock('@sentry/react-native', () => ({ jest.mock('@libs/PolicyUtils', () => ({ getActivePolicies: jest.fn(), - getHighestPolicyRole: jest.fn(), })); jest.mock('@libs/telemetry/sendMemoryContext', () => ({ From 0f735d85fb999ec5e6edfb849170fcae9d78b418 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:10:17 +0530 Subject: [PATCH 23/41] Fix CI failures from guest role changes --- src/libs/policyRolePermissions.ts | 29 ++++++++++--------- .../ReportDetailsNameSection.tsx | 2 +- src/pages/DynamicReportDetailsPage/index.tsx | 1 - src/pages/workspace/getWorkspaceMenuItems.ts | 3 ++ 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/libs/policyRolePermissions.ts b/src/libs/policyRolePermissions.ts index 0c8b6b3533af..91c7370982f9 100644 --- a/src/libs/policyRolePermissions.ts +++ b/src/libs/policyRolePermissions.ts @@ -7,19 +7,22 @@ type PolicyFeatureAccess = ValueOf; const ALL_POLICY_FEATURES = Object.values(CONST.POLICY.POLICY_FEATURE); -const WRITE_ALL_POLICY_FEATURES = ALL_POLICY_FEATURES.reduce>>( - (features, feature) => ({...features, [feature]: CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE}), - {}, -); - -const READ_ALL_POLICY_FEATURES = ALL_POLICY_FEATURES.reduce>>( - (features, feature) => ({...features, [feature]: CONST.POLICY.POLICY_FEATURE_ACCESS.READ}), - {}, -); - -const EDITOR_POLICY_FEATURES = ALL_POLICY_FEATURES.filter((feature) => feature !== CONST.POLICY.POLICY_FEATURE.ASSIGN_ELEVATED_ROLES).reduce< - Partial> ->((features, feature) => ({...features, [feature]: CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE}), {}); +function buildFeatureAccessMap(access: PolicyFeatureAccess, excludedFeature?: PolicyFeature): Partial> { + const features: Partial> = {}; + for (const feature of ALL_POLICY_FEATURES) { + if (feature === excludedFeature) { + continue; + } + features[feature] = access; + } + return features; +} + +const WRITE_ALL_POLICY_FEATURES = buildFeatureAccessMap(CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE); + +const READ_ALL_POLICY_FEATURES = buildFeatureAccessMap(CONST.POLICY.POLICY_FEATURE_ACCESS.READ); + +const EDITOR_POLICY_FEATURES = buildFeatureAccessMap(CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE, CONST.POLICY.POLICY_FEATURE.ASSIGN_ELEVATED_ROLES); const ROLE_PERMISSION_BUNDLES: Record>> = { [CONST.POLICY.ROLE.ADMIN]: WRITE_ALL_POLICY_FEATURES, diff --git a/src/pages/DynamicReportDetailsPage/ReportDetailsNameSection.tsx b/src/pages/DynamicReportDetailsPage/ReportDetailsNameSection.tsx index a9a74b039c76..3187363d7621 100644 --- a/src/pages/DynamicReportDetailsPage/ReportDetailsNameSection.tsx +++ b/src/pages/DynamicReportDetailsPage/ReportDetailsNameSection.tsx @@ -64,7 +64,7 @@ function ReportDetailsNameSectionContent({report}: ReportDetailsNameSectionConte const isGroupChat = isGroupChatUtil(report); const isThread = isThreadUtil(report); const isWorkspaceChat = isWorkspaceChatUtil(report.chatType ?? ''); - const shouldDisableRename = shouldDisableRenameUtil(report, isReportArchived); + const shouldDisableRename = shouldDisableRenameUtil(report, isReportArchived, policy); const chatRoomSubtitle = getChatRoomSubtitle(report, policy, conciergeReportID, translate, rules, false, isReportArchived) ?? ''; const additionalRoomDetails = isExpenseReportUtil(report) || isPolicyExpenseChatUtil(report) || isInvoiceRoomUtil(report) ? chatRoomSubtitle : `${translate('threads.in')} ${chatRoomSubtitle}`; diff --git a/src/pages/DynamicReportDetailsPage/index.tsx b/src/pages/DynamicReportDetailsPage/index.tsx index f0b82afe7d0d..ab5ed2c4a9fe 100644 --- a/src/pages/DynamicReportDetailsPage/index.tsx +++ b/src/pages/DynamicReportDetailsPage/index.tsx @@ -220,7 +220,6 @@ function DynamicReportDetailsPage({policy, report, route, reportMetadata, report const isSelfDMTrackExpenseReport = isTrackExpenseReport && isSelfDMUtil(parentReport); const isReportArchived = useReportIsArchived(report?.reportID); const isArchivedRoom = isArchivedNonExpenseReport(report, isReportArchived); - const shouldDisableRename = shouldDisableRenameUtil(report, isReportArchived, policy); const base62ReportID = getBase62ReportID(Number(report.reportID)); const ancestors = useAncestors(report); diff --git a/src/pages/workspace/getWorkspaceMenuItems.ts b/src/pages/workspace/getWorkspaceMenuItems.ts index 46cce7936864..19b2455cf548 100644 --- a/src/pages/workspace/getWorkspaceMenuItems.ts +++ b/src/pages/workspace/getWorkspaceMenuItems.ts @@ -228,6 +228,9 @@ function getWorkspaceMenuItems({ if (item.screenName === SCREENS.WORKSPACE.ROOMS) { return !isGuest; } + if (!policy) { + return true; + } return canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.MEMBERS); }); From e9d152fe4bbfa8e9a87fdc4fdb208557b4f57348 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:12:46 +0530 Subject: [PATCH 24/41] Update translations --- src/languages/de.ts | 4 ++-- src/languages/el.ts | 16 +++++++++------- src/languages/es.ts | 4 ++-- src/languages/fr.ts | 6 +++--- src/languages/it.ts | 4 ++-- src/languages/ja.ts | 4 ++-- src/languages/nl.ts | 8 ++++---- src/languages/pl.ts | 4 ++-- src/languages/pt-BR.ts | 8 ++++---- src/languages/zh-hans.ts | 12 ++++++------ 10 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 14e24a038b29..83035ebdd3ef 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4853,7 +4853,7 @@ ${amount} für ${merchant} – ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Kartenverwaltung'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Personalverwaltung'; + return 'Personenverwaltung'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Zahlungsadministrator'; case CONST.POLICY.ROLE.USER: @@ -7114,7 +7114,7 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, }), makeGuest: () => ({ one: 'Zum Gast machen', - other: 'Zu Gästen machen', + other: 'Gäste erstellen', }), makePeopleAdmin: () => ({ one: 'Zum Personaladministrator machen', diff --git a/src/languages/el.ts b/src/languages/el.ts index 54537634041b..f8ebec37cfb9 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -4905,7 +4905,7 @@ ${amount} για ${merchant} - ${date}`, memberAlternateText: 'Υποβάλετε και εγκρίνετε αναφορές.', adminAlternateText: 'Διαχειριστείτε αναφορές και ρυθμίσεις χώρου εργασίας.', auditorAlternateText: 'Προβολή και σχολιασμός αναφορών.', - guestAlternateText: 'Υποβάλετε και εγκρίνετε αναφορές, με συνομιλίες μόνο κατόπιν πρόσκλησης.', + guestAlternateText: 'Υποβολή και έγκριση αναφορών, με συνομιλίες μόνο με πρόσκληση.', cardAdminAlternateText: 'Διαχειριστείτε τις κάρτες χώρου εργασίας.', peopleAdminAlternateText: 'Διαχειριστείτε μέλη και ροές έγκρισης.', paymentsAdminAlternateText: 'Διαχειριστείτε τις πληρωμές ροής εργασιών.', @@ -4917,19 +4917,21 @@ ${amount} για ${merchant} - ${date}`, roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: - return 'Κάτοχος'; + return 'Ιδιοκτήτης'; case CONST.POLICY.ROLE.ADMIN: return 'Διαχειριστής χώρου εργασίας'; case CONST.POLICY.ROLE.AUDITOR: return 'Ελεγκτής'; + case CONST.POLICY.ROLE.GUEST: + return 'Επισκέπτης'; case CONST.POLICY.ROLE.EDITOR: return 'Επεξεργαστής'; case CONST.POLICY.ROLE.CARD_ADMIN: return 'Διαχείριση κάρτας'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Διαχείριση προσώπων'; + return 'Διαχείριση ατόμων'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: - return 'διαχείριση πληρωμών'; + return 'Διαχειριστής πληρωμών'; case CONST.POLICY.ROLE.USER: return 'Μέλος'; default: @@ -7237,8 +7239,8 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ other: 'Δημιουργία ελεγκτών', }), makeGuest: () => ({ - one: 'Ορισμός ως επισκέπτη', - other: 'Ορισμός ως επισκέπτες', + one: 'Κάντε επισκέπτη', + other: 'Προσθήκη επισκεπτών', }), makeCardAdmin: () => ({ one: 'Ορισμός διαχειριστή κάρτας', @@ -7270,7 +7272,7 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ paymentsAdmins: 'Διαχειριστές πληρωμών', approvers: 'Εγκρίνοντες', auditors: 'Ελεγκτές', - guests: 'Επισκέπτες', + guests: 'Καλεσμένοι', editors: 'Συντάκτες', members: 'Μέλη', emptyRoleFilter: { diff --git a/src/languages/es.ts b/src/languages/es.ts index b51bed31a186..7e2729f7fe41 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4763,7 +4763,7 @@ ${amount} para ${merchant} - ${date}`, memberAlternateText: 'Presentar y aprobar informes.', adminAlternateText: 'Gestionar informes y configuración del área de trabajo.', auditorAlternateText: 'Ver y comentar los informes.', - guestAlternateText: 'Enviar y aprobar informes, con chats solo por invitación.', + guestAlternateText: 'Envía y aprueba informes, con chats solo por invitación.', reimbursementChoice: { [CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES]: 'Directo', [CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO]: 'Ninguno', @@ -4784,7 +4784,7 @@ ${amount} para ${merchant} - ${date}`, case CONST.POLICY.ROLE.CARD_ADMIN: return 'Administrador de tarjeta'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Administrador de personas'; + return 'Administración de personas'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Administrador de pagos'; case CONST.POLICY.ROLE.USER: diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 94afbc989322..26eb775e1d9f 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4843,7 +4843,7 @@ ${amount} pour ${merchant} - ${date}`, memberAlternateText: 'Soumettre et approuver des notes de frais.', adminAlternateText: 'Gérer les notes de frais et les paramètres de l’espace de travail.', auditorAlternateText: 'Afficher et commenter les notes de frais.', - guestAlternateText: 'Envoyer et approuver des rapports, avec des discussions sur invitation uniquement.', + guestAlternateText: 'Soumettre et approuver des notes de frais, avec des discussions sur invitation uniquement.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -7131,8 +7131,8 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, other: 'Créer des auditeurs', }), makeGuest: () => ({ - one: 'Nommer invité', - other: 'Nommer invités', + one: 'Rendre invité', + other: 'Rendre invités', }), makePeopleAdmin: () => ({ one: 'Nommer administrateur des personnes', diff --git a/src/languages/it.ts b/src/languages/it.ts index 945fb208d991..0f21a4efc294 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4820,11 +4820,11 @@ ${amount} per ${merchant} - ${date}`, case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: - return 'Amministrazione carte'; + return 'Amministrazione carta'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: return 'Amministrazione persone'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: - return 'Amministrazione pagamenti'; + return 'Amministratore pagamenti'; case CONST.POLICY.ROLE.USER: return 'Membro'; default: diff --git a/src/languages/ja.ts b/src/languages/ja.ts index cae5715a4532..657d36e25b8e 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4771,7 +4771,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' memberAlternateText: 'レポートを提出して承認します。', adminAlternateText: 'レポートとワークスペースの設定を管理します。', auditorAlternateText: 'レポートを表示してコメントします。', - guestAlternateText: 'レポートを提出および承認し、招待されたチャットのみを使用します。', + guestAlternateText: 'レポートの提出と承認、招待制チャットができます。', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4779,7 +4779,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' case CONST.POLICY.ROLE.ADMIN: return 'ワークスペース管理者'; case CONST.POLICY.ROLE.AUDITOR: - return '監査担当者'; + return '監査人'; case CONST.POLICY.ROLE.GUEST: return 'ゲスト'; case CONST.POLICY.ROLE.EDITOR: diff --git a/src/languages/nl.ts b/src/languages/nl.ts index cf4c4b88402c..0fd82cb425b4 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4808,13 +4808,13 @@ ${amount} voor ${merchant} - ${date}`, memberAlternateText: 'Dien rapporten in en keur ze goed.', adminAlternateText: 'Beheer rapporten en werkruimte-instellingen.', auditorAlternateText: 'Rapporten bekijken en erop reageren.', - guestAlternateText: 'Rapporten indienen en goedkeuren, met chats alleen op uitnodiging.', + guestAlternateText: 'Dien rapporten in en keur ze goed, met chats op uitnodiging.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: return 'Eigenaar'; case CONST.POLICY.ROLE.ADMIN: - return 'Beheerder van werkruimte'; + return 'Beheerder van workspace'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; case CONST.POLICY.ROLE.GUEST: @@ -4822,9 +4822,9 @@ ${amount} voor ${merchant} - ${date}`, case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: - return 'Kaartbeheer'; + return 'Kaartbeheerder'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Beheer personen'; + return 'Personenbeheer'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Beheerder betalingen'; case CONST.POLICY.ROLE.USER: diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 1beab5f80d50..8e8f2db4d127 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4836,7 +4836,7 @@ ${amount} dla ${merchant} - ${date}`, memberAlternateText: 'Przesyłaj i zatwierdzaj raporty.', adminAlternateText: 'Zarządzaj raportami i ustawieniami przestrzeni roboczej.', auditorAlternateText: 'Przeglądaj i komentuj raporty.', - guestAlternateText: 'Przesyłaj i zatwierdzaj raporty, korzystając tylko z czatów na zaproszenie.', + guestAlternateText: 'Wysyłaj i zatwierdzaj raporty oraz korzystaj z czatów tylko na zaproszenie.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -7082,7 +7082,7 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, }), makeGuest: () => ({ one: 'Ustaw jako gościa', - other: 'Ustaw jako gości', + other: 'Utwórz gości', }), makePeopleAdmin: () => ({ one: 'Ustaw jako administratora osób', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 2490617756e8..672050107989 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4799,13 +4799,13 @@ ${amount} para ${merchant} - ${date}`, memberAlternateText: 'Enviar e aprovar relatórios.', adminAlternateText: 'Gerencie relatórios e configurações do espaço de trabalho.', auditorAlternateText: 'Visualize e comente relatórios.', - guestAlternateText: 'Envie e aprove relatórios, com chats somente por convite.', + guestAlternateText: 'Envie e aprove relatórios, com chats somente para convidados.', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: return 'Proprietário'; case CONST.POLICY.ROLE.ADMIN: - return 'Administrador da área de trabalho'; + return 'Admin da área de trabalho'; case CONST.POLICY.ROLE.AUDITOR: return 'Auditor'; case CONST.POLICY.ROLE.GUEST: @@ -4813,9 +4813,9 @@ ${amount} para ${merchant} - ${date}`, case CONST.POLICY.ROLE.EDITOR: return 'Editor'; case CONST.POLICY.ROLE.CARD_ADMIN: - return 'Admin do cartão'; + return 'Administrador do cartão'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return 'Administração de pessoas'; + return 'Admin de pessoas'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return 'Admin de pagamentos'; case CONST.POLICY.ROLE.USER: diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 13dd81006cdd..57f033404dd5 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4662,7 +4662,7 @@ ${amount},商户:${merchant} - 日期:${date}`, memberAlternateText: '提交并审批报销报告。', adminAlternateText: '管理报表和工作区设置。', auditorAlternateText: '查看并评论报表。', - guestAlternateText: '提交和批准报表,仅使用受邀聊天。', + guestAlternateText: '提交和审批报表,并使用仅限受邀的聊天。', roleName: (role?: string) => { switch (role) { case CONST.POLICY.ROLE.OWNER: @@ -4672,13 +4672,13 @@ ${amount},商户:${merchant} - 日期:${date}`, case CONST.POLICY.ROLE.AUDITOR: return '审计员'; case CONST.POLICY.ROLE.GUEST: - return '访客'; + return '来宾'; case CONST.POLICY.ROLE.EDITOR: - return '编辑者'; + return '编辑器'; case CONST.POLICY.ROLE.CARD_ADMIN: return '卡片管理员'; case CONST.POLICY.ROLE.PEOPLE_ADMIN: - return '人员管理员'; + return '人员管理'; case CONST.POLICY.ROLE.PAYMENTS_ADMIN: return '付款管理员'; case CONST.POLICY.ROLE.USER: @@ -6817,7 +6817,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM }), makeGuest: () => ({ one: '设为访客', - other: '设为访客', + other: '设为来宾', }), makePeopleAdmin: () => ({ one: '设为人员管理员', @@ -6849,7 +6849,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM admins: '工作区管理员', approvers: '审批人', auditors: '审计员', - guests: '访客', + guests: '来宾', editors: '编辑者', emptyRoleFilter: {title: '没有成员符合此筛选条件', subtitle: '邀请成员或更改上方的筛选条件。'}, configureHRSync: (providerName: string) => `配置 ${providerName} 同步。`, From 5a1531b4c518d180e4c51e9e58448b934fffb837 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:35:10 +0530 Subject: [PATCH 25/41] Show rooms to guests via their own chats --- src/components/Tables/WorkspaceRoomsTable/index.tsx | 6 ++++-- src/pages/workspace/getWorkspaceMenuItems.ts | 8 +------- src/pages/workspace/rooms/WorkspaceRoomsPage.tsx | 8 ++------ 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/components/Tables/WorkspaceRoomsTable/index.tsx b/src/components/Tables/WorkspaceRoomsTable/index.tsx index 7c0f73981d70..270414c0c527 100644 --- a/src/components/Tables/WorkspaceRoomsTable/index.tsx +++ b/src/components/Tables/WorkspaceRoomsTable/index.tsx @@ -45,9 +45,11 @@ type WorkspaceRoomsTableProps = { /** Content rendered above the table header inside the scrollable list */ headerComponent?: React.ReactElement; + + isContentLoaded?: boolean; }; -function WorkspaceRoomsTable({rooms, policyID, highlightedReportID, onSearchStringChange, onEndReached, onSortingChange, headerComponent}: WorkspaceRoomsTableProps) { +function WorkspaceRoomsTable({rooms, policyID, highlightedReportID, onSearchStringChange, onEndReached, onSortingChange, headerComponent, isContentLoaded}: WorkspaceRoomsTableProps) { const styles = useThemeStyles(); const {translate, localeCompare} = useLocalize(); const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); @@ -125,7 +127,7 @@ function WorkspaceRoomsTable({rooms, policyID, highlightedReportID, onSearchStri ) : undefined; - if (!roomsMetadata?.isLoaded) { + if (!(isContentLoaded ?? roomsMetadata?.isLoaded)) { // The page header stays visible above the loading skeleton so the layout doesn't jump once the table renders. return ( <> diff --git a/src/pages/workspace/getWorkspaceMenuItems.ts b/src/pages/workspace/getWorkspaceMenuItems.ts index 19b2455cf548..ef6bb5fb0f0c 100644 --- a/src/pages/workspace/getWorkspaceMenuItems.ts +++ b/src/pages/workspace/getWorkspaceMenuItems.ts @@ -20,7 +20,6 @@ import { isMCPEnabled, isPerDiemEnabled, isPolicyAdmin, - isPolicyGuest, isQBORefreshTokenExpiringSoonSelector, isTimeTrackingEnabled, shouldShowEmployeeListError, @@ -134,7 +133,6 @@ function getWorkspaceMenuItems({ convertToDisplayString, }: GetWorkspaceMenuItemsParams): WorkspaceMenuItem[] { const canReadPolicyFeature = (policyFeature: PolicyFeature) => canMemberRead(policy, currentUserLogin ?? '', policyFeature); - const isGuest = isPolicyGuest(policy, currentUserLogin ?? ''); const canReadMoreFeatures = canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.MORE_FEATURES); const shouldShowProtectedItems = [ CONST.POLICY.POLICY_FEATURE.REPORT_FIELDS, @@ -221,13 +219,9 @@ function getWorkspaceMenuItems({ }, ]; const items = defaultItems.filter((item) => { - if (item.screenName === SCREENS.WORKSPACE.PROFILE) { + if (item.screenName !== SCREENS.WORKSPACE.MEMBERS) { return true; } - // POLICY_FEATURE has no ROOMS entry yet, so guests are hidden from rooms by role - if (item.screenName === SCREENS.WORKSPACE.ROOMS) { - return !isGuest; - } if (!policy) { return true; } diff --git a/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx b/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx index c4731bb1d346..143cea5fcc0d 100644 --- a/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx +++ b/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx @@ -139,8 +139,6 @@ function WorkspaceRoomsPage({route}: WorkspaceRoomsPageProps) { // The fetch is driven by the requested page: loading more only bumps `pageNumber` and this effect issues the // request, the same way Search drives its own pagination from `offset`. Refocusing and coming back online refetch // the page that is currently displayed. - // Guests are blocked from this page, so they must not fetch room data either. Otherwise the rooms they - // should not browse would end up in Onyx. useEffect(() => { if (!isFocused || isOffline || isGuest) { return; @@ -175,10 +173,7 @@ function WorkspaceRoomsPage({route}: WorkspaceRoomsPageProps) { ) : undefined; return ( - + Date: Mon, 5 Oct 2026 17:50:42 +0530 Subject: [PATCH 26/41] Reuse isPolicyGuest util for guest role checks --- src/libs/ReportUtils.ts | 2 +- .../Workspace/WorkspaceRestrictedActionPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9964859ea67b..6eb7dcfb3adb 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -11510,7 +11510,7 @@ function shouldDisableRename(report: OnyxEntry, isReportArchived = false return true; } - if (policy?.role === CONST.POLICY.ROLE.GUEST) { + if (isPolicyGuest(policy)) { return true; } diff --git a/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx b/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx index 60fa65883f91..51c2c38b19ec 100644 --- a/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx +++ b/src/pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage.tsx @@ -105,7 +105,7 @@ function WorkspaceRestrictedActionPage({ } // Workspace User, Auditor or Guest - if (isPolicyUser(policy, email) || isPolicyAuditor(policy, email) || isPolicyGuest(policy)) { + if (isPolicyUser(policy, email) || isPolicyAuditor(policy, email) || isPolicyGuest(policy, email)) { return ; } From fb435c4615a4bb241905121bfb8a4507a1dc7464 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:47:02 +0530 Subject: [PATCH 27/41] Update guest supporting text and hide room create for guests --- src/languages/en.ts | 2 +- src/pages/workspace/rooms/WorkspaceRoomsPage.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b4459885e7c5..e360ecf69099 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4980,7 +4980,7 @@ const translations = { memberAlternateText: 'Submit and approve reports.', adminAlternateText: 'Manage reports and workspace settings.', auditorAlternateText: 'View and comment on reports.', - guestAlternateText: 'Submit and approve reports, with invite-only chats.', + guestAlternateText: 'Submit reports with limited visibility.', cardAdminAlternateText: 'Manage workspace cards.', peopleAdminAlternateText: 'Manage members and approval workflows.', paymentsAdminAlternateText: 'Manage workflow payments.', diff --git a/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx b/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx index 143cea5fcc0d..3e5e54de532f 100644 --- a/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx +++ b/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx @@ -158,8 +158,10 @@ function WorkspaceRoomsPage({route}: WorkspaceRoomsPageProps) { setPagination({queryKey: roomsQueryKey, pageNumber: pageNumber + 1}); }; + const canCreateRooms = !isArchived && !isGuest; + const roomsTableHeader = - shouldUseNarrowLayout && !isArchived ? ( + shouldUseNarrowLayout && canCreateRooms ? (