diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 005b9b46615d..6f00e1622654 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1009,7 +1009,6 @@ const CONST = { RILLET: 'rillet', DUALENTRY: 'dualEntry', INTUIT_ENTERPRISE_SUITE: 'intuitEnterpriseSuite', - RULES_REVAMP: 'rulesRevamp', COMMUTER_EXCLUSIONS: 'commuterExclusions', GLOBAL_REIMBURSEMENTS: 'globalReimbursements', GLOBAL_REIMBURSEMENT_FX: 'globalReimbursementFX', diff --git a/src/components/Tables/WorkspaceTagsTable/WorkspaceTagsTableRow.tsx b/src/components/Tables/WorkspaceTagsTable/WorkspaceTagsTableRow.tsx index 768e8081bac7..40adcdc07db1 100644 --- a/src/components/Tables/WorkspaceTagsTable/WorkspaceTagsTableRow.tsx +++ b/src/components/Tables/WorkspaceTagsTable/WorkspaceTagsTableRow.tsx @@ -66,16 +66,15 @@ export default function WorkspaceTagsTableRow({ item.name, tagCountSubtitle, enabledStatusLabel, - item.showRequiredSwitch && item.required ? translate('common.required') : null, shouldShowGLCodeColumn && item.glCode ? `${translate('workspace.tags.glCode')}: ${item.glCode}` : null, shouldShowApproverColumn && item.approverDisplayName ? `${translate('common.approver')}: ${item.approverDisplayName}` : null, ] .filter(Boolean) .join(', '); - const switchValue = item.showRequiredSwitch ? !!item.required : item.enabled; - const switchAccessibilityLabel = item.showRequiredSwitch ? translate('workspace.tags.requiresTag') : translate('workspace.tags.enableTag'); - const handleSwitchToggle = item.showRequiredSwitch ? item.onToggleRequired : item.onToggleEnabled; + const switchValue = item.enabled; + const switchAccessibilityLabel = translate('workspace.tags.enableTag'); + const handleSwitchToggle = item.onToggleEnabled; const isSwitchControlDisabled = !!item.disabled || !!item.isSwitchDisabled; return ( @@ -161,7 +160,7 @@ export default function WorkspaceTagsTableRow({ )} - {(item.showEnabledSwitch || item.showRequiredSwitch) && !!handleSwitchToggle && ( + {item.showEnabledSwitch && !!handleSwitchToggle && ( void; onToggleEnabled?: (enabled: boolean) => void; - onToggleRequired?: (required: boolean) => void; onClose: () => void; }; @@ -73,13 +69,10 @@ export default function WorkspaceTagsTable({ const styles = useThemeStyles(); const {translate, localeCompare} = useLocalize(); const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); - const {isBetaEnabled} = usePermissions(); const shouldUseNarrowTableLayout = shouldUseNarrowLayout || isMediumScreenWidth; const shouldShowTagCountColumn = isMultiLevelTags && !shouldUseNarrowTableLayout; const shouldShowEnabledColumn = !isMultiLevelTags; - // Required is configured from Rules once the revamp is on, so the rows have no switch to head. - const shouldShowRequiredColumn = isMultiLevelTags && !hasDependentTags && !isBetaEnabled(CONST.BETAS.RULES_REVAMP); const tagTableColumns: Array> = [ { @@ -128,19 +121,6 @@ export default function WorkspaceTagsTable({ }, ] : []), - ...(shouldShowRequiredColumn - ? [ - { - key: 'required' as const, - label: translate('common.required'), - sortable: true, - width: variables.tableSwitchColumnWidth, - styling: { - containerStyles: [styles.justifyContentEnd], - }, - }, - ] - : []), { key: 'actions', label: '', diff --git a/src/hooks/useControlOnlyRuleUpgradeRedirect.ts b/src/hooks/useControlOnlyRuleUpgradeRedirect.ts index 4ac72a16d369..bbb6bdf2bedb 100644 --- a/src/hooks/useControlOnlyRuleUpgradeRedirect.ts +++ b/src/hooks/useControlOnlyRuleUpgradeRedirect.ts @@ -8,7 +8,6 @@ import ROUTES from '@src/ROUTES'; import {useEffect, useRef} from 'react'; import useOnyx from './useOnyx'; -import usePermissions from './usePermissions'; import usePolicy from './usePolicy'; /** @@ -24,13 +23,12 @@ import usePolicy from './usePolicy'; */ function useControlOnlyRuleUpgradeRedirect(policyID: string, backTo?: Route) { const policy = usePolicy(policyID); - const {isBetaEnabled} = usePermissions(); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); const isCollect = isCollectPolicy(policy); // Mirrors the feature check in AccessOrNotFoundWrapper. When Rules itself is disabled, that wrapper already // redirects to More features, so redirecting to the upgrade page too would flash it on the way there. - const isRulesFeatureEnabled = arePolicyRulesEnabled(policy, policyCategories, isBetaEnabled(CONST.BETAS.RULES_REVAMP)); + const isRulesFeatureEnabled = arePolicyRulesEnabled(policy, policyCategories); const hasRedirectedToUpgrade = useRef(false); const upgradeBackTo = backTo ?? ROUTES.WORKSPACE_RULES.getRoute(policyID); diff --git a/src/languages/de.ts b/src/languages/de.ts index 3018a4412b7d..385c3f5a6343 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7701,7 +7701,6 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und alwaysReimbursableDescription: 'Spesen werden immer an Mitarbeitende zurückgezahlt', alwaysNonReimbursable: 'Immer nicht erstattungsfähig', alwaysNonReimbursableDescription: 'Auslagen werden Mitarbeitenden nie erstattet', - billableDefault: 'Standardmäßig verrechenbar', billableDefaultDescription: 'Wähle, ob Bar- und Kreditkartenausgaben standardmäßig verrechenbar sein sollen.', billable: 'Abrechenbar', billableDescription: 'Spesen werden meist an Kund:innen weiterberechnet', @@ -7728,10 +7727,12 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und publicReceiptVisibilityHintDisabled: 'Belege können nur von Expensify-Mitgliedern eingesehen werden, die Zugriff auf den Bericht haben, der den Beleg enthält.', enableTagsToUnlockTitle: 'Tags aktivieren?', enableTagsToUnlockPrompt: 'Aktivieren Sie Tags (unter Weitere Funktionen), um dies freizuschalten.', - enableTagsAndRequirePrompt: 'Sind Sie sicher, dass Sie Tags aktivieren und für alle Ausgaben verpflichtend machen möchten?', enableCategoriesToUnlockTitle: 'Kategorien aktivieren?', enableCategoriesToUnlockPrompt: 'Aktivieren Sie Kategorien (unter Weitere Funktionen), um dies freizuschalten.', enableCategoriesAndRequirePrompt: 'Sind Sie sicher, dass Sie Kategorien aktivieren und für alle Ausgaben verpflichtend machen möchten?', + enableTagsPrompt: 'Möchten Sie Tags wirklich aktivieren? Sie können sie für alle Ausgaben erforderlich machen, sobald Sie mindestens einen Tag haben.', + noTagsToRequirePrompt: 'Sie haben keine Tags. Bitte erstellen Sie einen Tag.', + noCategoriesToRequirePrompt: 'Sie haben keine Kategorien. Bitte erstellen Sie eine Kategorie.', }, expenseReportRules: { title: 'Erweitert', diff --git a/src/languages/el.ts b/src/languages/el.ts index 5e18b02fed05..7ba73b40fb7f 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -8025,7 +8025,6 @@ ${reportName}`, alwaysReimbursableDescription: 'Τα έξοδα αποζημιώνονται πάντα στους υπαλλήλους', alwaysNonReimbursable: 'Πάντα μη αποζημιώσιμες', alwaysNonReimbursableDescription: 'Τα έξοδα δεν αποζημιώνονται ποτέ στους υπαλλήλους', - billableDefault: 'Προεπιλογή χρεώσιμων', billableDefaultDescription: 'Επιλέξτε αν οι δαπάνες με μετρητά και με πιστωτική κάρτα θα είναι χρεώσιμες από προεπιλογή.', billable: 'Χρεώσιμη', billableDescription: 'Οι δαπάνες συνήθως τιμολογούνται ξανά στους πελάτες', @@ -8052,10 +8051,12 @@ ${reportName}`, publicReceiptVisibilityHintDisabled: 'Οι αποδείξεις είναι ορατές μόνο από μέλη του Expensify που έχουν πρόσβαση στην αναφορά που περιέχει την απόδειξη.', enableTagsToUnlockTitle: 'Ενεργοποίηση ετικετών;', enableTagsToUnlockPrompt: 'Ενεργοποιήστε τις ετικέτες (στην ενότητα «Περισσότερες δυνατότητες») για να τις ξεκλειδώσετε.', - enableTagsAndRequirePrompt: 'Είστε βέβαιοι ότι θέλετε να ενεργοποιήσετε τις ετικέτες και να τις απαιτείτε για όλες τις δαπάνες;', enableCategoriesToUnlockTitle: 'Ενεργοποίηση κατηγοριών;', enableCategoriesToUnlockPrompt: 'Ενεργοποιήστε τις κατηγορίες (στην ενότητα «Περισσότερες λειτουργίες») για να τις ξεκλειδώσετε.', enableCategoriesAndRequirePrompt: 'Είστε βέβαιοι ότι θέλετε να ενεργοποιήσετε τις κατηγορίες και να τις απαιτείτε για όλες τις δαπάνες;', + enableTagsPrompt: 'Είστε βέβαιοι ότι θέλετε να ενεργοποιήσετε τις ετικέτες; Μπορείτε να τις κάνετε υποχρεωτικές για όλες τις δαπάνες μόλις έχετε τουλάχιστον μία ετικέτα.', + noTagsToRequirePrompt: 'Δεν έχετε ετικέτες. Δημιουργήστε μία ετικέτα.', + noCategoriesToRequirePrompt: 'Δεν έχετε κατηγορίες. Δημιουργήστε μια κατηγορία.', }, expenseReportRules: { title: 'Για προχωρημένους', diff --git a/src/languages/en.ts b/src/languages/en.ts index 196466e6f881..69d385dd16c9 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7938,7 +7938,6 @@ const translations = { alwaysReimbursableDescription: 'Expenses are always paid back to employees', alwaysNonReimbursable: 'Always non-reimbursable', alwaysNonReimbursableDescription: 'Expenses are never paid back to employees', - billableDefault: 'Billable default', billableDefaultDescription: 'Choose whether cash and credit card expenses should be billable by default.', billable: 'Billable', billableDescription: 'Expenses are most often re-billed to clients', @@ -7965,10 +7964,12 @@ const translations = { requireCompanyCardDisabledTooltip: 'Enable Company cards (under More features) to unlock.', enableTagsToUnlockTitle: 'Enable tags?', enableTagsToUnlockPrompt: 'Enable Tags (under More features) to unlock.', - enableTagsAndRequirePrompt: 'Are you sure you want to enable tags and require them for all expenses?', + enableTagsPrompt: 'Are you sure you want to enable tags? You can require them for all expenses once you have at least one tag.', + noTagsToRequirePrompt: "You don't have any tags. Please create a tag.", enableCategoriesToUnlockTitle: 'Enable categories?', enableCategoriesToUnlockPrompt: 'Enable Categories (under More features) to unlock.', enableCategoriesAndRequirePrompt: 'Are you sure you want to enable categories and require them for all expenses?', + noCategoriesToRequirePrompt: "You don't have any categories. Please create a category.", }, expenseReportRules: { title: 'Advanced', diff --git a/src/languages/es.ts b/src/languages/es.ts index 751a1603b17c..5f93be08ec7c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7673,7 +7673,6 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, alwaysReimbursableDescription: 'Los gastos siempre se reembolsados a los empleados', alwaysNonReimbursable: 'Siempre no reembolsable', alwaysNonReimbursableDescription: 'Los gastos nunca son reembolsados a los empleados', - billableDefault: 'Valor predeterminado facturable', billableDefaultDescription: 'Elige si los gastos en efectivo y con tarjeta de crédito deben ser facturables por defecto.', billable: 'Facturable', billableDescription: 'Los gastos se vuelven a facturar a los clientes en la mayoría de los casos', @@ -7700,10 +7699,12 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, publicReceiptVisibilityHintDisabled: 'Solo los miembros de Expensify con acceso al informe que contiene el recibo pueden ver los recibos.', enableTagsToUnlockTitle: '¿Habilitar etiquetas?', enableTagsToUnlockPrompt: 'Habilita las etiquetas (en Más funciones) para desbloquear.', - enableTagsAndRequirePrompt: '¿Seguro que quieres habilitar las etiquetas y hacerlas obligatorias para todos los gastos?', enableCategoriesToUnlockTitle: '¿Habilitar categorías?', enableCategoriesToUnlockPrompt: 'Activa Categorías (en Más funciones) para desbloquear.', enableCategoriesAndRequirePrompt: '¿Seguro que quieres habilitar las categorías y hacerlas obligatorias para todos los gastos?', + enableTagsPrompt: '¿Seguro que quieres habilitar las etiquetas? Podrás exigirlas para todos los gastos cuando tengas al menos una etiqueta.', + noTagsToRequirePrompt: 'No tienes ninguna etiqueta. Por favor, crea una etiqueta.', + noCategoriesToRequirePrompt: 'No tienes ninguna categoría. Por favor, crea una categoría.', }, expenseReportRules: { title: 'Avanzado', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index c80d2075299d..ba61ce97c412 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7724,7 +7724,6 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip alwaysReimbursableDescription: 'Les dépenses sont toujours remboursées aux employés', alwaysNonReimbursable: 'Toujours non remboursable', alwaysNonReimbursableDescription: 'Les dépenses ne sont jamais remboursées aux employés', - billableDefault: 'Facturable par défaut', billableDefaultDescription: 'Choisissez si les dépenses en espèces et par carte de crédit doivent être facturables par défaut.', billable: 'Facturable', billableDescription: 'Les dépenses sont le plus souvent refacturées aux clients', @@ -7751,10 +7750,12 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip publicReceiptVisibilityHintDisabled: 'Les reçus ne sont visibles que par les membres Expensify ayant accès à la note de frais contenant le reçu.', enableTagsToUnlockTitle: 'Activer les tags ?', enableTagsToUnlockPrompt: 'Activez les tags (sous Plus de fonctionnalités) pour débloquer.', - enableTagsAndRequirePrompt: 'Voulez-vous vraiment activer les tags et les rendre obligatoires pour toutes les dépenses ?', enableCategoriesToUnlockTitle: 'Activer les catégories ?', enableCategoriesToUnlockPrompt: 'Activez les catégories (dans Plus de fonctionnalités) pour déverrouiller.', enableCategoriesAndRequirePrompt: 'Voulez-vous vraiment activer les catégories et les rendre obligatoires pour toutes les dépenses ?', + enableTagsPrompt: 'Sei sicuro di voler abilitare i tag? Potrai renderli obbligatori per tutte le spese non appena avrai almeno un tag.', + noTagsToRequirePrompt: 'Non hai ancora nessun tag. Crea un tag.', + noCategoriesToRequirePrompt: 'Vous n’avez aucune catégorie. Veuillez créer une catégorie.', }, expenseReportRules: { title: 'Avancé', diff --git a/src/languages/it.ts b/src/languages/it.ts index bfbc613b8ffc..304d9f815322 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7668,7 +7668,6 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo alwaysReimbursableDescription: 'Le spese sono sempre rimborsate ai dipendenti', alwaysNonReimbursable: 'Sempre non rimborsabile', alwaysNonReimbursableDescription: 'Le spese non vengono mai rimborsate ai dipendenti', - billableDefault: 'Fatturabile predefinito', billableDefaultDescription: 'Scegli se le spese in contanti e con carta di credito devono essere fatturabili per impostazione predefinita.', billable: 'Fatturabile', billableDescription: 'Le spese sono più spesso rifatturate ai clienti', @@ -7695,10 +7694,12 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo publicReceiptVisibilityHintDisabled: 'Le ricevute possono essere visualizzate solo dai membri di Expensify che hanno accesso al report che contiene la ricevuta.', enableTagsToUnlockTitle: 'Abilitare i tag?', enableTagsToUnlockPrompt: 'Attiva i Tag (in Altre funzionalità) per sbloccare.', - enableTagsAndRequirePrompt: 'Sei sicuro di voler abilitare le etichette e renderle obbligatorie per tutte le spese?', enableCategoriesToUnlockTitle: 'Abilitare le categorie?', enableCategoriesToUnlockPrompt: 'Attiva Categorie (in Altre funzionalità) per sbloccare.', enableCategoriesAndRequirePrompt: 'Sei sicuro di voler abilitare le categorie e renderle obbligatorie per tutte le spese?', + enableTagsPrompt: 'Sei sicuro di voler abilitare i tag? Potrai renderli obbligatori per tutte le spese non appena avrai almeno un tag.', + noTagsToRequirePrompt: 'Non hai ancora nessun tag. Crea un tag.', + noCategoriesToRequirePrompt: 'Non hai ancora nessuna categoria. Crea una categoria.', }, expenseReportRules: { title: 'Avanzate', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 34ab98b20a63..1d756de4a426 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7575,7 +7575,6 @@ ${reportName}`, alwaysReimbursableDescription: '経費は常に従業員に払い戻されます', alwaysNonReimbursable: '常に精算対象外', alwaysNonReimbursableDescription: '経費は従業員に精算されません', - billableDefault: '請求可能のデフォルト', billableDefaultDescription: '現金およびクレジットカード経費をデフォルトで請求可能にするかどうかを選択してください。', billable: '請求可能', billableDescription: '経費は多くの場合、クライアントに再請求されます', @@ -7602,10 +7601,12 @@ ${reportName}`, publicReceiptVisibilityHintDisabled: 'レシートは、そのレシートを含むレポートへのアクセス権を持つ Expensify のメンバーのみが閲覧できます。', enableTagsToUnlockTitle: 'タグを有効にしますか?', enableTagsToUnlockPrompt: '有効にするには、「その他の機能」でタグを有効化してください。', - enableTagsAndRequirePrompt: 'タグを有効にし、すべての経費でタグを必須にしてもよろしいですか?', enableCategoriesToUnlockTitle: 'カテゴリーを有効にしますか?', enableCategoriesToUnlockPrompt: 'ロックを解除するには、[その他の機能]で[カテゴリ]を有効にしてください。', enableCategoriesAndRequirePrompt: 'カテゴリを有効にし、すべての経費でカテゴリを必須にしてもよろしいですか?', + enableTagsPrompt: 'タグを有効にしてもよろしいですか?タグが少なくとも1つある場合、すべての経費でタグの入力を必須にできます。', + noTagsToRequirePrompt: 'タグがありません。タグを作成してください。', + noCategoriesToRequirePrompt: 'カテゴリがありません。カテゴリを作成してください。', }, expenseReportRules: { title: '詳細設定', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 8cf7d84a9c84..0c25244692ef 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7649,7 +7649,6 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar alwaysReimbursableDescription: 'Onkosten worden altijd terugbetaald aan medewerkers', alwaysNonReimbursable: 'Altijd niet-declarabel', alwaysNonReimbursableDescription: 'Onkosten worden nooit aan medewerkers terugbetaald', - billableDefault: 'Factureerbaar als standaard', billableDefaultDescription: 'Kies of contante en creditcarduitgaven standaard factureerbaar moeten zijn.', billable: 'Factureerbaar', billableDescription: 'Uitgaven worden meestal doorbelast aan klanten', @@ -7676,10 +7675,12 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar publicReceiptVisibilityHintDisabled: 'Bonnetjes zijn alleen zichtbaar voor Expensify-leden met toegang tot het rapport dat het bonnetje bevat.', enableTagsToUnlockTitle: 'Tags inschakelen?', enableTagsToUnlockPrompt: 'Schakel Labels in (onder Meer functies) om te ontgrendelen.', - enableTagsAndRequirePrompt: 'Weet je zeker dat je tags wilt inschakelen en ze verplicht wilt maken voor alle uitgaven?', enableCategoriesToUnlockTitle: 'Categorieën inschakelen?', enableCategoriesToUnlockPrompt: 'Schakel Categorieën (onder Meer functies) in om te ontgrendelen.', enableCategoriesAndRequirePrompt: 'Weet je zeker dat je categorieën wilt inschakelen en ze verplicht wilt maken voor alle uitgaven?', + enableTagsPrompt: 'Weet je zeker dat je labels wilt inschakelen? Je kunt ze verplicht maken voor alle uitgaven zodra je minstens één label hebt.', + noTagsToRequirePrompt: 'Je hebt nog geen tags. Maak alsjeblieft een tag aan.', + noCategoriesToRequirePrompt: 'Je hebt nog geen categorieën. Maak een categorie aan.', }, expenseReportRules: { title: 'Geavanceerd', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index c11c97545f94..f78c29d3896b 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7633,7 +7633,6 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i alwaysReimbursableDescription: 'Wydatki są zawsze zwracane pracownikom', alwaysNonReimbursable: 'Zawsze niepodlegające zwrotowi', alwaysNonReimbursableDescription: 'Wydatki nigdy nie są zwracane pracownikom', - billableDefault: 'Domyślne obciążenie klienta', billableDefaultDescription: 'Wybierz, czy wydatki gotówkowe i kartą kredytową mają być domyślnie fakturowalne.', billable: 'Fakturowalne', billableDescription: 'Wydatki są najczęściej refakturowane klientom', @@ -7660,10 +7659,12 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i publicReceiptVisibilityHintDisabled: 'Paragony są widoczne tylko dla członków Expensify, którzy mają dostęp do raportu zawierającego paragon.', enableTagsToUnlockTitle: 'Włączyć tagi?', enableTagsToUnlockPrompt: 'Włącz Tagi (w sekcji Więcej funkcji), aby odblokować.', - enableTagsAndRequirePrompt: 'Na pewno chcesz włączyć tagi i wymagać ich dla wszystkich wydatków?', enableCategoriesToUnlockTitle: 'Włączyć kategorie?', enableCategoriesToUnlockPrompt: 'Włącz Kategorie (w sekcji Więcej funkcji), aby odblokować.', enableCategoriesAndRequirePrompt: 'Na pewno chcesz włączyć kategorie i wymagać ich dla wszystkich wydatków?', + enableTagsPrompt: 'Na pewno chcesz włączyć tagi? Będziesz mógł wymagać ich dla wszystkich wydatków, gdy będziesz mieć co najmniej jeden tag.', + noTagsToRequirePrompt: 'Nie masz żadnych tagów. Utwórz tag.', + noCategoriesToRequirePrompt: 'Nie masz żadnych kategorii. Utwórz kategorię.', }, expenseReportRules: { title: 'Zaawansowane', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index a7651f5ad8b4..e988cb228cf9 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7651,7 +7651,6 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e alwaysReimbursableDescription: 'As despesas são sempre reembolsadas aos funcionários', alwaysNonReimbursable: 'Sempre não reembolsável', alwaysNonReimbursableDescription: 'Despesas nunca são reembolsadas aos funcionários', - billableDefault: 'Cobrável por padrão', billableDefaultDescription: 'Escolha se as despesas em dinheiro e cartão de crédito devem ser faturáveis por padrão.', billable: 'Faturável', billableDescription: 'As despesas são mais frequentemente refaturadas aos clientes', @@ -7678,10 +7677,12 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e publicReceiptVisibilityHintDisabled: 'Os recibos só podem ser vistos por membros da Expensify com acesso ao relatório que contém o recibo.', enableTagsToUnlockTitle: 'Ativar tags?', enableTagsToUnlockPrompt: 'Ative as Tags (em Mais recursos) para desbloquear.', - enableTagsAndRequirePrompt: 'Tem certeza de que quer ativar etiquetas e exigi-las para todas as despesas?', enableCategoriesToUnlockTitle: 'Ativar categorias?', enableCategoriesToUnlockPrompt: 'Ative Categorias (em Mais recursos) para desbloquear.', enableCategoriesAndRequirePrompt: 'Tem certeza de que deseja ativar categorias e torná-las obrigatórias para todas as despesas?', + enableTagsPrompt: 'Tem certeza de que quer ativar tags? Você poderá exigi-las para todas as despesas assim que tiver pelo menos uma tag.', + noTagsToRequirePrompt: 'Você não tem nenhuma tag. Crie uma tag.', + noCategoriesToRequirePrompt: 'Você não tem nenhuma categoria. Crie uma categoria.', }, expenseReportRules: { title: 'Avançado', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 7924e6316c4a..985ced5d87aa 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7391,7 +7391,6 @@ ${reportName}`, alwaysReimbursableDescription: '费用始终报销给员工', alwaysNonReimbursable: '始终不予报销', alwaysNonReimbursableDescription: '从不向员工报销费用', - billableDefault: '默认计费', billableDefaultDescription: '选择现金和信用卡报销是否默认为可计费。', billable: '可计费', billableDescription: '费用通常会重新向客户计费', @@ -7418,10 +7417,12 @@ ${reportName}`, publicReceiptVisibilityHintDisabled: '只有拥有包含该收据的报表访问权限的 Expensify 成员才能查看收据。', enableTagsToUnlockTitle: '启用标签?', enableTagsToUnlockPrompt: '启用“标签”(位于“更多功能”下)以解锁。', - enableTagsAndRequirePrompt: '确定要启用标签,并将其设为所有报销的必填项吗?', enableCategoriesToUnlockTitle: '启用类别?', enableCategoriesToUnlockPrompt: '启用“类别”(位于“更多功能”下)以解锁。', enableCategoriesAndRequirePrompt: '确定要启用类别,并要求所有报销都必须选择类别吗?', + enableTagsPrompt: '确定要启用标签吗?在至少有一个标签后,您可以要求所有报销都必须选择标签。', + noTagsToRequirePrompt: '您还没有任何标签。请创建一个标签。', + noCategoriesToRequirePrompt: '您还没有任何类别。请创建一个类别。', }, expenseReportRules: { title: '高级', diff --git a/src/libs/AttendeeUtils.ts b/src/libs/AttendeeUtils.ts index b6084cc5a40c..1191df9e5b33 100644 --- a/src/libs/AttendeeUtils.ts +++ b/src/libs/AttendeeUtils.ts @@ -1,7 +1,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; -import type {LoginToAccountIDMapDerivedValue, PersonalDetailsList, PolicyCategories, PolicyCategory} from '@src/types/onyx'; +import type {LoginToAccountIDMapDerivedValue, PersonalDetailsList, PolicyCategories} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; import type {CurrentUserPersonalDetails} from '@src/types/onyx/PersonalDetails'; @@ -34,29 +34,6 @@ function normalizeAttendees(attendees: Attendee[] | undefined): Attendee[] { return (attendees ?? []).map(normalizeAttendee); } -/** Formats the title for requiredFields menu item based on which fields are enabled in the policy category */ -function formatRequiredFieldsTitle(translate: LocaleContextProps['translate'], policyCategory: PolicyCategory, isAttendeeTrackingEnabled = false): string { - const enabledFields: string[] = []; - - // Attendees field should show first when both are selected and attendee tracking is enabled - if (isAttendeeTrackingEnabled && policyCategory.areAttendeesRequired) { - enabledFields.push(translate('iou.attendees')); - } - - if (policyCategory.areCommentsRequired) { - enabledFields.push(translate('common.description')); - } - - if (enabledFields.length === 0) { - return ''; - } - - const [first, ...rest] = enabledFields; - const capitalizedFirst = first.charAt(0).toUpperCase() + first.slice(1); - const lowercasedRest = rest.map((field) => field.charAt(0).toLowerCase() + field.slice(1)); - return [capitalizedFirst, ...lowercasedRest].join(', '); -} - /** * Converts raw attendees value to an array. * Onyx may deserialize arrays as plain objects, so both shapes are handled. @@ -183,4 +160,4 @@ function enrichAndSortAttendees( ); } -export {enrichAndSortAttendees, formatRequiredFieldsTitle, getIsMissingAttendeesViolation, normalizeAttendee, normalizeAttendees, syncMissingAttendeesViolation, convertAttendeesToArray}; +export {enrichAndSortAttendees, getIsMissingAttendeesViolation, normalizeAttendee, normalizeAttendees, syncMissingAttendeesViolation, convertAttendeesToArray}; diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts index ee9e72b6cb4c..6fa111d57a40 100644 --- a/src/libs/CategoryUtils.ts +++ b/src/libs/CategoryUtils.ts @@ -31,30 +31,6 @@ function formatDefaultTaxRateText(translate: LocaleContextProps['translate'], ta return `${taxRateText}${suffix ? ` ${CONST.DOT_SEPARATOR} ${suffix}` : ``}`; } -function formatRequireReceiptsOverText( - translate: LocaleContextProps['translate'], - policy: Policy, - categoryMaxAmountNoReceipt: number | null | undefined, - convertToDisplayString: CurrencyListActionsContextType['convertToDisplayString'], -) { - const isAlwaysSelected = categoryMaxAmountNoReceipt === 0; - const isNeverSelected = categoryMaxAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE; - - if (isAlwaysSelected) { - return translate(`workspace.rules.categoryRules.requireReceiptsOverList.always`); - } - - if (isNeverSelected) { - return translate(`workspace.rules.categoryRules.requireReceiptsOverList.never`); - } - - if (policy?.maxExpenseAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE || policy?.maxExpenseAmountNoReceipt === undefined) { - return translate(`workspace.rules.categoryRules.requireReceiptsOverList.never`); - } - - return translate(`workspace.rules.categoryRules.requireReceiptsOverList.default`, convertToDisplayString(policy.maxExpenseAmountNoReceipt, policy?.outputCurrency ?? CONST.CURRENCY.USD)); -} - function formatRequireItemizedReceiptsOverText( translate: LocaleContextProps['translate'], policy: Policy, @@ -246,7 +222,6 @@ function hasAnyCategoryRules(categories: PolicyCategories | undefined): boolean export { formatDefaultTaxRateText, - formatRequireReceiptsOverText, formatRequireItemizedReceiptsOverText, getCategoryApproverRule, getCategoryExpenseRule, diff --git a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx index 5098a178ee97..8a754384305a 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx @@ -20,7 +20,7 @@ const loadWorkspaceInitialPage = () => require('../../../. const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../pages/workspace/WorkspaceOverviewPage').default, - [SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default, + [SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsPageRevamp').default, [SCREENS.WORKSPACE.INVOICES]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicesPage').default, [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../../pages/workspace/WorkspaceMembersPage').default, [SCREENS.WORKSPACE.ROOMS]: () => require('../../../../pages/workspace/rooms/WorkspaceRoomsPage').default, @@ -38,7 +38,7 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.RECEIPT_PARTNERS]: () => require('../../../../pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage').default, [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default, [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../../pages/workspace/travel/PolicyTravelPage').default, - [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPage').default, + [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPageRevamp').default, [SCREENS.WORKSPACE.TIME_TRACKING]: () => require('../../../../pages/workspace/timeTracking/WorkspaceTimeTrackingPage').default, } satisfies Screens; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 7049d163b907..3404a6a038ac 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1453,14 +1453,14 @@ function isAttendeeTrackingEnabled(policy: OnyxEntry): boolean { /** * Whether the policy can access a feature based on plan level. * Corporate-only features are restricted to control (Corporate) policies. - * Rules are available on Control always, and on Collect only when the rulesRevamp beta is enabled. + * Rules are available on both Control and Collect. */ -function canPolicyAccessFeature(policy: OnyxEntry, featureName: PolicyFeatureName, isRulesRevampEnabled = false): boolean { +function canPolicyAccessFeature(policy: OnyxEntry, featureName: PolicyFeatureName): boolean { if (!isPaidGroupPolicy(policy)) { return false; } if (featureName === CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED) { - return isControlPolicy(policy) || (isCollectPolicy(policy) && isRulesRevampEnabled); + return isControlPolicy(policy) || isCollectPolicy(policy); } const corporateOnlyFeatures = new Set([CONST.POLICY.MORE_FEATURES.ARE_PER_DIEM_RATES_ENABLED, CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED]); if (corporateOnlyFeatures.has(featureName)) { @@ -1671,17 +1671,10 @@ function canDisableOrDeleteTaxRate(policy: Policy, taxID: string): boolean { return policy.taxRates?.defaultExternalID !== taxID && policy.taxRates?.foreignTaxDefault !== taxID; } -/** - * @param isRulesRevampEnabled - Prefer `isBetaEnabled(CONST.BETAS.RULES_REVAMP)` from `usePermissions()`, not raw betas from Onyx. - * Collect workspaces can only access Rules when this beta is enabled. - */ -function arePolicyRulesEnabled(policy: OnyxEntry, policyCategories?: PolicyCategories | null, isRulesRevampEnabled = false): boolean { +function arePolicyRulesEnabled(policy: OnyxEntry, policyCategories?: PolicyCategories | null): boolean { if (!isPaidGroupPolicy(policy)) { return false; } - if (isCollectPolicy(policy) && !isRulesRevampEnabled) { - return false; - } if (policy?.areRulesEnabled === true) { return true; } @@ -1695,9 +1688,9 @@ function arePolicyRulesEnabled(policy: OnyxEntry, policyCategories?: Pol return hasAnyCategoryRules(policyCategories ?? undefined); } -function isPolicyFeatureEnabled(policy: OnyxEntry, featureName: PolicyFeatureName, policyCategories?: PolicyCategories | null, isRulesRevampEnabled = false): boolean { +function isPolicyFeatureEnabled(policy: OnyxEntry, featureName: PolicyFeatureName, policyCategories?: PolicyCategories | null): boolean { if (featureName === CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED) { - return arePolicyRulesEnabled(policy, policyCategories, isRulesRevampEnabled); + return arePolicyRulesEnabled(policy, policyCategories); } if (featureName === CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED) { return !!policy?.tax?.trackingEnabled; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 1bb653c6f6e4..d230ac6ffb56 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -6452,14 +6452,6 @@ function setPolicyProhibitedExpenses(policyID: string, prohibitedExpenses: Prohi * @param policyID * @param prohibitedExpense */ -function setPolicyProhibitedExpense(policyID: string, prohibitedExpense: keyof ProhibitedExpenses, currentProhibitedExpense: ProhibitedExpenses | undefined) { - const prohibitedExpenses = { - ...currentProhibitedExpense, - [prohibitedExpense]: !currentProhibitedExpense?.[prohibitedExpense], - }; - - setPolicyProhibitedExpenses(policyID, prohibitedExpenses, currentProhibitedExpense); -} /** * Call the API to set the max expense age for the given policy @@ -7919,7 +7911,6 @@ export { setPolicyMaxExpenseAmount, setPolicyMaxExpenseAge, updateCustomRules, - setPolicyProhibitedExpense, setPolicyProhibitedExpenses, setDuplicateWorkspaceData, clearDuplicateWorkspace, diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index fb634339f700..e1b1e0f8cfa8 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -410,94 +410,6 @@ function setWorkspaceTagEnabled(policyData: PolicyData, tagsToUpdate: Record((acc, key) => { - if (tagListIndexes.includes(policyData.tags[key].orderWeight)) { - acc[key] = { - ...acc[key], - required: isRequired, - errors: undefined, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - pendingFields: { - required: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - }; - - return acc; - } - - return acc; - }, {}), - }; - - const onyxData: OnyxData = { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: policyTagsOptimisticData, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: { - ...Object.keys(policyData.tags).reduce((acc, key) => { - if (tagListIndexes.includes(policyData.tags[key].orderWeight)) { - acc[key] = { - ...acc[key], - errors: undefined, - pendingAction: null, - pendingFields: { - required: null, - }, - }; - return acc; - } - - return acc; - }, {}), - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: { - ...Object.keys(policyData.tags).reduce((acc, key) => { - acc[key] = { - ...acc[key], - errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), - pendingAction: null, - pendingFields: { - required: null, - }, - }; - return acc; - }, {}), - }, - }, - ], - }; - - pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, policyTagsOptimisticData); - const parameters: SetPolicyTagListsRequired = { - policyID, - tagListIndexes, - isRequired, - }; - - API.write(WRITE_COMMANDS.SET_POLICY_TAG_LISTS_REQUIRED, parameters, onyxData); -} - function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { const policyID = policyData.policy?.id; const policyTag = PolicyUtils.getTagLists(policyData.tags)?.at(0); @@ -1547,7 +1459,6 @@ export { clearPolicyTagListErrorField, deletePolicyTags, enablePolicyTags, - setWorkspaceTagRequired, openPolicyTagsPage, renamePolicyTag, renamePolicyTagList, diff --git a/src/pages/settings/Wallet/WalletExpensifyCardSpendRulesPage.tsx b/src/pages/settings/Wallet/WalletExpensifyCardSpendRulesPage.tsx index d68a07f4d2c8..87ef58ca8e8e 100644 --- a/src/pages/settings/Wallet/WalletExpensifyCardSpendRulesPage.tsx +++ b/src/pages/settings/Wallet/WalletExpensifyCardSpendRulesPage.tsx @@ -18,7 +18,6 @@ function WalletExpensifyCardSpendRulesPage({route}: WalletExpensifyCardSpendRule ({ diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx index 68ff5e96502b..bdc88eebb99f 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx @@ -89,7 +89,6 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const {isBetaEnabled} = usePermissions(); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {showConfirmModal} = useConfirmModal(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const isVendorMatchingEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); const illustrations = useMemoizedLazyIllustrations([ 'FolderOpen', @@ -97,7 +96,6 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro 'CompanyCard', 'Workflows', 'InvoiceBlue', - 'Rules', 'Flash', 'Tag', 'PerDiem', @@ -511,10 +509,10 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro }} /> category.previousCategoryName === categoryName); - const policyCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD; - const policyCategoryExpenseLimitType = policyCategory?.expenseLimitType ?? CONST.POLICY.EXPENSE_LIMIT_TYPES.EXPENSE; const decodedCategoryName = getDecodedCategoryName(policyCategory?.name ?? ''); - const categoryRulesEnabled = arePolicyRulesEnabled(policy, policyCategories, isRulesRevampEnabled); + const categoryRulesEnabled = arePolicyRulesEnabled(policy, policyCategories); const contextualRules = useMemo(() => { - if (!isRulesRevampEnabled || !policyCategory) { + if (!policyCategory) { return []; } @@ -99,7 +86,7 @@ function CategorySettingsPage({route: {params, name}, navigation}: CategorySetti convertToDisplayString, isOffline, }); - }, [convertToDisplayString, isOffline, isRulesRevampEnabled, policy, policyCategory, translate]); + }, [convertToDisplayString, isOffline, policy, policyCategory, translate]); const shouldPreventDisableOrDelete = isDisablingOrDeletingLastEnabledCategory(policy, policyData.categories, [policyCategory]); const isQuickSettingsFlow = name === SCREENS.SETTINGS_CATEGORIES.DYNAMIC_SETTINGS_CATEGORY_SETTINGS; @@ -137,16 +124,6 @@ function CategorySettingsPage({route: {params, name}, navigation}: CategorySetti navigation.setParams({categoryName: policyCategory?.name}); }, [categoryName, navigation, policyCategory?.name, isFocused]); - const flagAmountsOverText = useMemo(() => { - if (policyCategory?.maxExpenseAmount === CONST.DISABLED_MAX_EXPENSE_VALUE || !policyCategory?.maxExpenseAmount) { - return ''; - } - - return `${convertToDisplayString(policyCategory?.maxExpenseAmount, policyCurrency)} ${CONST.DOT_SEPARATOR} ${translate( - `workspace.rules.categoryRules.expenseLimitTypes.${policyCategoryExpenseLimitType}`, - )}`; - }, [convertToDisplayString, policyCategory?.maxExpenseAmount, policyCategoryExpenseLimitType, policyCurrency, translate]); - const approverText = useMemo(() => { const categoryApprover = getCategoryApproverRule(policy?.rules?.approvalRules ?? [], categoryName)?.approver ?? ''; const approver = getPersonalDetailByEmail(categoryApprover); @@ -169,36 +146,6 @@ function CategorySettingsPage({route: {params, name}, navigation}: CategorySetti return formatDefaultTaxRateText(translate, taxID, taxRate, policy?.taxRates); }, [categoryName, policy?.rules?.expenseRules, policy?.taxRates, translate]); - const requireReceiptsOverText = useMemo(() => { - if (!policy) { - return ''; - } - return formatRequireReceiptsOverText(translate, policy, policyCategory?.maxAmountNoReceipt, convertToDisplayString); - }, [policy, policyCategory?.maxAmountNoReceipt, translate, convertToDisplayString]); - - const requireItemizedReceiptsOverText = useMemo(() => { - if (!policy) { - return ''; - } - return formatRequireItemizedReceiptsOverText(translate, policy, policyCategory?.maxAmountNoItemizedReceipt, convertToDisplayString); - }, [policy, policyCategory?.maxAmountNoItemizedReceipt, translate, convertToDisplayString]); - - const requiredFieldsTitle = useMemo(() => { - if (!policyCategory) { - return ''; - } - return formatRequiredFieldsTitle(translate, policyCategory, isAttendeeTrackingEnabled(policy)); - }, [policyCategory, translate, policy]); - - const requireFieldsPendingAction = useMemo(() => { - if (isAttendeeTrackingEnabled(policy)) { - // Pending fields are objects so we can't use nullish coalescing - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return policyCategory?.pendingFields?.areAttendeesRequired || policyCategory?.pendingFields?.areCommentsRequired; - } - return policyCategory?.pendingFields?.areCommentsRequired; - }, [policyCategory?.pendingFields, policy]); - // eslint-disable-next-line rulesdir/no-negated-variables const showCannotDeleteOrDisableLastCategoryModal = useCallback(() => { showConfirmModal({ @@ -394,7 +341,7 @@ function CategorySettingsPage({route: {params, name}, navigation}: CategorySetti shouldShowRightIcon={canWriteCategories} /> - {categoryRulesEnabled && isRulesRevampEnabled && ( + {categoryRulesEnabled && ( <> )} - {categoryRulesEnabled && !isRulesRevampEnabled && ( - <> - - {translate('workspace.rules.categoryRules.title')} - - { - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_APPROVER.path)); - }} - interactive={canWriteCategories} - shouldShowRightIcon={canWriteCategories} - disabled={approverDisabled} - helperText={ - approverDisabled - ? translate('workspace.rules.categoryRules.enableWorkflows', `${environmentURL}/${ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)}`) - : undefined - } - shouldParseHelperText - /> - {!!policy?.tax?.trackingEnabled && ( - { - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_DEFAULT_TAX_RATE.path)); - }} - interactive={canWriteCategories} - shouldShowRightIcon={canWriteCategories} - /> - )} - {/* - * Legacy category rule entry points. When removing the RULES_REVAMP beta, - * delete this entire non-revamp block (and related useMemo values) instead - * of keeping these Category settings routes. - */} - - { - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_FLAG_AMOUNTS_OVER.path)); - }} - interactive={canWriteCategories} - shouldShowRightIcon={canWriteCategories} - /> - - - { - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_REQUIRE_RECEIPTS_OVER.path)); - }} - interactive={canWriteCategories} - shouldShowRightIcon={canWriteCategories} - /> - - - { - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_REQUIRE_ITEMIZED_RECEIPTS_OVER.path)); - }} - interactive={canWriteCategories} - shouldShowRightIcon={canWriteCategories} - /> - - - { - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_REQUIRED_FIELDS.path)); - }} - interactive={canWriteCategories} - shouldShowRightIcon={canWriteCategories} - /> - - - { - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_DESCRIPTION_HINT.path)); - }} - interactive={canWriteCategories} - shouldShowRightIcon={canWriteCategories} - shouldRenderAsHTML - /> - - - )} - - {categoryRulesEnabled && isRulesRevampEnabled && (contextualRules.length > 0 || canWriteRules) && ( + {categoryRulesEnabled && (contextualRules.length > 0 || canWriteRules) && ( <> {translate('workspace.rules.categoryRules.title')} diff --git a/src/pages/workspace/categories/DynamicWorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/DynamicWorkspaceCategoriesSettingsPage.tsx index 3891116b7ec4..07a42d753fe9 100644 --- a/src/pages/workspace/categories/DynamicWorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/DynamicWorkspaceCategoriesSettingsPage.tsx @@ -1,38 +1,28 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; -import SelectionList from '@components/SelectionList'; -import SpendCategorySelectorListItem from '@components/SelectionList/ListItem/SpendCategorySelectorListItem'; -import type {ListItem} from '@components/SelectionList/types'; -import Text from '@components/Text'; import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; -import usePermissions from '@hooks/usePermissions'; -import usePolicyData from '@hooks/usePolicyData'; import useThemeStyles from '@hooks/useThemeStyles'; -import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {hasEnabledOptions} from '@libs/OptionsListUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import {getCurrentAccountingIntegrationName} from '@pages/workspace/accounting/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; -import {setPolicyShowCategoryGLCodes, setWorkspaceRequiresCategory} from '@userActions/Policy/Category'; +import {setPolicyShowCategoryGLCodes} from '@userActions/Policy/Category'; import {clearPolicyErrorField} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import React, {useCallback, useMemo} from 'react'; -import {View} from 'react-native'; +import React from 'react'; type DynamicWorkspaceCategoriesSettingsPageProps = WithPolicyConnectionsProps & ( @@ -44,67 +34,15 @@ function DynamicWorkspaceCategoriesSettingsPage({policy, route}: DynamicWorkspac const {policyID} = route.params; const styles = useThemeStyles(); const {translate} = useLocalize(); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); - const policyData = usePolicyData(policyID); - const isConnectedToAccounting = Object.keys(policy?.connections ?? {}).length > 0; - const currentConnectionName = getCurrentAccountingIntegrationName(policy, translate); const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_CATEGORIES.DYNAMIC_SETTINGS_CATEGORIES_SETTINGS; const backPath = useDynamicBackPath(DYNAMIC_ROUTES.SETTINGS_CATEGORIES_SETTINGS.path); - const toggleSubtitle = isConnectedToAccounting && currentConnectionName ? translate('workspace.categories.needCategoryForExportToIntegration', currentConnectionName) : undefined; - - const updateWorkspaceRequiresCategory = useCallback( - (value: boolean) => { - setWorkspaceRequiresCategory(policyData, value); - }, - [policyData], - ); const updateShowCategoryGLCodes = (value: boolean) => { setPolicyShowCategoryGLCodes(policyID, value); }; - const data = useMemo(() => { - if (!policyData.policy?.mccGroup) { - return []; - } - - return Object.entries(policyData.policy?.mccGroup).map( - ([mccKey, mccGroup]): ListItem => ({ - categoryID: mccGroup.category, - keyForList: mccKey, - groupID: mccKey, - tabIndex: -1, - pendingAction: mccGroup?.pendingAction, - }), - ); - }, [policyData.policy]); - - const hasEnabledCategories = hasEnabledOptions(policyData.categories); - const isToggleDisabled = !policy?.areCategoriesEnabled || !hasEnabledCategories || isConnectedToAccounting; - - // Under the revamp only the GL codes toggle is left here, so the page has nothing to show without it. - const shouldBlockEmptySettings = isRulesRevampEnabled && !policy?.glCodes; - - const onSelectItem = (item: ListItem) => { - if (!item.groupID) { - return; - } - - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.SPEND_CATEGORY_SELECTOR.getRoute(item.groupID))); - }; - - const selectionListHeaderContent = ( - - - {translate('workspace.categories.defaultSpendCategories')} - - {translate('workspace.categories.spendCategoriesDescription')} - - ); + // Only the GL codes toggle is left here, so the page has nothing to show without it. + const shouldBlockEmptySettings = !policy?.glCodes; return ( Navigation.goBack(isQuickSettingsFlow ? backPath : undefined)} /> - {!isRulesRevampEnabled && ( - clearPolicyErrorField(policy?.id, 'requiresCategory')} - shouldPlaceSubtitleBelowSwitch - /> - )} {!!policy?.glCodes && ( clearPolicyErrorField(policy?.id, 'showCategoryGLCodes')} /> )} - {/* Default spend categories moved to Rules > Expense defaults, so they'd be a second source of truth here. */} - {!isRulesRevampEnabled && ( - <> - - - {!!policyData.policy && (data?.length ?? 0) > 0 && ( - - )} - - - )} diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 2b93b7d80681..97b65a332698 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -23,7 +23,6 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import useOnboardingTaskInformation from '@hooks/useOnboardingTaskInformation'; import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; import usePolicyData from '@hooks/usePolicyData'; import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -87,8 +86,6 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_ROOT; const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {canWrite: canWriteCategories, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.CATEGORIES); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const [selectedCategoryKeys, setSelectedCategoryKeys] = useState([]); const canSelectMultiple = canWriteCategories && (isSmallScreenWidth ? isMobileSelectionModeEnabled : true); @@ -363,8 +360,8 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const secondaryActions = useMemo(() => { const menuItems = []; - // Under the revamp the other settings moved to Rules, so this is only worth showing for the GL codes toggle. - if (canWriteCategories && (!isRulesRevampEnabled || !!policy?.glCodes)) { + // The other settings moved to Rules, so this is only worth showing for the GL codes toggle. + if (canWriteCategories && !!policy?.glCodes) { menuItems.push({ icon: icons.Gear, text: translate('common.settings'), @@ -410,9 +407,8 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { icons.Gear, icons.Table, translate, - navigateToCategoriesSettings, canWriteCategories, - isRulesRevampEnabled, + navigateToCategoriesSettings, policy?.glCodes, policyHasAccountingConnections, hasVisibleCategories, diff --git a/src/pages/workspace/downgrade/DynamicWorkspaceDowngradePage.tsx b/src/pages/workspace/downgrade/DynamicWorkspaceDowngradePage.tsx index 1b3d9d314a97..5281d5723883 100644 --- a/src/pages/workspace/downgrade/DynamicWorkspaceDowngradePage.tsx +++ b/src/pages/workspace/downgrade/DynamicWorkspaceDowngradePage.tsx @@ -11,7 +11,6 @@ import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import {getCompanyFeeds} from '@libs/CardUtils'; @@ -22,7 +21,6 @@ import {arePolicyRulesEnabled, canModifyPlan, isCollectPolicy} from '@libs/Polic import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import CONST from '@src/CONST'; import {downgradeToTeam} from '@src/libs/actions/Policy/Policy'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; @@ -56,8 +54,6 @@ function DynamicWorkspaceDowngradePage({route}: DynamicWorkspaceDowngradePagePro const {showConfirmModal, closeModal} = useConfirmModal(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const canPerformDowngrade = () => canModifyPlan(ownerPolicies, policy); const isDowngraded = isCollectPolicy(policy); @@ -102,7 +98,7 @@ function DynamicWorkspaceDowngradePage({route}: DynamicWorkspaceDowngradePagePro Navigation.dismissModal(); return; } - downgradeToTeam(policy.id, policy.type, policy.isAttendeeTrackingEnabled, isRulesRevampEnabled && arePolicyRulesEnabled(policy, policyCategories, isRulesRevampEnabled)); + downgradeToTeam(policy.id, policy.type, policy.isAttendeeTrackingEnabled, arePolicyRulesEnabled(policy, policyCategories)); }; if (!canPerformDowngrade()) { diff --git a/src/pages/workspace/expensifyCard/DynamicExpensifyCardLimitTypePage.tsx b/src/pages/workspace/expensifyCard/DynamicExpensifyCardLimitTypePage.tsx index ec7d8e39d970..83f1beb562e5 100644 --- a/src/pages/workspace/expensifyCard/DynamicExpensifyCardLimitTypePage.tsx +++ b/src/pages/workspace/expensifyCard/DynamicExpensifyCardLimitTypePage.tsx @@ -170,7 +170,7 @@ function DynamicExpensifyCardLimitTypePage({route}: WorkspaceEditCardLimitTypePa } } - const workspaceWorkflowsPageURL = `${environmentURL}/${ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)}`; + const workspaceWorkflowsPageURL = `${environmentURL}/${ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID, CONST.TAB.WORKFLOWS.APPROVALS)}`; const data = []; diff --git a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx index 0f3d438fe678..52a3d024b86e 100644 --- a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx @@ -102,7 +102,7 @@ function LimitTypeStep({policy, stepNames, startStepIndex}: LimitTypeStepProps) setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CARD_TYPE, policyID}); }, [isEditing, policyID]); - const workspaceWorkflowsPageURL = `${environmentURL}/${ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)}`; + const workspaceWorkflowsPageURL = `${environmentURL}/${ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID, CONST.TAB.WORKFLOWS.APPROVALS)}`; const data = useMemo(() => { const options = []; diff --git a/src/pages/workspace/getWorkspaceMenuItems.ts b/src/pages/workspace/getWorkspaceMenuItems.ts index f158eeadb1e1..2a39be05783c 100644 --- a/src/pages/workspace/getWorkspaceMenuItems.ts +++ b/src/pages/workspace/getWorkspaceMenuItems.ts @@ -56,7 +56,6 @@ type WorkspaceMenuIconMap = Record< | 'Tag' | 'Coins' | 'Workflows' - | 'Feed' | 'Car' | 'LuggageWithLines' | 'ExpensifyCard' @@ -103,8 +102,6 @@ type GetWorkspaceMenuItemsParams = { shouldShowEnterCredentialsError?: boolean; /** Whether the company cards row should show an error indicator. */ shouldShowRBR?: boolean; - /** Whether the Rules revamp beta is enabled. */ - isRulesRevampBetaEnabled?: boolean; /** Whether the vendor matching beta is enabled. */ isVendorMatchingBetaEnabled?: boolean; /** Formats the invoice account balance for its menu badge. */ @@ -121,7 +118,6 @@ function getWorkspaceMenuItems({ previousPendingFields, shouldShowEnterCredentialsError = false, shouldShowRBR = false, - isRulesRevampBetaEnabled = false, isVendorMatchingBetaEnabled = false, convertToDisplayString, }: GetWorkspaceMenuItemsParams): WorkspaceMenuItem[] { @@ -172,7 +168,7 @@ function getWorkspaceMenuItems({ [CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED]: (policy?.isHREnabled === true || isAnyHRConnected(policy)) && canPolicyAccessFeature(policy, CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED), [CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED]: policy?.areExpensifyCardsEnabled, [CONST.POLICY.MORE_FEATURES.ARE_REPORT_FIELDS_ENABLED]: policy?.areReportFieldsEnabled, - [CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED]: arePolicyRulesEnabled(policy, policyCategories, isRulesRevampBetaEnabled), + [CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED]: arePolicyRulesEnabled(policy, policyCategories), [CONST.POLICY.MORE_FEATURES.ARE_INVOICES_ENABLED]: policy?.areInvoicesEnabled, [CONST.POLICY.MORE_FEATURES.ARE_PER_DIEM_RATES_ENABLED]: isPerDiemEnabled(policy) && canPolicyAccessFeature(policy, CONST.POLICY.MORE_FEATURES.ARE_PER_DIEM_RATES_ENABLED), [CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED]: policy?.receiptPartners?.enabled ?? false, @@ -314,7 +310,7 @@ function getWorkspaceMenuItems({ if (policyFeatureStates[CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.RULES)) { items.push({ translationKey: 'workspace.common.rules', - icon: isRulesRevampBetaEnabled ? icons.Bolt : icons.Feed, + icon: icons.Bolt, getRoute: () => ROUTES.WORKSPACE_RULES.getRoute(policyID), brickRoadIndicator: hasPolicyRulesError(policy) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, screenName: SCREENS.WORKSPACE.RULES, diff --git a/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx index c1b7e6fe9cd0..10114bd68ea4 100644 --- a/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx +++ b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx @@ -14,7 +14,6 @@ import useImportSpreadsheetConfirmModal from '@hooks/useImportSpreadsheetConfirm import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -59,8 +58,6 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag const role = canMemberAssignRole(policy, currentUserLogin, roleFromOnyx) ? roleFromOnyx : CONST.POLICY.ROLE.USER; const [isImporting, setIsImporting] = useState(false); const {isOffline} = useNetwork(); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const personalDetails = usePersonalDetails(); const {setIsClosing} = useCloseImportPage(); @@ -113,7 +110,7 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag setIsClosing(true); setIsImporting(false); closeImportPage(); - if (isWorkflowsImport && isRulesRevampEnabled) { + if (isWorkflowsImport) { Tab.setSelectedTab(CONST.TAB.WORKFLOWS_TAB_TYPE, CONST.TAB.WORKFLOWS.APPROVALS); } Navigation.goBack(isWorkflowsImport ? ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID) : ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)); diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index 36d81ab6807d..c91b5010dbc4 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -8,7 +8,6 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useImportSpreadsheetConfirmModal from '@hooks/useImportSpreadsheetConfirmModal'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import {importPolicyMembers, setImportedSpreadsheetMemberData} from '@libs/actions/Policy/Member'; @@ -42,8 +41,6 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { const policy = usePolicy(policyID); const {login: currentUserLogin = ''} = useCurrentUserPersonalDetails(); const canAssignElevatedRoles = canMemberAssignElevatedRole(policy, currentUserLogin); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); // The same mapping screen is reused for the Members importer and the Workflows importer. When it is reached from the // Workflows page we keep the user in the Workflows context (title + back + return + confirmation navigation). @@ -100,7 +97,7 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { // reopens on the tab the user left, so point it at Approvals — otherwise the imported workflows are hidden behind it. const navigateBackToMembers = () => { const returnRoute = isWorkflowsImport ? ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID) : ROUTES.WORKSPACE_MEMBERS.getRoute(policyID); - if (isWorkflowsImport && isRulesRevampEnabled) { + if (isWorkflowsImport) { Tab.setSelectedTab(CONST.TAB.WORKFLOWS_TAB_TYPE, CONST.TAB.WORKFLOWS.APPROVALS); } Navigation.goBack(returnRoute, {waitForTransition: true}); diff --git a/src/pages/workspace/rules/AgentRules/AddAgentRulePage.tsx b/src/pages/workspace/rules/AgentRules/AddAgentRulePage.tsx index 39b01e56f079..01c16a4d794a 100644 --- a/src/pages/workspace/rules/AgentRules/AddAgentRulePage.tsx +++ b/src/pages/workspace/rules/AgentRules/AddAgentRulePage.tsx @@ -67,7 +67,6 @@ function AddAgentRulePage({ const {isOffline} = useNetwork(); const {isBetaEnabled} = usePermissions(); const isCustomAgentEnabled = isBetaEnabled(CONST.BETAS.CUSTOM_AGENT); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const policy = usePolicy(policyID); const linkPressedRef = useRef(false); const {showConfirmModal, closeModal} = useConfirmModal(); @@ -138,13 +137,8 @@ function AddAgentRulePage({ }; const navigateBackToAgentsTab = () => { - if (isRulesRevampEnabled) { - Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, CONST.TAB.RULES.AGENTS); - Navigation.goBack(ROUTES.WORKSPACE_RULES.getRoute(policyID)); - return; - } - - Navigation.goBack(); + Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, CONST.TAB.RULES.AGENTS); + Navigation.goBack(ROUTES.WORKSPACE_RULES.getRoute(policyID)); }; const saveRule = (values: FormOnyxValues): void => { @@ -164,9 +158,7 @@ function AddAgentRulePage({ closeModal(); }; - if (isRulesRevampEnabled) { - Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, CONST.TAB.RULES.AGENTS); - } + Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, CONST.TAB.RULES.AGENTS); Navigation.dismissModal({ afterTransition: () => { @@ -181,7 +173,7 @@ function AddAgentRulePage({ /> ), - confirmText: isRulesRevampEnabled ? translate('workspace.rules.agentRules.gotIt') : translate('common.buttonConfirm'), + confirmText: translate('workspace.rules.agentRules.gotIt'), shouldShowCancelButton: false, shouldUseSuccessStyleForConfirm: true, iconSource: BotAvatarBlue, @@ -218,7 +210,7 @@ function AddAgentRulePage({ shouldEnableMaxHeight > diff --git a/src/pages/workspace/rules/AgentRules/AddAgentRuleWriteTab.tsx b/src/pages/workspace/rules/AgentRules/AddAgentRuleWriteTab.tsx index 41b870ab3e38..9dfce55c223d 100644 --- a/src/pages/workspace/rules/AgentRules/AddAgentRuleWriteTab.tsx +++ b/src/pages/workspace/rules/AgentRules/AddAgentRuleWriteTab.tsx @@ -9,7 +9,6 @@ import TextInput from '@components/TextInput'; import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode'; import useLocalize from '@hooks/useLocalize'; -import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; @@ -32,11 +31,9 @@ function AddAgentRuleWriteTab({onSave}: AddAgentRuleWriteTabProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const shouldUseScrollableLayout = useIsInLandscapeMode(); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); - const shouldUseExpandedRevampFormLayout = isRulesRevampEnabled && !shouldUseScrollableLayout; + const shouldUseExpandedRevampFormLayout = !shouldUseScrollableLayout; const formRef = useRef(null); - const describeRuleLabel = isRulesRevampEnabled ? translate('workspace.rules.agentRules.describeRuleForConcierge') : translate('workspace.rules.agentRules.describeRuleTitle'); + const describeRuleLabel = translate('workspace.rules.agentRules.describeRuleForConcierge'); const submitFormOnModEnter = (event: TextInputKeyPressEvent | KeyboardEvent) => { if (!('key' in event)) { @@ -67,7 +64,7 @@ function AddAgentRuleWriteTab({onSave}: AddAgentRuleWriteTabProps) { formID={ONYXKEYS.FORMS.ADD_AGENT_RULE_FORM} validate={validate} onSubmit={onSave} - submitButtonText={isRulesRevampEnabled ? translate('workspace.rules.agentRules.createRule') : translate('common.save')} + submitButtonText={translate('workspace.rules.agentRules.createRule')} style={[styles.flex1, styles.ph5]} shouldUseScrollView={shouldUseScrollableLayout} submitFlexEnabled={shouldUseScrollableLayout ? undefined : false} @@ -78,12 +75,6 @@ function AddAgentRuleWriteTab({onSave}: AddAgentRuleWriteTabProps) { keyboardSubmitBehavior={CONST.KEYBOARD_SUBMIT_BEHAVIOR.SUBMIT_ONLY} > - {!isRulesRevampEnabled && ( - <> - {translate('workspace.rules.agentRules.describeRuleHeadline')} - {translate('workspace.rules.agentRules.describeRuleForConcierge')} - - )} (null); - const describeRuleLabel = isRulesRevampEnabled ? translate('workspace.rules.agentRules.describeRuleForConcierge') : translate('workspace.rules.agentRules.describeRuleTitle'); + const describeRuleLabel = translate('workspace.rules.agentRules.describeRuleForConcierge'); const submitFormOnModEnter = (event: TextInputKeyPressEvent | KeyboardEvent) => { if (!('key' in event)) { diff --git a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleAmountPageBase.tsx b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleAmountPageBase.tsx index 4bd701268a14..0d60d0a55d71 100644 --- a/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleAmountPageBase.tsx +++ b/src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleAmountPageBase.tsx @@ -12,7 +12,6 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; 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'; @@ -49,8 +48,6 @@ function FlagForReviewRuleAmountPageBase({policyID, categoryName, isCategoryLock const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); const {canWrite: canWriteRules} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.RULES); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const policyCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD; const categoryScopedBackPath = useDynamicBackPath(DYNAMIC_ROUTES.WORKSPACE_CATEGORY_RULES_FLAG_FOR_REVIEW_AMOUNT.path); @@ -112,7 +109,7 @@ function FlagForReviewRuleAmountPageBase({policyID, categoryName, isCategoryLock featureName={CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED} accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} policyFeature={CONST.POLICY.POLICY_FEATURE.RULES} - shouldBeBlocked={!isRulesRevampEnabled || !canWriteRules} + shouldBeBlocked={!canWriteRules} > void | Promise) => (() => void | Promise) | undefined; -}; - -type IndividualExpenseRulesSectionSubtitleProps = { - policy?: Policy; - translate: LocaleContextProps['translate']; - environmentURL: string; -}; - -type IndividualExpenseRulesMenuItem = { - title: string; - descriptionTranslationKey: TranslationPaths; - action: () => void; - pendingAction?: PendingAction; -}; - -function IndividualExpenseRulesSectionSubtitle({policy, translate, environmentURL}: IndividualExpenseRulesSectionSubtitleProps) { - const policyID = policy?.id; - - const categoriesPageLink = useMemo(() => { - if (policy?.areCategoriesEnabled) { - return `${environmentURL}/${ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)}`; - } - - return `${environmentURL}/${ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)}`; - }, [policy?.areCategoriesEnabled, policyID, environmentURL]); - - const tagsPageLink = useMemo(() => { - if (policy?.areTagsEnabled) { - return `${environmentURL}/${ROUTES.WORKSPACE_TAGS.getRoute(policyID)}`; - } - - return `${environmentURL}/${ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)}`; - }, [policy?.areTagsEnabled, policyID, environmentURL]); - - return ; -} - -function IndividualExpenseRulesSection({policyID, canWriteRules, withReadOnlyFallback}: IndividualExpenseRulesSectionProps) { - const {convertToDisplayString} = useCurrencyListActions(); - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const policy = usePolicy(policyID); - const {environmentURL} = useEnvironment(); - - const policyCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD; - - const handleAttendeeTrackingToggle = useCallback( - (newValue: boolean) => { - setPolicyAttendeeTrackingEnabled(policyID, newValue, policy?.isAttendeeTrackingEnabled); - }, - [policyID, policy?.isAttendeeTrackingEnabled], - ); - - const maxExpenseAmountNoReceiptText = useMemo(() => { - if (policy?.maxExpenseAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE) { - return ''; - } - - return convertToDisplayString(policy?.maxExpenseAmountNoReceipt, policyCurrency); - }, [convertToDisplayString, policy?.maxExpenseAmountNoReceipt, policyCurrency]); - - const maxExpenseAmountText = useMemo(() => { - if (policy?.maxExpenseAmount === CONST.DISABLED_MAX_EXPENSE_VALUE) { - return ''; - } - - return convertToDisplayString(policy?.maxExpenseAmount, policyCurrency); - }, [convertToDisplayString, policy?.maxExpenseAmount, policyCurrency]); - - const maxExpenseAgeText = useMemo(() => { - if (policy?.maxExpenseAge === CONST.DISABLED_MAX_EXPENSE_VALUE) { - return ''; - } - - return translate('workspace.rules.individualExpenseRules.maxExpenseAgeDays', {count: policy?.maxExpenseAge ?? 0}); - }, [policy?.maxExpenseAge, translate]); - - const reimbursableMode = getCashExpenseReimbursableMode(policy) ?? CONST.POLICY.CASH_EXPENSE_REIMBURSEMENT_CHOICES.REIMBURSABLE_DEFAULT; - const reimbursableModeText = translate(`workspace.rules.individualExpenseRules.${reimbursableMode}`); - const billableModeText = translate(`workspace.rules.individualExpenseRules.${policy?.defaultBillable ? 'billable' : 'nonBillable'}`); - - const prohibitedExpenses = useMemo(() => { - // Otherwise return which expenses are prohibited comma separated - const prohibitedExpensesList = []; - if (policy?.prohibitedExpenses?.adultEntertainment) { - prohibitedExpensesList.push(translate('workspace.rules.individualExpenseRules.adultEntertainment')); - } - - if (policy?.prohibitedExpenses?.alcohol) { - prohibitedExpensesList.push(translate('workspace.rules.individualExpenseRules.alcohol')); - } - - if (policy?.prohibitedExpenses?.gambling) { - prohibitedExpensesList.push(translate('workspace.rules.individualExpenseRules.gambling')); - } - - if (policy?.prohibitedExpenses?.hotelIncidentals) { - prohibitedExpensesList.push(translate('workspace.rules.individualExpenseRules.hotelIncidentals')); - } - - if (policy?.prohibitedExpenses?.tobacco) { - prohibitedExpensesList.push(translate('workspace.rules.individualExpenseRules.tobacco')); - } - - if (policy?.prohibitedExpenses?.handwrittenReceipt) { - prohibitedExpensesList.push(translate('workspace.rules.individualExpenseRules.handwrittenReceipt')); - } - - if (policy?.prohibitedExpenses?.giftCard) { - prohibitedExpensesList.push(translate('workspace.rules.individualExpenseRules.giftCard')); - } - - // If no expenses are prohibited, return empty string - if (!prohibitedExpensesList.length) { - return ''; - } - - return prohibitedExpensesList.join(', '); - }, [policy?.prohibitedExpenses, translate]); - - const maxExpenseAmountNoItemizedReceiptText = useMemo(() => { - if (policy?.maxExpenseAmountNoItemizedReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE || policy?.maxExpenseAmountNoItemizedReceipt === undefined) { - return ''; - } - - return convertToDisplayString(policy?.maxExpenseAmountNoItemizedReceipt, policyCurrency); - }, [convertToDisplayString, policy?.maxExpenseAmountNoItemizedReceipt, policyCurrency]); - - const individualExpenseRulesItems: IndividualExpenseRulesMenuItem[] = [ - { - title: maxExpenseAmountNoReceiptText, - descriptionTranslationKey: 'workspace.rules.individualExpenseRules.receiptRequiredAmount', - action: () => Navigation.navigate(ROUTES.RULES_RECEIPT_REQUIRED_AMOUNT.getRoute(policyID)), - pendingAction: policy?.pendingFields?.maxExpenseAmountNoReceipt, - }, - { - title: maxExpenseAmountNoItemizedReceiptText, - descriptionTranslationKey: 'workspace.rules.individualExpenseRules.itemizedReceiptRequiredAmount', - action: () => Navigation.navigate(ROUTES.RULES_ITEMIZED_RECEIPT_REQUIRED_AMOUNT.getRoute(policyID)), - pendingAction: policy?.pendingFields?.maxExpenseAmountNoItemizedReceipt, - }, - { - title: maxExpenseAmountText, - descriptionTranslationKey: 'workspace.rules.individualExpenseRules.maxExpenseAmount', - action: () => Navigation.navigate(ROUTES.RULES_MAX_EXPENSE_AMOUNT.getRoute(policyID)), - pendingAction: policy?.pendingFields?.maxExpenseAmount, - }, - { - title: maxExpenseAgeText, - descriptionTranslationKey: 'workspace.rules.individualExpenseRules.maxExpenseAge', - action: () => Navigation.navigate(ROUTES.RULES_MAX_EXPENSE_AGE.getRoute(policyID)), - pendingAction: policy?.pendingFields?.maxExpenseAge, - }, - { - title: reimbursableModeText, - descriptionTranslationKey: 'workspace.rules.individualExpenseRules.cashExpenseDefault', - action: () => Navigation.navigate(ROUTES.RULES_REIMBURSABLE_DEFAULT.getRoute(policyID)), - pendingAction: policy?.pendingFields?.defaultReimbursable, - }, - { - title: billableModeText, - descriptionTranslationKey: 'workspace.rules.individualExpenseRules.billableDefault', - action: () => Navigation.navigate(ROUTES.RULES_BILLABLE_DEFAULT.getRoute(policyID)), - pendingAction: policy?.pendingFields?.defaultBillable, - }, - ]; - - individualExpenseRulesItems.push({ - title: prohibitedExpenses, - descriptionTranslationKey: 'workspace.rules.individualExpenseRules.prohibitedExpenses', - action: () => Navigation.navigate(ROUTES.RULES_PROHIBITED_DEFAULT.getRoute(policyID)), - pendingAction: !isEmptyObject(policy?.prohibitedExpenses?.pendingFields) ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : undefined, - }); - - const areEReceiptsEnabled = policy?.eReceipts ?? false; - const requireCompanyCardsEnabled = policy?.requireCompanyCardsEnabled ?? false; - const disableRequireCompanyCardToggle = !policy?.areCompanyCardsEnabled && !policy?.areExpensifyCardsEnabled; - - const isAttendeeTrackingEnabledForPolicy = isAttendeeTrackingEnabled(policy); - - return ( -
( - - )} - titleStyles={styles.accountSettingsSectionTitle} - > - - {individualExpenseRulesItems.map((item) => ( - - - - ))} - (canWriteRules && policy ? setPolicyRequireCompanyCardsEnabled(policy, !requireCompanyCardsEnabled) : undefined)} - /> - - (canWriteRules ? setWorkspaceEReceiptsEnabled(policyID, !areEReceiptsEnabled, policy?.eReceipts) : undefined)} - pendingAction={policy?.pendingFields?.eReceipts} - /> - (canWriteRules ? handleAttendeeTrackingToggle(!isAttendeeTrackingEnabledForPolicy) : undefined)} - pendingAction={policy?.pendingFields?.isAttendeeTrackingEnabled} - /> - - -
- ); -} - -export default IndividualExpenseRulesSection; diff --git a/src/pages/workspace/rules/MerchantRules/AddMerchantRulePage.tsx b/src/pages/workspace/rules/MerchantRules/AddMerchantRulePage.tsx index 74693dcbfb3c..6ad56256935c 100644 --- a/src/pages/workspace/rules/MerchantRules/AddMerchantRulePage.tsx +++ b/src/pages/workspace/rules/MerchantRules/AddMerchantRulePage.tsx @@ -14,7 +14,6 @@ function AddMerchantRulePage({route}: AddMerchantRulePageProps) { ); diff --git a/src/pages/workspace/rules/MerchantRules/EditMerchantRulePage.tsx b/src/pages/workspace/rules/MerchantRules/EditMerchantRulePage.tsx index 3a3c4a10ca11..a970d111c1d4 100644 --- a/src/pages/workspace/rules/MerchantRules/EditMerchantRulePage.tsx +++ b/src/pages/workspace/rules/MerchantRules/EditMerchantRulePage.tsx @@ -14,7 +14,6 @@ function EditMerchantRulePage({route}: EditMerchantRulePageProps) { ); diff --git a/src/pages/workspace/rules/MerchantRules/ImportedMerchantRulesPage.tsx b/src/pages/workspace/rules/MerchantRules/ImportedMerchantRulesPage.tsx index dc488a12d0c3..d4e14f0d5ed2 100644 --- a/src/pages/workspace/rules/MerchantRules/ImportedMerchantRulesPage.tsx +++ b/src/pages/workspace/rules/MerchantRules/ImportedMerchantRulesPage.tsx @@ -8,7 +8,6 @@ import useImportSpreadsheetConfirmModal from '@hooks/useImportSpreadsheetConfirm import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import {openPolicyCategoriesPage} from '@libs/actions/Policy/Category'; @@ -254,8 +253,6 @@ function ImportedMerchantRulesPage({route}: ImportedMerchantRulesPageProps) { const policyID = route.params.policyID; const policy = usePolicy(policyID); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); // Fetch categories if they're not loaded (e.g. after a cache clear) so imported category cells are // validated against the policy's real category list instead of an empty one @@ -316,10 +313,8 @@ function ImportedMerchantRulesPage({route}: ImportedMerchantRulesPageProps) { const closeImportPageAndModal = () => { setIsClosing(true); setIsImportingRules(false); - if (isRulesRevampEnabled) { - // Import can start from any tab, so land on the one holding the imported rules. - Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, CONST.TAB.RULES.EXPENSE_DEFAULTS); - } + // Import can start from any tab, so land on the one holding the imported rules. + Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, CONST.TAB.RULES.EXPENSE_DEFAULTS); Navigation.goBack(ROUTES.WORKSPACE_RULES.getRoute(policyID)); }; diff --git a/src/pages/workspace/rules/MerchantRules/MerchantRulePageBase.tsx b/src/pages/workspace/rules/MerchantRules/MerchantRulePageBase.tsx index a4ee32094e91..bf016cacfffb 100644 --- a/src/pages/workspace/rules/MerchantRules/MerchantRulePageBase.tsx +++ b/src/pages/workspace/rules/MerchantRules/MerchantRulePageBase.tsx @@ -41,7 +41,6 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {MerchantRuleForm} from '@src/types/form'; @@ -62,7 +61,6 @@ type MerchantRulePageBaseProps = { ruleID?: string; /** Pre-scopes the category default when creating a rule (e.g. from the category details RHP). */ initialCategoryName?: string; - titleKey: TranslationPaths; testID: string; }; @@ -111,7 +109,7 @@ const getErrorMessage = (translate: LocalizedTranslate, form?: MerchantRuleForm) return translate('workspace.rules.merchantRules.confirmError'); }; -function MerchantRulePageBase({policyID, ruleID, initialCategoryName, titleKey, testID}: MerchantRulePageBaseProps) { +function MerchantRulePageBase({policyID, ruleID, initialCategoryName, testID}: MerchantRulePageBaseProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policy = usePolicy(policyID); @@ -121,9 +119,7 @@ function MerchantRulePageBase({policyID, ruleID, initialCategoryName, titleKey, const isEditing = !!ruleID; const isInLandscapeMode = useIsInLandscapeMode(); const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const icons = useMemoizedLazyExpensifyIcons(['Basket', 'Folder', 'Pencil', 'InvoiceGeneric', 'Tag', 'Paycheck']); - const getItemIcon = (icon: IconAsset) => (isRulesRevampEnabled ? icon : undefined); const [form] = useOnyx(ONYXKEYS.FORMS.MERCHANT_RULE_FORM); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); @@ -307,7 +303,7 @@ function MerchantRulePageBase({policyID, ruleID, initialCategoryName, titleKey, return; } setPolicyCodingRule(policyID, form, policy, ruleID, shouldUpdateMatchingTransactions); - if (!isEditing && isRulesRevampEnabled) { + if (!isEditing) { Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, CONST.TAB.RULES.EXPENSE_DEFAULTS); Navigation.goBack(ROUTES.WORKSPACE_RULES.getRoute(policyID)); } else { @@ -381,7 +377,7 @@ function MerchantRulePageBase({policyID, ruleID, initialCategoryName, titleKey, required: true, title: form?.merchantToMatch, onPress: () => Navigation.navigate(ROUTES.RULES_MERCHANT_MERCHANT_TO_MATCH.getRoute(policyID, ruleID)), - icon: getItemIcon(icons.Basket), + icon: icons.Basket, }, ], }, @@ -393,7 +389,7 @@ function MerchantRulePageBase({policyID, ruleID, initialCategoryName, titleKey, description: translate('common.merchant'), title: form?.merchant, onPress: () => Navigation.navigate(ROUTES.RULES_MERCHANT_MERCHANT.getRoute(policyID, ruleID)), - icon: getItemIcon(icons.Basket), + icon: icons.Basket, }, hasCategories() ? { @@ -401,7 +397,7 @@ function MerchantRulePageBase({policyID, ruleID, initialCategoryName, titleKey, description: translate('common.category'), title: categoryDisplayName, onPress: () => Navigation.navigate(ROUTES.RULES_MERCHANT_CATEGORY.getRoute(policyID, ruleID)), - icon: getItemIcon(icons.Folder), + icon: icons.Folder, } : undefined, ...(hasTags() @@ -414,7 +410,7 @@ function MerchantRulePageBase({policyID, ruleID, initialCategoryName, titleKey, description: name, title: formTag ? getCleanedTagName(formTag) : undefined, onPress: () => Navigation.navigate(ROUTES.RULES_MERCHANT_TAG.getRoute(policyID, ruleID, orderWeight)), - icon: getItemIcon(icons.Tag), + icon: icons.Tag, }; }) : []), @@ -424,7 +420,7 @@ function MerchantRulePageBase({policyID, ruleID, initialCategoryName, titleKey, description: translate('common.tax'), title: taxDisplayName(), onPress: () => Navigation.navigate(ROUTES.RULES_MERCHANT_TAX.getRoute(policyID, ruleID)), - icon: getItemIcon(icons.InvoiceGeneric), + icon: icons.InvoiceGeneric, } : undefined, isVendorFeatureEnabled @@ -433,7 +429,7 @@ function MerchantRulePageBase({policyID, ruleID, initialCategoryName, titleKey, description: vendorFieldLabel, title: vendorDisplayName, onPress: () => Navigation.navigate(ROUTES.RULES_MERCHANT_VENDOR.getRoute(policyID, ruleID)), - icon: getItemIcon(icons.Basket), + icon: icons.Basket, } : undefined, { @@ -442,14 +438,14 @@ function MerchantRulePageBase({policyID, ruleID, initialCategoryName, titleKey, title: form?.comment ? Parser.replace(form.comment) : undefined, onPress: () => Navigation.navigate(ROUTES.RULES_MERCHANT_DESCRIPTION.getRoute(policyID, ruleID)), shouldRenderAsHTML: true, - icon: getItemIcon(icons.Pencil), + icon: icons.Pencil, }, { key: 'reimbursable', description: translate('common.reimbursable'), title: getBooleanTitle(form?.reimbursable, translate), onPress: () => Navigation.navigate(ROUTES.RULES_MERCHANT_REIMBURSABLE.getRoute(policyID, ruleID)), - icon: getItemIcon(icons.Paycheck), + icon: icons.Paycheck, }, isBillableEnabled ? { @@ -457,7 +453,7 @@ function MerchantRulePageBase({policyID, ruleID, initialCategoryName, titleKey, description: translate('common.billable'), title: getBooleanTitle(form?.billable, translate), onPress: () => Navigation.navigate(ROUTES.RULES_MERCHANT_BILLABLE.getRoute(policyID, ruleID)), - icon: getItemIcon(icons.Paycheck), + icon: icons.Paycheck, } : undefined, ], @@ -484,7 +480,7 @@ function MerchantRulePageBase({policyID, ruleID, initialCategoryName, titleKey, const footer = canWriteRules ? ( sections.map((section, sectionIndex) => ( - {isRulesRevampEnabled ? ( - sectionIndex > 0 && ( - <> - - - {translate('workspace.rules.merchantRules.thenApplyFollowingDefaults')} - - - ) - ) : ( - {translate(section.titleTranslationKey)} + {sectionIndex > 0 && ( + <> + + + {translate('workspace.rules.merchantRules.thenApplyFollowingDefaults')} + + )} {section.items.filter((item): item is SectionItemType => !!item).map(renderSectionItem)} @@ -587,14 +579,12 @@ function MerchantRulePageBase({policyID, ruleID, initialCategoryName, titleKey, offlineIndicatorStyle={styles.mtAuto} includeSafeAreaPaddingBottom > - + - {isRulesRevampEnabled && ( - - {translate('workspace.rules.merchantRules.expenseDefaultsSubtitle')} - {translate('workspace.rules.merchantRules.ifAnyExpenseMatches')} - - )} + + {translate('workspace.rules.merchantRules.expenseDefaultsSubtitle')} + {translate('workspace.rules.merchantRules.ifAnyExpenseMatches')} + {renderSections()} {isInLandscapeMode && footer} diff --git a/src/pages/workspace/rules/MerchantRulesSection.tsx b/src/pages/workspace/rules/MerchantRulesSection.tsx deleted file mode 100644 index 730a3fde7b87..000000000000 --- a/src/pages/workspace/rules/MerchantRulesSection.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import Badge from '@components/Badge'; -import MenuItem from '@components/MenuItem'; -import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import SearchBar from '@components/SearchBar'; -import Section from '@components/Section'; -import Text from '@components/Text'; - -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import usePolicy from '@hooks/usePolicy'; -import useSearchResults from '@hooks/useSearchResults'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; - -import {getDecodedCategoryName} from '@libs/CategoryUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import Parser from '@libs/Parser'; -import {findVendorByID, getCommaSeparatedTagNameWithSanitizedColons, getMatchingVendorByID, isMatchingVendorListLoaded, isXeroActiveMatchingSource} from '@libs/PolicyUtils'; -import tokenizedSearch from '@libs/tokenizedSearch'; - -import variables from '@styles/variables'; - -import {clearPolicyCodingRuleErrors} from '@userActions/Policy/Rules'; - -import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; -import type {Policy} from '@src/types/onyx'; -import type {CodingRule} from '@src/types/onyx/Policy'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; - -import React, {useEffect, useMemo} from 'react'; -import {View} from 'react-native'; - -type MerchantRulesSectionProps = { - policyID: string; - canWriteRules: boolean; - showReadOnlyModal: () => void; -}; - -type FieldLabels = { - category: string; - tag: string; - description: string; - tax: string; - vendor: string; -}; - -/** - * Generates a human-readable description of what a coding rule does - */ -function getRuleDescription(rule: CodingRule, translate: ReturnType['translate'], labels: FieldLabels, policy: Policy | undefined): string { - const actions: string[] = []; - - if (rule.merchant) { - actions.push(translate('workspace.rules.merchantRules.ruleSummarySubtitleMerchant', rule.merchant)); - } - if (rule.category) { - actions.push(translate('workspace.rules.merchantRules.ruleSummarySubtitleUpdateField', labels.category, getDecodedCategoryName(rule.category))); - } - if (rule.tag) { - actions.push(translate('workspace.rules.merchantRules.ruleSummarySubtitleUpdateField', labels.tag, getCommaSeparatedTagNameWithSanitizedColons(rule.tag))); - } - if (rule.comment) { - const commentMarkdown = Parser.htmlToMarkdown(rule.comment); - actions.push(translate('workspace.rules.merchantRules.ruleSummarySubtitleUpdateField', labels.description, commentMarkdown)); - } - if (rule.tax?.field_id_TAX?.value) { - actions.push(translate('workspace.rules.merchantRules.ruleSummarySubtitleUpdateField', labels.tax, `${rule.tax.field_id_TAX.name} (${rule.tax.field_id_TAX.value})`)); - } - if (rule.vendorID) { - // Three-tier resolution mirrors the revamp table (see MerchantTypeRulesUtils.ts): - // 1. Active-source hit → vendor name. - // 2. Active source loaded but this ID is not in it → "unavailable" (so a rule pointing at a - // stale/inactive connection never surfaces a misleading name). - // 3. No active vendor-matching source (e.g. admin switched the non-reimbursable export mode - // away from vendor-matching) → permissive lookup across every connection so the historical - // vendor name still renders. Raw external ID as a last resort while connection data hasn't - // hydrated. - const activeVendorName = getMatchingVendorByID(policy, rule.vendorID)?.name; - let vendorValue: string; - if (activeVendorName) { - vendorValue = activeVendorName; - } else if (isMatchingVendorListLoaded(policy)) { - vendorValue = translate(isXeroActiveMatchingSource(policy) ? 'workspace.rules.merchantRules.supplierUnavailable' : 'workspace.rules.merchantRules.vendorUnavailable'); - } else { - vendorValue = findVendorByID(policy, rule.vendorID)?.name ?? rule.vendorID; - } - actions.push(translate('workspace.rules.merchantRules.ruleSummarySubtitleUpdateField', labels.vendor, vendorValue)); - } - if (rule.reimbursable !== undefined) { - actions.push(translate('workspace.rules.merchantRules.ruleSummarySubtitleReimbursable', rule.reimbursable)); - } - if (rule.billable !== undefined) { - actions.push(translate('workspace.rules.merchantRules.ruleSummarySubtitleBillable', rule.billable)); - } - - // Lowercase any subsequent rule after the first one - return actions.map((action, index) => (index === 0 ? action : action.charAt(0).toLowerCase() + action.slice(1))).join(', '); -} - -function MerchantRulesSection({policyID, canWriteRules, showReadOnlyModal}: MerchantRulesSectionProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const theme = useTheme(); - const {isOffline} = useNetwork(); - const policy = usePolicy(policyID); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['Plus']); - - // Hoist iterator-independent translations to avoid redundant calls in the loop - const fieldLabels: FieldLabels = useMemo( - () => ({ - category: translate('common.category').toLowerCase(), - tag: translate('common.tag').toLowerCase(), - description: translate('common.description').toLowerCase(), - tax: translate('common.tax').toLowerCase(), - vendor: translate(isXeroActiveMatchingSource(policy) ? 'common.supplier' : 'common.vendor').toLowerCase(), - }), - [translate, policy], - ); - - const codingRules = policy?.rules?.codingRules; - const hasRules = !isEmptyObject(codingRules); - - const sortedRules = useMemo(() => { - if (!codingRules) { - return []; - } - - return Object.entries(codingRules) - .filter(([, rule]) => !!rule) - .map(([ruleID, rule]) => ({...rule, ruleID})) - .sort((a, b) => { - if (a.created && b.created) { - return a.created < b.created ? 1 : -1; - } - return 0; - }); - }, [codingRules]); - - // Exclude pending-delete rules when online because OfflineWithFeedback hides them visually. - // When offline, keep them so OfflineWithFeedback can show strikethrough styling. - const visibleRules = useMemo(() => sortedRules.filter((rule) => isOffline || rule.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE), [sortedRules, isOffline]); - - const filterRule = (rule: CodingRule & {ruleID: string}, searchInput: string) => tokenizedSearch([rule], searchInput, () => [rule.filters?.right ?? '']).length > 0; - - const [ruleSearchInput, setRuleSearchInput, filteredRules] = useSearchResults(visibleRules, filterRule); - - useEffect(() => { - if (visibleRules.length > CONST.SEARCH_BAR_THRESHOLD) { - return; - } - setRuleSearchInput(''); - }, [visibleRules.length, setRuleSearchInput]); - - const renderTitle = () => ( - - {translate('workspace.rules.merchantRules.title')} - - - ); - - return ( -
- {hasRules && ( - - {visibleRules.length > CONST.SEARCH_BAR_THRESHOLD && ( - - )} - {filteredRules.map((rule) => { - const merchantName = rule.filters?.right ?? ''; - const isExactMatch = rule.filters?.operator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO; - const matchDescription = translate('workspace.rules.merchantRules.ruleSummaryTitle', merchantName, isExactMatch); - const ruleDescription = getRuleDescription(rule, translate, fieldLabels, policy); - - return ( - - clearPolicyCodingRuleErrors(policyID, rule.ruleID, rule)} - > - Navigation.navigate(ROUTES.RULES_MERCHANT_EDIT.getRoute(policyID, rule.ruleID))} - sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.MERCHANT_RULE_ITEM} - disabled={rule.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE} - /> - - - ); - })} - - )} - { - if (!canWriteRules) { - showReadOnlyModal(); - return; - } - Navigation.navigate(ROUTES.RULES_MERCHANT_NEW.getRoute(policyID)); - }} - sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.ADD_MERCHANT_RULE} - /> -
- ); -} - -MerchantRulesSection.displayName = 'MerchantRulesSection'; - -export default MerchantRulesSection; -export {getRuleDescription}; diff --git a/src/pages/workspace/rules/MerchantTypeRules/MerchantTypeRuleCategoryPage.tsx b/src/pages/workspace/rules/MerchantTypeRules/MerchantTypeRuleCategoryPage.tsx index e4fe8eb826a3..03c348643189 100644 --- a/src/pages/workspace/rules/MerchantTypeRules/MerchantTypeRuleCategoryPage.tsx +++ b/src/pages/workspace/rules/MerchantTypeRules/MerchantTypeRuleCategoryPage.tsx @@ -1,7 +1,6 @@ import RuleSelectionBase from '@components/Rule/RuleSelectionBase'; import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; @@ -27,8 +26,6 @@ function MerchantTypeRuleCategoryPage({route}: MerchantTypeRuleCategoryPageProps const {policyID, groupID} = route.params; const policy = usePolicy(policyID); const {canWrite: canWriteRules} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.RULES); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const [form] = useOnyx(ONYXKEYS.FORMS.MERCHANT_TYPE_RULE_FORM); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); @@ -55,7 +52,7 @@ function MerchantTypeRuleCategoryPage({route}: MerchantTypeRuleCategoryPageProps featureName={CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED} accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} policyFeature={CONST.POLICY.POLICY_FEATURE.RULES} - shouldBeBlocked={!isRulesRevampEnabled || !canWriteRules} + shouldBeBlocked={!canWriteRules} > ; - -const agentsRulesBannerDismissedSelector = (value: OnyxEntry): boolean => !!value?.[CONST.AGENTS_RULES_BANNER]; - -function PolicyRulesPage(props: PolicyRulesPageProps) { - const {route} = props; - const {translate} = useLocalize(); - const {policyID} = route.params; - const policy = usePolicy(policyID); - useWorkspaceDocumentTitle(policy?.name, 'workspace.common.rules'); - const styles = useThemeStyles(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine(); - const icons = useMemoizedLazyExpensifyIcons(['Table']); - const {canWrite: canWriteRules, showReadOnlyModal, withReadOnlyFallback} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.RULES); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); - const isCustomAgentBetaEnabled = isBetaEnabled(CONST.BETAS.CUSTOM_AGENT); - const [isAgentsRulesBannerDismissed = false] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {selector: agentsRulesBannerDismissedSelector}); - - const fetchRules = useCallback(() => { - openPolicyRulesPage(policyID); - }, [policyID]); - - useEffect(() => { - // PolicyRulesPageRevamp fetches rules on its own mount — skip here to avoid duplicate OpenPolicyRulesPage calls. - if (isRulesRevampEnabled) { - return; - } - fetchRules(); - }, [fetchRules, isRulesRevampEnabled]); - - if (isRulesRevampEnabled) { - return ; - } - - const moreOptions: Array>> = [ - getImportMerchantRulesOption({policyID, canWriteRules, showReadOnlyModal, translate, icon: icons.Table}), - ]; - - const headerButtons = ( - {}} - shouldAlwaysShowDropdownMenu - customText={translate('common.more')} - options={moreOptions} - isSplitButton={false} - wrapperStyle={styles.flexGrow0} - style={[shouldDisplayButtonsInSeparateLine && styles.w100]} - sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.IMPORT_MERCHANT_RULES} - /> - ); - - return ( - - - {shouldDisplayButtonsInSeparateLine && {headerButtons}} - - {isCustomAgentBetaEnabled && !isAgentsRulesBannerDismissed && ( - { - if (!canWriteRules) { - showReadOnlyModal(); - return; - } - Navigation.navigate(ROUTES.RULES_AGENT_NEW.getRoute(policyID)); - }} - ctaSentryLabel={CONST.SENTRY_LABEL.AGENTS_RULES_BANNER.CTA} - onDismiss={() => dismissProductTraining(CONST.AGENTS_RULES_BANNER, true)} - dismissSentryLabel={CONST.SENTRY_LABEL.AGENTS_RULES_BANNER.DISMISS} - style={[styles.mh5, styles.mb5]} - /> - )} - - - {!!policy?.areExpensifyCardsEnabled && ( - - )} - {isCustomAgentBetaEnabled && ( - - )} - - - - ); -} - -export default PolicyRulesPage; diff --git a/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx b/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx index 8eabcd8f4e76..f27e3b92b8aa 100644 --- a/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx +++ b/src/pages/workspace/rules/PolicyRulesPageRevamp.tsx @@ -84,7 +84,6 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { const icons = useMemoizedLazyExpensifyIcons(['Plus', 'Feed', 'CreditCardExclamation', 'DocumentMagicWand', 'Task', 'Flag', 'Bot', 'Trashcan', 'Table']); const {canWrite: canWriteRules, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.RULES); const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const isCustomAgentBetaEnabled = isBetaEnabled(CONST.BETAS.CUSTOM_AGENT); const isMobileSelectionModeEnabled = useMobileSelectionMode(); const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine(); @@ -352,7 +351,6 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { featureName={CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED} accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyFeature={CONST.POLICY.POLICY_FEATURE.RULES} - shouldBeBlocked={!isRulesRevampEnabled} > { if (isCollect && value && navigateToBillableUpgrade()) { @@ -108,24 +104,22 @@ function RulesBillableDefaultPage({ testID="RulesBillableDefaultPage" > Navigation.goBack()} /> - + {translate('workspace.rules.individualExpenseRules.billableDefaultDescription')} - {isRevamp && ( - toggleBillableExpenses(policy)} - /> - )} - {shouldShowBillableModeList && ( + toggleBillableExpenses(policy)} + /> + {isBillableTrackingEnabled && ( Navigation.goBack()} /> Navigation.goBack()} /> getProhibitedExpensesState(policy?.prohibitedExpenses), [policy?.prohibitedExpenses]); const [draftProhibitedExpenses, setDraftProhibitedExpenses] = useState(initialProhibitedExpenses); @@ -120,21 +117,21 @@ function RulesProhibitedDefaultPage({ testID="RulesProhibitedDefaultPage" > Navigation.goBack()} /> - + {translate('workspace.rules.individualExpenseRules.prohibitedDefaultDescription')} {PROHIBITED_EXPENSE_KEYS.map((prohibitedExpense) => { const switchComponent = ( - + { - if (isRevamp) { - handleToggle(prohibitedExpense); - return; - } - setPolicyProhibitedExpense(policyID, prohibitedExpense, policy?.prohibitedExpenses); - }} + onToggle={() => handleToggle(prohibitedExpense)} /> ); @@ -166,21 +157,19 @@ function RulesProhibitedDefaultPage({ ); })} - {isRevamp && ( - + - - )} + {translate('common.save')} + + ); diff --git a/src/pages/workspace/rules/RulesReimbursableDefaultPage.tsx b/src/pages/workspace/rules/RulesReimbursableDefaultPage.tsx index 04ac4d8d2476..a18116bb3213 100644 --- a/src/pages/workspace/rules/RulesReimbursableDefaultPage.tsx +++ b/src/pages/workspace/rules/RulesReimbursableDefaultPage.tsx @@ -5,7 +5,6 @@ import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelec import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -31,8 +30,6 @@ function RulesReimbursableDefaultPage({ const {translate} = useLocalize(); const styles = useThemeStyles(); const policy = usePolicy(policyID); - const {isBetaEnabled} = usePermissions(); - const isRevamp = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const reimbursableMode = getCashExpenseReimbursableMode(policy); @@ -78,7 +75,7 @@ function RulesReimbursableDefaultPage({ testID="RulesReimbursableDefaultPage" > Navigation.goBack()} /> diff --git a/src/pages/workspace/rules/RulesRequireFieldsPage.tsx b/src/pages/workspace/rules/RulesRequireFieldsPage.tsx index 2a8498920974..8c94c70ad33f 100644 --- a/src/pages/workspace/rules/RulesRequireFieldsPage.tsx +++ b/src/pages/workspace/rules/RulesRequireFieldsPage.tsx @@ -8,7 +8,6 @@ import ScrollView from '@components/ScrollView'; import useConfirmModal from '@hooks/useConfirmModal'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; import usePolicyData from '@hooks/usePolicyData'; import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -47,11 +46,9 @@ function RulesRequireFieldsPage({ const {policy} = policyData; const {translate} = useLocalize(); const styles = useThemeStyles(); - const {isBetaEnabled} = usePermissions(); const {showConfirmModal} = useConfirmModal(); // The self-heal below writes to the server, so it needs the same Tags write check the Tags table uses. const {canWrite: canWriteTags} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.TAGS); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); // Per WorkspaceMoreFeaturesPage: only accounting connections own these features, not the HR ones in policy.connections. @@ -96,8 +93,8 @@ function RulesRequireFieldsPage({ }, [policyID, policyTags]); useEffect(() => { - // Same self-heal as the Tags table: a required level with no enabled tags can never be satisfied, and it still - // fires a violation, so clear it. Pending edits are skipped so this can't fight a toggle the admin just flipped. + // Same self-heal as the Tags table: a required level with no enabled tags is a rule the workspace can't meet, so + // clear it rather than leave it set. Pending edits are skipped so this can't fight a toggle the admin just flipped. if (!canWriteTags || !hasPerLevelTagRequired) { return; } @@ -179,7 +176,8 @@ function RulesRequireFieldsPage({ setTagRequiredByLevel((previous) => ({...previous, [tagList.orderWeight]: required})); }; - // Lock only when the feature is off (or categories are accounting-controlled). No enabled items just disables, no lock/modal. + // Only the feature being off (or categories being accounting-controlled) gets a tap-to-unlock modal. Switch draws its lock + // icon for anything disabled, so a row disabled purely for having nothing to require still needs its own tooltip. const shouldShowCategoryLock = isCategoryFeatureDisabled || isConnectedToAccounting; const shouldShowTagLock = isTagFeatureDisabled; @@ -190,7 +188,8 @@ function RulesRequireFieldsPage({ const categoryDisabledText = (() => { if (!shouldShowCategoryLock) { - return undefined; + // Feature is on, so the only thing left that disables this row is having no categories to require. + return translate('workspace.rules.individualExpenseRules.noCategoriesToRequirePrompt'); } if (isConnectedToAccounting) { return translate('workspace.moreFeatures.connectionsWarningModal.featureEnabledText'); @@ -226,6 +225,7 @@ function RulesRequireFieldsPage({ if (action !== ModalActions.CONFIRM) { return; } + enablePolicyCategories(policyData, true, false); setWorkspaceRequiresCategory(policyData, true); setCategoryRequired(true); @@ -233,7 +233,8 @@ function RulesRequireFieldsPage({ const tagDisabledText = (() => { if (!shouldShowTagLock) { - return undefined; + // Feature is on, so the only thing left that disables this row is having no tags to require. + return translate('workspace.rules.individualExpenseRules.noTagsToRequirePrompt'); } if (isConnectedToAccounting) { return translate('workspace.moreFeatures.connectionsWarningModal.featureEnabledText'); @@ -241,8 +242,7 @@ function RulesRequireFieldsPage({ return translate('workspace.rules.individualExpenseRules.enableTagsToUnlockPrompt'); })(); - /** Pass orderWeight from a per-level row so only that level is required; omit it for the policy-wide row. */ - const promptEnableTagsForRequireTag = async (orderWeight?: number) => { + const promptEnableTags = async () => { if (!isTagFeatureDisabled) { return; } @@ -264,23 +264,17 @@ function RulesRequireFieldsPage({ const {action} = await showConfirmModal({ title: translate('workspace.rules.individualExpenseRules.enableTagsToUnlockTitle'), - prompt: translate('workspace.rules.individualExpenseRules.enableTagsAndRequirePrompt'), + prompt: translate('workspace.rules.individualExpenseRules.enableTagsPrompt'), confirmText: translate('common.ok'), cancelText: translate('common.cancel'), }); if (action !== ModalActions.CONFIRM) { return; } - enablePolicyTags(policyData, true); - - if (orderWeight !== undefined) { - // setPolicyRequiresTag would require every list, not just this level. - setPolicyTagsRequired(policyData, true, orderWeight); - return; - } - setPolicyRequiresTag(policyData, true); - setTagRequired(true); + // Only turn the feature on. Enabling Tags seeds an empty tag list, so requiring a tag here would set a rule the + // workspace can't meet yet; the admin flips Required themselves once a tag exists. + enablePolicyTags(policyData, true); }; return ( @@ -290,7 +284,6 @@ function RulesRequireFieldsPage({ featureName={CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED} policyFeature={CONST.POLICY.POLICY_FEATURE.RULES} policyFeatureAccess={CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE} - shouldBeBlocked={!isRulesRevampEnabled} > promptEnableTagsForRequireTag(tagList.orderWeight) : undefined} + disabledAction={shouldShowTagLock ? promptEnableTags : undefined} pendingAction={tagList.pendingFields?.required} errors={tagList.errorFields?.required ?? undefined} onCloseError={() => clearPolicyTagListErrorField({policyID, tagListIndex, errorField: 'required', policyTags})} @@ -357,7 +350,7 @@ function RulesRequireFieldsPage({ disabled={isTagToggleDisabled} showLockIcon={shouldShowTagLock} disabledText={tagDisabledText} - disabledAction={shouldShowTagLock ? promptEnableTagsForRequireTag : undefined} + disabledAction={shouldShowTagLock ? promptEnableTags : undefined} pendingAction={policy?.pendingFields?.requiresTag} errors={policy?.errorFields?.requiresTag ?? undefined} onCloseError={() => clearPolicyErrorField(policyID, 'requiresTag')} diff --git a/src/pages/workspace/rules/RulesRequireReceiptsPage.tsx b/src/pages/workspace/rules/RulesRequireReceiptsPage.tsx index fbab89d5d2bf..ab8cc42dc487 100644 --- a/src/pages/workspace/rules/RulesRequireReceiptsPage.tsx +++ b/src/pages/workspace/rules/RulesRequireReceiptsPage.tsx @@ -8,7 +8,6 @@ import Text from '@components/Text'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useLocalize from '@hooks/useLocalize'; -import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -45,8 +44,6 @@ function RulesRequireReceiptsPage({ const policy = usePolicy(policyID); const {translate} = useLocalize(); const styles = useThemeStyles(); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const {getCurrencyDecimals} = useCurrencyListActions(); const policyCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD; const decimals = getCurrencyDecimals(policyCurrency); @@ -182,7 +179,6 @@ function RulesRequireReceiptsPage({ featureName={CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED} policyFeature={CONST.POLICY.POLICY_FEATURE.RULES} policyFeatureAccess={CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE} - shouldBeBlocked={!isRulesRevampEnabled} > ); diff --git a/src/pages/workspace/rules/SpendRules/EditSpendRulePage.tsx b/src/pages/workspace/rules/SpendRules/EditSpendRulePage.tsx index 5645b41bf80c..99f566a27181 100644 --- a/src/pages/workspace/rules/SpendRules/EditSpendRulePage.tsx +++ b/src/pages/workspace/rules/SpendRules/EditSpendRulePage.tsx @@ -14,7 +14,6 @@ function EditSpendRulePage({route}: EditSpendRulePageProps) { ); diff --git a/src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx b/src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx index f8cbfb4d6cc4..f95d69e20896 100644 --- a/src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx +++ b/src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx @@ -5,7 +5,6 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; -import SpendRuleRestrictionTypeToggle from '@components/SpendRules/SpendRuleRestrictionTypeToggle'; import SpendRuleRestrictionTypeToggleRevamp from '@components/SpendRules/SpendRuleRestrictionTypeToggleRevamp'; import Text from '@components/Text'; @@ -17,7 +16,6 @@ import useDefaultFundID from '@hooks/useDefaultFundID'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; import usePressLoading from '@hooks/usePressLoading'; @@ -54,7 +52,6 @@ import {View} from 'react-native'; type SpendRulePageBaseProps = { policyID: string; ruleID?: string; - titleKey: TranslationPaths; testID: string; /** Where the Control upgrade page should return to. Defaults to the workspace Rules page. */ @@ -74,7 +71,7 @@ function getErrorMessage(hasSelectedCards: boolean, hasAnyRuleApplied: boolean, return ''; } -function SpendRulePageBase({policyID, ruleID, titleKey, testID, upgradeBackTo}: SpendRulePageBaseProps) { +function SpendRulePageBase({policyID, ruleID, testID, upgradeBackTo}: SpendRulePageBaseProps) { const {convertToDisplayString} = useCurrencyListActions(); const styles = useThemeStyles(); const {translate, formatPhoneNumber} = useLocalize(); @@ -84,8 +81,6 @@ function SpendRulePageBase({policyID, ruleID, titleKey, testID, upgradeBackTo}: const {showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.RULES); const canWriteSpendRules = useCanWriteCardSpendRules(policyID); useControlOnlyRuleUpgradeRedirect(policyID, upgradeBackTo); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const icons = useMemoizedLazyExpensifyIcons(['CreditCardHourglass', 'MoneyCircle', 'CoinsButton', 'Basket']); const domainAccountID = useDefaultFundID(policyID); const [spendRuleForm] = useOnyx(ONYXKEYS.FORMS.SPEND_RULE_FORM); @@ -235,7 +230,7 @@ function SpendRulePageBase({policyID, ruleID, titleKey, testID, upgradeBackTo}: setExpensifyCardRule(domainAccountID, isEditingRule ? currentRuleID : rand64(), updatedSpendRuleForm, existingRule); clearDraftSpendRule(); - if (!isEditingRule && isRulesRevampEnabled) { + if (!isEditingRule) { Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, CONST.TAB.RULES.CARD_RESTRICTIONS); Navigation.goBack(ROUTES.WORKSPACE_RULES.getRoute(policyID)); return; @@ -367,7 +362,6 @@ function SpendRulePageBase({policyID, ruleID, titleKey, testID, upgradeBackTo}: const handleRestrictionActionSelect = setSpendRuleRestrictionType; const spendRuleSectionSentryLabel = CONST.SENTRY_LABEL.WORKSPACE.RULES.SPEND_RULE_SECTION_ITEM; - const merchantRuleSectionSentryLabel = CONST.SENTRY_LABEL.WORKSPACE.RULES.MERCHANT_RULE_SECTION_ITEM; const renderEditableMenuItem = ({description, title, onPress, sentryLabel, icon}: {description: string; title: string; onPress: () => void; sentryLabel: string; icon?: IconAsset}) => ( ); - const legacyFormContent = ( - <> - {translate('workspace.rules.spendRules.cardsSectionTitle')} - - {translate('workspace.rules.spendRules.spendRuleSectionTitle')} - {currenciesMenuItem} - {maxAmountMenuItem} - - - - - {!isRestrictMerchantsOff && ( - <> - - - - )} - - ); - return ( - - {isRulesRevampEnabled ? revampFormContent : legacyFormContent} + + {revampFormContent} {canWriteSpendRules && ( )} - {arePolicyRulesEnabled(policy, policyData.categories, isRulesRevampEnabled) && !isMultiLevelTags && ( + {arePolicyRulesEnabled(policy, policyData.categories) && !isMultiLevelTags && ( <> {translate('workspace.tags.tagRules')} diff --git a/src/pages/workspace/tags/DynamicWorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/DynamicWorkspaceViewTagsPage.tsx index d90362fe3207..453de2704769 100644 --- a/src/pages/workspace/tags/DynamicWorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/DynamicWorkspaceViewTagsPage.tsx @@ -16,7 +16,6 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; -import usePermissions from '@hooks/usePermissions'; import usePolicyData from '@hooks/usePolicyData'; import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -25,19 +24,11 @@ import useShouldDisplayButtonsInSeparateLine from '@hooks/useShouldDisplayButton import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; -import { - clearPolicyTagErrors, - clearPolicyTagListErrorField, - clearPolicyTagListErrors, - deletePolicyTags, - openPolicyTagsPage, - setPolicyTagsRequired, - setWorkspaceTagEnabled, -} from '@libs/actions/Policy/Tag'; +import {clearPolicyTagErrors, clearPolicyTagListErrors, deletePolicyTags, openPolicyTagsPage, setPolicyTagsRequired, setWorkspaceTagEnabled} from '@libs/actions/Policy/Tag'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import {isDisablingOrDeletingLastEnabledTag, isMakingLastRequiredTagListOptional} from '@libs/OptionsListUtils'; +import {isDisablingOrDeletingLastEnabledTag} from '@libs/OptionsListUtils'; import { getCleanedTagName, getCountOfEnabledTagsOfList, @@ -50,7 +41,6 @@ import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import CONST from '@src/CONST'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; @@ -77,8 +67,6 @@ function DynamicWorkspaceViewTagsPage({route}: DynamicWorkspaceViewTagsProps) { const styles = useThemeStyles(); const icons = useMemoizedLazyExpensifyIcons(['Close', 'Checkmark', 'Trashcan']); const {translate} = useLocalize(); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); const {showConfirmModal} = useConfirmModal(); const dropdownButtonRef = useRef(null); const isFocused = useIsFocused(); @@ -89,7 +77,7 @@ function DynamicWorkspaceViewTagsPage({route}: DynamicWorkspaceViewTagsProps) { const hasDependentTags = useMemo(() => hasDependentTagsPolicyUtils(policy, policyTags), [policy, policyTags]); const isMultiLevelTags = isMultiLevelTagsPolicyUtils(policyTags); const currentPolicyTag = policyTags?.[currentTagListName]; - const {canWrite: canWriteTags, showReadOnlyModal, withReadOnlyFallback} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.TAGS); + const {canWrite: canWriteTags, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.TAGS); const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.DYNAMIC_SETTINGS_TAG_LIST_VIEW; const backPath = useDynamicBackPath(isQuickSettingsFlow ? DYNAMIC_ROUTES.SETTINGS_TAG_LIST_VIEW.path : DYNAMIC_ROUTES.WORKSPACE_TAG_LIST_VIEW.path); const fetchTags = useCallback(() => { @@ -171,7 +159,6 @@ function DynamicWorkspaceViewTagsPage({route}: DynamicWorkspaceViewTagsProps) { pendingAction: tag.pendingAction, isLocked: !canWriteTags || isDisablingLastEnabledTag, showEnabledSwitch: !hasDependentTags, - showRequiredSwitch: false, action: () => navigateToTagSettings(tag), onToggleEnabled: (enabled: boolean) => { if (isDisablingLastEnabledTag) { @@ -358,55 +345,6 @@ function DynamicWorkspaceViewTagsPage({route}: DynamicWorkspaceViewTagsProps) { {!shouldDisplayButtonsInSeparateLine && headerButtons} {shouldDisplayButtonsInSeparateLine && !!headerButtons && {headerButtons}} - {/* Required is configured from Rules once the revamp is on, so this toggle is pre-revamp only. */} - {!hasDependentTags && !isRulesRevampEnabled && ( - - { - if (!canWriteTags) { - showReadOnlyModal(); - return; - } - - if (!isMultiLevelTags) { - showConfirmModal({ - title: translate('workspace.tags.cannotMakeTagListRequired.title'), - prompt: translate('workspace.tags.cannotMakeTagListRequired.description'), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - }); - return; - } - if (isMakingLastRequiredTagListOptional(policy, policyTags, [currentPolicyTag])) { - showConfirmModal({ - title: translate('workspace.tags.cannotMakeAllTagsOptional.title'), - prompt: translate('workspace.tags.cannotMakeAllTagsOptional.description'), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - }); - return; - } - setPolicyTagsRequired(policyData, on, orderWeight); - }} - pendingAction={currentPolicyTag.pendingFields?.required} - errors={currentPolicyTag?.errorFields?.required ?? undefined} - onCloseError={() => - clearPolicyTagListErrorField({ - policyID, - tagListIndex: orderWeight, - errorField: 'required', - policyTags, - }) - } - disabled={!canWriteTags || (!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled))} - disabledAction={withReadOnlyFallback()} - showLockIcon={!canWriteTags || !isMultiLevelTags || isMakingLastRequiredTagListOptional(policy, policyTags, [currentPolicyTag])} - /> - - )} @@ -425,7 +363,7 @@ function DynamicWorkspaceViewTagsPage({route}: DynamicWorkspaceViewTagsProps) { onPress={navigateToEditTag} shouldShowRightIcon={canWriteTags} interactive={canWriteTags} - wrapperStyle={isRulesRevampEnabled ? styles.mb5 : undefined} + wrapperStyle={styles.mb5} /> {isLoading && ( diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 134874df079c..9d4dce2ed27c 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -21,7 +21,6 @@ import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; import usePolicyData from '@hooks/usePolicyData'; import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -40,7 +39,6 @@ import { openPolicyTagsPage, setPolicyTagsRequired, setWorkspaceTagEnabled, - setWorkspaceTagRequired, } from '@libs/actions/Policy/Tag'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; @@ -114,13 +112,11 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { ); const {canWrite: canWriteTags, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.TAGS); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); - // The revamp moves the multi-level tag settings to Rules, but the GL codes toggle stays here and needs a way in. - const shouldShowTagsSettings = canWriteTags && (!(isRulesRevampEnabled && isMultiLevelTags) || !!policy?.glCodes); + // The multi-level tag settings live in Rules, but the GL codes toggle stays here and needs a way in. + const shouldShowTagsSettings = canWriteTags && (!isMultiLevelTags || !!policy?.glCodes); // Multi-level tag rows only ever offered the Required bulk actions, and those moved to Rules, so selecting them // would open a dropdown with nothing in it. - const isSelectionEnabled = canWriteTags && !hasDependentTags && !(isRulesRevampEnabled && isMultiLevelTags); + const isSelectionEnabled = canWriteTags && !hasDependentTags && !isMultiLevelTags; const canSelectMultiple = isSelectionEnabled && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true); const isControlPolicyWithWideLayout = !shouldUseNarrowLayout && isControlPolicy(policy); const tagApproverEmails = useMemo(() => { @@ -269,15 +265,6 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }); }, [showConfirmModal, translate]); - const showAllTagsOptionalWarning = useCallback(() => { - showConfirmModal({ - title: translate('workspace.tags.cannotMakeAllTagsOptional.title'), - prompt: translate('workspace.tags.cannotMakeAllTagsOptional.description'), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - }); - }, [showConfirmModal, translate]); - const handleTagEnabledToggle = useCallback( (enabled: boolean, tag: PolicyTag) => { if (!canWriteTags) { @@ -295,23 +282,6 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { [canWriteTags, policyTagLists, showAllTagsDisabledWarning, showReadOnlyModal, updateWorkspaceTagEnabled], ); - const handleTagListRequiredToggle = useCallback( - (required: boolean, policyTagList: PolicyTagList) => { - if (!canWriteTags) { - showReadOnlyModal(); - return; - } - - if (!required && isMakingLastRequiredTagListOptional(policy, policyTags, [policyTagList])) { - showAllTagsOptionalWarning(); - return; - } - - updateWorkspaceRequiresTag(required, policyTagList.orderWeight); - }, - [canWriteTags, policy, policyTags, showAllTagsOptionalWarning, showReadOnlyModal, updateWorkspaceRequiresTag], - ); - const navigateToTagSettings = useCallback( (tagValue: string, orderWeight?: number) => { if (orderWeight !== undefined) { @@ -370,10 +340,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { pendingAction: getPendingAction(policyTagList), isLocked: !canWriteTags || isMakingLastRequiredTagListOptional(policy, policyTags, [policyTagList]), showEnabledSwitch: false, - // Required is configured from Rules once the revamp is on. - showRequiredSwitch: !hasDependentTags && !isRulesRevampEnabled, action: () => navigateToTagSettings(policyTagList.name, policyTagList.orderWeight), - onToggleRequired: (required: boolean) => handleTagListRequiredToggle(required, policyTagList), onClose: () => {}, }); @@ -412,7 +379,6 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { pendingAction: tag.pendingAction, isLocked: !canWriteTags || isLastEnabledTagAndEnabled, showEnabledSwitch: true, - showRequiredSwitch: false, action: () => navigateToTagSettings(tag.name), onToggleEnabled: (enabled: boolean) => handleTagEnabledToggle(enabled, tag), onClose: () => clearPolicyTagErrors({policyID, tagName: tag.name, tagListIndex: 0, policyTags}), @@ -423,11 +389,8 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }, [ canWriteTags, handleTagEnabledToggle, - handleTagListRequiredToggle, - hasDependentTags, isMultiLevelTags, isOffline, - isRulesRevampEnabled, navigateToTagSettings, policy, policyID, @@ -571,7 +534,6 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { } const selectedTagsObject = selectedTagKeys.map((key) => policyTagLists.at(0)?.tags?.[key]); - const selectedTagLists = selectedTagKeys.map((selectedTag) => policyTagLists.find((policyTagList) => policyTagList.name === selectedTag)); // Without selection there are no bulk actions, so keep the normal header even if selection mode lingered from elsewhere. if (!canWriteTags || !isSelectionEnabled || (shouldUseNarrowLayout ? !isMobileSelectionModeEnabled : selectedTagKeys.length === 0)) { @@ -691,55 +653,6 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }); } - let requiredTagCount = 0; - const tagListIndexesToMarkRequired: number[] = []; - - let optionalTagCount = 0; - const tagListIndexesToMarkOptional: number[] = []; - - for (const tagName of selectedTagKeys) { - if (tagRowsKeyedByName[tagName]?.required) { - requiredTagCount++; - tagListIndexesToMarkOptional.push(tagRowsKeyedByName[tagName]?.orderWeight ?? 0); - } else { - optionalTagCount++; - tagListIndexesToMarkRequired.push(tagRowsKeyedByName[tagName]?.orderWeight ?? 0); - } - } - - if (requiredTagCount > 0 && !hasDependentTags && isMultiLevelTags && !isRulesRevampEnabled) { - options.push({ - icon: expensifyIcons.Close, - text: translate('workspace.tags.notRequireTags'), - value: CONST.POLICY.BULK_ACTION_TYPES.REQUIRE, - onSelected: () => { - if (isMakingLastRequiredTagListOptional(policy, policyTags, selectedTagLists)) { - showConfirmModal({ - title: translate('workspace.tags.cannotMakeAllTagsOptional.title'), - prompt: translate('workspace.tags.cannotMakeAllTagsOptional.description'), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - }); - return; - } - clearTableSelection(); - setWorkspaceTagRequired(policyData, tagListIndexesToMarkOptional, false); - }, - }); - } - - if (optionalTagCount > 0 && !hasDependentTags && isMultiLevelTags && !isRulesRevampEnabled) { - options.push({ - icon: expensifyIcons.Checkmark, - text: translate(requiredTagCount === 1 ? 'workspace.tags.requireTag' : 'workspace.tags.requireTags'), - value: CONST.POLICY.BULK_ACTION_TYPES.NOT_REQUIRED, - onSelected: () => { - clearTableSelection(); - setWorkspaceTagRequired(policyData, tagListIndexesToMarkRequired, true); - }, - }); - } - return ( [getTagListsUtil(policyTags), isMultiLevelTagsUtil(policyTags)], [policyTags]); const isLoading = !getTagListsUtil(policyTags)?.at(0) || Object.keys(policyTags ?? {}).at(0) === 'undefined'; const {isOffline} = useNetwork(); - const hasEnabledOptions = hasEnabledOptionsUtil(Object.values(policyTags ?? {}).flatMap(({tags}) => Object.values(tags))); - const updateWorkspaceRequiresTag = useCallback( - (value: boolean) => { - setPolicyRequiresTag(policyData, value); - }, - [policyData], - ); const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.DYNAMIC_SETTINGS_TAGS_SETTINGS; const backPath = useDynamicBackPath(DYNAMIC_ROUTES.SETTINGS_TAGS_SETTINGS.path); - const shouldBlockEmptySettings = isRulesRevampEnabled && isMultiLevelTags && !isLoading && !policyData.policy?.glCodes; + // Multi-level tag settings moved to Rules, so the GL codes toggle is the only thing that can keep this page alive. + const shouldBlockEmptySettings = isMultiLevelTags && !isLoading && !policyData.policy?.glCodes; const getTagsSettings = (policy: OnyxEntry) => { const updateShowTagGLCodes = (value: boolean) => { setPolicyShowTagGLCodes(policyID, value, policy?.showTagGLCodes); }; - const hasDependentTags = hasDependentTagsUtil(policy, policyTags); return ( {!isMultiLevelTags && ( @@ -95,50 +84,6 @@ function WorkspaceTagsSettingsPage({route}: WorkspaceTagsSettingsPageProps) { />
)} - {!isRulesRevampEnabled && ( - <> - {(!isMultiLevelTags || hasDependentTags) && ( - - - - {translate('workspace.tags.requiresTag')} - - - - - )} - - - - {translate('workspace.tags.trackBillable')} - - toggleBillableExpenses(policy)} - disabled={!policy?.areTagsEnabled} - /> - - - - )} {!!policy?.glCodes && ( ; - -function WorkspaceWorkflowsPage(props: WorkspaceWorkflowsPageProps) { - const {policy, route} = props; - const {policyID} = route.params; - useWorkspaceDocumentTitle(policy?.name, 'workspace.common.workflows'); - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['Table', 'Download']); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {showConfirmModal} = useConfirmModal(); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); - const {login: currentUserLogin = ''} = useCurrentUserPersonalDetails(); - - const isSubmitPolicyWorkspace = isSubmitPolicy(policy); - - const fetchData = useCallback(() => { - // This component still mounts (and keeps its hooks running) when the revamp renders below, so let the revamp - // page own fetching to avoid a duplicate OpenPolicyWorkflowsPage on mount and on every reconnect. - if (isRulesRevampEnabled) { - return; - } - openPolicyWorkflowsPage(policyID, true); - getPaymentMethods(); - }, [policyID, isRulesRevampEnabled]); - - const {isOffline} = useNetwork({onReconnect: fetchData}); - const canReadWorkflows = canMemberRead(policy, currentUserLogin, CONST.POLICY.POLICY_FEATURE.WORKFLOWS); - const {canWrite: canWriteWorkflows} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.WORKFLOWS); - const {canWrite: canWriteApprovals, withReadOnlyFallback: withApprovalsReadOnlyFallback} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.WORKFLOWS_APPROVALS); - const {canWrite: canWritePayments, withReadOnlyFallback: withPaymentsReadOnlyFallback} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.WORKFLOWS_PAYMENTS); - - const {isAccountLocked} = useLockedAccountState(); - const {showLockedAccountModal} = useLockedAccountActions(); - - useEffect(() => { - fetchData(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const navigateToSubmitWorkspaceApprovalsUpgrade = useCallback(() => { - Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvalSubmit.alias, ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID))); - }, [policyID]); - - // Reuses the Members spreadsheet importer (it already maps the `submitsTo` / `approvesTo` columns) so approval - // workflows can be bulk-imported directly from the Workflows page. - const importWorkflowsAction = useCallback(() => { - if (isAccountLocked) { - showLockedAccountModal(); - return; - } - if (isOffline) { - showConfirmModal({ - title: translate('common.youAppearToBeOffline'), - prompt: translate('common.thisFeatureRequiresInternet'), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - shouldHandleNavigationBack: true, - }); - return; - } - // Submit 2026 workspaces gate approvals behind the Submit approvals upgrade, so route them there instead of the importer. - if (isSubmitPolicyWorkspace) { - navigateToSubmitWorkspaceApprovalsUpgrade(); - return; - } - Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_IMPORT.getRoute(policyID)); - }, [isAccountLocked, showLockedAccountModal, isOffline, showConfirmModal, translate, policyID, isSubmitPolicyWorkspace, navigateToSubmitWorkspaceApprovalsUpgrade]); - - // The Workflows CSV export reuses the Members export command so the downloaded file is identical to Members > Download CSV. - const downloadWorkflowsAction = useCallback(() => { - if (isOffline) { - showConfirmModal({ - title: translate('common.youAppearToBeOffline'), - prompt: translate('common.thisFeatureRequiresInternet'), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - shouldHandleNavigationBack: true, - }); - return; - } - downloadMembersCSV( - policyID, - () => { - showConfirmModal({ - title: translate('common.downloadFailedTitle'), - prompt: translate('common.downloadFailedDescription'), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - }); - }, - translate, - ); - }, [isOffline, showConfirmModal, translate, policyID]); - - if (isRulesRevampEnabled) { - return ; - } - - const shouldBlockApprovalWorkflowEditing = isAnyHRReadOnlyWorkflowMode(policy); - - const approvalSecondaryActions: Array>> = []; - // Importing modifies the workflows, so only offer it when editing is allowed. - if (!shouldBlockApprovalWorkflowEditing) { - approvalSecondaryActions.push({ - icon: expensifyIcons.Table, - text: translate('spreadsheet.importWorkflows'), - onSelected: importWorkflowsAction, - value: CONST.POLICY.SECONDARY_ACTIONS.IMPORT_SPREADSHEET, - }); - } - // Downloading is read-only, so it stays available even when editing is blocked. - approvalSecondaryActions.push({ - icon: expensifyIcons.Download, - text: translate('spreadsheet.downloadWorkflows'), - onSelected: downloadWorkflowsAction, - value: CONST.POLICY.SECONDARY_ACTIONS.DOWNLOAD_CSV, - }); - - const shouldShowSubmissionFrequency = canWriteWorkflows || !canWriteApprovals; - const shouldShowPayments = canMemberRead(policy, currentUserLogin, CONST.POLICY.POLICY_FEATURE.WORKFLOWS_PAYMENTS); - - const isGroupPolicy = isGroupPolicyUtil(policy); - const isLoading = !!(policy?.isLoading && policy?.reimbursementChoice === undefined); - - // Show the More dropdown whenever the user can manage workflows. When editing is blocked it renders download-only - // (the Import action is filtered out of approvalSecondaryActions above). - const headerButtons = canWriteApprovals ? ( - - {}} - shouldAlwaysShowDropdownMenu - customText={translate('common.more')} - sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.WORKFLOWS.MORE_DROPDOWN} - options={approvalSecondaryActions} - isSplitButton={false} - wrapperStyle={styles.flexGrow0} - /> - - ) : undefined; - - return ( - - - - {shouldShowSubmissionFrequency && } - - {shouldShowPayments && } - - - - - ); -} - -export default withPolicy(WorkspaceWorkflowsPage); diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPageRevamp.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPageRevamp.tsx index 7a62254907da..44291de6c5d2 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPageRevamp.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPageRevamp.tsx @@ -29,6 +29,7 @@ import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import ExpenseReportRulesSection from '@pages/workspace/rules/ExpenseReportRulesSection'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import withPolicy from '@pages/workspace/withPolicy'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; import {getPaymentMethods} from '@userActions/PaymentMethods'; @@ -40,7 +41,7 @@ import type SCREENS from '@src/SCREENS'; import type {ValueOf} from 'type-fest'; -import React, {useCallback, useEffect} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import WorkflowsApprovalsTab from './tabs/WorkflowsApprovalsTab'; @@ -111,18 +112,29 @@ function WorkspaceWorkflowsPageRevamp({policy, route}: WorkspaceWorkflowsPageRev const persistedTab = WORKFLOWS_TABS_BY_KEY.get(lastSelectedTab ?? '') ?? WORKFLOWS_TAB.SUBMISSIONS; const requestedTab = routeTab ?? persistedTab; + const hasPersistedRouteTabRef = useRef(false); + useEffect(() => { if (!routeTab) { + hasPersistedRouteTabRef.current = false; return; } // Persist the deep-linked tab first so reopening this page lands on it again. if (persistedTab !== routeTab) { Tab.setSelectedTab(CONST.TAB.WORKFLOWS_TAB_TYPE, routeTab); + hasPersistedRouteTabRef.current = true; return; } // Onyx now holds the tab, so drop the param — `goBack` compares params, and a leftover one stops child flows returning here with the plain route from popping. + // Only clear the param this mount handed over: on a repeat visit Onyx already matches, so clearing it straight from mount + // rewrites the route while the navigation is still in flight and drops us out of the split navigator. + if (!hasPersistedRouteTabRef.current) { + return; + } + + hasPersistedRouteTabRef.current = false; Navigation.setParams({tab: undefined}); }, [routeTab, persistedTab]); @@ -316,4 +328,4 @@ function WorkspaceWorkflowsPageRevamp({policy, route}: WorkspaceWorkflowsPageRev ); } -export default WorkspaceWorkflowsPageRevamp; +export default withPolicy(WorkspaceWorkflowsPageRevamp); diff --git a/src/pages/workspace/workflows/tabs/WorkflowsSectionCard.tsx b/src/pages/workspace/workflows/tabs/WorkflowsSectionCard.tsx index 231b0963b118..66bd1d0ccee1 100644 --- a/src/pages/workspace/workflows/tabs/WorkflowsSectionCard.tsx +++ b/src/pages/workspace/workflows/tabs/WorkflowsSectionCard.tsx @@ -1,6 +1,5 @@ import Section from '@components/Section'; -import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -21,8 +20,6 @@ function WorkflowsSectionCard(item: ToggleSettingOptionRowProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply a correct padding style // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); - const {isBetaEnabled} = usePermissions(); - const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); return (
{ }); }); - describe('setPolicyProhibitedExpense', () => { + describe('setPolicyProhibitedExpenses', () => { it('should enable prohibited expense optimistically and succeed', async () => { // Given a policy with prohibited expenses const policyID = '1'; @@ -6407,9 +6407,9 @@ describe('actions/Policy', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); await waitForBatchedUpdates(); - // When setPolicyProhibitedExpense is called to enable alcohol + // When setPolicyProhibitedExpenses is called to enable alcohol mockFetch.pause(); - Policy.setPolicyProhibitedExpense(policyID, 'alcohol', currentProhibitedExpenses); + Policy.setPolicyProhibitedExpenses(policyID, {...currentProhibitedExpenses, alcohol: true}, currentProhibitedExpenses); await waitForBatchedUpdates(); // Then the prohibited expense should be updated optimistically @@ -6441,9 +6441,9 @@ describe('actions/Policy', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); await waitForBatchedUpdates(); - // When setPolicyProhibitedExpense is called and fails + // When setPolicyProhibitedExpenses is called and fails mockFetch.fail(); - Policy.setPolicyProhibitedExpense(policyID, 'alcohol', currentProhibitedExpenses); + Policy.setPolicyProhibitedExpenses(policyID, {...currentProhibitedExpenses, alcohol: true}, currentProhibitedExpenses); await waitForBatchedUpdates(); // Then the prohibited expense should be reverted diff --git a/tests/ui/AgentsPromoBannersTest.tsx b/tests/ui/AgentsPromoBannersTest.tsx index 47b9d0099445..3e13fbc3e9dd 100644 --- a/tests/ui/AgentsPromoBannersTest.tsx +++ b/tests/ui/AgentsPromoBannersTest.tsx @@ -4,7 +4,7 @@ import ComposeProviders from '@components/ComposeProviders'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; -import PolicyRulesPage from '@pages/workspace/rules/PolicyRulesPage'; +import PolicyRulesPageRevamp from '@pages/workspace/rules/PolicyRulesPageRevamp'; import CONST from '@src/CONST'; import en from '@src/languages/en'; @@ -96,7 +96,7 @@ function buildPolicy(): Policy { } as Policy; } -const rulesRoute: React.ComponentProps['route'] = { +const rulesRoute: React.ComponentProps['route'] = { key: 'rules-route', name: SCREENS.WORKSPACE.RULES, params: {policyID: POLICY_ID}, @@ -106,7 +106,7 @@ const renderRulesPage = () => render( {/* @ts-expect-error - navigation prop is not used by the page in tests */} - + , ); @@ -137,7 +137,7 @@ describe('Agents promo banners', () => { }); }); - it('renders agentsRulesBanner above IndividualExpenseRulesSection when customAgent beta is active, and hides it after dismissal', async () => { + it('renders agentsRulesBanner on the General tab when customAgent beta is active, and hides it after dismissal', async () => { await act(async () => { await setupOnyxBaseline({withCustomAgentBeta: true}); await waitForBatchedUpdatesWithAct(); @@ -148,8 +148,6 @@ describe('Agents promo banners', () => { // Title includes a nested "New" badge, so match on subtitle instead of the full title string. expect(screen.getByText(en.workspace.rules.agentsPromoBanner.subtitle)).toBeTruthy(); - // Section title for IndividualExpenseRulesSection sits below the banner. - expect(screen.getByText(en.workspace.rules.individualExpenseRules.title)).toBeTruthy(); await act(async () => { await Onyx.merge(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, { diff --git a/tests/ui/RulesRequireReceiptsPageTest.tsx b/tests/ui/RulesRequireReceiptsPageTest.tsx index a1ba7d233638..a9d04cb82c42 100644 --- a/tests/ui/RulesRequireReceiptsPageTest.tsx +++ b/tests/ui/RulesRequireReceiptsPageTest.tsx @@ -73,11 +73,11 @@ const getItemizedToggleLabel = () => TestHelper.translateLocal('workspace.rules. const getAmountLabel = () => TestHelper.translateLocal('workspace.rules.requireReceipts.requireAboveAmount'); const getSaveLabel = () => TestHelper.translateLocal('workspace.rules.requireReceipts.saveRule'); -/** The page is gated behind the rulesRevamp beta, and its wrapper needs an admin on a Control workspace with Rules on. */ +/** The page wrapper needs an admin on a Control workspace with Rules on. */ const setupPolicy = async (policyOverrides: Partial) => { await TestHelper.signInWithTestUser(); await act(async () => { - await Onyx.set(ONYXKEYS.BETAS, [CONST.BETAS.RULES_REVAMP]); + await Onyx.set(ONYXKEYS.BETAS, []); }); const policy = { ...LHNTestUtils.getFakePolicy(), diff --git a/tests/ui/WorkspaceTagsSettingsToggleTest.tsx b/tests/ui/WorkspaceTagsSettingsToggleTest.tsx index ceae221c14ea..04dc14e06bbf 100644 --- a/tests/ui/WorkspaceTagsSettingsToggleTest.tsx +++ b/tests/ui/WorkspaceTagsSettingsToggleTest.tsx @@ -13,7 +13,6 @@ import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigati import type {SettingsNavigatorParamList} from '@navigation/types'; import RulesRequireFieldsPage from '@pages/workspace/rules/RulesRequireFieldsPage'; -import WorkspaceTagsSettingsPage from '@pages/workspace/tags/WorkspaceTagsSettingsPage'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -33,26 +32,6 @@ TestHelper.setupGlobalFetchMock(); const Stack = createPlatformStackNavigator(); -const renderSettingsPage = (policyID: string) => { - return render( - - - - - - - - - - - , - ); -}; - const renderRulesPage = (policyID: string) => { return render( @@ -128,8 +107,6 @@ const dependentMultiLevelTags: PolicyTagLists = { }, }; -const getRequiresTagLabel = () => TestHelper.translateLocal('workspace.tags.requiresTag'); - beforeAll(() => { Onyx.init({keys: ONYXKEYS}); }); @@ -162,8 +139,7 @@ afterEach(async () => { const setupPolicy = async (policyTags: PolicyTagLists, hasMultipleTagLists: boolean, policyOverrides: Partial = {}, betas: Beta[] = []) => { await TestHelper.signInWithTestUser(); - // signInWithTestUser seeds BETAS with 'all', which turns on rulesRevamp everywhere. Each page needs a different - // side of that beta, so pin the exact list after signing in. + // signInWithTestUser seeds BETAS with 'all'. Pin the exact list so an unrelated beta can't change what renders. await act(async () => { await Onyx.set(ONYXKEYS.BETAS, betas); }); @@ -181,62 +157,13 @@ const setupPolicy = async (policyTags: PolicyTagLists, hasMultipleTagLists: bool return policy; }; -// With rulesRevamp on, this page no longer renders the toggle at all; it lives on RulesRequireFieldsPage. These cases -// therefore run with the beta off, which is the path where the gate still applies. -describe('WorkspaceTagsSettingsPage required-tag toggle visibility', () => { - it('shows the "Members must tag all expenses" toggle for single-level tags', async () => { - const policy = await setupPolicy(singleLevelTags, false); - const {unmount} = renderSettingsPage(policy.id); - await waitForBatchedUpdatesWithAct(); - - // The billable toggle is always present; wait on its accessibility label before asserting the required toggle. - await waitFor(() => { - expect(screen.getByLabelText(TestHelper.translateLocal('workspace.tags.trackBillable'))).toBeOnTheScreen(); - }); - expect(screen.getByLabelText(getRequiresTagLabel())).toBeOnTheScreen(); - - unmount(); - await waitForBatchedUpdatesWithAct(); - }); - - it('shows the "Members must tag all expenses" toggle for dependent multi-level tags', async () => { - const policy = await setupPolicy(dependentMultiLevelTags, true); - const {unmount} = renderSettingsPage(policy.id); - await waitForBatchedUpdatesWithAct(); - - // The billable toggle is always present; wait on its accessibility label before asserting the required toggle. - await waitFor(() => { - expect(screen.getByLabelText(TestHelper.translateLocal('workspace.tags.trackBillable'))).toBeOnTheScreen(); - }); - expect(screen.getByLabelText(getRequiresTagLabel())).toBeOnTheScreen(); - - unmount(); - await waitForBatchedUpdatesWithAct(); - }); - - it('hides the "Members must tag all expenses" toggle for independent multi-level tags', async () => { - const policy = await setupPolicy(independentMultiLevelTags, true); - const {unmount} = renderSettingsPage(policy.id); - await waitForBatchedUpdatesWithAct(); - - // The billable toggle is always present, so wait on its label before asserting the required toggle is absent. - await waitFor(() => { - expect(screen.getByLabelText(TestHelper.translateLocal('workspace.tags.trackBillable'))).toBeOnTheScreen(); - }); - expect(screen.queryByLabelText(getRequiresTagLabel())).not.toBeOnTheScreen(); - - unmount(); - await waitForBatchedUpdatesWithAct(); - }); -}); - -describe('RulesRequireFieldsPage tag toggles', () => { +describe('RulesRequireFieldsPage tag toggle visibility', () => { const getRulesTagLabel = () => TestHelper.translateLocal('workspace.rules.requireFields.tag'); const getRulesCategoryLabel = () => TestHelper.translateLocal('workspace.rules.requireFields.category'); - // The page is gated behind the rulesRevamp beta and the Rules feature (Control plan). + // The page needs an admin on a Control workspace with the Rules feature on. const setupRulesPolicy = async (policyTags: PolicyTagLists, hasMultipleTagLists: boolean) => - setupPolicy(policyTags, hasMultipleTagLists, {type: CONST.POLICY.TYPE.CORPORATE, areRulesEnabled: true}, [CONST.BETAS.RULES_REVAMP]); + setupPolicy(policyTags, hasMultipleTagLists, {type: CONST.POLICY.TYPE.CORPORATE, areRulesEnabled: true}); it('names the single toggle after the tag list for single-level tags', async () => { const policy = await setupRulesPolicy(singleLevelTags, false); diff --git a/tests/ui/WorkspaceViewTagsTableTest.tsx b/tests/ui/WorkspaceViewTagsTableTest.tsx index ecd27ec67404..f803fb8abf9a 100644 --- a/tests/ui/WorkspaceViewTagsTableTest.tsx +++ b/tests/ui/WorkspaceViewTagsTableTest.tsx @@ -51,7 +51,6 @@ const buildTag = (name: string, enabled: boolean): WorkspaceTagTableRowData => ( enabled, isLocked: false, showEnabledSwitch: true, - showRequiredSwitch: false, action: jest.fn(), onClose: jest.fn(), }); diff --git a/tests/ui/WorkspaceWorkflowsLoadMoreTest.tsx b/tests/ui/WorkspaceWorkflowsLoadMoreTest.tsx index fbbd8a00f429..2fec4668cc76 100644 --- a/tests/ui/WorkspaceWorkflowsLoadMoreTest.tsx +++ b/tests/ui/WorkspaceWorkflowsLoadMoreTest.tsx @@ -13,7 +13,7 @@ import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigati import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; -import WorkspaceWorkflowsPage from '@pages/workspace/workflows/WorkspaceWorkflowsPage'; +import WorkspaceWorkflowsPageRevamp from '@pages/workspace/workflows/WorkspaceWorkflowsPageRevamp'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -111,7 +111,7 @@ const renderPage = () => @@ -125,7 +125,7 @@ const renderPage = () => const loadMoreLabel = (count: number) => TestHelper.translateLocal('workflowsPage.loadMoreWorkflows', {count}); const countWorkflowCards = () => screen.queryAllByText(TestHelper.translateLocal('workflowsExpensesFromPage.title')).length; -describe('WorkspaceWorkflowsPage - Approvals Load more', () => { +describe('WorkspaceWorkflowsPageRevamp - Approvals Load more', () => { beforeAll(() => { Onyx.init({keys: ONYXKEYS}); }); diff --git a/tests/ui/WorkspaceWorkflowsPayerRowTest.tsx b/tests/ui/WorkspaceWorkflowsPayerRowTest.tsx index 2e25014d7356..13769df1666a 100644 --- a/tests/ui/WorkspaceWorkflowsPayerRowTest.tsx +++ b/tests/ui/WorkspaceWorkflowsPayerRowTest.tsx @@ -15,7 +15,7 @@ import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigati import type {SettingsNavigatorParamList, WorkspaceSplitNavigatorParamList} from '@navigation/types'; -import WorkspaceWorkflowsPage from '@pages/workspace/workflows/WorkspaceWorkflowsPage'; +import WorkspaceWorkflowsPageRevamp from '@pages/workspace/workflows/WorkspaceWorkflowsPageRevamp'; import WorkspaceWorkflowsPayerPage from '@pages/workspace/workflows/WorkspaceWorkflowsPayerPage'; import CONST from '@src/CONST'; @@ -78,7 +78,7 @@ const renderPage = (initialRouteName: keyof TestNavigatorParamList = SCREENS.WOR @@ -94,7 +94,7 @@ const renderPage = (initialRouteName: keyof TestNavigatorParamList = SCREENS.WOR , ); -describe('WorkspaceWorkflowsPage - Payer row visibility', () => { +describe('WorkspaceWorkflowsPageRevamp - Payer row visibility', () => { beforeAll(() => { Onyx.init({keys: ONYXKEYS}); }); diff --git a/tests/unit/CopyPolicySettingsConfirmTest.tsx b/tests/unit/CopyPolicySettingsConfirmTest.tsx index cdfa798df2af..4f9a493ed2d8 100644 --- a/tests/unit/CopyPolicySettingsConfirmTest.tsx +++ b/tests/unit/CopyPolicySettingsConfirmTest.tsx @@ -172,7 +172,7 @@ describe('CopyPolicySettingsConfirmPage', () => { await Onyx.set(ONYXKEYS.COPY_POLICY_SETTINGS, { sourcePolicyID: SOURCE_POLICY_ID, targetPolicyIDs: [TARGET_POLICY_ID], - parts: ['rules'] as Part[], + parts: ['perDiem'] as Part[], }); await waitForBatchedUpdates(); @@ -195,7 +195,7 @@ describe('CopyPolicySettingsConfirmPage', () => { await Onyx.set(ONYXKEYS.COPY_POLICY_SETTINGS, { sourcePolicyID: SOURCE_POLICY_ID, targetPolicyIDs: [TARGET_POLICY_ID], - parts: ['rules'] as Part[], + parts: ['perDiem'] as Part[], }); await waitForBatchedUpdates(); diff --git a/tests/unit/CopyPolicySettingsUpgradeTest.tsx b/tests/unit/CopyPolicySettingsUpgradeTest.tsx index 2564236b1eee..1d2ac36edab8 100644 --- a/tests/unit/CopyPolicySettingsUpgradeTest.tsx +++ b/tests/unit/CopyPolicySettingsUpgradeTest.tsx @@ -137,7 +137,7 @@ describe('CopyPolicySettingsUpgradePage', () => { await Onyx.set(ONYXKEYS.COPY_POLICY_SETTINGS, { sourcePolicyID: SOURCE_POLICY_ID, targetPolicyIDs: [TARGET_POLICY_ID], - parts: ['rules'] as Part[], + parts: ['perDiem'] as Part[], }); await waitForBatchedUpdates(); }); diff --git a/tests/unit/CopyPolicySettingsUtilsTest.ts b/tests/unit/CopyPolicySettingsUtilsTest.ts index c112f8013860..e3747879931e 100644 --- a/tests/unit/CopyPolicySettingsUtilsTest.ts +++ b/tests/unit/CopyPolicySettingsUtilsTest.ts @@ -435,8 +435,8 @@ describe('CopyPolicySettingsUtils', () => { describe('getControlOnlySelectedParts', () => { it('returns the selected parts a Collect target cannot access', () => { - const result = getControlOnlySelectedParts([collectTarget(1)], ['rules', 'perDiem', 'categories'] as Part[]); - expect(result).toEqual(expect.arrayContaining(['rules', 'perDiem'])); + const result = getControlOnlySelectedParts([collectTarget(1)], ['perDiem', 'categories'] as Part[]); + expect(result).toEqual(expect.arrayContaining(['perDiem'])); expect(result).not.toContain('categories'); }); @@ -444,8 +444,12 @@ describe('CopyPolicySettingsUtils', () => { expect(getControlOnlySelectedParts([collectTarget(1)], ['categories', 'tags'] as Part[])).toEqual([]); }); + it('does not treat rules as Control-only, since Collect can access them', () => { + expect(getControlOnlySelectedParts([collectTarget(1)], ['rules'] as Part[])).toEqual([]); + }); + it('returns nothing when there are no Collect targets', () => { - expect(getControlOnlySelectedParts([controlTarget(1)], ['rules'] as Part[])).toEqual([]); + expect(getControlOnlySelectedParts([controlTarget(1)], ['perDiem'] as Part[])).toEqual([]); }); }); @@ -453,7 +457,7 @@ describe('CopyPolicySettingsUtils', () => { it('returns every Collect target when a Control-only part is selected', () => { const collectA = collectTarget(1); const collectB = collectTarget(2); - const result = getCollectTargetsToUpgrade([collectA, collectB, controlTarget(3)], ['rules'] as Part[]); + const result = getCollectTargetsToUpgrade([collectA, collectB, controlTarget(3)], ['perDiem'] as Part[]); expect(result).toHaveLength(2); expect(result.map((policy) => policy.id)).toEqual(expect.arrayContaining([collectA.id, collectB.id])); }); @@ -463,17 +467,17 @@ describe('CopyPolicySettingsUtils', () => { }); it('ignores unresolved targets', () => { - expect(getCollectTargetsToUpgrade([undefined, controlTarget(1)], ['rules'] as Part[])).toEqual([]); + expect(getCollectTargetsToUpgrade([undefined, controlTarget(1)], ['perDiem'] as Part[])).toEqual([]); }); }); describe('shouldShowCopyPolicySettingsUpgradeStep', () => { it('is true when a Control-only part targets a Collect workspace', () => { - expect(shouldShowCopyPolicySettingsUpgradeStep([collectTarget(1)], ['rules'] as Part[])).toBe(true); + expect(shouldShowCopyPolicySettingsUpgradeStep([collectTarget(1)], ['perDiem'] as Part[])).toBe(true); }); it('is false when every target is already Control', () => { - expect(shouldShowCopyPolicySettingsUpgradeStep([controlTarget(1)], ['rules'] as Part[])).toBe(false); + expect(shouldShowCopyPolicySettingsUpgradeStep([controlTarget(1)], ['perDiem'] as Part[])).toBe(false); }); it('is false when no Control-only part is selected', () => { diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index a918d57b14fb..eac6265a6ac3 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -4236,12 +4236,8 @@ describe('arePolicyRulesEnabled', () => { expect(arePolicyRulesEnabled({...teamBase, areRulesEnabled: undefined})).toBe(false); }); - it('returns false for a team policy with areRulesEnabled explicitly true when rules revamp beta is disabled', () => { - expect(arePolicyRulesEnabled({...teamBase, areRulesEnabled: true})).toBe(false); - }); - - it('returns true for a team policy with areRulesEnabled explicitly true when rules revamp beta is enabled', () => { - expect(arePolicyRulesEnabled({...teamBase, areRulesEnabled: true}, undefined, true)).toBe(true); + it('returns true for a team policy with areRulesEnabled explicitly true', () => { + expect(arePolicyRulesEnabled({...teamBase, areRulesEnabled: true}, undefined)).toBe(true); }); it('returns false for a team policy with areRulesEnabled explicitly false', () => { diff --git a/tests/unit/VendorMatchingMerchantRulesTest.ts b/tests/unit/VendorMatchingMerchantRulesTest.ts index fe57d05b7541..a17606edd74d 100644 --- a/tests/unit/VendorMatchingMerchantRulesTest.ts +++ b/tests/unit/VendorMatchingMerchantRulesTest.ts @@ -2,8 +2,6 @@ import {mapFormFieldsToRuleForAPI, mapFormFieldsToRuleForOnyx} from '@libs/actio import {getMerchantCodingRulesTableData} from '@libs/MerchantTypeRulesUtils'; import {hasVendorFeature, isXeroActiveMatchingSource} from '@libs/PolicyUtils'; -import {getRuleDescription} from '@pages/workspace/rules/MerchantRulesSection'; - import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import type {MerchantRuleForm} from '@src/types/form/MerchantRuleForm'; @@ -182,56 +180,6 @@ describe('Vendor matching on merchant rules', () => { }); }); - describe('legacy MerchantRulesSection.getRuleDescription vendor summary', () => { - beforeEach(() => { - IntlStore.load(CONST.LOCALES.EN); - return waitForBatchedUpdates(); - }); - - const buildLabels = (policy: Policy) => ({ - category: translateLocal('common.category').toLowerCase(), - tag: translateLocal('common.tag').toLowerCase(), - description: translateLocal('common.description').toLowerCase(), - tax: translateLocal('common.tax').toLowerCase(), - vendor: translateLocal(isXeroActiveMatchingSource(policy) ? 'common.supplier' : 'common.vendor').toLowerCase(), - }); - - const describeRule = (policy: Policy, vendorID: string) => getRuleDescription(buildVendorRule(vendorID), translateLocal, buildLabels(policy), policy); - - it('resolves the vendor name when the vendor is in the loaded list', () => { - const policy = buildQBOPolicy([{id: 'v-1', name: 'Acme Co', currency: 'USD'}]); - expect(describeRule(policy, 'v-1')).toContain('Update vendor to "Acme Co"'); - }); - - it('shows "Vendor unavailable" when the list is loaded but the vendor is missing', () => { - expect(describeRule(buildQBOPolicy([]), 'v-1')).toContain('Update vendor to "Vendor unavailable"'); - }); - - it('falls back to the raw external ID while the list is not yet loaded and no other connection knows the vendor', () => { - expect(describeRule(buildQBOPolicy(undefined), 'v-1')).toContain('Update vendor to "v-1"'); - }); - - it('resolves the historical vendor name when the workspace has switched its export mode away from vendor-matching mode', () => { - const policy = buildQBOWithVendorBillExportPolicy([{id: 'v-1', name: 'Acme Co', currency: 'USD'}]); - expect(describeRule(policy, 'v-1')).toContain('Update vendor to "Acme Co"'); - }); - - it('shows "Vendor unavailable" when the vendorID only resolves against a stale/inactive connection', () => { - const policy = buildQBOWithStaleXeroPolicy([], {xeroVendor: {id: 'xeroVendor', name: 'Stale Xero Vendor', email: 'stale@example.com'}}); - const description = describeRule(policy, 'xeroVendor'); - expect(description).toContain('Update vendor to "Vendor unavailable"'); - expect(description).not.toContain('Stale Xero Vendor'); - }); - - it('uses "supplier" wording and "Supplier unavailable" on Xero workspaces', () => { - const resolved = buildXeroPolicy({xc1: {id: 'xc1', name: 'Acme Xero', email: 'acme@example.com'}}); - expect(describeRule(resolved, 'xc1')).toContain('Update supplier to "Acme Xero"'); - - const missing = buildXeroPolicy({}); - expect(describeRule(missing, 'xc1')).toContain('Update supplier to "Supplier unavailable"'); - }); - }); - describe('vendor row gating (hasVendorFeature governs MerchantRulePageBase row visibility)', () => { it('is visible when the beta is on and a vendor integration is connected', () => { expect(hasVendorFeature(buildQBOPolicy([{id: 'v-1', name: 'Acme Co', currency: 'USD'}]), true)).toBe(true); diff --git a/tests/unit/getWorkspaceMenuItemsTest.ts b/tests/unit/getWorkspaceMenuItemsTest.ts index 1f6c88547d1f..5a19b8095652 100644 --- a/tests/unit/getWorkspaceMenuItemsTest.ts +++ b/tests/unit/getWorkspaceMenuItemsTest.ts @@ -338,20 +338,6 @@ describe('getWorkspaceMenuItems', () => { expect(items.find((item) => item.translationKey === 'workspace.common.hr')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); }); - it('uses the existing Rules icon when the Rules revamp beta is disabled', () => { - const policy = createMock({...buildPolicy(CONST.POLICY.ROLE.ADMIN), areRulesEnabled: true}); - - const items = getWorkspaceMenuItems({ - policy, - policyID: policy.id, - currentUserLogin, - icons, - convertToDisplayString: () => '', - }); - - expect(items.find((item) => item.translationKey === 'workspace.common.rules')?.icon).toBe(icons.Feed); - }); - it('preserves the complete enabled Workspace menu order and presentation data', () => { const policy = createMock({ ...buildPolicy(CONST.POLICY.ROLE.ADMIN), @@ -385,7 +371,6 @@ describe('getWorkspaceMenuItems', () => { policyID: policy.id, currentUserLogin, icons, - isRulesRevampBetaEnabled: true, isVendorMatchingBetaEnabled: true, convertToDisplayString, });