Skip to content
5 changes: 3 additions & 2 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3365,11 +3365,12 @@ const ROUTES = {
},
WORKSPACE_RULES: {
route: 'workspaces/:policyID/rules',
Comment thread
Krishna2323 marked this conversation as resolved.
getRoute: (policyID: string | undefined) => {
/** @param tab preselects a Rules tab. The page otherwise restores the last one used. */
getRoute: (policyID: string | undefined, tab?: string) => {
if (!policyID) {
Log.warn('Invalid policyID is used to build the WORKSPACE_RULES route');
}
return `workspaces/${policyID}/rules` as const;
return `workspaces/${policyID}/rules${tab ? `?tab=${tab}` : ''}` as const;
},
},
WORKSPACE_DISTANCE_RATES: {
Expand Down
42 changes: 33 additions & 9 deletions src/components/Rule/RuleCategoriesDisabledEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import Button from '@components/ButtonComposed';
import FixedFooter from '@components/FixedFooter';
import {ModalActions} from '@components/Modal/Global/ModalContext';
import ScrollView from '@components/ScrollView';
import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection';

import useConfirmModal from '@hooks/useConfirmModal';
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import usePolicyData from '@hooks/usePolicyData';
import useThemeStyles from '@hooks/useThemeStyles';

import {enablePolicyCategories, openPolicyCategoriesPage} from '@libs/actions/Policy/Category';
import Navigation from '@libs/Navigation/Navigation';
import {hasAccountingConnections} from '@libs/PolicyUtils';

import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';

import React from 'react';
import {View} from 'react-native';
Expand All @@ -24,8 +30,25 @@ function RuleCategoriesDisabledEmptyState({policyID}: RuleCategoriesDisabledEmpt
const {translate} = useLocalize();
const illustrations = useMemoizedLazyIllustrations(['FolderOpen']);
const policyData = usePolicyData(policyID);
const {showConfirmModal} = useConfirmModal();
const isConnectedToAccounting = hasAccountingConnections(policyData.policy);

const enableCategories = async () => {
// Accounting owns Categories while a connection is active, same as the Categories toggle on More features.
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;
}

const enableCategories = () => {
enablePolicyCategories(policyData, true, false);

// The categories collection is empty while the feature is disabled, and enabling it only merges the
Expand All @@ -35,14 +58,15 @@ function RuleCategoriesDisabledEmptyState({policyID}: RuleCategoriesDisabledEmpt

return (
<View style={[styles.flex1]}>
<WorkspaceEmptyStateSection
shouldStyleAsCard={false}
icon={illustrations.FolderOpen}
title={translate('workspace.rules.categoriesDisabledEmptyState.title')}
subtitle={translate('workspace.rules.categoriesDisabledEmptyState.subtitle')}
containerStyle={[styles.flex1, styles.justifyContentCenter]}
/>
<FixedFooter style={[styles.mtAuto, styles.pt5]}>
<ScrollView contentContainerStyle={[styles.flexGrow1, styles.justifyContentCenter, styles.alignItemsCenter]}>
<WorkspaceEmptyStateSection
shouldStyleAsCard={false}
icon={illustrations.FolderOpen}
title={translate('workspace.rules.categoriesDisabledEmptyState.title')}
subtitle={translate('workspace.rules.categoriesDisabledEmptyState.subtitle')}
/>
</ScrollView>
<FixedFooter style={[styles.pt5]}>
Comment thread
JS00001 marked this conversation as resolved.
<Button
variant={CONST.BUTTON_VARIANT.SUCCESS}
size={CONST.BUTTON_SIZE.LARGE}
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3027,6 +3027,9 @@ type WorkspaceSplitNavigatorParamList = {
};
[SCREENS.WORKSPACE.RULES]: {
policyID: string;

/** Preselects a Rules tab. The page otherwise restores the last one used. */
tab?: string;
Comment thread
Krishna2323 marked this conversation as resolved.
};
[SCREENS.WORKSPACE.TIME_TRACKING]: {
policyID: string;
Expand Down
23 changes: 16 additions & 7 deletions src/pages/workspace/categories/CategorySettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import {isDisablingOrDeletingLastEnabledCategory} from '@libs/OptionsListUtils';
import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
import {arePolicyRulesEnabled, getWorkflowApprovalsUnavailable, hasTags, isAttendeeTrackingEnabled, isControlPolicy} from '@libs/PolicyUtils';
import {arePolicyRulesEnabled, getWorkflowApprovalsUnavailable, hasTags, isAttendeeTrackingEnabled, isControlPolicy, tryNavigateToControlPolicyUpgrade} from '@libs/PolicyUtils';

import type {SettingsNavigatorParamList} from '@navigation/types';

Expand Down Expand Up @@ -83,7 +83,7 @@ function CategorySettingsPage({route: {params, name}, navigation}: CategorySetti
const policyCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD;
const policyCategoryExpenseLimitType = policyCategory?.expenseLimitType ?? CONST.POLICY.EXPENSE_LIMIT_TYPES.EXPENSE;
const decodedCategoryName = getDecodedCategoryName(policyCategory?.name ?? '');
const categoryRulesEnabled = arePolicyRulesEnabled(policy, policyCategories);
const categoryRulesEnabled = arePolicyRulesEnabled(policy, policyCategories, isRulesRevampEnabled);

const contextualRules = useMemo(() => {
if (!isRulesRevampEnabled || !policyCategory) {
Expand Down Expand Up @@ -273,6 +273,15 @@ function CategorySettingsPage({route: {params, name}, navigation}: CategorySetti
const workflowApprovalsUnavailable = getWorkflowApprovalsUnavailable(policy);
const approverDisabled = !policy?.areWorkflowsEnabled || workflowApprovalsUnavailable;

/** Collect sees this section but every destination is Control-only, so upgrade instead of hitting Not Found. */
const navigateToCategoryRule = (dynamicRouteSuffix: string) => {
const ruleRoute = createDynamicRoute(dynamicRouteSuffix);
if (tryNavigateToControlPolicyUpgrade(policy, CONST.UPGRADE_FEATURE_INTRO_MAPPING.rules.alias, ruleRoute)) {
return;
}
Navigation.navigate(ruleRoute);
};

if (!policyCategory) {
return <NotFoundPage />;
}
Expand Down Expand Up @@ -391,7 +400,7 @@ function CategorySettingsPage({route: {params, name}, navigation}: CategorySetti
title={policyCategory?.commentHint}
description={translate('workspace.rules.categoryRules.descriptionHint')}
onPress={() => {
Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_DESCRIPTION_HINT.path));
navigateToCategoryRule(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_DESCRIPTION_HINT.path);
}}
interactive={canWriteCategories}
shouldShowRightIcon={canWriteCategories}
Expand All @@ -402,7 +411,7 @@ function CategorySettingsPage({route: {params, name}, navigation}: CategorySetti
title={approverText}
description={translate('workspace.rules.categoryRules.approver')}
onPress={() => {
Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_APPROVER.path));
navigateToCategoryRule(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_APPROVER.path);
}}
interactive={canWriteCategories}
shouldShowRightIcon={canWriteCategories}
Expand All @@ -419,7 +428,7 @@ function CategorySettingsPage({route: {params, name}, navigation}: CategorySetti
title={defaultTaxRateText}
description={translate('workspace.rules.categoryRules.defaultTaxRate')}
onPress={() => {
Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_DEFAULT_TAX_RATE.path));
navigateToCategoryRule(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_DEFAULT_TAX_RATE.path);
}}
interactive={canWriteCategories}
shouldShowRightIcon={canWriteCategories}
Expand Down Expand Up @@ -561,7 +570,7 @@ function CategorySettingsPage({route: {params, name}, navigation}: CategorySetti
title={rule.summary}
numberOfLinesTitle={3}
shouldShowBasicTitle
onPress={() => Navigation.navigate(createDynamicRoute(rule.dynamicRoutePath))}
onPress={() => navigateToCategoryRule(rule.dynamicRoutePath)}
shouldShowRightIcon={!rule.isDisabled}
interactive={!rule.isDisabled}
disabled={rule.isDisabled}
Expand All @@ -572,7 +581,7 @@ function CategorySettingsPage({route: {params, name}, navigation}: CategorySetti
<MenuItem
icon={expensifyIcons.Plus}
title={translate('workspace.rules.categoryRules.createNewRule')}
onPress={() => Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_RULES_NEW.path))}
onPress={() => navigateToCategoryRule(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_RULES_NEW.path)}
/>
)}
</>
Expand Down
19 changes: 17 additions & 2 deletions src/pages/workspace/rules/PolicyRulesPageRevamp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const agentsRulesBannerDismissedSelector = (value: OnyxEntry<DismissedProductTra

function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) {
const {translate} = useLocalize();
const {policyID} = route.params;
const {policyID, tab: requestedTab} = route.params;
const policy = usePolicy(policyID);
useWorkspaceDocumentTitle(policy?.name, 'workspace.common.rules');
const styles = useThemeStyles();
Expand Down Expand Up @@ -114,6 +114,15 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) {
Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, RULES_TAB.GENERAL);
}, [activeTab, policy]);

useEffect(() => {
// The tab param is an entry hint (deep link, post-upgrade bounce-back); the selected tab itself lives in Onyx.
if (!requestedTab || !isRulesTab(requestedTab)) {
return;
}

Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, requestedTab);
}, [requestedTab]);

const clearAllTableSelection = useCallback(() => {
setSelectedRuleKeysByTab((prev) => (Object.keys(prev).length > 0 ? {} : prev));
turnOffMobileSelectionMode();
Expand Down Expand Up @@ -260,13 +269,19 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) {
return;
}

if (key !== RULES_TAB.GENERAL && tryNavigateToControlPolicyUpgrade(policy, rulesUpgradeAlias, rulesUpgradeBackTo)) {
// Come back to the tab the user asked for, so upgrading lands them where they were headed.
if (key !== RULES_TAB.GENERAL && tryNavigateToControlPolicyUpgrade(policy, rulesUpgradeAlias, ROUTES.WORKSPACE_RULES.getRoute(policyID, key))) {
return;
}

setSelectedRuleKeysByTab({});
turnOffMobileSelectionMode();
Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, key);

// Drop the entry hint once the user picks their own tab, otherwise a refresh would re-apply it over that choice.
if (requestedTab) {
Navigation.setParams({tab: undefined});
}
};

const getHeaderContent = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ function RequireFieldsRulePageBase({policyID, categoryName, initialCategoryName,
}
}

// Otherwise cleared only on save, so backing out left the draft for the next rule to inherit.
useEffect(() => () => clearDraftRequireFieldsRule(), []);

useEffect(() => {
if (!isEditing) {
if (initializedDraftForRuleKeyRef.current !== ROUTES.NEW) {
Expand Down
13 changes: 11 additions & 2 deletions src/pages/workspace/tags/DynamicTagSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import useDynamicBackPath from '@hooks/useDynamicBackPath';
import useEnvironment from '@hooks/useEnvironment';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import usePolicyData from '@hooks/usePolicyData';
import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -33,6 +34,7 @@ import {
hasDependentTags as hasDependentTagsPolicyUtils,
isControlPolicy,
isMultiLevelTags as isMultiLevelTagsPolicyUtils,
tryNavigateToControlPolicyUpgrade,
} from '@libs/PolicyUtils';

import type {SettingsNavigatorParamList} from '@navigation/types';
Expand Down Expand Up @@ -62,6 +64,8 @@ function DynamicTagSettingsPage({route, navigation}: DynamicTagSettingsPageProps
const policyData = usePolicyData(policyID);
const {policy, tags: policyTags} = policyData;
const {canWrite: canWriteTags, withReadOnlyFallback} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.TAGS);
const {isBetaEnabled} = usePermissions();
const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP);
const policyTag = getTagListByOrderWeight(policyTags, orderWeight);
const {environmentURL} = useEnvironment();
const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy);
Expand Down Expand Up @@ -131,7 +135,12 @@ function DynamicTagSettingsPage({route, navigation}: DynamicTagSettingsPageProps
};

const navigateToEditTagApprover = () => {
Navigation.navigate(isQuickSettingsFlow ? createDynamicRoute(DYNAMIC_ROUTES.SETTINGS_TAG_APPROVER.path) : createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_TAG_APPROVER.path));
const approverRoute = isQuickSettingsFlow ? createDynamicRoute(DYNAMIC_ROUTES.SETTINGS_TAG_APPROVER.path) : createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_TAG_APPROVER.path);
// Collect sees this section but the approver page is Control-only, so upgrade instead of hitting Not Found.
if (tryNavigateToControlPolicyUpgrade(policy, CONST.UPGRADE_FEATURE_INTRO_MAPPING.rules.alias, approverRoute)) {
return;
}
Navigation.navigate(approverRoute);
};

const isThereAnyAccountingConnection = Object.keys(policy?.connections ?? {}).length !== 0;
Expand Down Expand Up @@ -209,7 +218,7 @@ function DynamicTagSettingsPage({route, navigation}: DynamicTagSettingsPageProps
</OfflineWithFeedback>
)}

{arePolicyRulesEnabled(policy, policyData.categories) && !isMultiLevelTags && (
{arePolicyRulesEnabled(policy, policyData.categories, isRulesRevampEnabled) && !isMultiLevelTags && (
Comment thread
Krishna2323 marked this conversation as resolved.
<>
<View style={[styles.mh5, styles.mv3, styles.pt3, styles.borderTop]}>
<Text style={[styles.textNormal, styles.textStrong, styles.mv3]}>{translate('workspace.tags.tagRules')}</Text>
Expand Down
8 changes: 6 additions & 2 deletions src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) {
case CONST.UPGRADE_FEATURE_INTRO_MAPPING.companyCards.id:
return route.params.backTo ? Navigation.goBack(route.params.backTo) : Navigation.goBack();
case CONST.UPGRADE_FEATURE_INTRO_MAPPING.rules.id:
return Navigation.goBack(route.params.backTo ?? ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID));
// The Rules backTo can carry a tab, and comparing params would replace the mounted route instead of popping.
return Navigation.goBack(route.params.backTo ?? ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID), {compareParams: false});
case CONST.UPGRADE_FEATURE_INTRO_MAPPING.perDiem.id:
case CONST.UPGRADE_FEATURE_INTRO_MAPPING.invoicing.id:
case CONST.UPGRADE_FEATURE_INTRO_MAPPING.companyCardSubmit.id:
Expand Down Expand Up @@ -249,7 +250,10 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) {
}
break;
case CONST.UPGRADE_FEATURE_INTRO_MAPPING.rules.id:
enablePolicyRules(policy, true, false, policyDataRef.current);
// Re-enabling would re-run the sidebar's "just enabled" highlight on a row that already shows.
if (!policy?.areRulesEnabled) {
enablePolicyRules(policy, true, false, policyDataRef.current);
}
break;
case CONST.UPGRADE_FEATURE_INTRO_MAPPING.publicReceiptVisibility.id:
setPolicyReceiptVisibilityPublic(policyID, true, policy?.isReceiptVisibilityPublic);
Expand Down
Loading