From c9637e2e8edfb500d2d54ef8e2d1a6847aac7608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Fri, 24 Jan 2025 17:48:09 +0100 Subject: [PATCH 01/31] add utility functions related to self approval --- src/libs/PolicyUtils.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 9e5e03acd1b0..b6790891409f 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1205,11 +1205,47 @@ function canModifyPlan(policyID?: string) { return !!policy && isPolicyAdmin(policy); } + +/** + * Returns an array of user emails who are currently self-approving: + * i.e. user.submitsTo === their own email. + */ +function getAllSelfApprovers(policy: OnyxEntry): string[] { + if (!policy?.employeeList) { + return []; + } + return Object.keys(policy.employeeList).filter((email) => { + const employee = policy?.employeeList?.[email] ?? {}; + return employee?.submitsTo === email; + }); +} + +/** + * Check if the policy has a default approver that is self-approving. + * If so, we cannot enable the "Prevent Self Approvals" feature. + */ +function canEnablePreventSelfApprovals(policy: OnyxEntry): boolean { + if (!policy?.employeeList || !policy.approver) { + return false; + } + + const defaultApprover = policy.employeeList[policy.approver]; + // If the default approver is missing or self-approving themselves, + // we have no valid fallback. + if (!defaultApprover || defaultApprover.submitsTo === policy.approver) { + return false; + } + + return true; +} + export { canEditTaxRate, + canEnablePreventSelfApprovals, extractPolicyIDFromPath, escapeTagName, getActivePolicies, + getAllSelfApprovers, getAdminEmployees, getCleanedTagName, getConnectedIntegration, From 220b8696bef8be8a13cd043df4136ba40805853a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Fri, 24 Jan 2025 17:48:20 +0100 Subject: [PATCH 02/31] add translation keys for prevent self approval --- src/languages/en.ts | 5 +++++ src/languages/es.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 3b55d6fbc75c..458b25fd2025 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4526,6 +4526,11 @@ const translations = { unlockFeatureGoToSubtitle: 'Go to', unlockFeatureEnableWorkflowsSubtitle: ({featureName}: FeatureNameParams) => `and enable workflows, then add ${featureName} to unlock this feature.`, enableFeatureSubtitle: ({featureName}: FeatureNameParams) => `and enable ${featureName} to unlock this feature.`, + preventSelfApprovalsModalText: ({managerEmail}: {managerEmail: string}) => + `Any members currently approving their own expenses will be removed and replaced with the default approver for this workspace (${managerEmail}).`, + preventSelfApprovalsConfirmButton: 'Prevent self-approvals', + preventSelfApprovalsModalTitle: 'Prevent self-approvals?', + preventSelfApprovalsDisabledSubtitle: 'Self-approvals cannot be disabled because the workspace owner is the only approver. Add another approver first.', }, categoryRules: { title: 'Category rules', diff --git a/src/languages/es.ts b/src/languages/es.ts index dff3dcd575c0..9c7b6a5b2b15 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4592,6 +4592,11 @@ const translations = { unlockFeatureGoToSubtitle: 'Ir a', unlockFeatureEnableWorkflowsSubtitle: ({featureName}: FeatureNameParams) => `y habilita flujos de trabajo, luego agrega ${featureName} para desbloquear esta función.`, enableFeatureSubtitle: ({featureName}: FeatureNameParams) => `y habilita ${featureName} para desbloquear esta función.`, + preventSelfApprovalsModalText: ({managerEmail}: {managerEmail: string}) => + `Cualquier miembro que apruebe sus propios gastos será eliminado y reemplazado con el aprobador predeterminado para este espacio de trabajo (${managerEmail}).`, + preventSelfApprovalsConfirmButton: 'Evitar autoaprobaciones', + preventSelfApprovalsModalTitle: '¿Evitar autoaprobaciones?', + preventSelfApprovalsDisabledSubtitle: 'No se pueden deshabilitar las autoaprobaciones porque el propietario del espacio de trabajo es el único aprobador. Agregue otro aprobador primero.', }, categoryRules: { title: 'Reglas de categoría', From ae7274cbce2a3d890edf8c0f481fa13442f32892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Fri, 24 Jan 2025 17:48:57 +0100 Subject: [PATCH 03/31] WIP prevent self approval --- .../rules/ExpenseReportRulesSection.tsx | 165 ++++++++++++++---- 1 file changed, 130 insertions(+), 35 deletions(-) diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index 6b793e5e4588..f38d38dee4d0 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Section from '@components/Section'; @@ -14,6 +14,13 @@ import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOpt import * as PolicyActions from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import ConfirmModal from '@components/ConfirmModal'; +import * as Workflow from '@userActions/Workflow'; +import {useOnyx} from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; +import { InteractionManager } from 'react-native'; +import { Approver, Member } from '@src/types/onyx/ApprovalWorkflow'; type ExpenseReportRulesSectionProps = { policyID: string; @@ -25,10 +32,42 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { const policy = usePolicy(policyID); const customReportNamesUnavailable = !policy?.areReportFieldsEnabled; + const [approvalWorkflow] = useOnyx(ONYXKEYS.APPROVAL_WORKFLOW); // Auto-approvals and self-approvals are unavailable due to the policy workflows settings const workflowApprovalsUnavailable = PolicyUtils.getWorkflowApprovalsUnavailable(policy); const autoPayApprovedReportsUnavailable = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; + + const [isPreventSelfApprovalsModalVisible, setIsPreventSelfApprovalsModalVisible] = useState(false); + // const isPreventSelfApprovalsDisabled = !PolicyUtils.canEnablePreventSelfApprovals(policy) && !policy?.preventSelfApproval; + const isPreventSelfApprovalsDisabled = false; + /** + * Handle toggling of “Prevent Self Approvals” + * - If toggling OFF, we just call the action and skip any modal. + * - If toggling ON: + * - If nobody is self-approving, enable immediately. + * - Otherwise, show a modal for confirmation. + */ + function handleTogglePreventSelfApprovals(isEnabled: boolean) { + if (!isEnabled) { + PolicyActions.setPolicyPreventSelfApproval(policyID, false); + return; + } + + // Only check for self approvers when enabling the setting + const selfApprovers = PolicyUtils.getAllSelfApprovers(policy); + if (selfApprovers.length === 0) { + PolicyActions.setPolicyPreventSelfApproval(policyID, true); + } else { + setIsPreventSelfApprovalsModalVisible(true); + } + } + + function isSelfApprover(email: string): boolean { + const employee = policy?.employeeList?.[email]; + return !!(employee && employee.submitsTo === email); + } + const renderFallbackSubtitle = ({featureName, variant = 'unlock'}: {featureName: string; variant?: 'unlock' | 'enable'}) => { return ( @@ -89,14 +128,16 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { { title: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), subtitle: workflowApprovalsUnavailable - ? renderFallbackSubtitle({featureName: translate('common.approvals').toLowerCase()}) - : translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle'), + ? renderFallbackSubtitle({ featureName: translate('common.approvals').toLowerCase() }) + : isPreventSelfApprovalsDisabled + ? translate('workspace.rules.expenseReportRules.preventSelfApprovalsDisabledSubtitle') + : translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle'), switchAccessibilityLabel: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), isActive: policy?.preventSelfApproval && !workflowApprovalsUnavailable, - disabled: workflowApprovalsUnavailable, - showLockIcon: workflowApprovalsUnavailable, + disabled: workflowApprovalsUnavailable || isPreventSelfApprovalsDisabled, + showLockIcon: workflowApprovalsUnavailable || isPreventSelfApprovalsDisabled, pendingAction: policy?.pendingFields?.preventSelfApproval, - onToggle: (isEnabled: boolean) => PolicyActions.setPolicyPreventSelfApproval(policyID, isEnabled), + onToggle: (isEnabled: boolean) => handleTogglePreventSelfApprovals(isEnabled), }, { title: translate('workspace.rules.expenseReportRules.autoApproveCompliantReportsTitle'), @@ -181,36 +222,90 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { ]; return ( -
- {optionItems.map(({title, subtitle, isActive, subMenuItems, showLockIcon, disabled, onToggle, pendingAction}, index) => { - const showBorderBottom = index !== optionItems.length - 1; + <> +
+ {optionItems.map(({title, subtitle, isActive, subMenuItems, showLockIcon, disabled, onToggle, pendingAction}, index) => { + const showBorderBottom = index !== optionItems.length - 1; - return ( - - ); - })} -
+ return ( + + ); + })} +
+ { + // First, enable the setting that blocks self-approvals. + PolicyActions.setPolicyPreventSelfApproval(policyID, true); + + // Build a new workflow where all self-approvers are replaced by the policy owner. + const newWorkflow: ApprovalWorkflow = { + ...approvalWorkflow, + // Remove any members who are self-approvers + members: (approvalWorkflow?.members ?? []).filter((member) => !isSelfApprover(member.email)), + // Add the policy owner as the first approver; filter out existing self-approvers + approvers: [ + { + email: policy?.owner ?? '', + displayName: policy?.owner ?? '', + }, + ...(approvalWorkflow?.approvers?.filter( + (approver: Approver) => approver && !isSelfApprover(approver.email), + ) ?? []), + ], + // Assume this is the "default" workflow + isDefault: approvalWorkflow?.isDefault ?? true, + }; + + // Compute which members and approvers were removed + const membersToRemove = + approvalWorkflow?.members?.filter( + (oldMember: Member) => !newWorkflow.members.some((newMember) => newMember.email === oldMember.email), + ) ?? []; + + const approversToRemove = + approvalWorkflow?.approvers?.filter( + (oldApprover: Approver) => + oldApprover && + !newWorkflow.approvers.some((newApprover) => newApprover.email === oldApprover.email), + ) as Approver[] ?? []; + + setIsPreventSelfApprovalsModalVisible(false); + + InteractionManager.runAfterInteractions(() => { + Workflow.updateApprovalWorkflow(policyID, newWorkflow, membersToRemove, approversToRemove); + }); + }} + onCancel={() => setIsPreventSelfApprovalsModalVisible(false)} + /> + ); } From 57c102686b2a415af2da4fda0738479d1ff3af29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 27 Jan 2025 09:49:20 +0100 Subject: [PATCH 04/31] working logic --- .../rules/ExpenseReportRulesSection.tsx | 130 ++++++++++-------- 1 file changed, 74 insertions(+), 56 deletions(-) diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index f38d38dee4d0..a53234ccbfb5 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Section from '@components/Section'; @@ -21,6 +21,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; import { InteractionManager } from 'react-native'; import { Approver, Member } from '@src/types/onyx/ApprovalWorkflow'; +import { convertPolicyEmployeesToApprovalWorkflows } from '@libs/WorkflowUtils'; type ExpenseReportRulesSectionProps = { policyID: string; @@ -30,43 +31,47 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policy = usePolicy(policyID); - + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const customReportNamesUnavailable = !policy?.areReportFieldsEnabled; - const [approvalWorkflow] = useOnyx(ONYXKEYS.APPROVAL_WORKFLOW); // Auto-approvals and self-approvals are unavailable due to the policy workflows settings const workflowApprovalsUnavailable = PolicyUtils.getWorkflowApprovalsUnavailable(policy); const autoPayApprovedReportsUnavailable = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; const [isPreventSelfApprovalsModalVisible, setIsPreventSelfApprovalsModalVisible] = useState(false); - // const isPreventSelfApprovalsDisabled = !PolicyUtils.canEnablePreventSelfApprovals(policy) && !policy?.preventSelfApproval; - const isPreventSelfApprovalsDisabled = false; + const isPreventSelfApprovalsDisabled = !PolicyUtils.canEnablePreventSelfApprovals(policy) && !policy?.preventSelfApproval; + const selfApproversEmails = PolicyUtils.getAllSelfApprovers(policy); - /** - * Handle toggling of “Prevent Self Approvals” - * - If toggling OFF, we just call the action and skip any modal. - * - If toggling ON: - * - If nobody is self-approving, enable immediately. - * - Otherwise, show a modal for confirmation. - */ function handleTogglePreventSelfApprovals(isEnabled: boolean) { if (!isEnabled) { PolicyActions.setPolicyPreventSelfApproval(policyID, false); return; } - - // Only check for self approvers when enabling the setting - const selfApprovers = PolicyUtils.getAllSelfApprovers(policy); - if (selfApprovers.length === 0) { + + if (selfApproversEmails.length === 0) { PolicyActions.setPolicyPreventSelfApproval(policyID, true); } else { setIsPreventSelfApprovalsModalVisible(true); } } + + const { currentApprovalWorkflows, defaultWorkflowMembers, usedApproverEmails } = useMemo(() => { + if (!policy || !personalDetails) { + return {}; + } - function isSelfApprover(email: string): boolean { - const employee = policy?.employeeList?.[email]; - return !!(employee && employee.submitsTo === email); - } + const defaultApprover = policy?.approver ?? policy.owner; + const result = convertPolicyEmployeesToApprovalWorkflows({ + employees: policy.employeeList ?? {}, + defaultApprover, + personalDetails, + }); + + return { + defaultWorkflowMembers: result.availableMembers, + usedApproverEmails: result.usedApproverEmails, + currentApprovalWorkflows: result.approvalWorkflows.filter((workflow) => !workflow.isDefault), + }; + }, [personalDetails, policy]); const renderFallbackSubtitle = ({featureName, variant = 'unlock'}: {featureName: string; variant?: 'unlock' | 'enable'}) => { return ( @@ -262,49 +267,62 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { confirmText={translate('workspace.rules.expenseReportRules.preventSelfApprovalsConfirmButton')} cancelText={translate('common.cancel')} onConfirm={() => { - // First, enable the setting that blocks self-approvals. - PolicyActions.setPolicyPreventSelfApproval(policyID, true); + PolicyActions.setPolicyPreventSelfApproval(policyID, true); - // Build a new workflow where all self-approvers are replaced by the policy owner. - const newWorkflow: ApprovalWorkflow = { - ...approvalWorkflow, - // Remove any members who are self-approvers - members: (approvalWorkflow?.members ?? []).filter((member) => !isSelfApprover(member.email)), - // Add the policy owner as the first approver; filter out existing self-approvers - approvers: [ - { - email: policy?.owner ?? '', - displayName: policy?.owner ?? '', - }, - ...(approvalWorkflow?.approvers?.filter( - (approver: Approver) => approver && !isSelfApprover(approver.email), - ) ?? []), - ], - // Assume this is the "default" workflow - isDefault: approvalWorkflow?.isDefault ?? true, - }; + const defaultApprover = policy?.approver || policy?.owner; + if (!defaultApprover) { + setIsPreventSelfApprovalsModalVisible(false); + return; + } - // Compute which members and approvers were removed - const membersToRemove = - approvalWorkflow?.members?.filter( - (oldMember: Member) => !newWorkflow.members.some((newMember) => newMember.email === oldMember.email), - ) ?? []; + currentApprovalWorkflows?.forEach((workflow: ApprovalWorkflow) => { + const oldApprovers = workflow.approvers ?? []; + const approversToRemove = oldApprovers.filter((approver: Approver) => + selfApproversEmails.includes(approver?.email) + ); + const newApprovers = oldApprovers.filter( + (approver: Approver) => !selfApproversEmails.includes(approver?.email), + ); - const approversToRemove = - approvalWorkflow?.approvers?.filter( - (oldApprover: Approver) => - oldApprover && - !newWorkflow.approvers.some((newApprover) => newApprover.email === oldApprover.email), - ) as Approver[] ?? []; + if (!newApprovers.some((a) => a.email === defaultApprover)) { + newApprovers.unshift({ + email: defaultApprover, + displayName: defaultApprover, + }); + } - setIsPreventSelfApprovalsModalVisible(false); + const oldMembers = workflow.members ?? []; + const newMembers = oldMembers.map((member: Member) => { + const isSelfApprover = selfApproversEmails.includes(member.email); + return isSelfApprover + ? {...member, submitsTo: defaultApprover} + : member; + }); + + const newWorkflow = { + ...workflow, + approvers: newApprovers, + availableMembers: [...workflow.members, ...defaultWorkflowMembers], + members: newMembers, + usedApproverEmails: usedApproverEmails, + isDefault: workflow.isDefault ?? false, + action: CONST.APPROVAL_WORKFLOW.ACTION.EDIT, + errors: null, + }; - InteractionManager.runAfterInteractions(() => { - Workflow.updateApprovalWorkflow(policyID, newWorkflow, membersToRemove, approversToRemove); + const membersToRemove: Member[] = []; + + Workflow.updateApprovalWorkflow( + policyID, + newWorkflow, + membersToRemove, + approversToRemove, + ); }); + setIsPreventSelfApprovalsModalVisible(false); }} - onCancel={() => setIsPreventSelfApprovalsModalVisible(false)} - /> + onCancel={() => setIsPreventSelfApprovalsModalVisible(false)} + /> ); } From e4ac957b9d1d50edf48f6edac9f4eb0a0e9c8cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 27 Jan 2025 09:51:30 +0100 Subject: [PATCH 05/31] remove logic related to default workspace owner --- src/languages/en.ts | 1 - src/languages/es.ts | 1 - src/libs/PolicyUtils.ts | 20 ------------------- .../rules/ExpenseReportRulesSection.tsx | 9 +++------ 4 files changed, 3 insertions(+), 28 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 458b25fd2025..2e4962bed182 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4530,7 +4530,6 @@ const translations = { `Any members currently approving their own expenses will be removed and replaced with the default approver for this workspace (${managerEmail}).`, preventSelfApprovalsConfirmButton: 'Prevent self-approvals', preventSelfApprovalsModalTitle: 'Prevent self-approvals?', - preventSelfApprovalsDisabledSubtitle: 'Self-approvals cannot be disabled because the workspace owner is the only approver. Add another approver first.', }, categoryRules: { title: 'Category rules', diff --git a/src/languages/es.ts b/src/languages/es.ts index 9c7b6a5b2b15..90eb2185f064 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4596,7 +4596,6 @@ const translations = { `Cualquier miembro que apruebe sus propios gastos será eliminado y reemplazado con el aprobador predeterminado para este espacio de trabajo (${managerEmail}).`, preventSelfApprovalsConfirmButton: 'Evitar autoaprobaciones', preventSelfApprovalsModalTitle: '¿Evitar autoaprobaciones?', - preventSelfApprovalsDisabledSubtitle: 'No se pueden deshabilitar las autoaprobaciones porque el propietario del espacio de trabajo es el único aprobador. Agregue otro aprobador primero.', }, categoryRules: { title: 'Reglas de categoría', diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index b6790891409f..e01ed1cc5b89 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1220,28 +1220,8 @@ function getAllSelfApprovers(policy: OnyxEntry): string[] { }); } -/** - * Check if the policy has a default approver that is self-approving. - * If so, we cannot enable the "Prevent Self Approvals" feature. - */ -function canEnablePreventSelfApprovals(policy: OnyxEntry): boolean { - if (!policy?.employeeList || !policy.approver) { - return false; - } - - const defaultApprover = policy.employeeList[policy.approver]; - // If the default approver is missing or self-approving themselves, - // we have no valid fallback. - if (!defaultApprover || defaultApprover.submitsTo === policy.approver) { - return false; - } - - return true; -} - export { canEditTaxRate, - canEnablePreventSelfApprovals, extractPolicyIDFromPath, escapeTagName, getActivePolicies, diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index a53234ccbfb5..2233f2570faa 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -38,7 +38,6 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { const autoPayApprovedReportsUnavailable = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; const [isPreventSelfApprovalsModalVisible, setIsPreventSelfApprovalsModalVisible] = useState(false); - const isPreventSelfApprovalsDisabled = !PolicyUtils.canEnablePreventSelfApprovals(policy) && !policy?.preventSelfApproval; const selfApproversEmails = PolicyUtils.getAllSelfApprovers(policy); function handleTogglePreventSelfApprovals(isEnabled: boolean) { @@ -134,13 +133,11 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { title: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), subtitle: workflowApprovalsUnavailable ? renderFallbackSubtitle({ featureName: translate('common.approvals').toLowerCase() }) - : isPreventSelfApprovalsDisabled - ? translate('workspace.rules.expenseReportRules.preventSelfApprovalsDisabledSubtitle') - : translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle'), + : translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle'), switchAccessibilityLabel: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), isActive: policy?.preventSelfApproval && !workflowApprovalsUnavailable, - disabled: workflowApprovalsUnavailable || isPreventSelfApprovalsDisabled, - showLockIcon: workflowApprovalsUnavailable || isPreventSelfApprovalsDisabled, + disabled: workflowApprovalsUnavailable, + showLockIcon: workflowApprovalsUnavailable, pendingAction: policy?.pendingFields?.preventSelfApproval, onToggle: (isEnabled: boolean) => handleTogglePreventSelfApprovals(isEnabled), }, From 545adfb89df9d3a313866b6ef0060466edad6579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 27 Jan 2025 10:07:52 +0100 Subject: [PATCH 06/31] exclude policy owner from logic --- src/libs/PolicyUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index e01ed1cc5b89..83beccee159f 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1211,12 +1211,13 @@ function canModifyPlan(policyID?: string) { * i.e. user.submitsTo === their own email. */ function getAllSelfApprovers(policy: OnyxEntry): string[] { - if (!policy?.employeeList) { + const defaultApprover = policy?.approver ?? policy?.owner; + if (!policy?.employeeList || !defaultApprover) { return []; } return Object.keys(policy.employeeList).filter((email) => { const employee = policy?.employeeList?.[email] ?? {}; - return employee?.submitsTo === email; + return employee?.submitsTo === email && employee?.email !== defaultApprover; }); } From 6cb1387d05e3296f78ed633b3810de61cbf53ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 27 Jan 2025 10:33:54 +0100 Subject: [PATCH 07/31] fix prettier --- src/libs/PolicyUtils.ts | 1 - .../rules/ExpenseReportRulesSection.tsx | 131 ++++++++---------- 2 files changed, 60 insertions(+), 72 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 83beccee159f..7ec2487dde97 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1205,7 +1205,6 @@ function canModifyPlan(policyID?: string) { return !!policy && isPolicyAdmin(policy); } - /** * Returns an array of user emails who are currently self-approving: * i.e. user.submitsTo === their own email. diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index 2233f2570faa..e31589ca9330 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -1,4 +1,7 @@ -import React, { useMemo, useState } from 'react'; +import React, {useMemo, useState} from 'react'; +import {InteractionManager} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import ConfirmModal from '@components/ConfirmModal'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Section from '@components/Section'; @@ -10,18 +13,15 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {convertPolicyEmployeesToApprovalWorkflows} from '@libs/WorkflowUtils'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import * as PolicyActions from '@userActions/Policy/Policy'; -import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; -import ConfirmModal from '@components/ConfirmModal'; import * as Workflow from '@userActions/Workflow'; -import {useOnyx} from 'react-native-onyx'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; -import { InteractionManager } from 'react-native'; -import { Approver, Member } from '@src/types/onyx/ApprovalWorkflow'; -import { convertPolicyEmployeesToApprovalWorkflows } from '@libs/WorkflowUtils'; +import {Approver, Member} from '@src/types/onyx/ApprovalWorkflow'; type ExpenseReportRulesSectionProps = { policyID: string; @@ -36,7 +36,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { // Auto-approvals and self-approvals are unavailable due to the policy workflows settings const workflowApprovalsUnavailable = PolicyUtils.getWorkflowApprovalsUnavailable(policy); const autoPayApprovedReportsUnavailable = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; - + const [isPreventSelfApprovalsModalVisible, setIsPreventSelfApprovalsModalVisible] = useState(false); const selfApproversEmails = PolicyUtils.getAllSelfApprovers(policy); @@ -45,7 +45,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { PolicyActions.setPolicyPreventSelfApproval(policyID, false); return; } - + if (selfApproversEmails.length === 0) { PolicyActions.setPolicyPreventSelfApproval(policyID, true); } else { @@ -53,25 +53,25 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { } } - const { currentApprovalWorkflows, defaultWorkflowMembers, usedApproverEmails } = useMemo(() => { + const {currentApprovalWorkflows, defaultWorkflowMembers, usedApproverEmails} = useMemo(() => { if (!policy || !personalDetails) { return {}; } - + const defaultApprover = policy?.approver ?? policy.owner; const result = convertPolicyEmployeesToApprovalWorkflows({ employees: policy.employeeList ?? {}, defaultApprover, personalDetails, }); - + return { defaultWorkflowMembers: result.availableMembers, usedApproverEmails: result.usedApproverEmails, currentApprovalWorkflows: result.approvalWorkflows.filter((workflow) => !workflow.isDefault), }; }, [personalDetails, policy]); - + const renderFallbackSubtitle = ({featureName, variant = 'unlock'}: {featureName: string; variant?: 'unlock' | 'enable'}) => { return ( @@ -132,7 +132,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { { title: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), subtitle: workflowApprovalsUnavailable - ? renderFallbackSubtitle({ featureName: translate('common.approvals').toLowerCase() }) + ? renderFallbackSubtitle({featureName: translate('common.approvals').toLowerCase()}) : translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle'), switchAccessibilityLabel: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), isActive: policy?.preventSelfApproval && !workflowApprovalsUnavailable, @@ -256,69 +256,58 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { })} { - PolicyActions.setPolicyPreventSelfApproval(policyID, true); - - const defaultApprover = policy?.approver || policy?.owner; - if (!defaultApprover) { - setIsPreventSelfApprovalsModalVisible(false); - return; - } + isVisible={isPreventSelfApprovalsModalVisible} + title={translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle')} + prompt={translate('workspace.rules.expenseReportRules.preventSelfApprovalsModalText', { + managerEmail: policy?.approver ?? '', + })} + confirmText={translate('workspace.rules.expenseReportRules.preventSelfApprovalsConfirmButton')} + cancelText={translate('common.cancel')} + onConfirm={() => { + PolicyActions.setPolicyPreventSelfApproval(policyID, true); - currentApprovalWorkflows?.forEach((workflow: ApprovalWorkflow) => { - const oldApprovers = workflow.approvers ?? []; - const approversToRemove = oldApprovers.filter((approver: Approver) => - selfApproversEmails.includes(approver?.email) - ); - const newApprovers = oldApprovers.filter( - (approver: Approver) => !selfApproversEmails.includes(approver?.email), - ); + const defaultApprover = policy?.approver || policy?.owner; + if (!defaultApprover) { + setIsPreventSelfApprovalsModalVisible(false); + return; + } - if (!newApprovers.some((a) => a.email === defaultApprover)) { - newApprovers.unshift({ - email: defaultApprover, - displayName: defaultApprover, - }); - } + currentApprovalWorkflows?.forEach((workflow: ApprovalWorkflow) => { + const oldApprovers = workflow.approvers ?? []; + const approversToRemove = oldApprovers.filter((approver: Approver) => selfApproversEmails.includes(approver?.email)); + const newApprovers = oldApprovers.filter((approver: Approver) => !selfApproversEmails.includes(approver?.email)); - const oldMembers = workflow.members ?? []; - const newMembers = oldMembers.map((member: Member) => { - const isSelfApprover = selfApproversEmails.includes(member.email); - return isSelfApprover - ? {...member, submitsTo: defaultApprover} - : member; + if (!newApprovers.some((a) => a.email === defaultApprover)) { + newApprovers.unshift({ + email: defaultApprover, + displayName: defaultApprover, }); + } + + const oldMembers = workflow.members ?? []; + const newMembers = oldMembers.map((member: Member) => { + const isSelfApprover = selfApproversEmails.includes(member.email); + return isSelfApprover ? {...member, submitsTo: defaultApprover} : member; + }); - const newWorkflow = { - ...workflow, - approvers: newApprovers, - availableMembers: [...workflow.members, ...defaultWorkflowMembers], - members: newMembers, - usedApproverEmails: usedApproverEmails, - isDefault: workflow.isDefault ?? false, - action: CONST.APPROVAL_WORKFLOW.ACTION.EDIT, - errors: null, - }; + const newWorkflow = { + ...workflow, + approvers: newApprovers, + availableMembers: [...workflow.members, ...defaultWorkflowMembers], + members: newMembers, + usedApproverEmails: usedApproverEmails, + isDefault: workflow.isDefault ?? false, + action: CONST.APPROVAL_WORKFLOW.ACTION.EDIT, + errors: null, + }; - const membersToRemove: Member[] = []; + const membersToRemove: Member[] = []; - Workflow.updateApprovalWorkflow( - policyID, - newWorkflow, - membersToRemove, - approversToRemove, - ); - }); - setIsPreventSelfApprovalsModalVisible(false); - }} - onCancel={() => setIsPreventSelfApprovalsModalVisible(false)} + Workflow.updateApprovalWorkflow(policyID, newWorkflow, membersToRemove, approversToRemove); + }); + setIsPreventSelfApprovalsModalVisible(false); + }} + onCancel={() => setIsPreventSelfApprovalsModalVisible(false)} /> ); From 3dcd36d678c6eaedac6820edbcd2553377f4b26b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 27 Jan 2025 10:38:30 +0100 Subject: [PATCH 08/31] fix spanish translation --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 90eb2185f064..141b1e394a87 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4593,7 +4593,7 @@ const translations = { unlockFeatureEnableWorkflowsSubtitle: ({featureName}: FeatureNameParams) => `y habilita flujos de trabajo, luego agrega ${featureName} para desbloquear esta función.`, enableFeatureSubtitle: ({featureName}: FeatureNameParams) => `y habilita ${featureName} para desbloquear esta función.`, preventSelfApprovalsModalText: ({managerEmail}: {managerEmail: string}) => - `Cualquier miembro que apruebe sus propios gastos será eliminado y reemplazado con el aprobador predeterminado para este espacio de trabajo (${managerEmail}).`, + `Todos los miembros que actualmente estén aprobando sus propios gastos serán eliminados y reemplazados con el aprobador predeterminado de este espacio de trabajo (${managerEmail}).`, preventSelfApprovalsConfirmButton: 'Evitar autoaprobaciones', preventSelfApprovalsModalTitle: '¿Evitar autoaprobaciones?', }, From 084a2aa2d5154db787adc533653b4c301707a324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 27 Jan 2025 10:46:07 +0100 Subject: [PATCH 09/31] fix lint --- src/pages/workspace/rules/ExpenseReportRulesSection.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index e31589ca9330..9a135ea70e53 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -1,5 +1,4 @@ import React, {useMemo, useState} from 'react'; -import {InteractionManager} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -21,7 +20,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; -import {Approver, Member} from '@src/types/onyx/ApprovalWorkflow'; +import type {Approver, Member} from '@src/types/onyx/ApprovalWorkflow'; type ExpenseReportRulesSectionProps = { policyID: string; @@ -266,7 +265,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { onConfirm={() => { PolicyActions.setPolicyPreventSelfApproval(policyID, true); - const defaultApprover = policy?.approver || policy?.owner; + const defaultApprover = policy?.approver ?? policy?.owner; if (!defaultApprover) { setIsPreventSelfApprovalsModalVisible(false); return; @@ -295,7 +294,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { approvers: newApprovers, availableMembers: [...workflow.members, ...defaultWorkflowMembers], members: newMembers, - usedApproverEmails: usedApproverEmails, + usedApproverEmails, isDefault: workflow.isDefault ?? false, action: CONST.APPROVAL_WORKFLOW.ACTION.EDIT, errors: null, From 524efc37461cbc9c5d7add4fab8eba907936e8aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 27 Jan 2025 10:56:36 +0100 Subject: [PATCH 10/31] fix lint --- .../rules/ExpenseReportRulesSection.tsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index 9a135ea70e53..eca37a335f56 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -9,13 +9,13 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; +import {getWorkflowApprovalsUnavailable, getAllSelfApprovers} from '@libs/PolicyUtils'; import {convertPolicyEmployeesToApprovalWorkflows} from '@libs/WorkflowUtils'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; -import * as PolicyActions from '@userActions/Policy/Policy'; -import * as Workflow from '@userActions/Workflow'; +import {setPolicyPreventSelfApproval, enablePolicyDefaultReportTitle, setPolicyPreventMemberCreatedTitle, enableAutoApprovalOptions, enablePolicyAutoReimbursementLimit} from '@userActions/Policy/Policy'; +import {updateApprovalWorkflow} from '@userActions/Workflow'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -33,20 +33,20 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const customReportNamesUnavailable = !policy?.areReportFieldsEnabled; // Auto-approvals and self-approvals are unavailable due to the policy workflows settings - const workflowApprovalsUnavailable = PolicyUtils.getWorkflowApprovalsUnavailable(policy); + const workflowApprovalsUnavailable = getWorkflowApprovalsUnavailable(policy); const autoPayApprovedReportsUnavailable = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; const [isPreventSelfApprovalsModalVisible, setIsPreventSelfApprovalsModalVisible] = useState(false); - const selfApproversEmails = PolicyUtils.getAllSelfApprovers(policy); + const selfApproversEmails = getAllSelfApprovers(policy); function handleTogglePreventSelfApprovals(isEnabled: boolean) { if (!isEnabled) { - PolicyActions.setPolicyPreventSelfApproval(policyID, false); + setPolicyPreventSelfApproval(policyID, false); return; } if (selfApproversEmails.length === 0) { - PolicyActions.setPolicyPreventSelfApproval(policyID, true); + setPolicyPreventSelfApproval(policyID, true); } else { setIsPreventSelfApprovalsModalVisible(true); } @@ -102,7 +102,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { disabled: customReportNamesUnavailable, showLockIcon: customReportNamesUnavailable, pendingAction: policy?.pendingFields?.shouldShowCustomReportTitleOption, - onToggle: (isEnabled: boolean) => PolicyActions.enablePolicyDefaultReportTitle(policyID, isEnabled), + onToggle: (isEnabled: boolean) => enablePolicyDefaultReportTitle(policyID, isEnabled), subMenuItems: [ PolicyActions.setPolicyPreventMemberCreatedTitle(policyID, isEnabled)} + onToggle={(isEnabled) => setPolicyPreventMemberCreatedTitle(policyID, isEnabled)} />, ], }, @@ -151,7 +151,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { showLockIcon: workflowApprovalsUnavailable, pendingAction: policy?.pendingFields?.shouldShowAutoApprovalOptions, onToggle: (isEnabled: boolean) => { - PolicyActions.enableAutoApprovalOptions(policyID, isEnabled); + enableAutoApprovalOptions(policyID, isEnabled); }, subMenuItems: [ { - PolicyActions.enablePolicyAutoReimbursementLimit(policyID, isEnabled); + enablePolicyAutoReimbursementLimit(policyID, isEnabled); }, disabled: autoPayApprovedReportsUnavailable, showLockIcon: autoPayApprovedReportsUnavailable, @@ -209,7 +209,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { > { - PolicyActions.setPolicyPreventSelfApproval(policyID, true); + setPolicyPreventSelfApproval(policyID, true); const defaultApprover = policy?.approver ?? policy?.owner; if (!defaultApprover) { @@ -302,7 +302,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { const membersToRemove: Member[] = []; - Workflow.updateApprovalWorkflow(policyID, newWorkflow, membersToRemove, approversToRemove); + updateApprovalWorkflow(policyID, newWorkflow, membersToRemove, approversToRemove); }); setIsPreventSelfApprovalsModalVisible(false); }} From 0a500f4909aa29a7894590887817f7f24f3c5c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 27 Jan 2025 11:00:48 +0100 Subject: [PATCH 11/31] fix style --- .../rules/ExpenseReportRulesSection.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index eca37a335f56..852f8fad7893 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -11,10 +11,16 @@ import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getWorkflowApprovalsUnavailable, getAllSelfApprovers} from '@libs/PolicyUtils'; +import {getAllSelfApprovers, getWorkflowApprovalsUnavailable} from '@libs/PolicyUtils'; import {convertPolicyEmployeesToApprovalWorkflows} from '@libs/WorkflowUtils'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; -import {setPolicyPreventSelfApproval, enablePolicyDefaultReportTitle, setPolicyPreventMemberCreatedTitle, enableAutoApprovalOptions, enablePolicyAutoReimbursementLimit} from '@userActions/Policy/Policy'; +import { + enableAutoApprovalOptions, + enablePolicyAutoReimbursementLimit, + enablePolicyDefaultReportTitle, + setPolicyPreventMemberCreatedTitle, + setPolicyPreventSelfApproval, +} from '@userActions/Policy/Policy'; import {updateApprovalWorkflow} from '@userActions/Workflow'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -160,10 +166,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { > Navigation.navigate(ROUTES.RULES_AUTO_APPROVE_REPORTS_UNDER.getRoute(policyID))} @@ -209,10 +212,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { > Navigation.navigate(ROUTES.RULES_AUTO_PAY_REPORTS_UNDER.getRoute(policyID))} From 56f3cbd06f8a63faf64641773379cbc98ed827f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 27 Jan 2025 14:18:52 +0100 Subject: [PATCH 12/31] Revert "remove logic related to default workspace owner" This reverts commit e4ac957b9d1d50edf48f6edac9f4eb0a0e9c8cf5. --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/PolicyUtils.ts | 20 +++++++++++++++++++ .../rules/ExpenseReportRulesSection.tsx | 11 ++++++---- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 2e4962bed182..458b25fd2025 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4530,6 +4530,7 @@ const translations = { `Any members currently approving their own expenses will be removed and replaced with the default approver for this workspace (${managerEmail}).`, preventSelfApprovalsConfirmButton: 'Prevent self-approvals', preventSelfApprovalsModalTitle: 'Prevent self-approvals?', + preventSelfApprovalsDisabledSubtitle: 'Self-approvals cannot be disabled because the workspace owner is the only approver. Add another approver first.', }, categoryRules: { title: 'Category rules', diff --git a/src/languages/es.ts b/src/languages/es.ts index 141b1e394a87..ac62de8a2621 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4596,6 +4596,7 @@ const translations = { `Todos los miembros que actualmente estén aprobando sus propios gastos serán eliminados y reemplazados con el aprobador predeterminado de este espacio de trabajo (${managerEmail}).`, preventSelfApprovalsConfirmButton: 'Evitar autoaprobaciones', preventSelfApprovalsModalTitle: '¿Evitar autoaprobaciones?', + preventSelfApprovalsDisabledSubtitle: 'No se pueden deshabilitar las autoaprobaciones porque el propietario del espacio de trabajo es el único aprobador. Agregue otro aprobador primero.', }, categoryRules: { title: 'Reglas de categoría', diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 7ec2487dde97..5d3c38b2b8c4 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1220,8 +1220,28 @@ function getAllSelfApprovers(policy: OnyxEntry): string[] { }); } +/** + * Check if the policy has a default approver that is self-approving. + * If so, we cannot enable the "Prevent Self Approvals" feature. + */ +function canEnablePreventSelfApprovals(policy: OnyxEntry): boolean { + if (!policy?.employeeList || !policy.approver) { + return false; + } + + const defaultApprover = policy.employeeList[policy.approver]; + // If the default approver is missing or self-approving themselves, + // we have no valid fallback. + if (!defaultApprover || defaultApprover.submitsTo === policy.approver) { + return false; + } + + return true; +} + export { canEditTaxRate, + canEnablePreventSelfApprovals, extractPolicyIDFromPath, escapeTagName, getActivePolicies, diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index 852f8fad7893..73ff2f3c8e81 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -43,6 +43,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { const autoPayApprovedReportsUnavailable = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; const [isPreventSelfApprovalsModalVisible, setIsPreventSelfApprovalsModalVisible] = useState(false); + const isPreventSelfApprovalsDisabled = !PolicyUtils.canEnablePreventSelfApprovals(policy) && !policy?.preventSelfApproval; const selfApproversEmails = getAllSelfApprovers(policy); function handleTogglePreventSelfApprovals(isEnabled: boolean) { @@ -137,12 +138,14 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { { title: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), subtitle: workflowApprovalsUnavailable - ? renderFallbackSubtitle({featureName: translate('common.approvals').toLowerCase()}) - : translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle'), + ? renderFallbackSubtitle({ featureName: translate('common.approvals').toLowerCase() }) + : isPreventSelfApprovalsDisabled + ? translate('workspace.rules.expenseReportRules.preventSelfApprovalsDisabledSubtitle') + : translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle'), switchAccessibilityLabel: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), isActive: policy?.preventSelfApproval && !workflowApprovalsUnavailable, - disabled: workflowApprovalsUnavailable, - showLockIcon: workflowApprovalsUnavailable, + disabled: workflowApprovalsUnavailable || isPreventSelfApprovalsDisabled, + showLockIcon: workflowApprovalsUnavailable || isPreventSelfApprovalsDisabled, pendingAction: policy?.pendingFields?.preventSelfApproval, onToggle: (isEnabled: boolean) => handleTogglePreventSelfApprovals(isEnabled), }, From d429bac5092f8b3d60c92044715ce98af272dae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 27 Jan 2025 14:43:43 +0100 Subject: [PATCH 13/31] update logic for self approvals check --- src/libs/PolicyUtils.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 5d3c38b2b8c4..0e69a98e5a8a 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1221,7 +1221,7 @@ function getAllSelfApprovers(policy: OnyxEntry): string[] { } /** - * Check if the policy has a default approver that is self-approving. + * Checks if the policy has a default approver that is self-approving and the workspace has only one user. * If so, we cannot enable the "Prevent Self Approvals" feature. */ function canEnablePreventSelfApprovals(policy: OnyxEntry): boolean { @@ -1229,14 +1229,9 @@ function canEnablePreventSelfApprovals(policy: OnyxEntry): boolean { return false; } - const defaultApprover = policy.employeeList[policy.approver]; - // If the default approver is missing or self-approving themselves, - // we have no valid fallback. - if (!defaultApprover || defaultApprover.submitsTo === policy.approver) { - return false; - } + const employeeEmails = Object.keys(policy.employeeList); - return true; + return employeeEmails.length > 1; } export { From 13b71763ecdf8f17d90c05603d190b8a29f2ee92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 27 Jan 2025 14:44:07 +0100 Subject: [PATCH 14/31] update translation for self approvals disabled --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 458b25fd2025..c9e8f44afe82 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4530,7 +4530,7 @@ const translations = { `Any members currently approving their own expenses will be removed and replaced with the default approver for this workspace (${managerEmail}).`, preventSelfApprovalsConfirmButton: 'Prevent self-approvals', preventSelfApprovalsModalTitle: 'Prevent self-approvals?', - preventSelfApprovalsDisabledSubtitle: 'Self-approvals cannot be disabled because the workspace owner is the only approver. Add another approver first.', + preventSelfApprovalsDisabledSubtitle: 'Self approvals cannot be enabled until this workspace has at least two members.', }, categoryRules: { title: 'Category rules', diff --git a/src/languages/es.ts b/src/languages/es.ts index ac62de8a2621..3f453587fce0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4596,7 +4596,7 @@ const translations = { `Todos los miembros que actualmente estén aprobando sus propios gastos serán eliminados y reemplazados con el aprobador predeterminado de este espacio de trabajo (${managerEmail}).`, preventSelfApprovalsConfirmButton: 'Evitar autoaprobaciones', preventSelfApprovalsModalTitle: '¿Evitar autoaprobaciones?', - preventSelfApprovalsDisabledSubtitle: 'No se pueden deshabilitar las autoaprobaciones porque el propietario del espacio de trabajo es el único aprobador. Agregue otro aprobador primero.', + preventSelfApprovalsDisabledSubtitle: 'No se pueden habilitar las autoaprobaciones hasta que este espacio de trabajo tenga al menos dos miembros.', }, categoryRules: { title: 'Reglas de categoría', From 6d3b5281f0cbfa50fb9f47d833e96231874ec665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 27 Jan 2025 14:44:22 +0100 Subject: [PATCH 15/31] add logic for self approvals disabled --- .../workspace/rules/ExpenseReportRulesSection.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index 73ff2f3c8e81..2e30a24de500 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -11,7 +11,7 @@ import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getAllSelfApprovers, getWorkflowApprovalsUnavailable} from '@libs/PolicyUtils'; +import {canEnablePreventSelfApprovals, getAllSelfApprovers, getWorkflowApprovalsUnavailable} from '@libs/PolicyUtils'; import {convertPolicyEmployeesToApprovalWorkflows} from '@libs/WorkflowUtils'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import { @@ -43,7 +43,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { const autoPayApprovedReportsUnavailable = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; const [isPreventSelfApprovalsModalVisible, setIsPreventSelfApprovalsModalVisible] = useState(false); - const isPreventSelfApprovalsDisabled = !PolicyUtils.canEnablePreventSelfApprovals(policy) && !policy?.preventSelfApproval; + const isPreventSelfApprovalsDisabled = !canEnablePreventSelfApprovals(policy) && !policy?.preventSelfApproval; const selfApproversEmails = getAllSelfApprovers(policy); function handleTogglePreventSelfApprovals(isEnabled: boolean) { @@ -138,10 +138,10 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { { title: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), subtitle: workflowApprovalsUnavailable - ? renderFallbackSubtitle({ featureName: translate('common.approvals').toLowerCase() }) + ? renderFallbackSubtitle({featureName: translate('common.approvals').toLowerCase()}) : isPreventSelfApprovalsDisabled - ? translate('workspace.rules.expenseReportRules.preventSelfApprovalsDisabledSubtitle') - : translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle'), + ? translate('workspace.rules.expenseReportRules.preventSelfApprovalsDisabledSubtitle') + : translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle'), switchAccessibilityLabel: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), isActive: policy?.preventSelfApproval && !workflowApprovalsUnavailable, disabled: workflowApprovalsUnavailable || isPreventSelfApprovalsDisabled, From 6b6f2e1bd1d59f650587b779318518a7b1d06658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 27 Jan 2025 17:02:36 +0100 Subject: [PATCH 16/31] remove ternary --- .../workspace/rules/ExpenseReportRulesSection.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index 2e30a24de500..1ec0ee65e956 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -137,11 +137,15 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { }, { title: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), - subtitle: workflowApprovalsUnavailable - ? renderFallbackSubtitle({featureName: translate('common.approvals').toLowerCase()}) - : isPreventSelfApprovalsDisabled - ? translate('workspace.rules.expenseReportRules.preventSelfApprovalsDisabledSubtitle') - : translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle'), + subtitle: (() => { + if (workflowApprovalsUnavailable) { + return renderFallbackSubtitle({featureName: translate('common.approvals').toLowerCase()}); + } + if (isPreventSelfApprovalsDisabled) { + return translate('workspace.rules.expenseReportRules.preventSelfApprovalsDisabledSubtitle'); + } + return translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle'); + })(), switchAccessibilityLabel: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), isActive: policy?.preventSelfApproval && !workflowApprovalsUnavailable, disabled: workflowApprovalsUnavailable || isPreventSelfApprovalsDisabled, From d1d4b0702f324522699d1e23b1bffa2651f74213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Tue, 28 Jan 2025 08:41:54 +0100 Subject: [PATCH 17/31] fix translation --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index c9e8f44afe82..d6a57909fd9c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4530,7 +4530,7 @@ const translations = { `Any members currently approving their own expenses will be removed and replaced with the default approver for this workspace (${managerEmail}).`, preventSelfApprovalsConfirmButton: 'Prevent self-approvals', preventSelfApprovalsModalTitle: 'Prevent self-approvals?', - preventSelfApprovalsDisabledSubtitle: 'Self approvals cannot be enabled until this workspace has at least two members.', + preventSelfApprovalsDisabledSubtitle: 'Self approvals can\'t be enabled until this workspace has at least two members.', }, categoryRules: { title: 'Category rules', diff --git a/src/languages/es.ts b/src/languages/es.ts index 3f453587fce0..281511a5946a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4596,7 +4596,7 @@ const translations = { `Todos los miembros que actualmente estén aprobando sus propios gastos serán eliminados y reemplazados con el aprobador predeterminado de este espacio de trabajo (${managerEmail}).`, preventSelfApprovalsConfirmButton: 'Evitar autoaprobaciones', preventSelfApprovalsModalTitle: '¿Evitar autoaprobaciones?', - preventSelfApprovalsDisabledSubtitle: 'No se pueden habilitar las autoaprobaciones hasta que este espacio de trabajo tenga al menos dos miembros.', + preventSelfApprovalsDisabledSubtitle: 'Las aprobaciones propias no pueden habilitarse hasta que este espacio de trabajo tenga al menos dos miembros.', }, categoryRules: { title: 'Reglas de categoría', From 6d8c2fe74675f751b5acdccb15cd8f72cd58ac6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Tue, 28 Jan 2025 09:59:43 +0100 Subject: [PATCH 18/31] fix style --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 66ee9707e31e..5a0419843642 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4549,7 +4549,7 @@ const translations = { `Any members currently approving their own expenses will be removed and replaced with the default approver for this workspace (${managerEmail}).`, preventSelfApprovalsConfirmButton: 'Prevent self-approvals', preventSelfApprovalsModalTitle: 'Prevent self-approvals?', - preventSelfApprovalsDisabledSubtitle: 'Self approvals can\'t be enabled until this workspace has at least two members.', + preventSelfApprovalsDisabledSubtitle: "Self approvals can't be enabled until this workspace has at least two members.", }, categoryRules: { title: 'Category rules', From 659d332a92f1a083009e9f287421db7797c39100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Wed, 29 Jan 2025 16:20:22 +0100 Subject: [PATCH 19/31] make comment more meaningful --- src/libs/PolicyUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index d06dd9f3d186..5d77b2c7ba40 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1280,8 +1280,8 @@ function getAllSelfApprovers(policy: OnyxEntry): string[] { } /** - * Checks if the policy has a default approver that is self-approving and the workspace has only one user. - * If so, we cannot enable the "Prevent Self Approvals" feature. + * Checks if the workspace has only one user. + * If so, we can't enable the "Prevent Self Approvals" feature. */ function canEnablePreventSelfApprovals(policy: OnyxEntry): boolean { if (!policy?.employeeList || !policy.approver) { From 0c2e7e02b8bdddbb97b6a16a66aefa6756380613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Wed, 29 Jan 2025 16:22:22 +0100 Subject: [PATCH 20/31] disable self approvals automatically if only one user in the workspace --- src/pages/workspace/rules/ExpenseReportRulesSection.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index 1ec0ee65e956..9fe0b592b3be 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -46,6 +46,13 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { const isPreventSelfApprovalsDisabled = !canEnablePreventSelfApprovals(policy) && !policy?.preventSelfApproval; const selfApproversEmails = getAllSelfApprovers(policy); + useEffect(() => { + if (!canEnablePreventSelfApprovals(policy) && policy?.preventSelfApproval) { + // If the policy has only one user, we disable the "Prevent Self Approvals" feature + setPolicyPreventSelfApproval(policyID, false); + } + }, [policy?.employeeList]); + function handleTogglePreventSelfApprovals(isEnabled: boolean) { if (!isEnabled) { setPolicyPreventSelfApproval(policyID, false); From 29a5acc5b7b4966cf376f3521f50516f643fa83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Wed, 29 Jan 2025 16:35:17 +0100 Subject: [PATCH 21/31] fix lint --- src/pages/workspace/rules/ExpenseReportRulesSection.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index 9fe0b592b3be..88b8ed3ab73b 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -47,11 +47,12 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { const selfApproversEmails = getAllSelfApprovers(policy); useEffect(() => { - if (!canEnablePreventSelfApprovals(policy) && policy?.preventSelfApproval) { - // If the policy has only one user, we disable the "Prevent Self Approvals" feature - setPolicyPreventSelfApproval(policyID, false); + if (!canEnablePreventSelfApprovals(policy) || !policy?.preventSelfApproval) { + return; } - }, [policy?.employeeList]); + // If the policy has only one user, we disable the "Prevent Self Approvals" feature + setPolicyPreventSelfApproval(policyID, false); + }, [policy, policyID]); function handleTogglePreventSelfApprovals(isEnabled: boolean) { if (!isEnabled) { From fc00b9ec45c0cfa134fc380108cf46029f7c4625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 3 Feb 2025 13:52:30 -0800 Subject: [PATCH 22/31] remove auto prevent self approvals --- src/pages/workspace/rules/ExpenseReportRulesSection.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index 88b8ed3ab73b..8416cc886a86 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -46,13 +46,6 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { const isPreventSelfApprovalsDisabled = !canEnablePreventSelfApprovals(policy) && !policy?.preventSelfApproval; const selfApproversEmails = getAllSelfApprovers(policy); - useEffect(() => { - if (!canEnablePreventSelfApprovals(policy) || !policy?.preventSelfApproval) { - return; - } - // If the policy has only one user, we disable the "Prevent Self Approvals" feature - setPolicyPreventSelfApproval(policyID, false); - }, [policy, policyID]); function handleTogglePreventSelfApprovals(isEnabled: boolean) { if (!isEnabled) { From f9a087c606a6ecdde5e5a484588fe4d362004e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 3 Feb 2025 13:52:55 -0800 Subject: [PATCH 23/31] handle prevent self approvals --- src/pages/workspace/WorkspaceMembersPage.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 8c3e4152af38..286eaa4e82ed 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -53,6 +53,9 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import WorkspacePageWithSections from './WorkspacePageWithSections'; +import { + setPolicyPreventSelfApproval, +} from '@userActions/Policy/Policy'; type WorkspaceMembersPageProps = WithPolicyAndFullscreenLoadingProps & WithCurrentUserPersonalDetailsProps & @@ -214,12 +217,18 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson return; } + const previousEmployeesCount = Object.keys(policy?.employeeList ?? {}).length; // Remove the admin from the list const accountIDsToRemove = session?.accountID ? selectedEmployees.filter((id) => id !== session.accountID) : selectedEmployees; + const newEmployeesCount = previousEmployeesCount - accountIDsToRemove.length; setSelectedEmployees([]); setRemoveMembersConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { Member.removeMembers(accountIDsToRemove, route.params.policyID); + if (newEmployeesCount === 1 && policy?.preventSelfApproval) { + // We can't let the "Prevent Self Approvals" enabled if there's only one workspace user + setPolicyPreventSelfApproval(route.params.policyID, false); + } }); }; From 78506de7d21fb750ea0021f39adaa9f4624b8f39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 3 Feb 2025 13:53:32 -0800 Subject: [PATCH 24/31] fix style --- src/pages/workspace/WorkspaceMembersPage.tsx | 4 +--- src/pages/workspace/rules/ExpenseReportRulesSection.tsx | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 286eaa4e82ed..55cffc6b0bd3 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -43,6 +43,7 @@ import {getDisplayNameForParticipant} from '@libs/ReportUtils'; import * as Modal from '@userActions/Modal'; import * as Member from '@userActions/Policy/Member'; import * as Policy from '@userActions/Policy/Policy'; +import {setPolicyPreventSelfApproval} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -53,9 +54,6 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import WorkspacePageWithSections from './WorkspacePageWithSections'; -import { - setPolicyPreventSelfApproval, -} from '@userActions/Policy/Policy'; type WorkspaceMembersPageProps = WithPolicyAndFullscreenLoadingProps & WithCurrentUserPersonalDetailsProps & diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index 8416cc886a86..336b2b5795f7 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -46,7 +46,6 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { const isPreventSelfApprovalsDisabled = !canEnablePreventSelfApprovals(policy) && !policy?.preventSelfApproval; const selfApproversEmails = getAllSelfApprovers(policy); - function handleTogglePreventSelfApprovals(isEnabled: boolean) { if (!isEnabled) { setPolicyPreventSelfApproval(policyID, false); From a1d930d1bbc1355515e51c72b7b33a4c520ddc7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 3 Feb 2025 14:09:09 -0800 Subject: [PATCH 25/31] remove unnecessary `useEffect` --- src/pages/workspace/rules/ExpenseReportRulesSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index 336b2b5795f7..1ec0ee65e956 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useMemo, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; From 632516ac8c851389be55b34fbb1c74109b67c9e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 3 Feb 2025 14:09:28 -0800 Subject: [PATCH 26/31] use named imports --- src/pages/workspace/WorkspaceMembersPage.tsx | 84 ++++++++++---------- 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 55cffc6b0bd3..ca4155ea9a61 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -30,20 +30,18 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; +import {isPersonalDetailsReady, sortAlphabetically} from '@libs/OptionsListUtils'; +import {getPersonalDetailsByIDs, getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; +import {getMemberAccountIDsForWorkspace, isPolicyAdmin, isPaidGroupPolicy, isDeletedPolicyEmployee, isExpensifyTeam} from '@libs/PolicyUtils'; import {getDisplayNameForParticipant} from '@libs/ReportUtils'; -import * as Modal from '@userActions/Modal'; -import * as Member from '@userActions/Policy/Member'; -import * as Policy from '@userActions/Policy/Policy'; -import {setPolicyPreventSelfApproval} from '@userActions/Policy/Policy'; +import {close} from '@userActions/Modal'; +import {isApprover, openWorkspaceMembersPage, clearInviteDraft, removeMembers, clearWorkspaceOwnerChangeFlow, clearDeleteMemberError, clearAddMemberError, updateWorkspaceMembersRole, downloadMembersCSV} from '@userActions/Policy/Member'; +import {dismissAddedWithPrimaryLoginMessages, setPolicyPreventSelfApproval} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -70,7 +68,7 @@ function invertObject(object: Record): Record { type MemberOption = Omit & {accountID: number}; function WorkspaceMembersPage({personalDetails, route, policy, currentUserPersonalDetails}: WorkspaceMembersPageProps) { - const policyMemberEmailsToAccountIDs = useMemo(() => PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList, true), [policy?.employeeList]); + const policyMemberEmailsToAccountIDs = useMemo(() => getMemberAccountIDsForWorkspace(policy?.employeeList, true), [policy?.employeeList]); const styles = useThemeStyles(); const [selectedEmployees, setSelectedEmployees] = useState([]); const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false); @@ -90,9 +88,9 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); - const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); + const isPolicyWorkspaceAdmin = isPolicyAdmin(policy); const isLoading = useMemo( - () => !isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policy?.employeeList)), + () => !isOfflineAndNoMemberDataAvailable && (!isPersonalDetailsReady(personalDetails) || isEmptyObject(policy?.employeeList)), [isOfflineAndNoMemberDataAvailable, personalDetails, policy?.employeeList], ); @@ -104,14 +102,14 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const isFocused = useIsFocused(); const policyID = route.params.policyID; - const canSelectMultiple = isPolicyAdmin && (shouldUseNarrowLayout ? selectionMode?.isEnabled : true); + const canSelectMultiple = isPolicyWorkspaceAdmin && (shouldUseNarrowLayout ? selectionMode?.isEnabled : true); const confirmModalPrompt = useMemo(() => { - const approverAccountID = selectedEmployees.find((selectedEmployee) => Member.isApprover(policy, selectedEmployee)); + const approverAccountID = selectedEmployees.find((selectedEmployee) => isApprover(policy, selectedEmployee)); if (!approverAccountID) { return translate('workspace.people.removeMembersPrompt', { count: selectedEmployees.length, - memberName: LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getPersonalDetailsByIDs(selectedEmployees, currentUserAccountID).at(0)?.displayName ?? ''), + memberName: formatPhoneNumber(getPersonalDetailsByIDs(selectedEmployees, currentUserAccountID).at(0)?.displayName ?? ''), }); } return translate('workspace.people.removeMembersWarningPrompt', { @@ -134,7 +132,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson * Get members for the current workspace */ const getWorkspaceMembers = useCallback(() => { - Member.openWorkspaceMembersPage(route.params.policyID, Object.keys(PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList))); + openWorkspaceMembersPage(route.params.policyID, Object.keys(getMemberAccountIDsForWorkspace(policy?.employeeList))); }, [route.params.policyID, policy?.employeeList]); /** @@ -180,7 +178,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson return res?.accountID ?? id; }); - const currentSelectedElements = Object.entries(PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList)) + const currentSelectedElements = Object.entries(getMemberAccountIDsForWorkspace(policy?.employeeList)) .filter((employee) => policy?.employeeList?.[employee[0]]?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) .map((employee) => employee[1]); @@ -202,7 +200,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson * Open the modal to invite a user */ const inviteUser = useCallback(() => { - Member.clearInviteDraft(route.params.policyID); + clearInviteDraft(route.params.policyID); Navigation.navigate(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID, Navigation.getActiveRouteWithoutParams())); }, [route.params.policyID]); @@ -222,7 +220,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson setSelectedEmployees([]); setRemoveMembersConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { - Member.removeMembers(accountIDsToRemove, route.params.policyID); + removeMembers(accountIDsToRemove, route.params.policyID); if (newEmployeesCount === 1 && policy?.preventSelfApproval) { // We can't let the "Prevent Self Approvals" enabled if there's only one workspace user setPolicyPreventSelfApproval(route.params.policyID, false); @@ -305,14 +303,14 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson /** Opens the member details page */ const openMemberDetails = useCallback( (item: MemberOption) => { - if (!isPolicyAdmin || !PolicyUtils.isPaidGroupPolicy(policy)) { + if (!isPolicyWorkspaceAdmin || !isPaidGroupPolicy(policy)) { Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); return; } - Member.clearWorkspaceOwnerChangeFlow(policyID); + clearWorkspaceOwnerChangeFlow(policyID); Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(route.params.policyID, item.accountID)); }, - [isPolicyAdmin, policy, policyID, route.params.policyID], + [isPolicyWorkspaceAdmin, policy, policyID, route.params.policyID], ); /** @@ -321,9 +319,9 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const dismissError = useCallback( (item: MemberOption) => { if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - Member.clearDeleteMemberError(route.params.policyID, item.accountID); + clearDeleteMemberError(route.params.policyID, item.accountID); } else { - Member.clearAddMemberError(route.params.policyID, item.accountID); + clearAddMemberError(route.params.policyID, item.accountID); } }, [route.params.policyID], @@ -337,7 +335,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson Object.entries(policy?.employeeList ?? {}).forEach(([email, policyEmployee]) => { const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); - if (PolicyUtils.isDeletedPolicyEmployee(policyEmployee, isOffline)) { + if (isDeletedPolicyEmployee(policyEmployee, isOffline)) { return; } @@ -351,8 +349,8 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson // If this policy is owned by Expensify then show all support (expensify.com or team.expensify.com) emails // We don't want to show guides as policy members unless the user is a guide. Some customers get confused when they // see random people added to their policy, but guides having access to the policies help set them up. - if (PolicyUtils.isExpensifyTeam(details?.login ?? details?.displayName)) { - if (policyOwner && currentUserLogin && !PolicyUtils.isExpensifyTeam(policyOwner) && !PolicyUtils.isExpensifyTeam(currentUserLogin)) { + if (isExpensifyTeam(details?.login ?? details?.displayName)) { + if (policyOwner && currentUserLogin && !isExpensifyTeam(policyOwner) && !isExpensifyTeam(currentUserLogin)) { return; } } @@ -368,17 +366,17 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson } else if (isAuditor) { roleBadge = ; } - const isPendingDeleteOrError = isPolicyAdmin && (policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyEmployee.errors)); + const isPendingDeleteOrError = isPolicyWorkspaceAdmin && (policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyEmployee.errors)); result.push({ keyForList: String(accountID), accountID, isSelected, - isDisabledCheckbox: !(isPolicyAdmin && accountID !== policy?.ownerAccountID && accountID !== session?.accountID), + isDisabledCheckbox: !(isPolicyWorkspaceAdmin && accountID !== policy?.ownerAccountID && accountID !== session?.accountID), isDisabled: isPendingDeleteOrError, isInteractive: !details.isOptimisticPersonalDetail, cursorStyle: details.isOptimisticPersonalDetail ? styles.cursorDefault : {}, - text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), + text: formatPhoneNumber(getDisplayNameOrDefault(details)), alternateText: formatPhoneNumber(details?.login ?? ''), rightElement: roleBadge, icons: [ @@ -395,7 +393,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson invitedSecondaryLogin: details?.login ? invitedPrimaryToSecondaryLogins[details.login] ?? '' : '', }); }); - result = OptionsListUtils.sortAlphabetically(result, 'text'); + result = sortAlphabetically(result, 'text'); return result; }, [ isOffline, @@ -427,7 +425,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson } const invitedEmails = Object.values(invitedEmailsToAccountIDsDraft).map(String); selectionListRef.current?.scrollAndHighlightItem?.(invitedEmails); - Member.clearInviteDraft(route.params.policyID); + clearInviteDraft(route.params.policyID); }, [invitedEmailsToAccountIDsDraft, isFocused, accountIDs, prevAccountIDs, route.params.policyID]); const getHeaderMessage = () => { @@ -447,7 +445,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson // eslint-disable-next-line @typescript-eslint/naming-convention messages={{0: translate('workspace.people.addedWithPrimary')}} containerStyles={[styles.pb5, styles.ph5]} - onClose={() => Policy.dismissAddedWithPrimaryLoginMessages(policyID)} + onClose={() => dismissAddedWithPrimaryLoginMessages(policyID)} /> )} @@ -481,7 +479,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson return policy?.employeeList?.[email]?.role !== role; }); - Member.updateWorkspaceMembersRole(route.params.policyID, accountIDsToUpdate, role); + updateWorkspaceMembersRole(route.params.policyID, accountIDsToUpdate, role); setSelectedEmployees([]); }; @@ -495,7 +493,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson }, ]; - if (!PolicyUtils.isPaidGroupPolicy(policy)) { + if (!isPaidGroupPolicy(policy)) { return options; } @@ -582,7 +580,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson text: translate('spreadsheet.importSpreadsheet'), onSelected: () => { if (isOffline) { - Modal.close(() => setIsOfflineModalVisible(true)); + close(() => setIsOfflineModalVisible(true)); return; } Navigation.navigate(ROUTES.WORKSPACE_MEMBERS_IMPORT.getRoute(policyID)); @@ -593,12 +591,12 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson text: translate('spreadsheet.downloadCSV'), onSelected: () => { if (isOffline) { - Modal.close(() => setIsOfflineModalVisible(true)); + close(() => setIsOfflineModalVisible(true)); return; } - Modal.close(() => { - Member.downloadMembersCSV(policyID, () => { + close(() => { + downloadMembersCSV(policyID, () => { setIsDownloadFailureModalVisible(true); }); }); @@ -621,7 +619,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson testID={WorkspaceMembersPage.displayName} shouldShowLoading={false} shouldShowOfflineIndicatorInWideScreen - shouldShowThreeDotsButton={isPolicyAdmin} + shouldShowThreeDotsButton={isPolicyWorkspaceAdmin} threeDotsMenuItems={threeDotsMenuItems} threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} shouldShowNonAdmin @@ -679,19 +677,19 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson canSelectMultiple={canSelectMultiple} sections={[{data, isDisabled: false}]} ListItem={TableListItem} - turnOnSelectionModeOnLongPress={isPolicyAdmin} + turnOnSelectionModeOnLongPress={isPolicyWorkspaceAdmin} onTurnOnSelectionMode={(item) => item && toggleUser(item?.accountID)} shouldUseUserSkeletonView disableKeyboardShortcuts={removeMembersConfirmModalVisible} headerMessage={getHeaderMessage()} headerContent={!shouldUseNarrowLayout && getHeaderContent()} onSelectRow={openMemberDetails} - shouldSingleExecuteRowSelect={!isPolicyAdmin} + shouldSingleExecuteRowSelect={!isPolicyWorkspaceAdmin} onCheckboxPress={(item) => toggleUser(item.accountID)} onSelectAll={() => toggleAllUsers(data)} onDismissError={dismissError} showLoadingPlaceholder={isLoading} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} textInputRef={textInputRef} customListHeader={getCustomListHeader()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} From edc72fbb2cc7877fcecb0818aa38bfd51bb3a875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 3 Feb 2025 14:10:57 -0800 Subject: [PATCH 27/31] fix style --- src/pages/workspace/WorkspaceMembersPage.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index ca4155ea9a61..0ea8a1fbd6fa 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -36,11 +36,21 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; import {isPersonalDetailsReady, sortAlphabetically} from '@libs/OptionsListUtils'; -import {getPersonalDetailsByIDs, getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; -import {getMemberAccountIDsForWorkspace, isPolicyAdmin, isPaidGroupPolicy, isDeletedPolicyEmployee, isExpensifyTeam} from '@libs/PolicyUtils'; +import {getDisplayNameOrDefault, getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils'; +import {getMemberAccountIDsForWorkspace, isDeletedPolicyEmployee, isExpensifyTeam, isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import {getDisplayNameForParticipant} from '@libs/ReportUtils'; import {close} from '@userActions/Modal'; -import {isApprover, openWorkspaceMembersPage, clearInviteDraft, removeMembers, clearWorkspaceOwnerChangeFlow, clearDeleteMemberError, clearAddMemberError, updateWorkspaceMembersRole, downloadMembersCSV} from '@userActions/Policy/Member'; +import { + clearAddMemberError, + clearDeleteMemberError, + clearInviteDraft, + clearWorkspaceOwnerChangeFlow, + downloadMembersCSV, + isApprover, + openWorkspaceMembersPage, + removeMembers, + updateWorkspaceMembersRole, +} from '@userActions/Policy/Member'; import {dismissAddedWithPrimaryLoginMessages, setPolicyPreventSelfApproval} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; From 237bb4d589612da8ff2310207171cf5145709574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 3 Feb 2025 14:17:48 -0800 Subject: [PATCH 28/31] fix lint --- src/pages/workspace/WorkspaceMembersPage.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 0ea8a1fbd6fa..16436487558e 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -126,7 +126,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson memberName: getDisplayNameForParticipant(approverAccountID), ownerName: getDisplayNameForParticipant(policy?.ownerAccountID), }); - }, [selectedEmployees, translate, policy, currentUserAccountID]); + }, [selectedEmployees, translate, policy, currentUserAccountID, formatPhoneNumber, getPersonalDetailsByIDs, getDisplayNameForParticipant]); /** * Get filtered personalDetails list with current employeeList */ @@ -422,6 +422,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson translate, styles.cursorDefault, canSelectMultiple, + isPolicyWorkspaceAdmin, ]); const data = useMemo(() => getUsers(), [getUsers]); @@ -580,7 +581,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson }; const threeDotsMenuItems = useMemo(() => { - if (!isPolicyAdmin) { + if (!isPolicyWorkspaceAdmin) { return []; } @@ -615,7 +616,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson ]; return menuItems; - }, [policyID, translate, isOffline, isPolicyAdmin]); + }, [policyID, translate, isOffline, isPolicyWorkspaceAdmin]); const selectionModeHeader = selectionMode?.isEnabled && shouldUseNarrowLayout; From 87d51ca49aed553a189b0385577c62302e6e20a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Mon, 3 Feb 2025 14:22:25 -0800 Subject: [PATCH 29/31] fix lint --- src/pages/workspace/WorkspaceMembersPage.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 16436487558e..14ba67ed492a 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -126,7 +126,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson memberName: getDisplayNameForParticipant(approverAccountID), ownerName: getDisplayNameForParticipant(policy?.ownerAccountID), }); - }, [selectedEmployees, translate, policy, currentUserAccountID, formatPhoneNumber, getPersonalDetailsByIDs, getDisplayNameForParticipant]); + }, [selectedEmployees, translate, policy, currentUserAccountID, formatPhoneNumber]); /** * Get filtered personalDetails list with current employeeList */ @@ -410,7 +410,6 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson currentUserLogin, formatPhoneNumber, invitedPrimaryToSecondaryLogins, - isPolicyAdmin, personalDetails, policy?.owner, policy?.ownerAccountID, @@ -581,7 +580,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson }; const threeDotsMenuItems = useMemo(() => { - if (!isPolicyWorkspaceAdmin) { + if (!isPolicyAdmin) { return []; } @@ -616,7 +615,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson ]; return menuItems; - }, [policyID, translate, isOffline, isPolicyWorkspaceAdmin]); + }, [policyID, translate, isOffline]); const selectionModeHeader = selectionMode?.isEnabled && shouldUseNarrowLayout; From 813a886e4e9bb84d71ef98d635474da79779c6fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Thu, 6 Feb 2025 11:52:58 -0800 Subject: [PATCH 30/31] improve comment --- 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 9ffc9ee283f9..0a2d4113564a 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1291,7 +1291,7 @@ function getAllSelfApprovers(policy: OnyxEntry): string[] { } /** - * Checks if the workspace has only one user. + * Checks if the workspace has only one user and if there no approver for the policy. * If so, we can't enable the "Prevent Self Approvals" feature. */ function canEnablePreventSelfApprovals(policy: OnyxEntry): boolean { From d318ce309a0f87968a277ce8baba32529874a8ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucien=20Akchot=C3=A9?= Date: Thu, 6 Feb 2025 12:02:51 -0800 Subject: [PATCH 31/31] lint fix --- src/pages/workspace/WorkspaceMembersPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 485836e61ad8..152796028d6a 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -128,7 +128,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson memberName: getDisplayNameForParticipant({accountID: approverAccountID}), ownerName: getDisplayNameForParticipant({accountID: policy?.ownerAccountID}), }); - }, [selectedEmployees, translate, policy, currentUserAccountID, formatPhoneNumber]); + }, [selectedEmployees, translate, policy, currentUserAccountID]); /** * Get filtered personalDetails list with current employeeList */ @@ -622,7 +622,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson ]; return menuItems; - }, [policyID, translate, isOffline]); + }, [policyID, translate, isOffline, isPolicyAdmin]); const selectionModeHeader = selectionMode?.isEnabled && shouldUseNarrowLayout;