diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 99ca859f4030..1a06b60f8760 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -4072,6 +4072,7 @@ const CONST = { SETTINGS: 'settings', EXPORT: 'export', SYNC_WITH_HR: 'syncWithHR', + ADD_APPROVAL_WORKFLOW: 'addApprovalWorkflow', }, MEMBERS_BULK_ACTION_TYPES: { REMOVE: 'remove', @@ -8870,6 +8871,7 @@ const CONST = { WORKFLOWS: { AUTO_REPORTING_FREQUENCY: 'WorkspaceWorkflows-AutoReportingFrequency', ADD_APPROVAL: 'WorkspaceWorkflows-AddApproval', + MORE_DROPDOWN: 'WorkspaceWorkflows-MoreDropdown', LOAD_MORE_APPROVALS: 'WorkspaceWorkflows-LoadMoreApprovals', BANK_ACCOUNT: 'WorkspaceWorkflows-BankAccount', ADD_BANK_ACCOUNT: 'WorkspaceWorkflows-AddBankAccount', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0712b35fe3ee..97417238df50 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -162,7 +162,7 @@ const DYNAMIC_ROUTES = { }, IMPORTED_MEMBERS_ROLE: { path: 'imported-members-role', - entryScreens: [SCREENS.WORKSPACE.MEMBERS_IMPORTED_CONFIRMATION], + entryScreens: [SCREENS.WORKSPACE.MEMBERS_IMPORTED_CONFIRMATION, SCREENS.WORKSPACE.WORKFLOWS_IMPORTED_CONFIRMATION], }, REPORT_SETTINGS: { path: 'report-settings', @@ -2730,6 +2730,18 @@ const ROUTES = { return `workspaces/${policyID}/workflows` as const; }, }, + WORKSPACE_WORKFLOWS_IMPORT: { + route: 'workspaces/:policyID/workflows/import', + getRoute: (policyID: string) => `workspaces/${policyID}/workflows/import` as const, + }, + WORKSPACE_WORKFLOWS_IMPORTED: { + route: 'workspaces/:policyID/workflows/imported', + getRoute: (policyID: string) => `workspaces/${policyID}/workflows/imported` as const, + }, + WORKSPACE_WORKFLOWS_IMPORTED_CONFIRMATION: { + route: 'workspaces/:policyID/workflows/imported/confirmation', + getRoute: (policyID: string) => `workspaces/${policyID}/workflows/imported/confirmation` as const, + }, WORKSPACE_WORKFLOWS_APPROVALS_NEW: { route: 'workspaces/:policyID/workflows/approvals/new', getRoute: (policyID: string) => `workspaces/${policyID}/workflows/approvals/new` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 0176fc984a71..f3193f9f9d5a 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -803,6 +803,9 @@ const SCREENS = { DYNAMIC_WORKSPACE_OVERVIEW_ADDRESS: 'Dynamic_Workspace_Overview_Address', DYNAMIC_WORKSPACE_OVERVIEW_PLAN: 'Dynamic_Workspace_Overview_Plan_Type', WORKFLOWS: 'Workspace_Workflows', + WORKFLOWS_IMPORT: 'Workspace_Workflows_Import', + WORKFLOWS_IMPORTED: 'Workspace_Workflows_Imported', + WORKFLOWS_IMPORTED_CONFIRMATION: 'Workspace_Workflows_Imported_Confirmation', WORKFLOWS_PAYER: 'Workspace_Workflows_Payer', WORKFLOWS_APPROVALS_NEW: 'Workspace_Approvals_New', WORKFLOWS_APPROVALS_EDIT: 'Workspace_Approvals_Edit', diff --git a/src/languages/de.ts b/src/languages/de.ts index 028b4400eb16..cab92563ad8b 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1197,6 +1197,7 @@ const translations: TranslationDeepObject = { 'Die hochgeladene Datei ist entweder leer oder enthält ungültige Daten. Bitte stelle sicher, dass die Datei korrekt formatiert ist und alle erforderlichen Informationen enthält, bevor du sie erneut hochlädst.', importSpreadsheetLibraryError: 'Laden des Tabellenmoduls fehlgeschlagen. Bitte überprüfe deine Internetverbindung und versuche es erneut.', importSpreadsheet: 'Tabellenkalkulation importieren', + importWorkflows: 'Workflows importieren', downloadCSV: 'CSV herunterladen', importMemberConfirmation: () => ({ one: `Bitte bestätige die folgenden Details für ein neues Workspace-Mitglied, das im Rahmen dieses Uploads hinzugefügt wird. Bestehende Mitglieder erhalten keine Rollenaktualisierungen oder Einladungsnachrichten.`, diff --git a/src/languages/en.ts b/src/languages/en.ts index 6caf97e61d20..eecee050d7b8 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1272,6 +1272,7 @@ const translations = { 'The file you uploaded is either empty or contains invalid data. Please ensure that the file is correctly formatted and contains the necessary information before uploading it again.', importSpreadsheetLibraryError: 'Failed to load spreadsheet module. Please check your internet connection and try again.', importSpreadsheet: 'Import spreadsheet', + importWorkflows: 'Import workflows', downloadCSV: 'Download CSV', importMemberConfirmation: () => ({ one: `Please confirm the details below for a new workspace member that will be added as part of this upload. Existing members won’t receive any role updates or invite messages.`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 2cbb85895718..5992572af3c6 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1166,6 +1166,7 @@ const translations: TranslationDeepObject = { 'El archivo que subiste está vacío o contiene datos no válidos. Asegúrate de que el archivo esté correctamente formateado y contenga la información necesaria antes de volver a subirlo.', importSpreadsheetLibraryError: 'Error al cargar el módulo de hojas de cálculo. Por favor, verifica tu conexión a internet e inténtalo de nuevo.', importSpreadsheet: 'Importar hoja de cálculo', + importWorkflows: 'Importar flujos de trabajo', downloadCSV: 'Descargar CSV', importMemberConfirmation: () => ({ one: `Por favor confirma los detalles a continuación para un nuevo miembro del espacio de trabajo que se agregará como parte de esta carga. Los miembros existentes no recibirán actualizaciones de rol ni mensajes de invitación.`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index b513f49e9009..1755efe6fea1 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1201,6 +1201,7 @@ const translations: TranslationDeepObject = { 'Le fichier que vous avez téléchargé est soit vide, soit contient des données non valides. Veuillez vous assurer que le fichier est correctement formaté et contient les informations nécessaires avant de le télécharger à nouveau.', importSpreadsheetLibraryError: 'Échec du chargement du module de feuille de calcul. Veuillez vérifier votre connexion Internet et réessayer.', importSpreadsheet: 'Importer un tableau', + importWorkflows: 'Importer les workflows', downloadCSV: 'Télécharger le CSV', importMemberConfirmation: () => ({ one: `Veuillez confirmer les détails ci-dessous pour un nouveau membre de l’espace de travail qui sera ajouté dans le cadre de ce téléversement. Les membres existants ne recevront aucune mise à jour de rôle ni message d’invitation.`, diff --git a/src/languages/it.ts b/src/languages/it.ts index a6d1f8586779..3652b83eecbd 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1197,6 +1197,7 @@ const translations: TranslationDeepObject = { 'Il file che hai caricato è vuoto oppure contiene dati non validi. Assicurati che il file sia formattato correttamente e contenga le informazioni necessarie prima di caricarlo di nuovo.', importSpreadsheetLibraryError: 'Caricamento del modulo foglio di calcolo non riuscito. Controlla la connessione a Internet e riprova.', importSpreadsheet: 'Importa foglio di calcolo', + importWorkflows: 'Importa flussi di lavoro', downloadCSV: 'Scarica CSV', importMemberConfirmation: () => ({ one: `Conferma i dettagli seguenti per il nuovo membro dello spazio di lavoro che verrà aggiunto come parte di questo caricamento. I membri esistenti non riceveranno aggiornamenti di ruolo né messaggi di invito.`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 912416c138b1..a63f25df15b8 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1180,6 +1180,7 @@ const translations: TranslationDeepObject = { 'アップロードしたファイルは空であるか、無効なデータが含まれています。再度アップロードする前に、ファイルの形式が正しく、必要な情報が含まれていることを確認してください。', importSpreadsheetLibraryError: 'スプレッドシートモジュールの読み込みに失敗しました。インターネット接続を確認して、もう一度お試しください。', importSpreadsheet: 'スプレッドシートをインポート', + importWorkflows: 'ワークフローをインポート', downloadCSV: 'CSV をダウンロード', importMemberConfirmation: () => ({ one: `このアップロードの一部として追加される新しいワークスペースメンバーの詳細を、以下で確認してください。既存のメンバーにはロールの更新や招待メッセージは送信されません。`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index d0ad03162ce4..cb9feaa8d504 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1196,6 +1196,7 @@ const translations: TranslationDeepObject = { 'Het bestand dat je hebt geüpload is leeg of bevat ongeldige gegevens. Controleer voordat je het opnieuw uploadt of het bestand correct is opgemaakt en alle vereiste informatie bevat.', importSpreadsheetLibraryError: 'Laden van spreadsheetmodule mislukt. Controleer je internetverbinding en probeer het opnieuw.', importSpreadsheet: 'Spreadsheet importeren', + importWorkflows: 'Workflows importeren', downloadCSV: 'CSV downloaden', importMemberConfirmation: () => ({ one: `Bevestig hieronder de gegevens voor een nieuw werkruimte‑lid dat wordt toegevoegd als onderdeel van deze upload. Bestaande leden ontvangen geen rolupdates of uitnodigingsberichten.`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 2ebb77a0f8be..0b8c4b143156 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1192,6 +1192,7 @@ const translations: TranslationDeepObject = { 'Przesłany plik jest pusty lub zawiera nieprawidłowe dane. Upewnij się, że plik ma prawidłowy format i zawiera wszystkie wymagane informacje, zanim prześlesz go ponownie.', importSpreadsheetLibraryError: 'Nie udało się załadować modułu arkusza kalkulacyjnego. Sprawdź połączenie z internetem i spróbuj ponownie.', importSpreadsheet: 'Importuj arkusz kalkulacyjny', + importWorkflows: 'Importuj przepływy pracy', downloadCSV: 'Pobierz plik CSV', importMemberConfirmation: () => ({ one: `Potwierdź poniższe dane nowego członka przestrzeni roboczej, który zostanie dodany w ramach tego przesyłania. Istniejący członkowie nie otrzymają żadnych aktualizacji ról ani zaproszeń.`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 098ee4812d1e..c7384897dabe 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1196,6 +1196,7 @@ const translations: TranslationDeepObject = { 'O arquivo que você enviou está vazio ou contém dados inválidos. Verifique se o arquivo está formatado corretamente e contém as informações necessárias antes de enviá-lo novamente.', importSpreadsheetLibraryError: 'Falha ao carregar o módulo de planilha. Verifique sua conexão com a internet e tente novamente.', importSpreadsheet: 'Importar planilha', + importWorkflows: 'Importar fluxos de trabalho', downloadCSV: 'Baixar CSV', importMemberConfirmation: () => ({ one: `Confirme os detalhes abaixo para o novo membro do espaço de trabalho que será adicionado como parte deste envio. Membros existentes não receberão atualizações de função nem mensagens de convite.`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 6ef0e723e500..a5c454f1cf11 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1149,6 +1149,7 @@ const translations: TranslationDeepObject = { invalidFileMessage: '您上传的文件为空或包含无效数据。请确保文件格式正确且包含所需信息,然后再重新上传。', importSpreadsheetLibraryError: '加载电子表格模块失败。请检查您的互联网连接,然后重试。', importSpreadsheet: '导入电子表格', + importWorkflows: '导入工作流程', downloadCSV: '下载 CSV', importMemberConfirmation: () => ({ one: `请确认以下新工作区成员的详细信息,该成员将作为此次上传的一部分被添加。现有成员不会收到任何角色更新或邀请消息。`, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 05f2da53eda1..91fe9099fc16 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -510,8 +510,11 @@ const SettingsModalStackNavigator = createModalStackNavigator('../../../../pages/settings/Subscription/SubscriptionPlan/DynamicSubscriptionPlanDowngradeBlockedPage').default, [SCREENS.WORKSPACE.DYNAMIC_WORKSPACE_INVITE]: () => require('../../../../pages/workspace/DynamicWorkspaceInvitePage').default, [SCREENS.WORKSPACE.MEMBERS_IMPORT]: () => require('../../../../pages/workspace/members/ImportMembersPage').default, + [SCREENS.WORKSPACE.WORKFLOWS_IMPORT]: () => require('../../../../pages/workspace/workflows/ImportWorkflowsPage').default, [SCREENS.WORKSPACE.MEMBERS_IMPORTED]: () => require('../../../../pages/workspace/members/ImportedMembersPage').default, [SCREENS.WORKSPACE.MEMBERS_IMPORTED_CONFIRMATION]: () => require('../../../../pages/workspace/members/ImportedMembersConfirmationPage').default, + [SCREENS.WORKSPACE.WORKFLOWS_IMPORTED]: () => require('../../../../pages/workspace/members/ImportedMembersPage').default, + [SCREENS.WORKSPACE.WORKFLOWS_IMPORTED_CONFIRMATION]: () => require('../../../../pages/workspace/members/ImportedMembersConfirmationPage').default, [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_NEW]: () => require('../../../../pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage').default, [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_EDIT]: () => require('../../../../pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage').default, [SCREENS.WORKSPACE.DYNAMIC_WORKFLOWS_APPROVALS_EXPENSES_FROM]: () => diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 2c10e14c72e0..ef63700113fa 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -25,11 +25,18 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.MEMBERS_IMPORT]: { path: ROUTES.WORKSPACE_MEMBERS_IMPORT.route, }, + [SCREENS.WORKSPACE.WORKFLOWS_IMPORT]: { + path: ROUTES.WORKSPACE_WORKFLOWS_IMPORT.route, + }, + [SCREENS.WORKSPACE.WORKFLOWS_IMPORTED]: { + path: ROUTES.WORKSPACE_WORKFLOWS_IMPORTED.route, + }, + [SCREENS.WORKSPACE.WORKFLOWS_IMPORTED_CONFIRMATION]: { + path: ROUTES.WORKSPACE_WORKFLOWS_IMPORTED_CONFIRMATION.route, + }, [SCREENS.WORKSPACE.MEMBERS_IMPORTED]: { path: ROUTES.WORKSPACE_MEMBERS_IMPORTED.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 9267554f87f7..013eaf7e0ce6 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -329,6 +329,15 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.MEMBERS_IMPORT]: { policyID: string; }; + [SCREENS.WORKSPACE.WORKFLOWS_IMPORT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.WORKFLOWS_IMPORTED]: { + policyID: string; + }; + [SCREENS.WORKSPACE.WORKFLOWS_IMPORTED_CONFIRMATION]: { + policyID: string; + }; [SCREENS.WORKSPACE.MEMBERS_IMPORTED]: { policyID: string; }; diff --git a/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx index 32a517c8ebae..8c1161db881c 100644 --- a/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx +++ b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx @@ -32,14 +32,17 @@ import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; +import SCREENS from '@src/SCREENS'; import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes'; import React, {useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; -type ImportedMembersConfirmationPageProps = PlatformStackScreenProps; +type ImportedMembersConfirmationPageProps = PlatformStackScreenProps< + SettingsNavigatorParamList, + typeof SCREENS.WORKSPACE.MEMBERS_IMPORTED_CONFIRMATION | typeof SCREENS.WORKSPACE.WORKFLOWS_IMPORTED_CONFIRMATION +>; function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPageProps) { const styles = useThemeStyles(); @@ -96,11 +99,15 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag openExternalLink(CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL); }; + // The same confirmation screen is reused for the Members importer and the Workflows importer, so we return the user + // to the page the import was started from. + const isWorkflowsImport = route.name === SCREENS.WORKSPACE.WORKFLOWS_IMPORTED_CONFIRMATION; + const closeImportPageAndModal = () => { setIsClosing(true); setIsImporting(false); closeImportPage(); - Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)); + Navigation.goBack(isWorkflowsImport ? ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID) : ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)); }; const importMembers = async () => { @@ -131,7 +138,7 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag shouldShowOfflineIndicatorInWideScreen > { @@ -174,7 +181,7 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag style={styles.mb3} > - {translate('common.import')} + {isWorkflowsImport ? translate('common.invite') : translate('common.import')} ; +type ImportedMembersPageProps = PlatformStackScreenProps; function ImportedMembersPage({route}: ImportedMembersPageProps) { const {translate} = useLocalize(); @@ -41,6 +41,10 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { const {login: currentUserLogin = ''} = useCurrentUserPersonalDetails(); const canAssignElevatedRoles = canMemberAssignElevatedRole(policy, currentUserLogin); + // 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). + const isWorkflowsImport = route.name === SCREENS.WORKSPACE.WORKFLOWS_IMPORTED; + const columnNames = generateColumnNames(spreadsheet?.data?.length ?? 0); const {containsHeader = true} = spreadsheet ?? {}; const isControlPolicy = isControlPolicyUtil(policy); @@ -89,7 +93,8 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { }; const navigateBackToMembers = () => { - Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID), {waitForTransition: true}); + const returnRoute = isWorkflowsImport ? ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID) : ROUTES.WORKSPACE_MEMBERS.getRoute(policyID); + Navigation.goBack(returnRoute, {waitForTransition: true}); }; const importMembers = async () => { @@ -254,7 +259,7 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { if (isRoleMissing) { await setImportedSpreadsheetMemberData(allMembers, shouldShowMemberRolePermissionWarning); - Navigation.navigate(ROUTES.WORKSPACE_MEMBERS_IMPORTED_CONFIRMATION.getRoute(policyID)); + Navigation.navigate(isWorkflowsImport ? ROUTES.WORKSPACE_WORKFLOWS_IMPORTED_CONFIRMATION.getRoute(policyID) : ROUTES.WORKSPACE_MEMBERS_IMPORTED_CONFIRMATION.getRoute(policyID)); } else { setIsImporting(true); const importFinalModal = await importPolicyMembers(policy, allMembers, shouldShowMemberRolePermissionWarning); @@ -283,8 +288,8 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { shouldShowOfflineIndicatorInWideScreen > Navigation.goBack(ROUTES.WORKSPACE_MEMBERS_IMPORT.getRoute(policyID))} + title={isWorkflowsImport ? translate('spreadsheet.importWorkflows') : translate('workspace.people.importMembers')} + onBackButtonPress={() => Navigation.goBack(isWorkflowsImport ? ROUTES.WORKSPACE_WORKFLOWS_IMPORT.getRoute(policyID) : ROUTES.WORKSPACE_MEMBERS_IMPORT.getRoute(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. `backTo` returns the upload screen to Workflows, +// and `goTo` sends the mapping step to the Workflows-context imported screen (instead of the Members one). +function ImportWorkflowsPage({route}: ImportWorkflowsPageProps) { + const policyID = route.params.policyID; + const policy = usePolicy(policyID); + + return ( + + + + ); +} + +export default ImportWorkflowsPage; diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 1fecb1510824..c2fa1da45b40 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -1,4 +1,6 @@ import ApprovalWorkflowSection from '@components/ApprovalWorkflowSection'; +import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import ConnectionStatusBadge from '@components/ConnectionStatusBadge'; import ConnectionStatusMessage from '@components/ConnectionStatusMessage'; import Hoverable from '@components/Hoverable'; @@ -86,7 +88,7 @@ import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; -import type {TupleToUnion} from 'type-fest'; +import type {TupleToUnion, ValueOf} from 'type-fest'; import {hasSeenTourSelector} from '@selectors/Onboarding'; import {Str} from 'expensify-common'; @@ -159,7 +161,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { const styles = useThemeStyles(); const theme = useTheme(); const illustrations = useMemoizedLazyIllustrations(['Workflows']); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator', 'Info', 'Plus']); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator', 'Info', 'Plus', 'Table']); // 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 {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); @@ -327,6 +329,43 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM.path)); }, [policy, route.params.policyID, availableMembers, usedApproverEmails, canAccessSubmit2026Features, navigateToSubmitWorkspaceApprovalsUpgrade]); + // 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 (canAccessSubmit2026Features) { + navigateToSubmitWorkspaceApprovalsUpgrade(); + return; + } + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_IMPORT.getRoute(route.params.policyID)); + }, [isAccountLocked, showLockedAccountModal, isOffline, showConfirmModal, translate, route.params.policyID, canAccessSubmit2026Features, navigateToSubmitWorkspaceApprovalsUpgrade]); + + const approvalSecondaryActions = useMemo>>>( + () => [ + { + icon: expensifyIcons.Table, + text: translate('spreadsheet.importWorkflows'), + onSelected: importWorkflowsAction, + value: CONST.POLICY.SECONDARY_ACTIONS.IMPORT_SPREADSHEET, + }, + ], + [expensifyIcons.Table, translate, importWorkflowsAction], + ); + const isHRAdvancedModeEnabled = isHRAdvancedMode(policy); const hrFinalApproverEmail = getHRFinalApprover(policy) ?? undefined; @@ -1003,6 +1042,21 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { const isGroupPolicy = isGroupPolicyUtil(policy); const isLoading = !!(policy?.isLoading && policy?.reimbursementChoice === undefined); + const headerButtons = + !shouldBlockApprovalWorkflowEditing && canWriteApprovals ? ( + + {}} + shouldAlwaysShowDropdownMenu + customText={translate('common.more')} + sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.WORKFLOWS.MORE_DROPDOWN} + options={approvalSecondaryActions} + isSplitButton={false} + wrapperStyle={styles.flexGrow0} + /> + + ) : undefined; + return (