From 88d9805bab6848d71c7b206b6c5787417c238d0f Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 7 Jul 2026 02:17:13 +0530 Subject: [PATCH 01/15] Fix Require fields and Flag for review tabs showing when Categories are disabled. Signed-off-by: krishna2323 --- .../workspace/rules/PolicyRulesPageRevamp.tsx | 72 ++++++++++++------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx b/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx index 3cf26a5e21fe..484c4b839bbb 100644 --- a/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx +++ b/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx @@ -99,6 +99,16 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { const {showConfirmModal} = useConfirmModal(); + const areCategoriesEnabled = !!policy?.areCategoriesEnabled; + + useEffect(() => { + if (areCategoriesEnabled || (activeTab !== RULES_TAB.REQUIRE_FIELDS && activeTab !== RULES_TAB.FLAG_FOR_REVIEW)) { + return; + } + + Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, RULES_TAB.GENERAL); + }, [activeTab, areCategoriesEnabled]); + useEffect(() => { // Fetch once on mount (and when policyID changes). setPolicyCodingRule already updates Onyx — refetching after saves can overwrite a newly added rule with stale data. openPolicyRulesPage(policyID); @@ -191,33 +201,41 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { ]; }; - const tabs: TabSelectorBaseItem[] = [ - { - key: RULES_TAB.GENERAL, - title: translate('workspace.rules.tabs.general'), - icon: icons.Feed, - }, - { - key: RULES_TAB.CARD_RESTRICTIONS, - title: translate('workspace.rules.tabs.cardRestrictions'), - icon: icons.CreditCardExclamation, - }, - { - key: RULES_TAB.EXPENSE_DEFAULTS, - title: translate('workspace.rules.tabs.expenseDefaults'), - icon: icons.DocumentMagicWand, - }, - { - key: RULES_TAB.REQUIRE_FIELDS, - title: translate('workspace.rules.tabs.requireFields'), - icon: icons.Task, - }, - { - key: RULES_TAB.FLAG_FOR_REVIEW, - title: translate('workspace.rules.tabs.flagForReview'), - icon: icons.Flag, - }, - ]; + const tabs: TabSelectorBaseItem[] = useMemo(() => { + const allTabs: TabSelectorBaseItem[] = [ + { + key: RULES_TAB.GENERAL, + title: translate('workspace.rules.tabs.general'), + icon: icons.Feed, + }, + { + key: RULES_TAB.CARD_RESTRICTIONS, + title: translate('workspace.rules.tabs.cardRestrictions'), + icon: icons.CreditCardExclamation, + }, + { + key: RULES_TAB.EXPENSE_DEFAULTS, + title: translate('workspace.rules.tabs.expenseDefaults'), + icon: icons.DocumentMagicWand, + }, + { + key: RULES_TAB.REQUIRE_FIELDS, + title: translate('workspace.rules.tabs.requireFields'), + icon: icons.Task, + }, + { + key: RULES_TAB.FLAG_FOR_REVIEW, + title: translate('workspace.rules.tabs.flagForReview'), + icon: icons.Flag, + }, + ]; + + if (areCategoriesEnabled) { + return allTabs; + } + + return allTabs.filter((tab) => tab.key !== RULES_TAB.REQUIRE_FIELDS && tab.key !== RULES_TAB.FLAG_FOR_REVIEW); + }, [areCategoriesEnabled, icons.CreditCardExclamation, icons.DocumentMagicWand, icons.Feed, icons.Flag, icons.Task, translate]); const handleNewRule = () => { if (!canWriteRules) { From b620886bfd46c4162fdf7f338b896929ed9ef182 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 7 Jul 2026 02:21:21 +0530 Subject: [PATCH 02/15] Fix Require fields rule row flicker and stale toggles during pending edits. Signed-off-by: krishna2323 --- src/libs/RequireFieldsRulesUtils.ts | 119 ++++++++++++++---- .../workspace/rules/PolicyRulesPageRevamp.tsx | 2 +- tests/unit/RequireFieldsRulesUtilsTest.ts | 65 ++++++++++ 3 files changed, 162 insertions(+), 24 deletions(-) create mode 100644 tests/unit/RequireFieldsRulesUtilsTest.ts diff --git a/src/libs/RequireFieldsRulesUtils.ts b/src/libs/RequireFieldsRulesUtils.ts index c13d3711eb84..50d21f87e092 100644 --- a/src/libs/RequireFieldsRulesUtils.ts +++ b/src/libs/RequireFieldsRulesUtils.ts @@ -57,10 +57,10 @@ function categoryHasLegacyReceiptRules(category: PolicyCategory | undefined): bo function categoryHasAnyRequireFieldsRule(category: PolicyCategory): boolean { return ( - !!category.areCommentsRequired || - !!category.areAttendeesRequired || - hasCategoryReceiptOverride(category.maxAmountNoReceipt) || - hasCategoryReceiptOverride(category.maxAmountNoItemizedReceipt) + isRequireFieldEnabledForDisplay(category, INPUT_IDS.REQUIRE_DESCRIPTION) || + isRequireFieldEnabledForDisplay(category, INPUT_IDS.REQUIRE_ATTENDEES) || + hasCategoryReceiptOverrideForDisplay(category, 'maxAmountNoReceipt') || + hasCategoryReceiptOverrideForDisplay(category, 'maxAmountNoItemizedReceipt') ); } @@ -83,12 +83,52 @@ function isRequireFieldEnabled(category: PolicyCategory | undefined, field: Requ } } +function isRequireFieldPendingDelete(category: PolicyCategory | undefined, field: RequireFieldsRuleToggleFieldKey): boolean { + if (!category?.pendingFields) { + return false; + } + + switch (field) { + case INPUT_IDS.REQUIRE_DESCRIPTION: + return category.pendingFields.areCommentsRequired === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + case INPUT_IDS.REQUIRE_ATTENDEES: + return category.pendingFields.areAttendeesRequired === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + case INPUT_IDS.REQUIRE_RECEIPT: + return category.pendingFields.maxAmountNoReceipt === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + case INPUT_IDS.REQUIRE_ITEMIZED_RECEIPT: + return category.pendingFields.maxAmountNoItemizedReceipt === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + default: + return false; + } +} + +function isRequireFieldEnabledForDisplay(category: PolicyCategory | undefined, field: RequireFieldsRuleToggleFieldKey): boolean { + if (isRequireFieldPendingDelete(category, field)) { + return false; + } + + return isRequireFieldEnabled(category, field); +} + +function hasCategoryReceiptOverrideForDisplay(category: PolicyCategory | undefined, field: 'maxAmountNoReceipt' | 'maxAmountNoItemizedReceipt'): boolean { + if (!category) { + return false; + } + + const pendingField = field === 'maxAmountNoReceipt' ? category.pendingFields?.maxAmountNoReceipt : category.pendingFields?.maxAmountNoItemizedReceipt; + if (pendingField === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return false; + } + + return hasCategoryReceiptOverride(category[field]); +} + function getRequireFieldsFormFromCategory(category: PolicyCategory | undefined): Partial { return { - [INPUT_IDS.REQUIRE_DESCRIPTION]: isRequireFieldEnabled(category, INPUT_IDS.REQUIRE_DESCRIPTION), - [INPUT_IDS.REQUIRE_ATTENDEES]: isRequireFieldEnabled(category, INPUT_IDS.REQUIRE_ATTENDEES), - [INPUT_IDS.REQUIRE_RECEIPT]: isRequireFieldEnabled(category, INPUT_IDS.REQUIRE_RECEIPT), - [INPUT_IDS.REQUIRE_ITEMIZED_RECEIPT]: isRequireFieldEnabled(category, INPUT_IDS.REQUIRE_ITEMIZED_RECEIPT), + [INPUT_IDS.REQUIRE_DESCRIPTION]: isRequireFieldEnabledForDisplay(category, INPUT_IDS.REQUIRE_DESCRIPTION), + [INPUT_IDS.REQUIRE_ATTENDEES]: isRequireFieldEnabledForDisplay(category, INPUT_IDS.REQUIRE_ATTENDEES), + [INPUT_IDS.REQUIRE_RECEIPT]: isRequireFieldEnabledForDisplay(category, INPUT_IDS.REQUIRE_RECEIPT), + [INPUT_IDS.REQUIRE_ITEMIZED_RECEIPT]: isRequireFieldEnabledForDisplay(category, INPUT_IDS.REQUIRE_ITEMIZED_RECEIPT), }; } @@ -187,12 +227,28 @@ function getRequireFieldsRuleDescription( } } -function getRequireFieldsPendingAction(pendingFields: PolicyCategories[string]['pendingFields']): PendingAction | undefined { - const pendingActions = [pendingFields?.areCommentsRequired, pendingFields?.areAttendeesRequired, pendingFields?.maxAmountNoReceipt, pendingFields?.maxAmountNoItemizedReceipt].filter( - (pendingAction): pendingAction is PendingAction => !!pendingAction, - ); +function getRequireFieldsPendingAction(category: PolicyCategory): PendingAction | undefined { + if (category.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + } + + const pendingFieldActions = [ + category.pendingFields?.areCommentsRequired, + category.pendingFields?.areAttendeesRequired, + category.pendingFields?.maxAmountNoReceipt, + category.pendingFields?.maxAmountNoItemizedReceipt, + ].filter((pendingAction): pendingAction is PendingAction => !!pendingAction); - return pendingActions.find((pendingAction) => isPendingDeleteOrUpdate(pendingAction)) ?? pendingActions.at(0); + if (pendingFieldActions.length === 0) { + return undefined; + } + + // Field-level removals use pending DELETE on the field, but the row should stay visible and editable. + if (pendingFieldActions.some((pendingAction) => isPendingDeleteOrUpdate(pendingAction))) { + return CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; + } + + return pendingFieldActions.at(0); } function formatRequireFieldsRuleDescriptions(descriptions: string[]): string { @@ -215,11 +271,11 @@ function getRequireFieldsRuleDescriptionsForCategory( ): string[] { const descriptions: string[] = []; - if (category.areCommentsRequired) { + if (isRequireFieldEnabledForDisplay(category, INPUT_IDS.REQUIRE_DESCRIPTION)) { descriptions.push(getRequireFieldsRuleDescription(translate, CONST.REQUIRE_FIELDS_RULE_TYPES.REQUIRE_DESCRIPTION, undefined, convertToDisplayString, policyCurrency)); } - if (hasCategoryReceiptOverride(category.maxAmountNoReceipt)) { + if (hasCategoryReceiptOverrideForDisplay(category, 'maxAmountNoReceipt')) { descriptions.push( getRequireFieldsRuleDescription( translate, @@ -231,7 +287,7 @@ function getRequireFieldsRuleDescriptionsForCategory( ); } - if (hasCategoryReceiptOverride(category.maxAmountNoItemizedReceipt)) { + if (hasCategoryReceiptOverrideForDisplay(category, 'maxAmountNoItemizedReceipt')) { descriptions.push( getRequireFieldsRuleDescription( translate, @@ -243,7 +299,7 @@ function getRequireFieldsRuleDescriptionsForCategory( ); } - if (category.areAttendeesRequired) { + if (isRequireFieldEnabledForDisplay(category, INPUT_IDS.REQUIRE_ATTENDEES)) { descriptions.push(getRequireFieldsRuleDescription(translate, CONST.REQUIRE_FIELDS_RULE_TYPES.REQUIRE_ATTENDEES, undefined, convertToDisplayString, policyCurrency)); } @@ -281,14 +337,14 @@ function getRequireFieldsTableData({ continue; } - const pendingAction = getRequireFieldsPendingAction(category.pendingFields); - const isPendingDelete = pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + const pendingAction = getRequireFieldsPendingAction(category); + const isCategoryPendingDelete = category.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - if (!isOffline && isPendingDelete) { + if (!isOffline && isCategoryPendingDelete) { continue; } - if (!categoryHasAnyRequireFieldsRule(category) && !isPendingDelete) { + if (!categoryHasAnyRequireFieldsRule(category) && !isCategoryPendingDelete) { continue; } @@ -306,7 +362,7 @@ function getRequireFieldsTableData({ ruleDescription, searchTokens: [decodedCategoryName, ruleDescription, typeLabel, ...ruleDescriptions], pendingAction, - disabled: pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + disabled: isCategoryPendingDelete, action: () => onNavigate(getRequireFieldsRuleNavigationRoute(policyID, categoryName)), }); } @@ -314,5 +370,22 @@ function getRequireFieldsTableData({ return rules.sort((a, b) => localeCompare(a.conditionText, b.conditionText)); } -export {categoryHasLegacyReceiptRules, deleteRequireFieldsRule, getEffectiveRequireFieldsRuleForm, getRequireFieldsFormFromCategory, getRequireFieldsTableData, saveRequireFieldsRule}; +function countCategoriesWithRequireFieldsRules(policyCategories: PolicyCategories | undefined): number { + if (!policyCategories) { + return 0; + } + + return Object.values(policyCategories).filter((category) => category?.enabled && categoryHasAnyRequireFieldsRule(category)).length; +} + +export { + categoryHasAnyRequireFieldsRule, + categoryHasLegacyReceiptRules, + countCategoriesWithRequireFieldsRules, + deleteRequireFieldsRule, + getEffectiveRequireFieldsRuleForm, + getRequireFieldsFormFromCategory, + getRequireFieldsTableData, + saveRequireFieldsRule, +}; export type {RequireFieldsTableItem}; diff --git a/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx b/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx index 484c4b839bbb..f8301374d43b 100644 --- a/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx +++ b/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx @@ -40,7 +40,7 @@ import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import React, {useCallback, useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import RulesCardRestrictionsTab from './tabs/RulesCardRestrictionsTab'; diff --git a/tests/unit/RequireFieldsRulesUtilsTest.ts b/tests/unit/RequireFieldsRulesUtilsTest.ts new file mode 100644 index 000000000000..4af2f7e9b803 --- /dev/null +++ b/tests/unit/RequireFieldsRulesUtilsTest.ts @@ -0,0 +1,65 @@ +import {getRequireFieldsFormFromCategory, getRequireFieldsTableData} from '@libs/RequireFieldsRulesUtils'; + +import CONST from '@src/CONST'; +import type {PolicyCategories} from '@src/types/onyx'; + +import createRandomPolicy from '../utils/collections/policies'; + +describe('RequireFieldsRulesUtils', () => { + describe('getRequireFieldsFormFromCategory', () => { + it('treats fields pending delete as disabled', () => { + const category = { + name: 'Meals', + enabled: true, + areCommentsRequired: true, + areAttendeesRequired: true, + pendingFields: { + areCommentsRequired: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + areAttendeesRequired: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }; + + expect(getRequireFieldsFormFromCategory(category)).toEqual({ + requireDescription: false, + requireAttendees: true, + requireReceipt: false, + requireItemizedReceipt: false, + }); + }); + }); + + describe('getRequireFieldsTableData', () => { + const translate = ((key: string) => key) as Parameters[0]['translate']; + const convertToDisplayString = ((amount: number | undefined) => `$${amount ?? 0}`) as Parameters[0]['convertToDisplayString']; + const localeCompare = (a: string, b: string) => a.localeCompare(b); + const onNavigate = jest.fn(); + + it('keeps rows visible while a field is pending delete', () => { + const policyCategories: PolicyCategories = { + Meals: { + name: 'Meals', + enabled: true, + areCommentsRequired: true, + areAttendeesRequired: true, + pendingFields: { + areCommentsRequired: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + }, + }, + }; + + const tableData = getRequireFieldsTableData({ + policy: createRandomPolicy(0), + policyCategories, + translate, + convertToDisplayString, + localeCompare, + isOffline: false, + onNavigate, + }); + + expect(tableData).toHaveLength(1); + expect(tableData.at(0)?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(tableData.at(0)?.disabled).toBe(false); + }); + }); +}); From 1bfb2f4b7eb38bacb1da619b8a0b61ae3972ba74 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 7 Jul 2026 02:30:00 +0530 Subject: [PATCH 03/15] Fix Require fields category picker not resetting draft on category change. Signed-off-by: krishna2323 --- .../RequireFieldsRuleCategoryPageBase.tsx | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx b/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx index e3d743f43e65..1f4695e5c901 100644 --- a/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx +++ b/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx @@ -8,7 +8,7 @@ import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; import {updateDraftRequireFieldsRule} from '@libs/actions/User'; import {getDecodedCategoryName} from '@libs/CategoryUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getEffectiveRequireFieldsRuleForm} from '@libs/RequireFieldsRulesUtils'; +import {getRequireFieldsFormFromCategory} from '@libs/RequireFieldsRulesUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; @@ -47,13 +47,22 @@ function RequireFieldsRuleCategoryPageBase({policyID, categoryName}: RequireFiel const backToRoute = isEditing ? ROUTES.RULES_REQUIRE_FIELDS_RULE_EDIT.getRoute(policyID, categoryName) : ROUTES.RULES_REQUIRE_FIELDS_RULE_NEW.getRoute(policyID); const onSave = (value?: string) => { - const selectedCategory = value ? policyCategories?.[value] : undefined; - const draftForm = { - ...form, - [INPUT_IDS.CATEGORY]: value, - }; + if (!value) { + updateDraftRequireFieldsRule({ + [INPUT_IDS.CATEGORY]: '', + [INPUT_IDS.REQUIRE_DESCRIPTION]: false, + [INPUT_IDS.REQUIRE_ATTENDEES]: false, + [INPUT_IDS.REQUIRE_RECEIPT]: false, + [INPUT_IDS.REQUIRE_ITEMIZED_RECEIPT]: false, + }); + return; + } - updateDraftRequireFieldsRule(selectedCategory ? getEffectiveRequireFieldsRuleForm(selectedCategory, draftForm) : draftForm); + const selectedCategory = policyCategories?.[value]; + updateDraftRequireFieldsRule({ + [INPUT_IDS.CATEGORY]: value, + ...getRequireFieldsFormFromCategory(selectedCategory), + }); }; return ( @@ -72,6 +81,7 @@ function RequireFieldsRuleCategoryPageBase({policyID, categoryName}: RequireFiel onSave={onSave} onBack={() => Navigation.goBack(backToRoute)} backToRoute={backToRoute} + allowNoneOption={false} /> ); From cb9a0d4acab617db0066de00651b6f59c49b7b21 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 7 Jul 2026 02:30:14 +0530 Subject: [PATCH 04/15] Fix None option appearing in Require fields category picker after selection. Signed-off-by: krishna2323 --- src/components/Rule/RuleSelectionBase.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/Rule/RuleSelectionBase.tsx b/src/components/Rule/RuleSelectionBase.tsx index 44fc49783444..c63d7aafbc00 100644 --- a/src/components/Rule/RuleSelectionBase.tsx +++ b/src/components/Rule/RuleSelectionBase.tsx @@ -43,11 +43,14 @@ type RuleSelectionBaseProps = { /** The route to navigate back to */ backToRoute: Route; + /** When true, shows a "None" option in the picker */ + allowNoneOption?: boolean; + /** Optional hash for rule not found validation */ hash?: string; }; -function RuleSelectionBase({titleKey, title, testID, selectedItem, items, onSave, onBack, backToRoute, hash}: RuleSelectionBaseProps) { +function RuleSelectionBase({titleKey, title, testID, selectedItem, items, onSave, onBack, backToRoute, allowNoneOption = false, hash}: RuleSelectionBaseProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -69,7 +72,7 @@ function RuleSelectionBase({titleKey, title, testID, selectedItem, items, onSave items={items} onSaveSelection={onSave} shouldAutoSave - allowNoneOption + allowNoneOption={allowNoneOption} /> From 76ffee7ce0111f7fbbdf7ecdd95b6ef1d227fe28 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 7 Jul 2026 02:31:26 +0530 Subject: [PATCH 05/15] Fix Require fields rules missing from Copy Settings preview and optimistic copy. Signed-off-by: krishna2323 --- src/libs/CopyPolicySettingsUtils.ts | 3 +- src/libs/FlagForReviewRulesUtils.ts | 18 +++++- src/libs/actions/Policy/CopyPolicySettings.ts | 61 +++++++++++++++++++ .../CopyPolicySettingsSelectFeaturesPage.tsx | 6 +- src/pages/workspace/duplicate/utils.ts | 14 ++++- tests/actions/CopyPolicySettingsTest.ts | 34 +++++++++++ tests/unit/CopyPolicySettingsUtilsTest.ts | 7 +++ 7 files changed, 138 insertions(+), 5 deletions(-) diff --git a/src/libs/CopyPolicySettingsUtils.ts b/src/libs/CopyPolicySettingsUtils.ts index 806e8990d525..31bac867d015 100644 --- a/src/libs/CopyPolicySettingsUtils.ts +++ b/src/libs/CopyPolicySettingsUtils.ts @@ -46,6 +46,7 @@ type CopyPolicySettingsSourceFeatureContext = { connectedIntegrationCount: number; hasWorkflowRules: boolean; hasWorkspaceRules: boolean; + hasCategoryRules: boolean; codingRulesCount: number; hasInvoiceConfiguration: boolean; isCollectPolicy: boolean; @@ -221,7 +222,7 @@ function isCopyPolicySettingsPartEnabledOnSource(part: Part, context: CopyPolicy case 'workflows': return context.hasWorkflowRules; case 'rules': - return context.hasWorkspaceRules && !context.isCollectPolicy; + return (context.hasWorkspaceRules || context.hasCategoryRules) && !context.isCollectPolicy; case 'codingRules': return context.codingRulesCount > 0 && !context.isCollectPolicy; case 'distanceRates': diff --git a/src/libs/FlagForReviewRulesUtils.ts b/src/libs/FlagForReviewRulesUtils.ts index 1d263567f5d9..d705a8c3d68b 100644 --- a/src/libs/FlagForReviewRulesUtils.ts +++ b/src/libs/FlagForReviewRulesUtils.ts @@ -168,5 +168,21 @@ function getFlagForReviewTableData({ return rules; } -export {deleteFlagForReviewRule, getEffectiveFlagForReviewRuleForm, getFlagForReviewFormFromCategory, getFlagForReviewRuleAmountError, getFlagForReviewTableData, saveFlagForReviewRule}; +function countCategoriesWithFlagForReviewRules(policyCategories: PolicyCategories | undefined): number { + if (!policyCategories) { + return 0; + } + + return Object.values(policyCategories).filter((category) => category?.enabled && hasExplicitFlagAmount(category.maxExpenseAmount)).length; +} + +export { + countCategoriesWithFlagForReviewRules, + deleteFlagForReviewRule, + getEffectiveFlagForReviewRuleForm, + getFlagForReviewFormFromCategory, + getFlagForReviewRuleAmountError, + getFlagForReviewTableData, + saveFlagForReviewRule, +}; export type {FlagForReviewTableItem}; diff --git a/src/libs/actions/Policy/CopyPolicySettings.ts b/src/libs/actions/Policy/CopyPolicySettings.ts index c89985ed03b8..90c1b7c1e3c7 100644 --- a/src/libs/actions/Policy/CopyPolicySettings.ts +++ b/src/libs/actions/Policy/CopyPolicySettings.ts @@ -182,6 +182,45 @@ function buildTravelSettingsPatch(sourcePolicy: Policy, targetPolicy: Policy): P return {travelSettings: {...targetPolicy.travelSettings, autoAddTripName: sourceAutoAddTripName}}; } +type CategoryRulesField = 'areCommentsRequired' | 'areAttendeesRequired' | 'maxAmountNoReceipt' | 'maxAmountNoItemizedReceipt' | 'maxExpenseAmount' | 'expenseLimitType'; + +/** + * When copying the rules part, also copy per-category rule fields (Require fields / Flag for review) + * onto matching categories in the target workspace. + */ +function buildCategoryRulesPatch(sourceCategories: PolicyCategories, targetCategories: PolicyCategories): PolicyCategories | undefined { + const patch: PolicyCategories = {}; + let hasPatch = false; + + for (const [categoryName, sourceCategory] of Object.entries(sourceCategories)) { + if (!sourceCategory?.enabled || sourceCategory.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + continue; + } + + const targetCategory = targetCategories[categoryName]; + if (!targetCategory) { + continue; + } + + const categoryRuleFields: Pick = { + areCommentsRequired: sourceCategory.areCommentsRequired, + areAttendeesRequired: sourceCategory.areAttendeesRequired, + maxAmountNoReceipt: sourceCategory.maxAmountNoReceipt, + maxAmountNoItemizedReceipt: sourceCategory.maxAmountNoItemizedReceipt, + maxExpenseAmount: sourceCategory.maxExpenseAmount, + expenseLimitType: sourceCategory.expenseLimitType, + }; + + patch[categoryName] = { + ...targetCategory, + ...categoryRuleFields, + }; + hasPatch = true; + } + + return hasPatch ? patch : undefined; +} + /** * Returns the partial Policy patch derived from the selected `parts`, excluding fields whose * mapping is handled separately (customUnits, timeTracking, receiptPartners, categories, tags collection keys). @@ -251,6 +290,7 @@ function buildCopyPolicySettingsData( const isCategoriesSelected = parts.includes('categories'); const isTagsSelected = parts.includes('tags'); + const isRulesSelected = parts.includes('rules'); const isDistanceSelected = parts.includes('distanceRates'); const isPerDiemSelected = parts.includes('perDiem'); const isTimeTrackingSelected = parts.includes('timeTracking'); @@ -379,6 +419,27 @@ function buildCopyPolicySettingsData( value: previousTags, }); } + + if (isRulesSelected && !isCategoriesSelected) { + const targetCategoriesKey = `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${targetPolicy.id}` as const; + const targetCategories = allPolicyCategories?.[targetCategoriesKey] ?? {}; + const categoryRulesPatch = buildCategoryRulesPatch(sourceCategories, targetCategories); + + if (categoryRulesPatch) { + const previousCategoryRules = Object.fromEntries(Object.keys(categoryRulesPatch).map((categoryName) => [categoryName, targetCategories[categoryName]])); + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: targetCategoriesKey, + value: categoryRulesPatch, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: targetCategoriesKey, + value: previousCategoryRules, + }); + } + } } // Surface an RBR on the source policy row so the admin knows the bulk copy failed diff --git a/src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx b/src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx index 5630b83bbd44..9f0ed29821c7 100644 --- a/src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx +++ b/src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx @@ -25,12 +25,14 @@ import { getTimeTrackingCopySettingsDescription, isCopyPolicySettingsPartEnabledOnSource, } from '@libs/CopyPolicySettingsUtils'; +import {countCategoriesWithFlagForReviewRules} from '@libs/FlagForReviewRulesUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {PolicyCopySettingsNavigatorParamList} from '@libs/Navigation/types'; import {createFilteredMemberCountSelector, createInvoiceConfigurationTextSelector, getDistanceRateCustomUnit, getPerDiemCustomUnit, isCollectPolicy} from '@libs/PolicyUtils'; import {formatAddressToString} from '@libs/ReportActionsUtils'; import {getReportFieldsByPolicyID} from '@libs/ReportUtils'; +import {countCategoriesWithRequireFieldsRules} from '@libs/RequireFieldsRulesUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import {getAllValidConnectedIntegration, getWorkflowRules, getWorkspaceRules} from '@pages/workspace/duplicate/utils'; @@ -108,7 +110,8 @@ function CopyPolicySettingsSelectFeaturesPage() { const perDiemCount = Object.values(perDiemRates).filter((rate) => rate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length; const formattedAddress = !isEmptyObject(sourcePolicy) && !isEmptyObject(sourcePolicy.address) ? formatAddressToString(sourcePolicy.address) : ''; const workflows = getWorkflowRules(sourcePolicy, translate); - const rules = getWorkspaceRules(sourcePolicy, translate); + const rules = getWorkspaceRules(sourcePolicy, translate, policyCategories); + const categoryRulesCount = countCategoriesWithRequireFieldsRules(policyCategories) + countCategoriesWithFlagForReviewRules(policyCategories); const sourceFeatureContext = { policy: sourcePolicy, @@ -122,6 +125,7 @@ function CopyPolicySettingsSelectFeaturesPage() { connectedIntegrationCount: connectedIntegration?.length ?? 0, hasWorkflowRules: !!workflows?.length, hasWorkspaceRules: !!rules?.length, + hasCategoryRules: categoryRulesCount > 0, codingRulesCount, hasInvoiceConfiguration: !!sourcePolicy?.areInvoicesEnabled && !!invoiceConfigurationText, isCollectPolicy: isCollectPolicy(sourcePolicy), diff --git a/src/pages/workspace/duplicate/utils.ts b/src/pages/workspace/duplicate/utils.ts index 7974f0ebc877..1b181713cf1f 100644 --- a/src/pages/workspace/duplicate/utils.ts +++ b/src/pages/workspace/duplicate/utils.ts @@ -1,16 +1,18 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; +import {countCategoriesWithFlagForReviewRules} from '@libs/FlagForReviewRulesUtils'; import {getCorrectedAutoReportingFrequency, getWorkflowApprovalsUnavailable} from '@libs/PolicyUtils'; +import {countCategoriesWithRequireFieldsRules} from '@libs/RequireFieldsRulesUtils'; import {getAutoReportingFrequencyDisplayNames} from '@pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage'; import {isAuthenticationError, isConnectionUnverified} from '@userActions/connections'; import CONST from '@src/CONST'; -import type {Policy} from '@src/types/onyx'; +import type {Policy, PolicyCategories} from '@src/types/onyx'; import type {ConnectionName} from '@src/types/onyx/Policy'; -function getWorkspaceRules(policy: Policy | undefined, translate: LocaleContextProps['translate']) { +function getWorkspaceRules(policy: Policy | undefined, translate: LocaleContextProps['translate'], policyCategories?: PolicyCategories) { const workflowApprovalsUnavailable = getWorkflowApprovalsUnavailable(policy); const autoPayApprovedReportsUnavailable = !policy?.areWorkflowsEnabled || policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES || !policy?.achAccount?.bankAccountID; @@ -49,6 +51,14 @@ function getWorkspaceRules(policy: Policy | undefined, translate: LocaleContextP total.push(translate('workspace.rules.expenseReportRules.autoPayApprovedReportsTitle')); } + if (countCategoriesWithRequireFieldsRules(policyCategories) > 0) { + total.push(translate('workspace.rules.tabs.requireFields')); + } + + if (countCategoriesWithFlagForReviewRules(policyCategories) > 0) { + total.push(translate('workspace.rules.tabs.flagForReview')); + } + return total.length > 0 ? total : null; } diff --git a/tests/actions/CopyPolicySettingsTest.ts b/tests/actions/CopyPolicySettingsTest.ts index b3002cea3bde..9fd3d4d063d6 100644 --- a/tests/actions/CopyPolicySettingsTest.ts +++ b/tests/actions/CopyPolicySettingsTest.ts @@ -487,6 +487,40 @@ describe('actions/Policy/CopyPolicySettings', () => { expect(optimisticData.find((u) => u.key === catKeyA && u.onyxMethod === Onyx.METHOD.SET)?.value).toEqual(sourceCategories); expect(optimisticData.find((u) => u.key === catKeyB && u.onyxMethod === Onyx.METHOD.SET)?.value).toEqual(sourceCategories); }); + + it('merges category rule fields onto matching target categories when rules selected without categories', () => { + const targetPolicy = makeTargetPolicy(); + const sourceCategories: PolicyCategories = { + Food: {name: 'Food', enabled: true, areCommentsRequired: true, areAttendeesRequired: false}, + }; + const targetCategories: PolicyCategories = { + Food: {name: 'Food', enabled: true, areCommentsRequired: false, areAttendeesRequired: false}, + }; + + const {optimisticData} = buildCopyPolicySettingsData( + makeSourcePolicy(), + [targetPolicy], + ['rules'], + { + [SOURCE_CATEGORIES_KEY]: sourceCategories, + [TARGET_CATEGORIES_KEY]: targetCategories, + }, + {}, + ); + + const categoryMerge = optimisticData.find((u) => u.key === TARGET_CATEGORIES_KEY && u.onyxMethod === Onyx.METHOD.MERGE); + expect(categoryMerge?.value).toEqual({ + Food: { + ...targetCategories.Food, + areCommentsRequired: true, + areAttendeesRequired: false, + maxAmountNoReceipt: undefined, + maxAmountNoItemizedReceipt: undefined, + maxExpenseAmount: undefined, + expenseLimitType: undefined, + }, + }); + }); }); describe('COPY_POLICY_SETTINGS lifecycle key', () => { diff --git a/tests/unit/CopyPolicySettingsUtilsTest.ts b/tests/unit/CopyPolicySettingsUtilsTest.ts index 908963815b34..676cd1fdfafe 100644 --- a/tests/unit/CopyPolicySettingsUtilsTest.ts +++ b/tests/unit/CopyPolicySettingsUtilsTest.ts @@ -156,6 +156,7 @@ describe('CopyPolicySettingsUtils', () => { connectedIntegrationCount: 1, hasWorkflowRules: true, hasWorkspaceRules: true, + hasCategoryRules: false, codingRulesCount: 1, hasInvoiceConfiguration: true, isCollectPolicy: false, @@ -186,6 +187,12 @@ describe('CopyPolicySettingsUtils', () => { expect(isCopyPolicySettingsPartEnabledOnSource('codingRules', baseContext)).toBe(true); }); + it('shows rules when the source has workspace or category rules and is not collect', () => { + expect(isCopyPolicySettingsPartEnabledOnSource('rules', {...baseContext, hasWorkspaceRules: false, hasCategoryRules: false, isCollectPolicy: true})).toBe(false); + expect(isCopyPolicySettingsPartEnabledOnSource('rules', {...baseContext, hasWorkspaceRules: false, hasCategoryRules: true})).toBe(true); + expect(isCopyPolicySettingsPartEnabledOnSource('rules', baseContext)).toBe(true); + }); + it('hides travel when the source policy does not have travel enabled', () => { expect(isCopyPolicySettingsPartEnabledOnSource('travel', baseContext)).toBe(false); From 19ee80ac129c7cbf40c39e7702f1500e352cecb5 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 7 Jul 2026 02:57:39 +0530 Subject: [PATCH 06/15] fix knip check. Signed-off-by: krishna2323 --- src/libs/RequireFieldsRulesUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/RequireFieldsRulesUtils.ts b/src/libs/RequireFieldsRulesUtils.ts index 50d21f87e092..b77d7002c7e6 100644 --- a/src/libs/RequireFieldsRulesUtils.ts +++ b/src/libs/RequireFieldsRulesUtils.ts @@ -379,7 +379,6 @@ function countCategoriesWithRequireFieldsRules(policyCategories: PolicyCategorie } export { - categoryHasAnyRequireFieldsRule, categoryHasLegacyReceiptRules, countCategoriesWithRequireFieldsRules, deleteRequireFieldsRule, From 483dfeaebf5d78f6faf91e64601633347ee8e5a6 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 7 Jul 2026 03:12:51 +0530 Subject: [PATCH 07/15] Address Codex review: preserve None default and defer category tab gating until policy loads. Signed-off-by: krishna2323 --- src/components/Rule/RuleSelectionBase.tsx | 2 +- src/pages/workspace/rules/PolicyRulesPageRevamp.tsx | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/Rule/RuleSelectionBase.tsx b/src/components/Rule/RuleSelectionBase.tsx index c63d7aafbc00..0e22189c8f04 100644 --- a/src/components/Rule/RuleSelectionBase.tsx +++ b/src/components/Rule/RuleSelectionBase.tsx @@ -50,7 +50,7 @@ type RuleSelectionBaseProps = { hash?: string; }; -function RuleSelectionBase({titleKey, title, testID, selectedItem, items, onSave, onBack, backToRoute, allowNoneOption = false, hash}: RuleSelectionBaseProps) { +function RuleSelectionBase({titleKey, title, testID, selectedItem, items, onSave, onBack, backToRoute, allowNoneOption = true, hash}: RuleSelectionBaseProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); diff --git a/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx b/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx index f8301374d43b..24550919417c 100644 --- a/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx +++ b/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx @@ -99,15 +99,17 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { const {showConfirmModal} = useConfirmModal(); + const isPolicyLoaded = !!policy?.id && !policy?.isLoading; const areCategoriesEnabled = !!policy?.areCategoriesEnabled; + const shouldShowCategoryRulesTabs = !isPolicyLoaded || areCategoriesEnabled; useEffect(() => { - if (areCategoriesEnabled || (activeTab !== RULES_TAB.REQUIRE_FIELDS && activeTab !== RULES_TAB.FLAG_FOR_REVIEW)) { + if (!isPolicyLoaded || areCategoriesEnabled || (activeTab !== RULES_TAB.REQUIRE_FIELDS && activeTab !== RULES_TAB.FLAG_FOR_REVIEW)) { return; } Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, RULES_TAB.GENERAL); - }, [activeTab, areCategoriesEnabled]); + }, [activeTab, areCategoriesEnabled, isPolicyLoaded]); useEffect(() => { // Fetch once on mount (and when policyID changes). setPolicyCodingRule already updates Onyx — refetching after saves can overwrite a newly added rule with stale data. @@ -230,12 +232,12 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { }, ]; - if (areCategoriesEnabled) { + if (shouldShowCategoryRulesTabs) { return allTabs; } return allTabs.filter((tab) => tab.key !== RULES_TAB.REQUIRE_FIELDS && tab.key !== RULES_TAB.FLAG_FOR_REVIEW); - }, [areCategoriesEnabled, icons.CreditCardExclamation, icons.DocumentMagicWand, icons.Feed, icons.Flag, icons.Task, translate]); + }, [shouldShowCategoryRulesTabs, icons.CreditCardExclamation, icons.DocumentMagicWand, icons.Feed, icons.Flag, icons.Task, translate]); const handleNewRule = () => { if (!canWriteRules) { From fd45277a65a59e4692df8b6cb509ab44bb3f621f Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 7 Jul 2026 03:24:46 +0530 Subject: [PATCH 08/15] Use read-only styling for locked category on category rule edit pages. Signed-off-by: krishna2323 --- .../FlagForReviewRules/FlagForReviewRulePageBase.tsx | 9 +++++---- .../RequireFieldsRules/RequireFieldsRulePageBase.tsx | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRulePageBase.tsx b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRulePageBase.tsx index 1a245237d7b1..aefd9357172c 100644 --- a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRulePageBase.tsx +++ b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRulePageBase.tsx @@ -154,6 +154,8 @@ function FlagForReviewRulePageBase({policyID, categoryName, testID}: FlagForRevi return ; } + const canEditCategory = canWriteRules && !isEditing; + const footer = canWriteRules ? ( Navigation.navigate(getFlagForReviewRuleCategoryRoute(policyID, categoryName)) : undefined} - shouldShowRightIcon={canWriteRules} - interactive={canWriteRules} + onPress={canEditCategory ? () => Navigation.navigate(getFlagForReviewRuleCategoryRoute(policyID, categoryName)) : undefined} + shouldShowRightIcon={canEditCategory} + interactive={canEditCategory} icon={icons.Folder} iconWidth={variables.iconSizeNormal} iconHeight={variables.iconSizeNormal} shouldIconUseAutoWidthStyle - disabled={isEditing} sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.FLAG_FOR_REVIEW_RULE_CATEGORY} /> ; } + const canEditCategory = canWriteRules && !isEditing; + const footer = canWriteRules ? ( Navigation.navigate(getRequireFieldsRuleCategoryRoute(policyID, categoryName)) : undefined} - shouldShowRightIcon={canWriteRules} - interactive={canWriteRules} + onPress={canEditCategory ? () => Navigation.navigate(getRequireFieldsRuleCategoryRoute(policyID, categoryName)) : undefined} + shouldShowRightIcon={canEditCategory} + interactive={canEditCategory} icon={icons.Folder} iconWidth={variables.iconSizeNormal} iconHeight={variables.iconSizeNormal} shouldIconUseAutoWidthStyle - disabled={isEditing} sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.REQUIRE_FIELDS_RULE_CATEGORY} /> From a06adadbba48c3bebcda9d77f957ffb8903eb61a Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 7 Jul 2026 03:56:32 +0530 Subject: [PATCH 09/15] remove none option from category picker. Signed-off-by: krishna2323 --- .../FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx index a457e71f1505..ec102237a7ca 100644 --- a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx +++ b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx @@ -75,6 +75,7 @@ function FlagForReviewRuleCategoryPageBase({policyID, categoryName}: FlagForRevi onSave={onSave} onBack={() => Navigation.goBack(backToRoute)} backToRoute={backToRoute} + allowNoneOption={false} /> ); From d1add78ccebe44d8ece675769e65e735c65cb480 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 30 Jul 2026 19:43:48 +0530 Subject: [PATCH 10/15] Merge branch 'main' into krishna2323/issue-95304-rules-revamp-regressions Signed-off-by: krishna2323 --- src/libs/CopyPolicySettingsUtils.ts | 3 +- src/libs/FlagForReviewRulesUtils.ts | 18 +----- src/libs/RequireFieldsRulesUtils.ts | 9 --- src/libs/actions/Policy/CopyPolicySettings.ts | 61 ------------------ .../CopyPolicySettingsSelectFeaturesPage.tsx | 6 +- src/pages/workspace/duplicate/utils.ts | 14 +--- tests/actions/CopyPolicySettingsTest.ts | 34 ---------- tests/unit/CopyPolicySettingsUtilsTest.ts | 7 -- tests/unit/RequireFieldsRulesUtilsTest.ts | 64 ------------------- 9 files changed, 5 insertions(+), 211 deletions(-) delete mode 100644 tests/unit/RequireFieldsRulesUtilsTest.ts diff --git a/src/libs/CopyPolicySettingsUtils.ts b/src/libs/CopyPolicySettingsUtils.ts index 9b8fd9ffb626..fd51447591ed 100644 --- a/src/libs/CopyPolicySettingsUtils.ts +++ b/src/libs/CopyPolicySettingsUtils.ts @@ -50,7 +50,6 @@ type CopyPolicySettingsSourceFeatureContext = { connectedIntegrationCount: number; hasWorkflowRules: boolean; hasWorkspaceRules: boolean; - hasCategoryRules: boolean; codingRulesCount: number; hasInvoiceConfiguration: boolean; isCollectPolicy: boolean; @@ -281,7 +280,7 @@ function isCopyPolicySettingsPartEnabledOnSource(part: Part, context: CopyPolicy case 'workflows': return context.hasWorkflowRules; case 'rules': - return (context.hasWorkspaceRules || context.hasCategoryRules) && !context.isCollectPolicy; + return context.hasWorkspaceRules && !context.isCollectPolicy; case 'codingRules': return context.codingRulesCount > 0 && !context.isCollectPolicy; case 'distanceRates': diff --git a/src/libs/FlagForReviewRulesUtils.ts b/src/libs/FlagForReviewRulesUtils.ts index 73d439011cd4..5bdb130668c1 100644 --- a/src/libs/FlagForReviewRulesUtils.ts +++ b/src/libs/FlagForReviewRulesUtils.ts @@ -157,21 +157,5 @@ function getFlagForReviewTableData({ return rules; } -function countCategoriesWithFlagForReviewRules(policyCategories: PolicyCategories | undefined): number { - if (!policyCategories) { - return 0; - } - - return Object.values(policyCategories).filter((category) => category?.enabled && hasExplicitFlagAmount(category.maxExpenseAmount)).length; -} - -export { - countCategoriesWithFlagForReviewRules, - deleteFlagForReviewRule, - getFlagForReviewFormFromCategory, - getFlagForReviewRuleAmountError, - getFlagForReviewTableData, - hasExplicitFlagAmount, - saveFlagForReviewRule, -}; +export {deleteFlagForReviewRule, getFlagForReviewFormFromCategory, getFlagForReviewRuleAmountError, getFlagForReviewTableData, hasExplicitFlagAmount, saveFlagForReviewRule}; export type {FlagForReviewTableItem}; diff --git a/src/libs/RequireFieldsRulesUtils.ts b/src/libs/RequireFieldsRulesUtils.ts index 4ab97551d741..1f7401963a60 100644 --- a/src/libs/RequireFieldsRulesUtils.ts +++ b/src/libs/RequireFieldsRulesUtils.ts @@ -113,14 +113,6 @@ function categoryHasAnyRequireFieldsRule(category: PolicyCategory): boolean { ); } -function countCategoriesWithRequireFieldsRules(policyCategories: PolicyCategories | undefined): number { - if (!policyCategories) { - return 0; - } - - return Object.values(policyCategories).filter((category) => category?.enabled && categoryHasAnyRequireFieldsRule(category)).length; -} - function getDescriptionSettingFromCategory(category: PolicyCategory | undefined): FieldRequirementsDirection { if (!category) { return CONST.FIELD_REQUIREMENTS_DIRECTION.DO_NOT_REQUIRE; @@ -948,7 +940,6 @@ function getRequireFieldsFieldCouplingTooltipKey( export { categoryHasAnyRequireFieldsRule, - countCategoriesWithRequireFieldsRules, deleteRequireFieldsRule, formatRequireFieldsRuleDescriptions, getActiveFieldRequirementsDirection, diff --git a/src/libs/actions/Policy/CopyPolicySettings.ts b/src/libs/actions/Policy/CopyPolicySettings.ts index ad93c0a61c3d..3aee0eb5a6d1 100644 --- a/src/libs/actions/Policy/CopyPolicySettings.ts +++ b/src/libs/actions/Policy/CopyPolicySettings.ts @@ -214,45 +214,6 @@ function buildTravelSettingsPatch(sourcePolicy: Policy, targetPolicy: Policy): P return {travelSettings: {...targetPolicy.travelSettings, autoAddTripName: sourceAutoAddTripName}}; } -type CategoryRulesField = 'areCommentsRequired' | 'areAttendeesRequired' | 'maxAmountNoReceipt' | 'maxAmountNoItemizedReceipt' | 'maxExpenseAmount' | 'expenseLimitType'; - -/** - * When copying the rules part, also copy per-category rule fields (Require fields / Flag for review) - * onto matching categories in the target workspace. - */ -function buildCategoryRulesPatch(sourceCategories: PolicyCategories, targetCategories: PolicyCategories): PolicyCategories | undefined { - const patch: PolicyCategories = {}; - let hasPatch = false; - - for (const [categoryName, sourceCategory] of Object.entries(sourceCategories)) { - if (!sourceCategory?.enabled || sourceCategory.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - continue; - } - - const targetCategory = targetCategories[categoryName]; - if (!targetCategory) { - continue; - } - - const categoryRuleFields: Pick = { - areCommentsRequired: sourceCategory.areCommentsRequired, - areAttendeesRequired: sourceCategory.areAttendeesRequired, - maxAmountNoReceipt: sourceCategory.maxAmountNoReceipt, - maxAmountNoItemizedReceipt: sourceCategory.maxAmountNoItemizedReceipt, - maxExpenseAmount: sourceCategory.maxExpenseAmount, - expenseLimitType: sourceCategory.expenseLimitType, - }; - - patch[categoryName] = { - ...targetCategory, - ...categoryRuleFields, - }; - hasPatch = true; - } - - return hasPatch ? patch : undefined; -} - /** * Returns the partial Policy patch derived from the selected `parts`, excluding fields whose * mapping is handled separately (customUnits, timeTracking, receiptPartners, categories, tags collection keys). @@ -322,7 +283,6 @@ function buildCopyPolicySettingsData( const isCategoriesSelected = parts.includes('categories'); const isTagsSelected = parts.includes('tags'); - const isRulesSelected = parts.includes('rules'); const isDistanceSelected = parts.includes('distanceRates'); const isPerDiemSelected = parts.includes('perDiem'); const isTimeTrackingSelected = parts.includes('timeTracking'); @@ -451,27 +411,6 @@ function buildCopyPolicySettingsData( value: previousTags, }); } - - if (isRulesSelected && !isCategoriesSelected) { - const targetCategoriesKey = `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${targetPolicy.id}` as const; - const targetCategories = allPolicyCategories?.[targetCategoriesKey] ?? {}; - const categoryRulesPatch = buildCategoryRulesPatch(sourceCategories, targetCategories); - - if (categoryRulesPatch) { - const previousCategoryRules = Object.fromEntries(Object.keys(categoryRulesPatch).map((categoryName) => [categoryName, targetCategories[categoryName]])); - - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: targetCategoriesKey, - value: categoryRulesPatch, - }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: targetCategoriesKey, - value: previousCategoryRules, - }); - } - } } // Surface an RBR on the source policy row so the admin knows the bulk copy failed diff --git a/src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx b/src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx index ef563da4718d..415453ca97c1 100644 --- a/src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx +++ b/src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx @@ -29,14 +29,12 @@ import { needsCurrencyForWorkflows, shouldShowCopyPolicySettingsUpgradeStep, } from '@libs/CopyPolicySettingsUtils'; -import {countCategoriesWithFlagForReviewRules} from '@libs/FlagForReviewRulesUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {PolicyCopySettingsNavigatorParamList} from '@libs/Navigation/types'; import {createFilteredMemberCountSelector, createInvoiceConfigurationTextSelector, getDistanceRateCustomUnit, getPerDiemCustomUnit, isCollectPolicy} from '@libs/PolicyUtils'; import {formatAddressToString} from '@libs/ReportActionsUtils'; import {getReportFieldsByPolicyID} from '@libs/ReportUtils'; -import {countCategoriesWithRequireFieldsRules} from '@libs/RequireFieldsRulesUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import {getAllValidConnectedIntegration, getWorkflowRules, getWorkspaceRules} from '@pages/workspace/duplicate/utils'; @@ -114,8 +112,7 @@ function CopyPolicySettingsSelectFeaturesPage() { const perDiemCount = Object.values(perDiemRates).filter((rate) => rate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length; const formattedAddress = !isEmptyObject(sourcePolicy) && !isEmptyObject(sourcePolicy.address) ? formatAddressToString(sourcePolicy.address) : ''; const workflows = getWorkflowRules(sourcePolicy, translate); - const rules = getWorkspaceRules(sourcePolicy, translate, policyCategories); - const categoryRulesCount = countCategoriesWithRequireFieldsRules(policyCategories) + countCategoriesWithFlagForReviewRules(policyCategories); + const rules = getWorkspaceRules(sourcePolicy, translate); const shouldShowCurrency = hasCurrencyConflictWithAnyTarget(sourcePolicy, targetPolicies); const currencyBlockedByBA = isCurrencyBlockedByTargetBA(sourcePolicy, targetPolicies); const currencyNeededForWorkflows = needsCurrencyForWorkflows(sourcePolicy, targetPolicies); @@ -135,7 +132,6 @@ function CopyPolicySettingsSelectFeaturesPage() { connectedIntegrationCount: connectedIntegration?.length ?? 0, hasWorkflowRules: !!workflows?.length, hasWorkspaceRules: !!rules?.length, - hasCategoryRules: categoryRulesCount > 0, codingRulesCount, hasInvoiceConfiguration: !!sourcePolicy?.areInvoicesEnabled && !!invoiceConfigurationText, isCollectPolicy: isCollectPolicy(sourcePolicy), diff --git a/src/pages/workspace/duplicate/utils.ts b/src/pages/workspace/duplicate/utils.ts index 6508f1d5686f..c373d790cf61 100644 --- a/src/pages/workspace/duplicate/utils.ts +++ b/src/pages/workspace/duplicate/utils.ts @@ -1,18 +1,16 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; -import {countCategoriesWithFlagForReviewRules} from '@libs/FlagForReviewRulesUtils'; import {getCorrectedAutoReportingFrequency, getWorkflowApprovalsUnavailable} from '@libs/PolicyUtils'; -import {countCategoriesWithRequireFieldsRules} from '@libs/RequireFieldsRulesUtils'; import {getAutoReportingFrequencyDisplayNames} from '@pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage'; import {isAuthenticationError, isConnectionUnverified} from '@userActions/connections'; import CONST from '@src/CONST'; -import type {Policy, PolicyCategories} from '@src/types/onyx'; +import type {Policy} from '@src/types/onyx'; import type {ConnectionName} from '@src/types/onyx/Policy'; -function getWorkspaceRules(policy: Policy | undefined, translate: LocaleContextProps['translate'], policyCategories?: PolicyCategories) { +function getWorkspaceRules(policy: Policy | undefined, translate: LocaleContextProps['translate']) { const workflowApprovalsUnavailable = getWorkflowApprovalsUnavailable(policy); const autoPayApprovedReportsUnavailable = !policy?.areWorkflowsEnabled || policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES || !policy?.achAccount?.bankAccountID; @@ -63,14 +61,6 @@ function getWorkspaceRules(policy: Policy | undefined, translate: LocaleContextP total.push(translate('workspace.rules.individualExpenseRules.cashExpenseDefault')); } - if (countCategoriesWithRequireFieldsRules(policyCategories) > 0) { - total.push(translate('workspace.rules.tabs.requireFields')); - } - - if (countCategoriesWithFlagForReviewRules(policyCategories) > 0) { - total.push(translate('workspace.rules.tabs.flagForReview')); - } - return total.length > 0 ? total : null; } diff --git a/tests/actions/CopyPolicySettingsTest.ts b/tests/actions/CopyPolicySettingsTest.ts index 8677e3c64cac..3166f4188d53 100644 --- a/tests/actions/CopyPolicySettingsTest.ts +++ b/tests/actions/CopyPolicySettingsTest.ts @@ -488,40 +488,6 @@ describe('actions/Policy/CopyPolicySettings', () => { expect(optimisticData.find((u) => u.key === catKeyA && u.onyxMethod === Onyx.METHOD.SET)?.value).toEqual(sourceCategories); expect(optimisticData.find((u) => u.key === catKeyB && u.onyxMethod === Onyx.METHOD.SET)?.value).toEqual(sourceCategories); }); - - it('merges category rule fields onto matching target categories when rules selected without categories', () => { - const targetPolicy = makeTargetPolicy(); - const sourceCategories: PolicyCategories = { - Food: {name: 'Food', enabled: true, areCommentsRequired: true, areAttendeesRequired: false}, - }; - const targetCategories: PolicyCategories = { - Food: {name: 'Food', enabled: true, areCommentsRequired: false, areAttendeesRequired: false}, - }; - - const {optimisticData} = buildCopyPolicySettingsData( - makeSourcePolicy(), - [targetPolicy], - ['rules'], - { - [SOURCE_CATEGORIES_KEY]: sourceCategories, - [TARGET_CATEGORIES_KEY]: targetCategories, - }, - {}, - ); - - const categoryMerge = optimisticData.find((u) => u.key === TARGET_CATEGORIES_KEY && u.onyxMethod === Onyx.METHOD.MERGE); - expect(categoryMerge?.value).toEqual({ - Food: { - ...targetCategories.Food, - areCommentsRequired: true, - areAttendeesRequired: false, - maxAmountNoReceipt: undefined, - maxAmountNoItemizedReceipt: undefined, - maxExpenseAmount: undefined, - expenseLimitType: undefined, - }, - }); - }); }); describe('COPY_POLICY_SETTINGS lifecycle key', () => { diff --git a/tests/unit/CopyPolicySettingsUtilsTest.ts b/tests/unit/CopyPolicySettingsUtilsTest.ts index b19f4e8dd59b..c112f8013860 100644 --- a/tests/unit/CopyPolicySettingsUtilsTest.ts +++ b/tests/unit/CopyPolicySettingsUtilsTest.ts @@ -172,7 +172,6 @@ describe('CopyPolicySettingsUtils', () => { connectedIntegrationCount: 1, hasWorkflowRules: true, hasWorkspaceRules: true, - hasCategoryRules: false, codingRulesCount: 1, hasInvoiceConfiguration: true, isCollectPolicy: false, @@ -209,12 +208,6 @@ describe('CopyPolicySettingsUtils', () => { expect(isCopyPolicySettingsPartEnabledOnSource('codingRules', baseContext)).toBe(true); }); - it('shows rules when the source has workspace or category rules and is not collect', () => { - expect(isCopyPolicySettingsPartEnabledOnSource('rules', {...baseContext, hasWorkspaceRules: false, hasCategoryRules: false, isCollectPolicy: true})).toBe(false); - expect(isCopyPolicySettingsPartEnabledOnSource('rules', {...baseContext, hasWorkspaceRules: false, hasCategoryRules: true})).toBe(true); - expect(isCopyPolicySettingsPartEnabledOnSource('rules', baseContext)).toBe(true); - }); - it('hides travel when the source policy does not have travel enabled', () => { expect(isCopyPolicySettingsPartEnabledOnSource('travel', baseContext)).toBe(false); diff --git a/tests/unit/RequireFieldsRulesUtilsTest.ts b/tests/unit/RequireFieldsRulesUtilsTest.ts deleted file mode 100644 index 7ee177fcf82f..000000000000 --- a/tests/unit/RequireFieldsRulesUtilsTest.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {getRequireFieldsFormFromCategory, getRequireFieldsTableData} from '@libs/RequireFieldsRulesUtils'; - -import CONST from '@src/CONST'; -import INPUT_IDS from '@src/types/form/RequireFieldsRuleForm'; -import type {PolicyCategories} from '@src/types/onyx'; - -import createRandomPolicy from '../utils/collections/policies'; -import {convertToDisplayString, localeCompare, translateLocal} from '../utils/TestHelper'; - -describe('RequireFieldsRulesUtils', () => { - describe('getRequireFieldsFormFromCategory', () => { - it('treats fields pending delete as disabled', () => { - const category = { - name: 'Meals', - enabled: true, - areCommentsRequired: true, - areAttendeesRequired: true, - pendingFields: { - areCommentsRequired: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - areAttendeesRequired: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - }; - - expect(getRequireFieldsFormFromCategory(category)).toEqual({ - [INPUT_IDS.DESCRIPTION_SETTING]: CONST.FIELD_REQUIREMENTS_DIRECTION.DO_NOT_REQUIRE, - [INPUT_IDS.ATTENDEES_SETTING]: CONST.FIELD_REQUIREMENTS_DIRECTION.REQUIRE, - [INPUT_IDS.RECEIPT_SETTING]: CONST.FIELD_REQUIREMENTS_DIRECTION.DO_NOT_REQUIRE, - [INPUT_IDS.ITEMIZED_RECEIPT_SETTING]: CONST.FIELD_REQUIREMENTS_DIRECTION.DO_NOT_REQUIRE, - }); - }); - }); - - describe('getRequireFieldsTableData', () => { - const onNavigate = jest.fn(); - - it('keeps rows visible while a field is pending delete', () => { - const policyCategories: PolicyCategories = { - Meals: { - name: 'Meals', - enabled: true, - areCommentsRequired: true, - areAttendeesRequired: true, - pendingFields: { - areCommentsRequired: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - }, - }, - }; - - const tableData = getRequireFieldsTableData({ - policy: createRandomPolicy(0), - policyCategories, - translate: translateLocal, - convertToDisplayString, - localeCompare, - isOffline: false, - onNavigate, - }); - - expect(tableData).toHaveLength(1); - expect(tableData.at(0)?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); - expect(tableData.at(0)?.disabled).toBe(false); - }); - }); -}); From 9ae1fbf4fa5723c20bb3c9e53f2de48874aabf8d Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 30 Jul 2026 19:59:38 +0530 Subject: [PATCH 11/15] Show enable-categories empty state in rule category RHP Signed-off-by: krishna2323 --- .../Rule/RuleCategoriesDisabledEmptyState.tsx | 52 +++++++++++++++++++ src/components/Rule/RuleSelectionBase.tsx | 38 ++++++++++---- src/languages/en.ts | 4 ++ .../FlagForReviewRuleCategoryPageBase.tsx | 2 + .../workspace/rules/PolicyRulesPageRevamp.tsx | 36 ++++--------- .../RequireFieldsRuleCategoryPageBase.tsx | 2 + 6 files changed, 98 insertions(+), 36 deletions(-) create mode 100644 src/components/Rule/RuleCategoriesDisabledEmptyState.tsx diff --git a/src/components/Rule/RuleCategoriesDisabledEmptyState.tsx b/src/components/Rule/RuleCategoriesDisabledEmptyState.tsx new file mode 100644 index 000000000000..696c1995d838 --- /dev/null +++ b/src/components/Rule/RuleCategoriesDisabledEmptyState.tsx @@ -0,0 +1,52 @@ +import Button from '@components/ButtonComposed'; +import FixedFooter from '@components/FixedFooter'; +import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; + +import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import usePolicyData from '@hooks/usePolicyData'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import {enablePolicyCategories} from '@libs/actions/Policy/Category'; + +import CONST from '@src/CONST'; + +import React from 'react'; +import {View} from 'react-native'; + +type RuleCategoriesDisabledEmptyStateProps = { + /** ID of the policy the rule belongs to */ + policyID: string; +}; + +function RuleCategoriesDisabledEmptyState({policyID}: RuleCategoriesDisabledEmptyStateProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const illustrations = useMemoizedLazyIllustrations(['FolderOpen']); + const policyData = usePolicyData(policyID); + + return ( + + + + + + + ); +} + +export default RuleCategoriesDisabledEmptyState; diff --git a/src/components/Rule/RuleSelectionBase.tsx b/src/components/Rule/RuleSelectionBase.tsx index a2f404100732..08caf71658c1 100644 --- a/src/components/Rule/RuleSelectionBase.tsx +++ b/src/components/Rule/RuleSelectionBase.tsx @@ -55,13 +55,29 @@ type RuleSelectionBaseProps = { /** Set at parents whose Save is `pressOnEnter` so an auto-save selection can't leave the row re-focused and hijack the next Enter. */ shouldSkipFocusRestoreOnSave?: boolean; + + /** Rendered instead of the picker, e.g. when the feature the items come from is disabled */ + emptyState?: React.ReactNode; }; function resolveBackToRoute(backToRoute: RuleSelectionBackToRoute, selectedValue?: string): Route { return typeof backToRoute === 'function' ? backToRoute(selectedValue) : backToRoute; } -function RuleSelectionBase({titleKey, title, testID, selectedItem, items, onSave, onBack, backToRoute, allowNoneOption = true, hash, shouldSkipFocusRestoreOnSave}: RuleSelectionBaseProps) { +function RuleSelectionBase({ + titleKey, + title, + testID, + selectedItem, + items, + onSave, + onBack, + backToRoute, + allowNoneOption = true, + hash, + shouldSkipFocusRestoreOnSave, + emptyState, +}: RuleSelectionBaseProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -82,15 +98,17 @@ function RuleSelectionBase({titleKey, title, testID, selectedItem, items, onSave onBackButtonPress={onBack} /> - + {emptyState ?? ( + + )} diff --git a/src/languages/en.ts b/src/languages/en.ts index e197bc5246ca..498f54933182 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7944,6 +7944,10 @@ const translations = { subtitle: 'Require or waive expense fields for specific categories.', cta: 'Create field requirement rule', }, + categoriesDisabledEmptyState: { + title: 'Categories are not enabled', + subtitle: 'Enable categories to gain more control of your spend.', + }, requireFieldsRule: { title: 'Field requirements', subtitle: 'Require specific expense fields or waive requiring them.', diff --git a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx index 7b495bf9bf1e..f70fe128a3e7 100644 --- a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx +++ b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx @@ -1,3 +1,4 @@ +import RuleCategoriesDisabledEmptyState from '@components/Rule/RuleCategoriesDisabledEmptyState'; import RuleSelectionBase from '@components/Rule/RuleSelectionBase'; import useNetwork from '@hooks/useNetwork'; @@ -92,6 +93,7 @@ function FlagForReviewRuleCategoryPageBase({policyID, categoryName}: FlagForRevi backToRoute={backToRoute} allowNoneOption={false} shouldSkipFocusRestoreOnSave + emptyState={policy?.areCategoriesEnabled ? undefined : } /> ); diff --git a/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx b/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx index e558f04c644e..c6ed161f0c78 100644 --- a/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx +++ b/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx @@ -98,18 +98,6 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { const {showConfirmModal} = useConfirmModal(); - const isPolicyLoaded = !!policy?.id && !policy?.isLoading; - const areCategoriesEnabled = !!policy?.areCategoriesEnabled; - const shouldShowCategoryRulesTabs = !isPolicyLoaded || areCategoriesEnabled; - - useEffect(() => { - if (!isPolicyLoaded || areCategoriesEnabled || (activeTab !== RULES_TAB.REQUIRE_FIELDS && activeTab !== RULES_TAB.FLAG_FOR_REVIEW)) { - return; - } - - Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, RULES_TAB.GENERAL); - }, [activeTab, areCategoriesEnabled, isPolicyLoaded]); - useEffect(() => { // Fetch once on mount (and when policyID changes). setPolicyCodingRule already updates Onyx — refetching after saves can overwrite a newly added rule with stale data. openPolicyRulesPage(policyID); @@ -221,20 +209,16 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { title: translate('workspace.rules.tabs.expenseDefaults'), icon: icons.DocumentMagicWand, }, - ...(shouldShowCategoryRulesTabs - ? [ - { - key: RULES_TAB.REQUIRE_FIELDS, - title: translate('workspace.rules.tabs.requireFields'), - icon: icons.Task, - }, - { - key: RULES_TAB.FLAG_FOR_REVIEW, - title: translate('workspace.rules.tabs.flagForReview'), - icon: icons.Flag, - }, - ] - : []), + { + key: RULES_TAB.REQUIRE_FIELDS, + title: translate('workspace.rules.tabs.requireFields'), + icon: icons.Task, + }, + { + key: RULES_TAB.FLAG_FOR_REVIEW, + title: translate('workspace.rules.tabs.flagForReview'), + icon: icons.Flag, + }, ...(isCustomAgentBetaEnabled ? [ { diff --git a/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx b/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx index 507a71aa46a2..7928b6e262ee 100644 --- a/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx +++ b/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx @@ -1,3 +1,4 @@ +import RuleCategoriesDisabledEmptyState from '@components/Rule/RuleCategoriesDisabledEmptyState'; import RuleSelectionBase from '@components/Rule/RuleSelectionBase'; import useNetwork from '@hooks/useNetwork'; @@ -135,6 +136,7 @@ function RequireFieldsRuleCategoryPageBase({policyID, categoryName}: RequireFiel onBack={() => Navigation.goBack(backToRoute())} backToRoute={backToRoute} allowNoneOption={false} + emptyState={policy?.areCategoriesEnabled ? undefined : } /> ); From aeda120b8cb6fb4351231686a9eea9fa6631838b Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 30 Jul 2026 20:31:57 +0530 Subject: [PATCH 12/15] fix: fetch categories after enabling them Signed-off-by: krishna2323 --- .../FlagForReviewRuleCategoryPageBase.tsx | 31 +++++++++++++++++-- .../RequireFieldsRuleCategoryPageBase.tsx | 31 +++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx index f70fe128a3e7..c79fe02a52b2 100644 --- a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx +++ b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx @@ -1,3 +1,4 @@ +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import RuleCategoriesDisabledEmptyState from '@components/Rule/RuleCategoriesDisabledEmptyState'; import RuleSelectionBase from '@components/Rule/RuleSelectionBase'; @@ -7,6 +8,7 @@ import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; +import {openPolicyCategoriesPage} from '@libs/actions/Policy/Category'; import {updateDraftFlagForReviewRule} from '@libs/actions/User'; import {getDecodedCategoryName} from '@libs/CategoryUtils'; import {hasExplicitFlagAmount} from '@libs/FlagForReviewRulesUtils'; @@ -19,7 +21,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/FlagForReviewRuleForm'; -import React from 'react'; +import {useFocusEffect} from '@react-navigation/native'; +import React, {useCallback} from 'react'; type FlagForReviewRuleCategoryPageBaseProps = { policyID: string; @@ -36,6 +39,8 @@ function FlagForReviewRuleCategoryPageBase({policyID, categoryName}: FlagForRevi const [form] = useOnyx(ONYXKEYS.FORMS.FLAG_FOR_REVIEW_RULE_FORM); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + const areCategoriesEnabled = !!policy?.areCategoriesEnabled; + const arePolicyCategoriesLoading = areCategoriesEnabled && policyCategories === undefined; const selectedCategoryName = form?.[INPUT_IDS.CATEGORY]; const selectedCategoryItem = selectedCategoryName ? {name: getDecodedCategoryName(selectedCategoryName), value: selectedCategoryName} : undefined; @@ -64,6 +69,21 @@ function FlagForReviewRuleCategoryPageBase({policyID, categoryName}: FlagForRevi return {name: decodedCategoryName, value: category.name}; }); + const fetchPolicyCategories = useCallback(() => { + if (!areCategoriesEnabled || policyCategories !== undefined) { + return; + } + openPolicyCategoriesPage(policyID); + }, [areCategoriesEnabled, policyCategories, policyID]); + + useNetwork({onReconnect: fetchPolicyCategories}); + + useFocusEffect( + useCallback(() => { + fetchPolicyCategories(); + }, [fetchPolicyCategories]), + ); + const backToRoute = isEditing ? ROUTES.RULES_FLAG_FOR_REVIEW_RULE_EDIT.getRoute(policyID, categoryName) : ROUTES.RULES_FLAG_FOR_REVIEW_RULE_NEW.getRoute(policyID); const onSave = (value?: string) => { @@ -75,6 +95,13 @@ function FlagForReviewRuleCategoryPageBase({policyID, categoryName}: FlagForRevi }); }; + let emptyState: React.ReactNode; + if (!areCategoriesEnabled) { + emptyState = ; + } else if (arePolicyCategoriesLoading) { + emptyState = ; + } + return ( } + emptyState={emptyState} /> ); diff --git a/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx b/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx index 7928b6e262ee..efc92e2b2452 100644 --- a/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx +++ b/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx @@ -1,3 +1,4 @@ +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import RuleCategoriesDisabledEmptyState from '@components/Rule/RuleCategoriesDisabledEmptyState'; import RuleSelectionBase from '@components/Rule/RuleSelectionBase'; @@ -7,6 +8,7 @@ import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; +import {openPolicyCategoriesPage} from '@libs/actions/Policy/Category'; import {setDraftRequireFieldsRule} from '@libs/actions/User'; import {getDecodedCategoryName} from '@libs/CategoryUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -19,7 +21,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {RequireFieldsRuleForm, RequireFieldsRuleSettingFieldKey} from '@src/types/form/RequireFieldsRuleForm'; import INPUT_IDS from '@src/types/form/RequireFieldsRuleForm'; -import React from 'react'; +import {useFocusEffect} from '@react-navigation/native'; +import React, {useCallback} from 'react'; type RequireFieldsRuleCategoryPageBaseProps = { policyID: string; @@ -43,6 +46,8 @@ function RequireFieldsRuleCategoryPageBase({policyID, categoryName}: RequireFiel const [form] = useOnyx(ONYXKEYS.FORMS.REQUIRE_FIELDS_RULE_FORM); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + const areCategoriesEnabled = !!policy?.areCategoriesEnabled; + const arePolicyCategoriesLoading = areCategoriesEnabled && policyCategories === undefined; const selectedCategoryName = form?.[INPUT_IDS.CATEGORY]; const selectedCategory = selectedCategoryName ? policyCategories?.[selectedCategoryName] : undefined; @@ -77,6 +82,21 @@ function RequireFieldsRuleCategoryPageBase({policyID, categoryName}: RequireFiel return {name: decodedCategoryName, value: category.name}; }); + const fetchPolicyCategories = useCallback(() => { + if (!areCategoriesEnabled || policyCategories !== undefined) { + return; + } + openPolicyCategoriesPage(policyID); + }, [areCategoriesEnabled, policyCategories, policyID]); + + useNetwork({onReconnect: fetchPolicyCategories}); + + useFocusEffect( + useCallback(() => { + fetchPolicyCategories(); + }, [fetchPolicyCategories]), + ); + const backToRoute = () => getRequireFieldsRuleBackToRoute({ policyID, @@ -119,6 +139,13 @@ function RequireFieldsRuleCategoryPageBase({policyID, categoryName}: RequireFiel setDraftRequireFieldsRule(preservedSettings); }; + let emptyState: React.ReactNode; + if (!areCategoriesEnabled) { + emptyState = ; + } else if (arePolicyCategoriesLoading) { + emptyState = ; + } + return ( Navigation.goBack(backToRoute())} backToRoute={backToRoute} allowNoneOption={false} - emptyState={policy?.areCategoriesEnabled ? undefined : } + emptyState={emptyState} /> ); From 796563f7a44ec3f8ceb2539eaad5ede4c4466da6 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 30 Jul 2026 20:35:27 +0530 Subject: [PATCH 13/15] fix: align category loading with RHP patterns Signed-off-by: krishna2323 --- .../FlagForReviewRuleCategoryPageBase.tsx | 45 +++++++++++-------- .../RequireFieldsRuleCategoryPageBase.tsx | 45 +++++++++++-------- 2 files changed, 52 insertions(+), 38 deletions(-) diff --git a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx index c79fe02a52b2..a6fd109f0b63 100644 --- a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx +++ b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx @@ -1,4 +1,4 @@ -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import ActivityIndicator from '@components/ActivityIndicator'; import RuleCategoriesDisabledEmptyState from '@components/Rule/RuleCategoriesDisabledEmptyState'; import RuleSelectionBase from '@components/Rule/RuleSelectionBase'; @@ -7,6 +7,7 @@ import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; +import useThemeStyles from '@hooks/useThemeStyles'; import {openPolicyCategoriesPage} from '@libs/actions/Policy/Category'; import {updateDraftFlagForReviewRule} from '@libs/actions/User'; @@ -22,7 +23,8 @@ import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/FlagForReviewRuleForm'; import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback} from 'react'; +import React from 'react'; +import {View} from 'react-native'; type FlagForReviewRuleCategoryPageBaseProps = { policyID: string; @@ -35,13 +37,26 @@ function FlagForReviewRuleCategoryPageBase({policyID, categoryName}: FlagForRevi const {canWrite: canWriteRules} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.RULES); const {isBetaEnabled} = usePermissions(); const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); - const {isOffline} = useNetwork(); + const styles = useThemeStyles(); const [form] = useOnyx(ONYXKEYS.FORMS.FLAG_FOR_REVIEW_RULE_FORM); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); const areCategoriesEnabled = !!policy?.areCategoriesEnabled; const arePolicyCategoriesLoading = areCategoriesEnabled && policyCategories === undefined; + const fetchPolicyCategories = () => { + if (!areCategoriesEnabled || policyCategories !== undefined) { + return; + } + openPolicyCategoriesPage(policyID); + }; + + const {isOffline} = useNetwork({onReconnect: fetchPolicyCategories}); + + useFocusEffect(() => { + fetchPolicyCategories(); + }); + const selectedCategoryName = form?.[INPUT_IDS.CATEGORY]; const selectedCategoryItem = selectedCategoryName ? {name: getDecodedCategoryName(selectedCategoryName), value: selectedCategoryName} : undefined; @@ -69,21 +84,6 @@ function FlagForReviewRuleCategoryPageBase({policyID, categoryName}: FlagForRevi return {name: decodedCategoryName, value: category.name}; }); - const fetchPolicyCategories = useCallback(() => { - if (!areCategoriesEnabled || policyCategories !== undefined) { - return; - } - openPolicyCategoriesPage(policyID); - }, [areCategoriesEnabled, policyCategories, policyID]); - - useNetwork({onReconnect: fetchPolicyCategories}); - - useFocusEffect( - useCallback(() => { - fetchPolicyCategories(); - }, [fetchPolicyCategories]), - ); - const backToRoute = isEditing ? ROUTES.RULES_FLAG_FOR_REVIEW_RULE_EDIT.getRoute(policyID, categoryName) : ROUTES.RULES_FLAG_FOR_REVIEW_RULE_NEW.getRoute(policyID); const onSave = (value?: string) => { @@ -99,7 +99,14 @@ function FlagForReviewRuleCategoryPageBase({policyID, categoryName}: FlagForRevi if (!areCategoriesEnabled) { emptyState = ; } else if (arePolicyCategoriesLoading) { - emptyState = ; + emptyState = ( + + + + ); } return ( diff --git a/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx b/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx index efc92e2b2452..c62b83dd5dbe 100644 --- a/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx +++ b/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx @@ -1,4 +1,4 @@ -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import ActivityIndicator from '@components/ActivityIndicator'; import RuleCategoriesDisabledEmptyState from '@components/Rule/RuleCategoriesDisabledEmptyState'; import RuleSelectionBase from '@components/Rule/RuleSelectionBase'; @@ -7,6 +7,7 @@ import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; +import useThemeStyles from '@hooks/useThemeStyles'; import {openPolicyCategoriesPage} from '@libs/actions/Policy/Category'; import {setDraftRequireFieldsRule} from '@libs/actions/User'; @@ -22,7 +23,8 @@ import type {RequireFieldsRuleForm, RequireFieldsRuleSettingFieldKey} from '@src import INPUT_IDS from '@src/types/form/RequireFieldsRuleForm'; import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback} from 'react'; +import React from 'react'; +import {View} from 'react-native'; type RequireFieldsRuleCategoryPageBaseProps = { policyID: string; @@ -42,13 +44,26 @@ function RequireFieldsRuleCategoryPageBase({policyID, categoryName}: RequireFiel const {canWrite: canWriteRules} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.RULES); const {isBetaEnabled} = usePermissions(); const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); - const {isOffline} = useNetwork(); + const styles = useThemeStyles(); const [form] = useOnyx(ONYXKEYS.FORMS.REQUIRE_FIELDS_RULE_FORM); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); const areCategoriesEnabled = !!policy?.areCategoriesEnabled; const arePolicyCategoriesLoading = areCategoriesEnabled && policyCategories === undefined; + const fetchPolicyCategories = () => { + if (!areCategoriesEnabled || policyCategories !== undefined) { + return; + } + openPolicyCategoriesPage(policyID); + }; + + const {isOffline} = useNetwork({onReconnect: fetchPolicyCategories}); + + useFocusEffect(() => { + fetchPolicyCategories(); + }); + const selectedCategoryName = form?.[INPUT_IDS.CATEGORY]; const selectedCategory = selectedCategoryName ? policyCategories?.[selectedCategoryName] : undefined; const selectedCategoryItem = selectedCategoryName @@ -82,21 +97,6 @@ function RequireFieldsRuleCategoryPageBase({policyID, categoryName}: RequireFiel return {name: decodedCategoryName, value: category.name}; }); - const fetchPolicyCategories = useCallback(() => { - if (!areCategoriesEnabled || policyCategories !== undefined) { - return; - } - openPolicyCategoriesPage(policyID); - }, [areCategoriesEnabled, policyCategories, policyID]); - - useNetwork({onReconnect: fetchPolicyCategories}); - - useFocusEffect( - useCallback(() => { - fetchPolicyCategories(); - }, [fetchPolicyCategories]), - ); - const backToRoute = () => getRequireFieldsRuleBackToRoute({ policyID, @@ -143,7 +143,14 @@ function RequireFieldsRuleCategoryPageBase({policyID, categoryName}: RequireFiel if (!areCategoriesEnabled) { emptyState = ; } else if (arePolicyCategoriesLoading) { - emptyState = ; + emptyState = ( + + + + ); } return ( From fa4b1d123fe8a723d04af305b35163bf95b3c2d0 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Fri, 31 Jul 2026 01:03:07 +0530 Subject: [PATCH 14/15] fix: compose rule category picker states Signed-off-by: krishna2323 --- .../Rule/RuleCategoriesDisabledEmptyState.tsx | 12 ++- src/components/Rule/RuleSelectionBase.tsx | 77 +++++++++---------- .../settings/Rules/Fields/AddCategoryPage.tsx | 13 ++-- .../settings/Rules/Fields/AddTagPage.tsx | 15 ++-- .../settings/Rules/Fields/AddTaxRatePage.tsx | 13 ++-- .../FlagForReviewRuleCategoryPageBase.tsx | 28 ++++--- .../rules/MerchantRules/AddCategoryPage.tsx | 15 ++-- .../rules/MerchantRules/AddTagPage.tsx | 15 ++-- .../rules/MerchantRules/AddTaxPage.tsx | 15 ++-- .../MerchantTypeRuleCategoryPage.tsx | 13 ++-- .../RequireFieldsRuleCategoryPageBase.tsx | 26 ++++--- 11 files changed, 136 insertions(+), 106 deletions(-) diff --git a/src/components/Rule/RuleCategoriesDisabledEmptyState.tsx b/src/components/Rule/RuleCategoriesDisabledEmptyState.tsx index 696c1995d838..b04946666cff 100644 --- a/src/components/Rule/RuleCategoriesDisabledEmptyState.tsx +++ b/src/components/Rule/RuleCategoriesDisabledEmptyState.tsx @@ -7,7 +7,7 @@ import useLocalize from '@hooks/useLocalize'; import usePolicyData from '@hooks/usePolicyData'; import useThemeStyles from '@hooks/useThemeStyles'; -import {enablePolicyCategories} from '@libs/actions/Policy/Category'; +import {enablePolicyCategories, openPolicyCategoriesPage} from '@libs/actions/Policy/Category'; import CONST from '@src/CONST'; @@ -25,6 +25,14 @@ function RuleCategoriesDisabledEmptyState({policyID}: RuleCategoriesDisabledEmpt const illustrations = useMemoizedLazyIllustrations(['FolderOpen']); const policyData = usePolicyData(policyID); + const enableCategories = () => { + enablePolicyCategories(policyData, true, false); + + // The categories collection is empty while the feature is disabled, and enabling it only merges the + // categories we already know about, so the collection has to be fetched for the picker to have rows. + openPolicyCategoriesPage(policyID); + }; + return ( enablePolicyCategories(policyData, true, false)} + onPress={enableCategories} > {translate('workspace.categories.enableCategories')} diff --git a/src/components/Rule/RuleSelectionBase.tsx b/src/components/Rule/RuleSelectionBase.tsx index 08caf71658c1..4b1d046ca01a 100644 --- a/src/components/Rule/RuleSelectionBase.tsx +++ b/src/components/Rule/RuleSelectionBase.tsx @@ -32,6 +32,17 @@ type RuleSelectionBaseProps = { /** Test ID for the screen wrapper */ testID: string; + /** Callback to go back */ + onBack: () => void; + + /** Optional hash for rule not found validation */ + hash?: string; + + /** Page content */ + children: React.ReactNode; +}; + +type RuleSelectionPickerProps = { /** The currently selected item */ selectedItem?: SelectionItem; @@ -41,51 +52,24 @@ type RuleSelectionBaseProps = { /** Callback when a value is selected */ onSave: (value?: string) => void; - /** Callback to go back */ - onBack: () => void; - /** The route to navigate back to */ backToRoute: RuleSelectionBackToRoute; /** When true, shows a "None" option in the picker */ allowNoneOption?: boolean; - /** Optional hash for rule not found validation */ - hash?: string; - /** Set at parents whose Save is `pressOnEnter` so an auto-save selection can't leave the row re-focused and hijack the next Enter. */ shouldSkipFocusRestoreOnSave?: boolean; - - /** Rendered instead of the picker, e.g. when the feature the items come from is disabled */ - emptyState?: React.ReactNode; }; function resolveBackToRoute(backToRoute: RuleSelectionBackToRoute, selectedValue?: string): Route { return typeof backToRoute === 'function' ? backToRoute(selectedValue) : backToRoute; } -function RuleSelectionBase({ - titleKey, - title, - testID, - selectedItem, - items, - onSave, - onBack, - backToRoute, - allowNoneOption = true, - hash, - shouldSkipFocusRestoreOnSave, - emptyState, -}: RuleSelectionBaseProps) { +function RuleSelectionBaseComponent({titleKey, title, testID, onBack, hash, children}: RuleSelectionBaseProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const handleSaveSelection = (value?: string) => { - onSave(value); - Navigation.goBack(resolveBackToRoute(backToRoute, value), {shouldSkipFocusRestore: shouldSkipFocusRestoreOnSave}); - }; - return ( - - {emptyState ?? ( - - )} - + {children} ); } +function RuleSelectionPicker({selectedItem, items, onSave, backToRoute, allowNoneOption = true, shouldSkipFocusRestoreOnSave}: RuleSelectionPickerProps) { + const handleSaveSelection = (value?: string) => { + onSave(value); + Navigation.goBack(resolveBackToRoute(backToRoute, value), {shouldSkipFocusRestore: shouldSkipFocusRestoreOnSave}); + }; + + return ( + + ); +} + +const RuleSelectionBase = Object.assign(RuleSelectionBaseComponent, { + Picker: RuleSelectionPicker, +}); + export default RuleSelectionBase; diff --git a/src/pages/settings/Rules/Fields/AddCategoryPage.tsx b/src/pages/settings/Rules/Fields/AddCategoryPage.tsx index a2c590609091..28de3313822f 100644 --- a/src/pages/settings/Rules/Fields/AddCategoryPage.tsx +++ b/src/pages/settings/Rules/Fields/AddCategoryPage.tsx @@ -64,13 +64,16 @@ function AddCategoryPage({route}: AddCategoryPageProps) { Navigation.goBack(backToRoute)} - backToRoute={backToRoute} hash={hash} - /> + > + + ); } diff --git a/src/pages/settings/Rules/Fields/AddTagPage.tsx b/src/pages/settings/Rules/Fields/AddTagPage.tsx index 14adc61ac9d3..9bc3b4205ce2 100644 --- a/src/pages/settings/Rules/Fields/AddTagPage.tsx +++ b/src/pages/settings/Rules/Fields/AddTagPage.tsx @@ -65,13 +65,16 @@ function AddTagPage({route}: AddTagPageProps) { titleKey="common.tag" title={tagList?.name} testID="AddTagPage" - selectedItem={selectedTagItem} - items={tagItems} - onSave={onSave} - onBack={() => Navigation.goBack(backToRoute)} - backToRoute={backToRoute} hash={hash} - /> + onBack={() => Navigation.goBack(backToRoute)} + > + + ); } diff --git a/src/pages/settings/Rules/Fields/AddTaxRatePage.tsx b/src/pages/settings/Rules/Fields/AddTaxRatePage.tsx index 231520c23985..ac1b2dac68cd 100644 --- a/src/pages/settings/Rules/Fields/AddTaxRatePage.tsx +++ b/src/pages/settings/Rules/Fields/AddTaxRatePage.tsx @@ -37,13 +37,16 @@ function AddTaxRatePage({route}: AddTaxRatePageProps) { Navigation.goBack(backToRoute)} - backToRoute={backToRoute} hash={hash} - /> + > + + ); } diff --git a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx index a6fd109f0b63..1aa6df56ba4d 100644 --- a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx +++ b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx @@ -95,11 +95,11 @@ function FlagForReviewRuleCategoryPageBase({policyID, categoryName}: FlagForRevi }); }; - let emptyState: React.ReactNode; + let content: React.ReactNode; if (!areCategoriesEnabled) { - emptyState = ; + content = ; } else if (arePolicyCategoriesLoading) { - emptyState = ( + content = ( ); + } else { + content = ( + + ); } return ( @@ -120,15 +131,10 @@ function FlagForReviewRuleCategoryPageBase({policyID, categoryName}: FlagForRevi Navigation.goBack(backToRoute)} - backToRoute={backToRoute} - allowNoneOption={false} - shouldSkipFocusRestoreOnSave - emptyState={emptyState} - /> + > + {content} + ); } diff --git a/src/pages/workspace/rules/MerchantRules/AddCategoryPage.tsx b/src/pages/workspace/rules/MerchantRules/AddCategoryPage.tsx index f1b1039de62c..b2e3212fb85c 100644 --- a/src/pages/workspace/rules/MerchantRules/AddCategoryPage.tsx +++ b/src/pages/workspace/rules/MerchantRules/AddCategoryPage.tsx @@ -45,15 +45,16 @@ function AddCategoryPage({route}: AddCategoryPageProps) { Navigation.goBack(backToRoute)} - backToRoute={backToRoute} - /> + > + + ); } -AddCategoryPage.displayName = 'AddCategoryPage'; - export default AddCategoryPage; diff --git a/src/pages/workspace/rules/MerchantRules/AddTagPage.tsx b/src/pages/workspace/rules/MerchantRules/AddTagPage.tsx index 2c96a595c937..4df89f40c1ca 100644 --- a/src/pages/workspace/rules/MerchantRules/AddTagPage.tsx +++ b/src/pages/workspace/rules/MerchantRules/AddTagPage.tsx @@ -65,15 +65,16 @@ function AddTagPage({route}: AddTagPageProps) { titleKey="common.tag" title={tagList?.name} testID="AddTagPage" - selectedItem={selectedTagItem} - items={tagItems} - onSave={onSave} onBack={() => Navigation.goBack(backToRoute)} - backToRoute={backToRoute} - /> + > + + ); } -AddTagPage.displayName = 'AddTagPage'; - export default AddTagPage; diff --git a/src/pages/workspace/rules/MerchantRules/AddTaxPage.tsx b/src/pages/workspace/rules/MerchantRules/AddTaxPage.tsx index 21c517bfc22e..d91cc7f13d90 100644 --- a/src/pages/workspace/rules/MerchantRules/AddTaxPage.tsx +++ b/src/pages/workspace/rules/MerchantRules/AddTaxPage.tsx @@ -43,15 +43,16 @@ function AddTaxPage({route}: AddTaxPageProps) { Navigation.goBack(backToRoute)} - backToRoute={backToRoute} - /> + > + + ); } -AddTaxPage.displayName = 'AddTaxPage'; - export default AddTaxPage; diff --git a/src/pages/workspace/rules/MerchantTypeRules/MerchantTypeRuleCategoryPage.tsx b/src/pages/workspace/rules/MerchantTypeRules/MerchantTypeRuleCategoryPage.tsx index 4a849ee6e3a6..b8d0f20cb3e0 100644 --- a/src/pages/workspace/rules/MerchantTypeRules/MerchantTypeRuleCategoryPage.tsx +++ b/src/pages/workspace/rules/MerchantTypeRules/MerchantTypeRuleCategoryPage.tsx @@ -60,12 +60,15 @@ function MerchantTypeRuleCategoryPage({route}: MerchantTypeRuleCategoryPageProps Navigation.goBack(backToRoute)} - backToRoute={backToRoute} - /> + > + + ); } diff --git a/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx b/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx index c62b83dd5dbe..a9d6b1d5e30d 100644 --- a/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx +++ b/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx @@ -139,11 +139,11 @@ function RequireFieldsRuleCategoryPageBase({policyID, categoryName}: RequireFiel setDraftRequireFieldsRule(preservedSettings); }; - let emptyState: React.ReactNode; + let content: React.ReactNode; if (!areCategoriesEnabled) { - emptyState = ; + content = ; } else if (arePolicyCategoriesLoading) { - emptyState = ( + content = ( ); + } else { + content = ( + + ); } return ( @@ -164,14 +174,10 @@ function RequireFieldsRuleCategoryPageBase({policyID, categoryName}: RequireFiel Navigation.goBack(backToRoute())} - backToRoute={backToRoute} - allowNoneOption={false} - emptyState={emptyState} - /> + > + {content} + ); } From a646496dac3536a222f15297d72f49d6aebe3e61 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Fri, 31 Jul 2026 01:30:34 +0530 Subject: [PATCH 15/15] update translations. Signed-off-by: krishna2323 --- src/languages/de.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 9 files changed, 9 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 5bbc4cc7f3d6..1e9f302ade4b 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -8027,6 +8027,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc subtitle: 'Erstellen Sie eine Regel, um Ihre Arbeitsbereichsrichtlinien zu automatisieren.', cta: 'KI-Regel hinzufügen', }, + categoriesDisabledEmptyState: {title: 'Kategorien sind nicht aktiviert', subtitle: 'Aktivieren Sie Kategorien, um Ihre Ausgaben besser zu kontrollieren.'}, }, planTypePage: { planTypes: { diff --git a/src/languages/es.ts b/src/languages/es.ts index d67808a475b5..6a713cc987ec 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7934,6 +7934,7 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, subtitle: 'Crea una regla para automatizar las políticas de tu espacio de trabajo.', cta: 'Añadir regla de IA', }, + categoriesDisabledEmptyState: {title: 'Las categorías no están habilitadas', subtitle: 'Habilita las categorías para tener más control sobre tus gastos.'}, }, emptyDomain: { title: 'Mejora tu seguridad con dominios', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index c8fe77799a24..652b19b58da6 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -8055,6 +8055,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e subtitle: 'Créez une règle pour automatiser les politiques de votre espace de travail.', cta: 'Ajouter une règle IA', }, + categoriesDisabledEmptyState: {title: 'Les catégories ne sont pas activées', subtitle: 'Activez les catégories pour mieux contrôler vos dépenses.'}, }, planTypePage: { planTypes: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 567be34e7770..b97b5a7d4735 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7996,6 +7996,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, thenFlagForReview: 'Poi segnala per revisione quando:', }, agentRulesEmptyState: {title: 'Nessuna regola agente aggiunta', subtitle: 'Crea una regola per automatizzare le policy del tuo workspace.', cta: 'Aggiungi regola IA'}, + categoriesDisabledEmptyState: {title: 'Le categorie non sono abilitate', subtitle: 'Attiva le categorie per avere un maggiore controllo sulle tue spese.'}, }, planTypePage: { planTypes: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index fca164907c3e..fb4fe7983c30 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7900,6 +7900,7 @@ ${reportName}`, thenFlagForReview: '次の条件で確認フラグを付けます:', }, agentRulesEmptyState: {title: 'エージェントルールが追加されていません', subtitle: 'ワークスペースのポリシーを自動化するルールを作成します。', cta: 'AIルールを追加'}, + categoriesDisabledEmptyState: {title: 'カテゴリが有効になっていません', subtitle: 'カテゴリを有効にして、支出をより細かく管理しましょう。'}, }, planTypePage: { planTypes: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index de5d34fb3f1e..f1419943363b 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7977,6 +7977,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, thenFlagForReview: 'Vlag dan voor controle wanneer:', }, agentRulesEmptyState: {title: 'Geen agentregels toegevoegd', subtitle: 'Maak een regel om je werkruimtebeleid te automatiseren.', cta: 'AI-regel toevoegen'}, + categoriesDisabledEmptyState: {title: 'Categorieën zijn niet ingeschakeld', subtitle: 'Schakel categorieën in om meer controle te krijgen over je uitgaven.'}, }, planTypePage: { planTypes: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 5e4e6d742be9..07565ac2f95f 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7957,6 +7957,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, thenFlagForReview: 'Następnie oznacz do przejrzenia, gdy:', }, agentRulesEmptyState: {title: 'Nie dodano reguł agenta', subtitle: 'Utwórz regułę, żeby zautomatyzować zasady swojego workspace’u.', cta: 'Dodaj regułę AI'}, + categoriesDisabledEmptyState: {title: 'Kategorie nie są włączone', subtitle: 'Włącz kategorie, żeby mieć większą kontrolę nad wydatkami.'}, }, planTypePage: { planTypes: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index fda3bc4b7772..7ec204aca01a 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7974,6 +7974,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, thenFlagForReview: 'Então sinalizar para revisão quando:', }, agentRulesEmptyState: {title: 'Nenhuma regra de agente adicionada', subtitle: 'Crie uma regra para automatizar as políticas do seu workspace.', cta: 'Adicionar regra de IA'}, + categoriesDisabledEmptyState: {title: 'Categorias não estão ativadas', subtitle: 'Ative categorias para ter mais controle sobre seus gastos.'}, }, planTypePage: { planTypes: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index c49c6cbc9b60..06cc0faa6937 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7710,6 +7710,7 @@ ${reportName}`, thenFlagForReview: '然后在以下情况下标记为待审核:', }, agentRulesEmptyState: {title: '未添加代理规则', subtitle: '创建规则以自动化您的工作区策略。', cta: '添加 AI 规则'}, + categoriesDisabledEmptyState: {title: '类别未启用', subtitle: '启用类别以更好地控制您的支出。'}, }, planTypePage: { planTypes: {