diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 1f283f26d4b4..c949d4243f2c 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1398,6 +1398,7 @@ const CONST = { BULK_EDIT: 'bulkEdit', BULK_SUBMIT_APPROVE_PAY: 'bulkSubmitApprovePay', VENDOR_MATCHING: 'vendorMatching', + INDIRECT_TAG_UPLOADS: 'indirectTagUploads', BUSINESS_CENTRAL: 'businessCentral', EXACT_ONLINE: 'exactOnline2026', COMMUTER_EXCLUSIONS: 'commuterExclusions', @@ -5040,6 +5041,7 @@ const CONST = { ID_FAKE: '_FAKE_', SECONDARY_ACTIONS: { IMPORT_SPREADSHEET: 'importSpreadsheet', + APPEND_CUSTOM_TAG_LIST: 'appendCustomTagList', DOWNLOAD_CSV: 'downloadCSV', SETTINGS: 'settings', EXPORT: 'export', diff --git a/src/components/ImportSpreadsheet.tsx b/src/components/ImportSpreadsheet.tsx index ea11f5046a3a..3304bf6da096 100644 --- a/src/components/ImportSpreadsheet.tsx +++ b/src/components/ImportSpreadsheet.tsx @@ -48,6 +48,9 @@ type ImportSpreadsheetProps = { isImportingMultiLevelTags?: boolean; + /** Whether the imported tag lists are appended to the existing (accounting connection) tag lists */ + isAppendingToExistingLists?: boolean; + /** Whether an OFX/QFX bank statement can be picked alongside a spreadsheet */ shouldAllowBankStatements?: boolean; @@ -55,7 +58,15 @@ type ImportSpreadsheetProps = { onStatementPicked?: (file: FileObject) => Promise; }; -function ImportSpreadsheet({backTo, goTo, shouldForceReplaceNavigation = false, isImportingMultiLevelTags, shouldAllowBankStatements, onStatementPicked}: ImportSpreadsheetProps) { +function ImportSpreadsheet({ + backTo, + goTo, + shouldForceReplaceNavigation = false, + isImportingMultiLevelTags, + isAppendingToExistingLists, + shouldAllowBankStatements, + onStatementPicked, +}: ImportSpreadsheetProps) { const [importedSpreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET); const icons = useMemoizedLazyExpensifyIcons(['SpreadsheetComputer']); const styles = useThemeStyles(); @@ -189,6 +200,7 @@ function ImportSpreadsheet({backTo, goTo, shouldForceReplaceNavigation = false, file.name, isImportingMultiLevelTags ?? false, importedSpreadsheet?.importTransactionSettings, + isAppendingToExistingLists, ); }) .then(() => { diff --git a/src/languages/de.ts b/src/languages/de.ts index bf4ba8048314..8c19237d422a 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7106,6 +7106,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU tagRules: 'Tag-Regeln', approverDescription: 'Genehmiger', importTags: 'Tags importieren', + appendCustomTagList: 'Benutzerdefinierte Tag-Liste anhängen', importTagsSupportingText: 'Kategorisiere deine Ausgaben mit einem oder mehreren Tags.', configureMultiLevelTags: 'Konfiguriere deine Liste von Tags für mehrstufiges Tagging.', importMultiLevelTagsSupportingText: `Hier ist eine Vorschau deiner Tags. Wenn alles gut aussieht, klicke unten, um sie zu importieren.`, diff --git a/src/languages/el.ts b/src/languages/el.ts index 4c07288e106f..a38449711cb8 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -7230,6 +7230,7 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ tagRules: 'Κανόνες ετικετών', approverDescription: 'Έγκριση', importTags: 'Εισαγωγή ετικετών', + appendCustomTagList: 'Προσάρτηση προσαρμοσμένης λίστας ετικετών', importTagsSupportingText: 'Κωδικοποιήστε τα έξοδά σας με έναν τύπο ετικέτας ή με πολλούς.', configureMultiLevelTags: 'Ρυθμίστε τη λίστα με τις ετικέτες σας για πολυεπίπεδη επισήμανση.', importMultiLevelTagsSupportingText: `Δείτε εδώ μια προεπισκόπηση των ετικετών σας. Αν όλα φαίνονται σωστά, κάντε κλικ παρακάτω για να τις εισαγάγετε.`, diff --git a/src/languages/en.ts b/src/languages/en.ts index fd1f910a8ffb..053908993bf9 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7503,6 +7503,7 @@ const translations = { tagRules: 'Tag rules', approverDescription: 'Approver', importTags: 'Import tags', + appendCustomTagList: 'Append custom tag list', importTagsSupportingText: 'Code your expenses with one type of tag or many.', configureMultiLevelTags: 'Configure your list of tags for multi-level tagging.', importMultiLevelTagsSupportingText: `Here's a preview of your tags. If everything looks good, click below to import them.`, diff --git a/src/languages/es.ts b/src/languages/es.ts index e48abb4820e2..f4cf40319d48 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6961,6 +6961,7 @@ ${amount} para ${merchant} - ${date}`, tagRules: 'Reglas de etiquetas', approverDescription: 'Aprobador', importTags: 'Importar categorías', + appendCustomTagList: 'Añadir lista de etiquetas personalizada', importTagsSupportingText: 'Clasifica tus gastos con un tipo de etiqueta o con varios.', configureMultiLevelTags: 'Configura etiquetas multinivel', importMultiLevelTagsSupportingText: `Aquí tienes una vista previa de tus etiquetas. Si todo se ve bien, haz clic abajo para importarlas.`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index c8fa12536a80..7f4d184e56fb 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7126,6 +7126,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. tagRules: 'Règles de tags', approverDescription: 'Approbateur', importTags: 'Importer des tags', + appendCustomTagList: 'Ajouter une liste de tags personnalisée', importTagsSupportingText: 'Codez vos dépenses avec un seul type de tag ou plusieurs.', configureMultiLevelTags: 'Configurez votre liste de tags pour le multi-niveau.', importMultiLevelTagsSupportingText: `Voici un aperçu de vos tags. Si tout semble correct, cliquez ci-dessous pour les importer.`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 649804952db7..f876b4b07fcf 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7065,6 +7065,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. tagRules: 'Regole dei tag', approverDescription: 'Approvante', importTags: 'Importa tag', + appendCustomTagList: 'Aggiungi elenco di tag personalizzato', importTagsSupportingText: 'Codifica le tue spese con un solo tipo di tag o con molti.', configureMultiLevelTags: 'Configura il tuo elenco di tag per la codifica multilivello.', importMultiLevelTagsSupportingText: `Ecco un’anteprima dei tuoi tag. Se ti sembra tutto corretto, fai clic qui sotto per importarli.`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 4a26d3d06048..bcb0c501cf81 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6976,6 +6976,7 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO tagRules: 'タグルール', approverDescription: '承認者', importTags: 'タグをインポート', + appendCustomTagList: 'カスタムタグリストを追加', importTagsSupportingText: '経費に 1 種類または複数のタグを付けて分類しましょう。', configureMultiLevelTags: '複数レベルのタグ付けのために、タグの一覧を設定します。', importMultiLevelTagsSupportingText: `こちらがタグのプレビューです。問題なければ、下をクリックしてインポートしてください。`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 2c65602f11dc..c0f1313b6e51 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7049,6 +7049,7 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ tagRules: 'Tagregels', approverDescription: 'Fiatteur', importTags: 'Labels importeren', + appendCustomTagList: 'Aangepaste taglijst toevoegen', importTagsSupportingText: 'Codeer je onkosten met één type tag of met meerdere.', configureMultiLevelTags: 'Configureer je lijst met tags voor taggen op meerdere niveaus.', importMultiLevelTagsSupportingText: `Hier is een voorbeeldweergave van je tags. Als alles er goed uitziet, klik hieronder om ze te importeren.`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 88aed178db23..3ebf371b6331 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7075,6 +7075,7 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy tagRules: 'Zasady tagów', approverDescription: 'Osoba zatwierdzająca', importTags: 'Importuj tagi', + appendCustomTagList: 'Dołącz niestandardową listę tagów', importTagsSupportingText: 'Oznaczaj wydatki jednym typem taga lub wieloma.', configureMultiLevelTags: 'Skonfiguruj swoją listę tagów do tagowania wielopoziomowego.', importMultiLevelTagsSupportingText: `Oto podgląd Twoich tagów. Jeśli wszystko wygląda dobrze, kliknij poniżej, aby je zaimportować.`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 27af3e7a8258..1cddb0bb15d6 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7051,6 +7051,7 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS tagRules: 'Regras de tag', approverDescription: 'Aprovador', importTags: 'Importar tags', + appendCustomTagList: 'Anexar lista de tags personalizada', importTagsSupportingText: 'Classifique suas despesas com um tipo de etiqueta ou várias.', configureMultiLevelTags: 'Configure sua lista de tags para marcação em vários níveis.', importMultiLevelTagsSupportingText: `Aqui está uma prévia de suas tags. Se tudo estiver certo, clique abaixo para importá-las.`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 6277886e229f..3b481cbc1923 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6799,6 +6799,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM tagRules: '标签规则', approverDescription: '审批人', importTags: '导入标签', + appendCustomTagList: '追加自定义标签列表', importTagsSupportingText: '为报销编码时可使用一种标签,也可使用多种标签。', configureMultiLevelTags: '为多级标签配置您的标签列表。', importMultiLevelTagsSupportingText: `这是您的标签预览。如果一切看起来都没问题,请点击下方以导入它们。`, diff --git a/src/libs/API/parameters/ImportMultiLevelTagsParams.ts b/src/libs/API/parameters/ImportMultiLevelTagsParams.ts index daa848b3d377..64f4af061199 100644 --- a/src/libs/API/parameters/ImportMultiLevelTagsParams.ts +++ b/src/libs/API/parameters/ImportMultiLevelTagsParams.ts @@ -3,6 +3,7 @@ type ImportMultiLevelTagsParams = { isFirstLineHeader: boolean; isIndependent: boolean; isGLAdjacent: boolean; + appendToExistingLists?: boolean; file: File; }; diff --git a/src/libs/actions/ImportSpreadsheet.ts b/src/libs/actions/ImportSpreadsheet.ts index 918968161781..2e1e10ebb9b9 100644 --- a/src/libs/actions/ImportSpreadsheet.ts +++ b/src/libs/actions/ImportSpreadsheet.ts @@ -19,6 +19,7 @@ function setSpreadsheetData( fileName: string, isImportingMultiLevelTags: boolean, importTransactionSettings?: ImportTransactionSettings, + isAppendingToExistingLists = false, ): Promise { // Validate that data is a non-empty array if (!Array.isArray(data) || data.length === 0) { @@ -55,6 +56,7 @@ function setSpreadsheetData( fileType, fileName, isImportingMultiLevelTags, + isAppendingToExistingLists, // Preserve transaction import settings that were configured before file upload importTransactionSettings, containsHeader: true, diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index d15048ef973d..15935c950ce1 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -901,6 +901,10 @@ function setImportedSpreadsheetIsImportingIndependentMultiLevelTags(isImportingI Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {isImportingIndependentMultiLevelTags}); } +function setImportedSpreadsheetIsAppendingToExistingLists(isAppendingToExistingLists: boolean) { + Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {isAppendingToExistingLists}); +} + function setImportedSpreadsheetIsFirstLineHeader(containsHeader: boolean) { Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {containsHeader}); } @@ -916,6 +920,19 @@ function importMultiLevelTags(policyID: string, spreadsheet: ImportedSpreadsheet const importFinalModalID = getImportFinalModalID(); const importFinalModalResult = waitForImportFinalModal(importFinalModalID); + const failureData: Array> = [ + getImportFinalModalOnyxData(importFinalModalID, getImportFailedFinalModal()), + ]; + // When appending, the accounting connection tag lists are kept, so a failed append shouldn't change whether the policy has multiple tag lists + if (!spreadsheet.isAppendingToExistingLists) { + failureData.unshift({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + hasMultipleTagLists: false, + }, + }); + } const onyxData: OnyxData = { successData: [ { @@ -927,16 +944,7 @@ function importMultiLevelTags(policyID: string, spreadsheet: ImportedSpreadsheet }, getImportFinalModalOnyxData(importFinalModalID, getImportMultiLevelTagsFinalModal()), ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - hasMultipleTagLists: false, - }, - }, - getImportFinalModalOnyxData(importFinalModalID, getImportFailedFinalModal()), - ], + failureData, }; return new Promise((resolve) => { @@ -949,6 +957,7 @@ function importMultiLevelTags(policyID: string, spreadsheet: ImportedSpreadsheet isFirstLineHeader: spreadsheet.containsHeader, isIndependent: spreadsheet.isImportingIndependentMultiLevelTags, isGLAdjacent: spreadsheet.isGLAdjacent, + appendToExistingLists: !!spreadsheet.isAppendingToExistingLists, file, }; @@ -1542,6 +1551,7 @@ export { cleanPolicyTags, setImportedSpreadsheetIsImportingMultiLevelTags, setImportedSpreadsheetIsImportingIndependentMultiLevelTags, + setImportedSpreadsheetIsAppendingToExistingLists, setImportedSpreadsheetIsFirstLineHeader, setImportedSpreadsheetIsGLAdjacent, importMultiLevelTags, diff --git a/src/pages/workspace/tags/ImportMultiLevelTagsSettingsPage.tsx b/src/pages/workspace/tags/ImportMultiLevelTagsSettingsPage.tsx index 4c7dbb8bc129..acc7ec24e696 100644 --- a/src/pages/workspace/tags/ImportMultiLevelTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/ImportMultiLevelTagsSettingsPage.tsx @@ -10,6 +10,7 @@ import useCloseImportPage from '@hooks/useCloseImportPage'; 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 useThemeStyles from '@hooks/useThemeStyles'; @@ -55,6 +56,8 @@ function ImportMultiLevelTagsSettingsPage({route}: ImportMultiLevelTagsSettingsP const {setIsClosing} = useCloseImportPage(); const [spreadsheet, spreadsheetMetadata] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET); const showImportSpreadsheetConfirmModal = useImportSpreadsheetConfirmModal(); + const {isBetaEnabled} = usePermissions(); + const isAppendingToExistingLists = hasAccountingConnections && isBetaEnabled(CONST.BETAS.INDIRECT_TAG_UPLOADS) && !!spreadsheet?.isAppendingToExistingLists; useEffect(() => { setImportedSpreadsheetIsFirstLineHeader(true); @@ -79,13 +82,13 @@ function ImportMultiLevelTagsSettingsPage({route}: ImportMultiLevelTagsSettingsP closeImportPageAndModal(); }; - if (hasAccountingConnections) { - return ; - } - if (!spreadsheet && isLoadingOnyxValue(spreadsheetMetadata)) { return; } + + if (hasAccountingConnections && !isAppendingToExistingLists) { + return ; + } return ( - - - {translate('workspace.tags.importMultiLevelTags.independentTags')} - - { - setImportedSpreadsheetIsImportingIndependentMultiLevelTags(value); - }} - /> - + {/* Appended tag lists are always independent, so the dependent option isn't offered */} + {!isAppendingToExistingLists && ( + + + {translate('workspace.tags.importMultiLevelTags.independentTags')} + + { + setImportedSpreadsheetIsImportingIndependentMultiLevelTags(value); + }} + /> + + )} ; } @@ -65,6 +73,7 @@ function ImportTagsPage({route}: ImportTagsPageProps) { return workspaceGoToImportedPath; })()} isImportingMultiLevelTags={spreadsheet?.isImportingMultiLevelTags} + isAppendingToExistingLists={isAppendingToExistingLists} shouldForceReplaceNavigation={!isQuickSettingsFlow && !spreadsheet?.isImportingMultiLevelTags} /> diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index a952a6ad4b7c..d228033b0574 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -43,6 +43,8 @@ import { downloadMultiLevelTagsCSV, downloadTagsCSV, openPolicyTagsPage, + setImportedSpreadsheetIsAppendingToExistingLists, + setImportedSpreadsheetIsImportingMultiLevelTags, setPolicyShowTagGLCodes, setPolicyTagsRequired, setWorkspaceTagEnabled, @@ -108,8 +110,9 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const {pageGutter} = useLayoutSpacing(); const styles = useThemeStyles(); const {translate, formatPhoneNumber} = useLocalize(); - const {isBetaEnabledOrUnknown} = usePermissions(); + const {isBetaEnabled, isBetaEnabledOrUnknown} = usePermissions(); const isVendorMatchingBetaEnabled = isBetaEnabledOrUnknown(CONST.BETAS.VENDOR_MATCHING); + const isIndirectTagUploadsBetaEnabled = isBetaEnabled(CONST.BETAS.INDIRECT_TAG_UPLOADS); const {showConfirmModal} = useConfirmModal(); const {showDownloadErrorModal} = useAlertModals(); const {backTo, policyID} = route.params; @@ -474,7 +477,26 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { ); }, [backTo, isOffline, isQuickSettingsFlow, policyID, showConfirmModal, translate]); + const navigateToAppendCustomTagList = useCallback(() => { + if (isOffline) { + showConfirmModal({ + title: translate('common.youAppearToBeOffline'), + prompt: translate('common.thisFeatureRequiresInternet'), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + shouldHandleNavigationBack: true, + }); + return; + } + // Appending keeps the accounting connection tag lists, so we skip the import options page and its override warnings + setImportedSpreadsheetIsImportingMultiLevelTags(true); + setImportedSpreadsheetIsAppendingToExistingLists(true); + Navigation.navigate(buildDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_TAGS_IMPORT.path)); + }, [buildDynamicRoute, isOffline, showConfirmModal, translate]); + const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy); + // Same as Classic, a custom tag list can only be appended to single-level or independent multi-level tags + const shouldShowAppendCustomTagList = canWriteTags && hasAccountingConnections && isIndirectTagUploadsBetaEnabled && !isQuickSettingsFlow && (!isMultiLevelTags || hasIndependentTags); const secondaryActions = useMemo(() => { const menuItems = []; // The former Settings page's rows are surfaced directly in this menu (the dedicated Settings page was removed). @@ -529,6 +551,15 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }); } + if (shouldShowAppendCustomTagList) { + menuItems.push({ + icon: expensifyIcons.Table, + text: translate('workspace.tags.appendCustomTagList'), + onSelected: navigateToAppendCustomTagList, + value: CONST.POLICY.SECONDARY_ACTIONS.APPEND_CUSTOM_TAG_LIST, + }); + } + if (hasVisibleTags) { menuItems.push({ icon: expensifyIcons.Download, @@ -560,6 +591,8 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }, [ translate, hasAccountingConnections, + shouldShowAppendCustomTagList, + navigateToAppendCustomTagList, hasVisibleTags, navigateToImportSpreadsheet, isOffline, diff --git a/src/types/onyx/ImportedSpreadsheet.ts b/src/types/onyx/ImportedSpreadsheet.ts index ccc231b0d1c0..5e9251ade717 100644 --- a/src/types/onyx/ImportedSpreadsheet.ts +++ b/src/types/onyx/ImportedSpreadsheet.ts @@ -81,6 +81,9 @@ type ImportedSpreadsheet = { isImportingMultiLevelTags: boolean; isImportingIndependentMultiLevelTags: boolean; + /** Whether the imported tag lists are appended to the existing (accounting connection) tag lists instead of replacing them */ + isAppendingToExistingLists?: boolean; + /** Whether the GL code is in the adjacent column */ isGLAdjacent: boolean; diff --git a/tests/ui/WorkspaceTagsTest.tsx b/tests/ui/WorkspaceTagsTest.tsx index 418121ca3041..5ffc881ac926 100644 --- a/tests/ui/WorkspaceTagsTest.tsx +++ b/tests/ui/WorkspaceTagsTest.tsx @@ -18,6 +18,7 @@ import WorkspaceTagsPage from '@pages/workspace/tags/WorkspaceTagsPage'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; +import type Beta from '@src/types/onyx/Beta'; import {PortalProvider} from '@gorhom/portal'; import {NavigationContainer} from '@react-navigation/native'; @@ -275,6 +276,96 @@ describe('WorkspaceTags', () => { await waitForBatchedUpdatesWithAct(); }); + describe('Append custom tag list', () => { + const connectedPolicy = { + ...LHNTestUtils.getFakePolicy(), + role: CONST.POLICY.ROLE.ADMIN, + areTagsEnabled: true, + connections: { + [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {}, + }, + }; + + const dependentMultiLevelTags = { + ParentList: { + name: 'ParentList', + required: false, + orderWeight: 0, + tags: { + Parent: {name: 'Parent', enabled: true}, + }, + }, + ChildList: { + name: 'ChildList', + required: false, + orderWeight: 1, + tags: { + Child: {name: 'Child', enabled: true, rules: {parentTagsFilter: '^Parent$'}}, + }, + }, + }; + + const renderAndOpenMoreMenu = async (policy: typeof connectedPolicy, policyTags: typeof tags | typeof dependentMultiLevelTags, betas: Beta[]) => { + await TestHelper.signInWithTestUser(); + await act(async () => { + await Onyx.set(ONYXKEYS.BETAS, betas); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy.id}`, policyTags); + }); + + const result = renderPage(SCREENS.WORKSPACE.TAGS, {policyID: policy.id}); + await waitForBatchedUpdatesWithAct(); + + await waitFor(() => { + expect(screen.getByText(TestHelper.translateLocal('common.more'))).toBeOnTheScreen(); + }); + fireEvent.press(screen.getByText(TestHelper.translateLocal('common.more'))); + await waitForBatchedUpdatesWithAct(); + + await waitFor(() => { + expect(screen.getByText(TestHelper.translateLocal('spreadsheet.downloadCSV'))).toBeOnTheScreen(); + }); + return result; + }; + + it('should show the option for single-level tags on a workspace with an accounting connection and the indirectTagUploads beta', async () => { + // Given a NetSuite workspace with single-level tags and an admin on the indirectTagUploads beta + // When the admin opens the More menu + const {unmount} = await renderAndOpenMoreMenu(connectedPolicy, tags, [CONST.BETAS.INDIRECT_TAG_UPLOADS]); + + // Then the option to append a custom tag list is offered, the same as Classic, while the regular import stays hidden + expect(screen.getByText(TestHelper.translateLocal('workspace.tags.appendCustomTagList'))).toBeOnTheScreen(); + expect(screen.queryByText(TestHelper.translateLocal('spreadsheet.importSpreadsheet'))).not.toBeOnTheScreen(); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + + it('should not show the option for dependent multi-level tags', async () => { + // Given a NetSuite workspace with dependent multi-level tags and an admin on the indirectTagUploads beta + // When the admin opens the More menu + const {unmount} = await renderAndOpenMoreMenu({...connectedPolicy, hasMultipleTagLists: true}, dependentMultiLevelTags, [CONST.BETAS.INDIRECT_TAG_UPLOADS]); + + // Then the option is hidden because a custom list can only be appended to single-level or independent multi-level tags + expect(screen.queryByText(TestHelper.translateLocal('workspace.tags.appendCustomTagList'))).not.toBeOnTheScreen(); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + + it('should not show the option without the indirectTagUploads beta', async () => { + // Given a NetSuite workspace with single-level tags and an admin who isn't on the indirectTagUploads beta + // When the admin opens the More menu + const {unmount} = await renderAndOpenMoreMenu(connectedPolicy, tags, []); + + // Then the option is hidden because the feature is gated by the beta + expect(screen.queryByText(TestHelper.translateLocal('workspace.tags.appendCustomTagList'))).not.toBeOnTheScreen(); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + }); + describe('multi-level tags', () => { it('keeps the imported level order and disables sorting on wide layouts', async () => { // Given a wide layout and independent multi-level tags imported as Charlie, Bravo, Alpha