diff --git a/src/components/Rule/RuleCategoriesDisabledEmptyState.tsx b/src/components/Rule/RuleCategoriesDisabledEmptyState.tsx new file mode 100644 index 000000000000..b04946666cff --- /dev/null +++ b/src/components/Rule/RuleCategoriesDisabledEmptyState.tsx @@ -0,0 +1,60 @@ +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, openPolicyCategoriesPage} 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); + + 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 ( + + + + + + + ); +} + +export default RuleCategoriesDisabledEmptyState; diff --git a/src/components/Rule/RuleSelectionBase.tsx b/src/components/Rule/RuleSelectionBase.tsx index a2f404100732..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,18 +52,12 @@ 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; }; @@ -61,15 +66,10 @@ function resolveBackToRoute(backToRoute: RuleSelectionBackToRoute, selectedValue return typeof backToRoute === 'function' ? backToRoute(selectedValue) : backToRoute; } -function RuleSelectionBase({titleKey, title, testID, selectedItem, items, onSave, onBack, backToRoute, allowNoneOption = true, hash, shouldSkipFocusRestoreOnSave}: 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 ( - - - + {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/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/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/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: { 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 ecf97735a67c..1aa6df56ba4d 100644 --- a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx +++ b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleCategoryPageBase.tsx @@ -1,3 +1,5 @@ +import ActivityIndicator from '@components/ActivityIndicator'; +import RuleCategoriesDisabledEmptyState from '@components/Rule/RuleCategoriesDisabledEmptyState'; import RuleSelectionBase from '@components/Rule/RuleSelectionBase'; import useNetwork from '@hooks/useNetwork'; @@ -5,7 +7,9 @@ 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'; import {getDecodedCategoryName} from '@libs/CategoryUtils'; import {hasExplicitFlagAmount} from '@libs/FlagForReviewRulesUtils'; @@ -18,7 +22,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/FlagForReviewRuleForm'; +import {useFocusEffect} from '@react-navigation/native'; import React from 'react'; +import {View} from 'react-native'; type FlagForReviewRuleCategoryPageBaseProps = { policyID: string; @@ -31,10 +37,25 @@ 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; @@ -74,6 +95,31 @@ function FlagForReviewRuleCategoryPageBase({policyID, categoryName}: FlagForRevi }); }; + let content: React.ReactNode; + if (!areCategoriesEnabled) { + content = ; + } else if (arePolicyCategoriesLoading) { + content = ( + + + + ); + } else { + content = ( + + ); + } + return ( Navigation.goBack(backToRoute)} - backToRoute={backToRoute} - shouldSkipFocusRestoreOnSave - /> + > + {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 507a71aa46a2..a9d6b1d5e30d 100644 --- a/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx +++ b/src/pages/workspace/rules/RequireFieldsRules/RequireFieldsRuleCategoryPageBase.tsx @@ -1,3 +1,5 @@ +import ActivityIndicator from '@components/ActivityIndicator'; +import RuleCategoriesDisabledEmptyState from '@components/Rule/RuleCategoriesDisabledEmptyState'; import RuleSelectionBase from '@components/Rule/RuleSelectionBase'; import useNetwork from '@hooks/useNetwork'; @@ -5,7 +7,9 @@ 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'; import {getDecodedCategoryName} from '@libs/CategoryUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -18,7 +22,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {RequireFieldsRuleForm, RequireFieldsRuleSettingFieldKey} from '@src/types/form/RequireFieldsRuleForm'; import INPUT_IDS from '@src/types/form/RequireFieldsRuleForm'; +import {useFocusEffect} from '@react-navigation/native'; import React from 'react'; +import {View} from 'react-native'; type RequireFieldsRuleCategoryPageBaseProps = { policyID: string; @@ -38,10 +44,25 @@ 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; @@ -118,6 +139,30 @@ function RequireFieldsRuleCategoryPageBase({policyID, categoryName}: RequireFiel setDraftRequireFieldsRule(preservedSettings); }; + let content: React.ReactNode; + if (!areCategoriesEnabled) { + content = ; + } else if (arePolicyCategoriesLoading) { + content = ( + + + + ); + } else { + content = ( + + ); + } + return ( Navigation.goBack(backToRoute())} - backToRoute={backToRoute} - allowNoneOption={false} - /> + > + {content} + ); }