diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx index 2536dbc9bbe9..bb87948f2100 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx @@ -36,6 +36,7 @@ import { hasAccountingConnections, hasAccountingFeatureConnection, hasVendorFeature, + isCollectPolicy, isControlPolicy, isPerDiemEnabled, isTimeTrackingEnabled, @@ -516,7 +517,11 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro if (!policyID) { return; } - if (isEnabled && !isControlPolicy(policy) && !isRulesRevampEnabled) { + // Only Control always has Rules, and Collect gains them with the revamp beta. Anything + // else (Submit) can't hold Rules at all — arePolicyRulesEnabled would keep reading + // false — so it has to keep going to the upgrade page rather than writing a flag that + // never takes effect. + if (isEnabled && !isControlPolicy(policy) && (!isRulesRevampEnabled || !isCollectPolicy(policy))) { Navigation.navigate( ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.rules.alias, ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)), ); diff --git a/src/pages/workspace/rules/IndividualExpenseRulesSectionRevamp.tsx b/src/pages/workspace/rules/IndividualExpenseRulesSectionRevamp.tsx index c794be13d246..f9f6750966b5 100644 --- a/src/pages/workspace/rules/IndividualExpenseRulesSectionRevamp.tsx +++ b/src/pages/workspace/rules/IndividualExpenseRulesSectionRevamp.tsx @@ -17,6 +17,7 @@ import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOpt import variables from '@styles/variables'; import CONST from '@src/CONST'; +import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -47,7 +48,7 @@ type BasicRuleMenuItem = { title: string; description?: string; icon: IconAsset; - action: () => void; + route: Route; pendingAction?: PendingAction; }; @@ -100,10 +101,12 @@ function IndividualExpenseRulesSectionRevamp({policyID, canWriteRules}: Individu const rulesUpgradeAlias = CONST.UPGRADE_FEATURE_INTRO_MAPPING.rules.alias; const handleMenuItemPress = (item: BasicRuleMenuItem) => { - if (isCollect && !COLLECT_ALLOWED_RULE_KEYS.has(item.key) && tryNavigateToControlPolicyUpgrade(policy, rulesUpgradeAlias, rulesUpgradeBackTo)) { + // Return to the row's own page after upgrading rather than the Rules list, so the user lands where they were + // headed — same as the GL code upgrade flow in tag settings. + if (isCollect && !COLLECT_ALLOWED_RULE_KEYS.has(item.key) && tryNavigateToControlPolicyUpgrade(policy, rulesUpgradeAlias, item.route)) { return; } - item.action(); + Navigation.navigate(item.route); }; const navigateToRulesControlUpgrade = () => { @@ -133,7 +136,7 @@ function IndividualExpenseRulesSectionRevamp({policyID, canWriteRules}: Individu title: translate('workspace.rules.generalTab.expensesOlderThan'), description: maxExpenseAgeText, icon: icons.CalendarSolid, - action: () => Navigation.navigate(ROUTES.RULES_MAX_EXPENSE_AGE.getRoute(policyID)), + route: ROUTES.RULES_MAX_EXPENSE_AGE.getRoute(policyID), pendingAction: policy?.pendingFields?.maxExpenseAge, }, { @@ -141,7 +144,7 @@ function IndividualExpenseRulesSectionRevamp({policyID, canWriteRules}: Individu title: translate('workspace.rules.generalTab.expensesAboveAmount'), description: maxExpenseAmountText, icon: icons.Coins, - action: () => Navigation.navigate(ROUTES.RULES_MAX_EXPENSE_AMOUNT.getRoute(policyID)), + route: ROUTES.RULES_MAX_EXPENSE_AMOUNT.getRoute(policyID), pendingAction: policy?.pendingFields?.maxExpenseAmount, }, { @@ -149,7 +152,7 @@ function IndividualExpenseRulesSectionRevamp({policyID, canWriteRules}: Individu title: translate('workspace.rules.generalTab.flagReceiptLineItems'), description: prohibitedExpensesText, icon: icons.Receipt, - action: () => Navigation.navigate(ROUTES.RULES_PROHIBITED_DEFAULT.getRoute(policyID)), + route: ROUTES.RULES_PROHIBITED_DEFAULT.getRoute(policyID), pendingAction: !isEmptyObject(policy?.prohibitedExpenses?.pendingFields) ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : undefined, }, { @@ -157,7 +160,7 @@ function IndividualExpenseRulesSectionRevamp({policyID, canWriteRules}: Individu title: translate('workspace.rules.generalTab.receiptRequirements'), description: receiptRequirementText, icon: icons.ReceiptCheck, - action: () => Navigation.navigate(ROUTES.RULES_REQUIRE_RECEIPTS.getRoute(policyID)), + route: ROUTES.RULES_REQUIRE_RECEIPTS.getRoute(policyID), pendingAction: policy?.pendingFields?.maxExpenseAmountNoReceipt ?? policy?.pendingFields?.maxExpenseAmountNoItemizedReceipt, }, { @@ -165,7 +168,7 @@ function IndividualExpenseRulesSectionRevamp({policyID, canWriteRules}: Individu title: translate('workspace.rules.generalTab.requireFieldsForAllExpenses'), description: requiredFieldsList, icon: icons.Task, - action: () => Navigation.navigate(ROUTES.RULES_REQUIRE_FIELDS.getRoute(policyID)), + route: ROUTES.RULES_REQUIRE_FIELDS.getRoute(policyID), pendingAction: policy?.pendingFields?.requiresCategory ?? policy?.pendingFields?.requiresTag, }, ]; @@ -176,7 +179,7 @@ function IndividualExpenseRulesSectionRevamp({policyID, canWriteRules}: Individu title: translate('workspace.rules.generalTab.cashExpenses'), description: reimbursableModeText, icon: icons.Cash, - action: () => Navigation.navigate(ROUTES.RULES_REIMBURSABLE_DEFAULT.getRoute(policyID)), + route: ROUTES.RULES_REIMBURSABLE_DEFAULT.getRoute(policyID), pendingAction: policy?.pendingFields?.defaultReimbursable, }, { @@ -184,7 +187,7 @@ function IndividualExpenseRulesSectionRevamp({policyID, canWriteRules}: Individu title: translate('workspace.rules.generalTab.billableExpenses'), description: billableModeText, icon: icons.Cash, - action: () => Navigation.navigate(ROUTES.RULES_BILLABLE_DEFAULT.getRoute(policyID)), + route: ROUTES.RULES_BILLABLE_DEFAULT.getRoute(policyID), pendingAction: getBillableExpensesPendingAction(policy), }, ]; diff --git a/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx b/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx index cb54e8b5eeed..6b1029a696ad 100644 --- a/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx +++ b/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx @@ -385,6 +385,7 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { policyID={policyID} canWriteRules={canWriteRules} isAgentsRulesBannerDismissed={isAgentsRulesBannerDismissed} + onOpenAgentsTab={() => handleTabPress(RULES_TAB.AGENTS)} /> )} {isTableTab && ( diff --git a/src/pages/workspace/rules/RulesRequireFieldsPage.tsx b/src/pages/workspace/rules/RulesRequireFieldsPage.tsx index 2c5e51d341a7..726ec79f2f8c 100644 --- a/src/pages/workspace/rules/RulesRequireFieldsPage.tsx +++ b/src/pages/workspace/rules/RulesRequireFieldsPage.tsx @@ -55,7 +55,7 @@ function RulesRequireFieldsPage({ const hasEnabledTags = hasEnabledOptions(Object.values(policyTags ?? {}).flatMap(({tags}) => Object.values(tags))); const isTagFeatureDisabled = !policy?.areTagsEnabled; - const isTagToggleDisabled = isTagFeatureDisabled || !hasEnabledTags; + const isTagToggleDisabled = isTagFeatureDisabled || !hasEnabledTags || isConnectedToAccounting; // For independent multi-level tags, Required is configured per level in each tag list's RHP, so the policy-wide toggle is hidden (same gate as WorkspaceTagsSettingsPage). const shouldShowTagToggle = !isMultiLevelTagsUtil(policyTags) || hasDependentTagsUtil(policy, policyTags); const initialCategoryRequired = !!policy?.requiresCategory; @@ -102,7 +102,7 @@ function RulesRequireFieldsPage({ // Lock UX only when the feature itself is off (or categories are accounting-controlled). // Feature on but no enabled items: toggle stays disabled without lock/modal. const shouldShowCategoryLock = isCategoryFeatureDisabled || isConnectedToAccounting; - const shouldShowTagLock = isTagFeatureDisabled; + const shouldShowTagLock = isTagFeatureDisabled || isConnectedToAccounting; const categoryDisabledText = (() => { if (!shouldShowCategoryLock) { @@ -147,7 +147,33 @@ function RulesRequireFieldsPage({ setCategoryRequired(true); }, [isCategoryFeatureDisabled, isConnectedToAccounting, policyData, policyID, showConfirmModal, translate]); + const tagDisabledText = (() => { + if (!shouldShowTagLock) { + return undefined; + } + if (isConnectedToAccounting) { + return translate('workspace.moreFeatures.connectionsWarningModal.featureEnabledText'); + } + return translate('workspace.rules.individualExpenseRules.enableTagsToUnlockPrompt'); + })(); + const promptEnableTagsForRequireTag = useCallback(async () => { + // Accounting owns Tags while a connection is active, same as the Tags toggle on More features, so this must + // not force the feature on from here. + if (isConnectedToAccounting) { + const {action} = await showConfirmModal({ + title: translate('workspace.moreFeatures.connectionsWarningModal.featureEnabledTitle'), + prompt: translate('workspace.moreFeatures.connectionsWarningModal.featureEnabledText'), + confirmText: translate('workspace.moreFeatures.connectionsWarningModal.manageSettings'), + cancelText: translate('common.cancel'), + }); + if (action !== ModalActions.CONFIRM) { + return; + } + Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); + return; + } + if (!isTagFeatureDisabled) { return; } @@ -164,7 +190,7 @@ function RulesRequireFieldsPage({ enablePolicyTags(policyData, true); setPolicyRequiresTag(policyData, true); setTagRequired(true); - }, [isTagFeatureDisabled, policyData, showConfirmModal, translate]); + }, [isConnectedToAccounting, isTagFeatureDisabled, policyData, policyID, showConfirmModal, translate]); return ( void; }; -function RulesGeneralTab({policyID, canWriteRules, isAgentsRulesBannerDismissed}: RulesGeneralTabProps) { +function RulesGeneralTab({policyID, canWriteRules, isAgentsRulesBannerDismissed, onOpenAgentsTab}: RulesGeneralTabProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const {isBetaEnabled} = usePermissions(); @@ -36,7 +37,7 @@ function RulesGeneralTab({policyID, canWriteRules, isAgentsRulesBannerDismissed} title={translate('workspace.rules.agentsPromoBanner.title')} subtitle={translate('workspace.rules.agentsPromoBanner.subtitle')} ctaText={translate('workspace.rules.agentsPromoBanner.cta')} - onCtaPress={() => Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, CONST.TAB.RULES.AGENTS)} + onCtaPress={onOpenAgentsTab} ctaSentryLabel={CONST.SENTRY_LABEL.AGENTS_RULES_BANNER.CTA} onDismiss={() => dismissProductTraining(CONST.AGENTS_RULES_BANNER, true)} dismissSentryLabel={CONST.SENTRY_LABEL.AGENTS_RULES_BANNER.DISMISS}