diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 34054147a56b..2fb0bd43c65b 100644
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -5091,6 +5091,8 @@ const CONST = {
STANDARD_LENGTH_LIMIT: 100,
STANDARD_LIST_ITEM_LIMIT: 12,
SEARCH_BAR_THRESHOLD: 3,
+ // Number of approval workflow cards rendered before the "Load more" affordance on Workspace → Workflows.
+ WORKFLOW_APPROVALS_INITIAL_BATCH: 5,
LEGAL_NAMES_CHARACTER_LIMIT: 150,
LOGIN_CHARACTER_LIMIT: 254,
CATEGORY_NAME_LIMIT: 256,
@@ -8487,6 +8489,7 @@ const CONST = {
WORKFLOWS: {
AUTO_REPORTING_FREQUENCY: 'WorkspaceWorkflows-AutoReportingFrequency',
ADD_APPROVAL: 'WorkspaceWorkflows-AddApproval',
+ LOAD_MORE_APPROVALS: 'WorkspaceWorkflows-LoadMoreApprovals',
BANK_ACCOUNT: 'WorkspaceWorkflows-BankAccount',
ADD_BANK_ACCOUNT: 'WorkspaceWorkflows-AddBankAccount',
AUTHORIZED_PAYER: 'WorkspaceWorkflows-AuthorizedPayer',
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 4d3ea2d5a05f..b5063f4d5e41 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -2631,6 +2631,7 @@ ${amount} für ${merchant} – ${date}`,
addApprovalsTitle: 'Genehmigungen',
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `Ausgaben von ${members} und die genehmigende Person ist ${approvers}`,
addApprovalButton: 'Genehmigungsablauf hinzufügen',
+ loadMoreWorkflows: ({count}: {count: number}) => `${count} weitere laden`,
editWorkflowAction: 'Bearbeiten',
findWorkflow: 'Workflow suchen',
addApprovalTip: 'Dieser Standard-Workflow gilt für alle Mitglieder, sofern kein spezifischerer Workflow vorhanden ist.',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index ce190738b047..cb7800e7144d 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -2698,6 +2698,7 @@ const translations = {
addApprovalsTitle: 'Approvals',
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `expenses from ${members}, and the approver is ${approvers}`,
addApprovalButton: 'Add approval workflow',
+ loadMoreWorkflows: ({count}: {count: number}) => `Load ${count} more`,
editWorkflowAction: 'Edit',
findWorkflow: 'Find workflow',
addApprovalTip: 'This default workflow applies to all members, unless a more specific workflow exists.',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 07608053ab22..1ee60bb6f0e8 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -2501,6 +2501,7 @@ ${amount} para ${merchant} - ${date}`,
addApprovalsTitle: 'Aprobaciones',
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `gastos de ${members}, y el aprobador es ${approvers}`,
addApprovalButton: 'Añadir flujo de aprobación',
+ loadMoreWorkflows: ({count}: {count: number}) => `Cargar ${count} más`,
editWorkflowAction: 'Editar',
findWorkflow: 'Buscar flujo de trabajo',
addApprovalTip: 'Este flujo de trabajo por defecto se aplica a todos los miembros, a menos que exista un flujo de trabajo más específico.',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index c3e494354b3d..35a3b0bceaf9 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -2637,6 +2637,7 @@ ${amount} pour ${merchant} - ${date}`,
addApprovalsTitle: 'Approbations',
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `dépenses de ${members}, et l'approbateur est ${approvers}`,
addApprovalButton: 'Ajouter un workflow d’approbation',
+ loadMoreWorkflows: ({count}: {count: number}) => `Charger ${count} de plus`,
editWorkflowAction: 'Modifier',
findWorkflow: 'Rechercher un flux de travail',
addApprovalTip: 'Ce workflow par défaut s’applique à tous les membres, sauf si un workflow plus spécifique existe.',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 61253d53f813..79a37f652b49 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -2626,6 +2626,7 @@ ${amount} per ${merchant} - ${date}`,
addApprovalsTitle: 'Approvazioni',
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `spese di ${members} e l'approvatore è ${approvers}`,
addApprovalButton: 'Aggiungi flusso di approvazione',
+ loadMoreWorkflows: ({count}: {count: number}) => `Carica altri ${count}`,
editWorkflowAction: 'Modifica',
findWorkflow: 'Cerca flusso di lavoro',
addApprovalTip: 'Questo flusso di lavoro predefinito si applica a tutti i membri, a meno che non esista un flusso di lavoro più specifico.',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 03cc392a81e6..5cd03f1bd30c 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -2604,6 +2604,7 @@ ${date} の ${merchant} への ${amount}`,
addApprovalsTitle: '承認',
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `${members} の経費で、承認者は ${approvers} です`,
addApprovalButton: '承認ワークフローを追加',
+ loadMoreWorkflows: ({count}: {count: number}) => `さらに${count}件を読み込む`,
editWorkflowAction: '編集',
findWorkflow: 'ワークフローを検索',
addApprovalTip: 'より詳細なワークフローが存在する場合を除き、このデフォルトのワークフローがすべてのメンバーに適用されます。',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 115a119c1362..f54146477ac3 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -2624,6 +2624,7 @@ ${amount} voor ${merchant} - ${date}`,
addApprovalsTitle: 'Goedkeuringen',
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `de uitgaven van ${members}, en de goedkeurder is ${approvers}`,
addApprovalButton: 'Goedkeuringsworkflow toevoegen',
+ loadMoreWorkflows: ({count}: {count: number}) => `${count} meer laden`,
editWorkflowAction: 'Bewerken',
findWorkflow: 'Workflow zoeken',
addApprovalTip: 'Deze standaardworkflow is van toepassing op alle leden, tenzij er een specifiekere workflow bestaat.',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index e13535b066ba..80b95cd3b0ea 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -2620,6 +2620,7 @@ ${amount} dla ${merchant} - ${date}`,
addApprovalsTitle: 'Zatwierdzenia',
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `wydatki od ${members}, a zatwierdzającym jest ${approvers}`,
addApprovalButton: 'Dodaj proces akceptacji',
+ loadMoreWorkflows: ({count}: {count: number}) => `Załaduj ${count} więcej`,
editWorkflowAction: 'Edytuj',
findWorkflow: 'Znajdź przepływ pracy',
addApprovalTip: 'Domyślny proces pracy ma zastosowanie do wszystkich członków, chyba że istnieje bardziej szczegółowy proces pracy.',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 61d0a368e0b0..3c65046f08d3 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -2618,6 +2618,7 @@ ${amount} para ${merchant} - ${date}`,
addApprovalsTitle: 'Aprovações',
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `despesas de ${members}, e o aprovador é ${approvers}`,
addApprovalButton: 'Adicionar fluxo de aprovação',
+ loadMoreWorkflows: ({count}: {count: number}) => `Carregar mais ${count}`,
editWorkflowAction: 'Editar',
findWorkflow: 'Buscar fluxo de trabalho',
addApprovalTip: 'Este fluxo de trabalho padrão se aplica a todos os membros, a menos que exista um fluxo de trabalho mais específico.',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index f94921af0b7a..a79d3f3ab3d5 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -2554,6 +2554,7 @@ ${amount},商户:${merchant} - 日期:${date}`,
addApprovalsTitle: '审批',
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `来自${members}的报销,审批人是${approvers}`,
addApprovalButton: '添加审批工作流',
+ loadMoreWorkflows: ({count}: {count: number}) => `加载更多 ${count} 个`,
editWorkflowAction: '编辑',
findWorkflow: '查找工作流',
addApprovalTip: '除非存在更具体的工作流程,否则此默认工作流程适用于所有成员。',
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
index 7993669516d2..ee669f8061e1 100644
--- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
@@ -1,6 +1,6 @@
import {hasSeenTourSelector} from '@selectors/Onboarding';
import {Str} from 'expensify-common';
-import React, {useCallback, useEffect, useMemo} from 'react';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
// eslint-disable-next-line no-restricted-imports
import {InteractionManager, View} from 'react-native';
import type {TupleToUnion} from 'type-fest';
@@ -13,6 +13,7 @@ import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import {ModalActions} from '@components/Modal/Global/ModalContext';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import RenderHTML from '@components/RenderHTML';
import SearchBar from '@components/SearchBar';
import Section from '@components/Section';
@@ -70,6 +71,7 @@ import ExpenseReportRulesSection from '@pages/workspace/rules/ExpenseReportRules
import type {WithPolicyProps} from '@pages/workspace/withPolicy';
import withPolicy from '@pages/workspace/withPolicy';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
+import variables from '@styles/variables';
import {pressLockedBankAccount} from '@userActions/BankAccounts';
import {getPaymentMethods} from '@userActions/PaymentMethods';
import {navigateToBankAccountRoute} from '@userActions/ReimbursementAccount';
@@ -79,6 +81,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow';
+import type IconAsset from '@src/types/utils/IconAsset';
import type {ToggleSettingOptionRowProps} from './ToggleSettingsOptionRow';
import ToggleSettingOptionRow from './ToggleSettingsOptionRow';
import {getAutoReportingFrequencyDisplayNames} from './WorkspaceAutoReportingFrequencyPage';
@@ -107,13 +110,43 @@ function WorkflowNoResultsView({message, shouldShow, searchValue}: {message: str
);
}
+// Bordered "Load more" card matching the workflow rows: the whole card is the tap target and gets the row-hover state (per #91727 design).
+function WorkflowsLoadMoreCard({count, icon, onPress}: {count: number; icon: IconAsset; onPress: () => void}) {
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const {translate} = useLocalize();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const label = translate('workflowsPage.loadMoreWorkflows', {count});
+
+ return (
+
+
+
+ {label}
+
+
+ );
+}
+
function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
useWorkspaceDocumentTitle(policy?.name, 'workspace.common.workflows');
const {translate, localeCompare} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();
const illustrations = useMemoizedLazyIllustrations(['Workflows']);
- const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator', 'Info', 'Plus']);
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['CircularArrowBackwards', 'DotIndicator', 'Info', 'Plus']);
// 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();
@@ -294,6 +327,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
};
const [workflowSearchInput, setWorkflowSearchInput, searchFilteredWorkflows] = useSearchResults(filteredApprovalWorkflows, filterWorkflow);
+ const [isWorkflowListExpanded, setIsWorkflowListExpanded] = useState(false);
useEffect(() => {
if (filteredApprovalWorkflows.length > CONST.SEARCH_BAR_THRESHOLD) {
@@ -302,6 +336,11 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
setWorkflowSearchInput('');
}, [filteredApprovalWorkflows.length, setWorkflowSearchInput]);
+ // Searching reveals every match, so pagination is bypassed while a query is active. Pressing "Load more" reveals all remaining workflows at once.
+ const isSearchingWorkflows = workflowSearchInput.length > 0;
+ const displayedWorkflows = isWorkflowListExpanded || isSearchingWorkflows ? searchFilteredWorkflows : searchFilteredWorkflows.slice(0, CONST.WORKFLOW_APPROVALS_INITIAL_BATCH);
+ const hiddenWorkflowsCount = searchFilteredWorkflows.length - displayedWorkflows.length;
+
const isDEWEnabled = hasDynamicExternalWorkflow(policy);
const isHRConnected = isAnyHRConnected(policy);
const shouldBlockApprovalWorkflowEditing = isAnyHRReadOnlyWorkflowMode(policy);
@@ -473,7 +512,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
shouldShow={searchFilteredWorkflows.length === 0 && workflowSearchInput.length > 0}
searchValue={workflowSearchInput}
/>
- {searchFilteredWorkflows.map((workflow) => {
+ {displayedWorkflows.map((workflow) => {
const firstApproverEmail = workflow.approvers.at(0)?.email ?? '';
return (
@@ -514,6 +553,13 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
);
})}
+ {hiddenWorkflowsCount > 0 && (
+ setIsWorkflowListExpanded(true)}
+ />
+ )}
{!shouldBlockApprovalWorkflowEditing && canWriteApprovals && (