Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
hasAccountingConnections,
hasAccountingFeatureConnection,
hasVendorFeature,
isCollectPolicy,
isControlPolicy,
isPerDiemEnabled,
isTimeTrackingEnabled,
Expand Down Expand Up @@ -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)),
);
Expand Down
23 changes: 13 additions & 10 deletions src/pages/workspace/rules/IndividualExpenseRulesSectionRevamp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -47,7 +48,7 @@ type BasicRuleMenuItem = {
title: string;
description?: string;
icon: IconAsset;
action: () => void;
route: Route;
pendingAction?: PendingAction;
};

Expand Down Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -133,39 +136,39 @@ 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,
},
{
key: RULE_MENU_ITEM_KEYS.EXPENSES_ABOVE_AMOUNT,
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,
},
{
key: RULE_MENU_ITEM_KEYS.FLAG_RECEIPT_LINE_ITEMS,
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,
},
{
key: RULE_MENU_ITEM_KEYS.RECEIPT_REQUIREMENTS,
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,
},
{
key: RULE_MENU_ITEM_KEYS.REQUIRE_FIELDS,
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,
},
];
Expand All @@ -176,15 +179,15 @@ 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,
},
{
key: RULE_MENU_ITEM_KEYS.BILLABLE_EXPENSES,
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),
},
];
Expand Down
1 change: 1 addition & 0 deletions src/pages/workspace/rules/PolicyRulesPageRevamp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) {
policyID={policyID}
canWriteRules={canWriteRules}
isAgentsRulesBannerDismissed={isAgentsRulesBannerDismissed}
onOpenAgentsTab={() => handleTabPress(RULES_TAB.AGENTS)}
/>
)}
{isTableTab && (
Expand Down
34 changes: 30 additions & 4 deletions src/pages/workspace/rules/RulesRequireFieldsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 (
<AccessOrNotFoundWrapper
Expand Down Expand Up @@ -214,7 +240,7 @@ function RulesRequireFieldsPage({
isActive={tagRequired}
disabled={isTagToggleDisabled}
showLockIcon={shouldShowTagLock}
disabledText={shouldShowTagLock ? translate('workspace.rules.individualExpenseRules.enableTagsToUnlockPrompt') : undefined}
disabledText={tagDisabledText}
disabledAction={shouldShowTagLock ? promptEnableTagsForRequireTag : undefined}
pendingAction={policy?.pendingFields?.requiresTag}
errors={policy?.errorFields?.requiresTag ?? undefined}
Expand Down
7 changes: 4 additions & 3 deletions src/pages/workspace/rules/tabs/RulesGeneralTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';

import Tab from '@libs/actions/Tab';
import {dismissProductTraining} from '@libs/actions/Welcome';

import IndividualExpenseRulesSectionRevamp from '@pages/workspace/rules/IndividualExpenseRulesSectionRevamp';
Expand All @@ -17,9 +16,11 @@ type RulesGeneralTabProps = {
policyID: string;
canWriteRules: boolean;
isAgentsRulesBannerDismissed: boolean;
/** Opens the Agents tab through the page's tab handler, so Collect gets the Control upgrade page instead. */
onOpenAgentsTab: () => void;
};

function RulesGeneralTab({policyID, canWriteRules, isAgentsRulesBannerDismissed}: RulesGeneralTabProps) {
function RulesGeneralTab({policyID, canWriteRules, isAgentsRulesBannerDismissed, onOpenAgentsTab}: RulesGeneralTabProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {isBetaEnabled} = usePermissions();
Expand All @@ -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}
Expand Down
Loading