diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 8a6d3e5c0dce..e7018e49efc1 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -250,6 +250,9 @@ const ONYXKEYS = { /** Details on whether an account is locked or not */ NVP_PRIVATE_LOCK_ACCOUNT_DETAILS: 'nvp_private_lockAccountDetails', + /** The NVP containing the user's custom IS templates */ + NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES: 'nvp_expensify_integrationServerExportTemplates', + /** Plaid data (access tokens, bank accounts ...) */ PLAID_DATA: 'plaidData', @@ -1213,6 +1216,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_LAST_IPHONE_LOGIN]: string; [ONYXKEYS.NVP_LAST_ANDROID_LOGIN]: string; [ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_REPORT_IDS]: string[]; + [ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES]: OnyxTypes.IntegrationServerExportTemplate[]; [ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION]: OnboardingAccounting; [ONYXKEYS.HYBRID_APP]: OnyxTypes.HybridApp; }; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 99296db79e0b..ca7d2813624d 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -172,6 +172,7 @@ function MoneyReportHeader({ const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${reportPDFFilename}`, {canBeMissing: true}); const isDownloadingPDF = download?.isDownloading ?? false; const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES, {canBeMissing: true}); const requestParentReportAction = useMemo(() => { if (!reportActions || !transactionThreadReport?.parentReportActionID) { return null; @@ -549,8 +550,8 @@ function MoneyReportHeader({ const [offlineModalVisible, setOfflineModalVisible] = useState(false); - const exportSubMenuOptions: Record, DropdownOption>> = useMemo( - () => ({ + const exportSubmenuOptions: Record> = useMemo(() => { + const options: Record> = { [CONST.REPORT.EXPORT_OPTIONS.DOWNLOAD_CSV]: { text: translate('export.basicExport'), icon: Expensicons.Table, @@ -611,9 +612,22 @@ function MoneyReportHeader({ markAsManuallyExported(moneyRequestReport?.reportID, connectedIntegration); }, }, - }), - [translate, connectedIntegrationFallback, connectedIntegration, moneyRequestReport, isOffline, transactionIDs, isExported, beginExportWithTemplate], - ); + }; + + // If the user has any custom integration export templates, add them as export options + if (integrationsExportTemplates && integrationsExportTemplates.length > 0) { + for (const template of integrationsExportTemplates) { + options[template.name] = { + text: template.name, + icon: Expensicons.Table, + value: template.name, + onSelected: () => beginExportWithTemplate(template.name, CONST.EXPORT_TEMPLATE_TYPES.INTEGRATIONS, transactionIDs), + }; + } + } + + return options; + }, [translate, connectedIntegrationFallback, connectedIntegration, moneyRequestReport, isOffline, transactionIDs, isExported, beginExportWithTemplate, integrationsExportTemplates]); const primaryActionsImplementation = { [CONST.REPORT.PRIMARY_ACTIONS.SUBMIT]: ( @@ -759,8 +773,8 @@ function MoneyReportHeader({ if (!moneyRequestReport) { return []; } - return getSecondaryExportReportActions(moneyRequestReport, policy, reportActions); - }, [moneyRequestReport, policy, reportActions]); + return getSecondaryExportReportActions(moneyRequestReport, policy, reportActions, integrationsExportTemplates ?? []); + }, [moneyRequestReport, policy, reportActions, integrationsExportTemplates]); const secondaryActionsImplementation: Record< ValueOf, @@ -779,7 +793,7 @@ function MoneyReportHeader({ text: translate('common.export'), backButtonText: translate('common.export'), icon: Expensicons.Export, - subMenuItems: secondaryExportActions.map((action) => exportSubMenuOptions[action]), + subMenuItems: secondaryExportActions.map((action) => exportSubmenuOptions[action as string]), }, [CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF]: { value: CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF, diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index ef7840cfe847..f5b13602836c 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -49,6 +49,7 @@ function useSelectedTransactionsActions({ }) { const {selectedTransactionIDs, clearSelectedTransactions} = useSearchContext(); const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false}); + const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES, {canBeMissing: true}); const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(selectedTransactionIDs); const isReportArchived = useReportIsArchived(report?.reportID); const selectedTransactions = useMemo( @@ -210,6 +211,17 @@ function useSelectedTransactionsActions({ }); } + // If the user has any custom integration export templates, add them as export options + if (integrationsExportTemplates && integrationsExportTemplates.length > 0) { + for (const template of integrationsExportTemplates) { + exportOptions.push({ + text: template.name, + icon: Expensicons.Table, + onSelected: () => beginExportWithTemplate(template.name, CONST.EXPORT_TEMPLATE_TYPES.INTEGRATIONS, selectedTransactionIDs), + }); + } + } + return exportOptions; }; @@ -279,6 +291,7 @@ function useSelectedTransactionsActions({ session?.accountID, showDeleteModal, beginExportWithTemplate, + integrationsExportTemplates, ]); return { diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index 8a2031e2fb31..1ea23262e4d4 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -2,7 +2,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, Report, ReportAction, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {IntegrationServerExportTemplate, Policy, Report, ReportAction, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; import {isApprover as isApproverUtils} from './actions/Policy/Member'; import {getCurrentUserAccountID, getCurrentUserEmail} from './actions/Report'; import { @@ -646,9 +646,13 @@ function getSecondaryReportActions({ return options; } -function getSecondaryExportReportActions(report: Report, policy?: Policy, reportActions?: ReportAction[]): Array> { - const options: Array> = []; - +function getSecondaryExportReportActions( + report: Report, + policy?: Policy, + reportActions?: ReportAction[], + integrationsExportTemplates?: IntegrationServerExportTemplate[], +): Array> { + const options: Array> = []; if (isExportAction(report, policy, reportActions)) { options.push(CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION); } @@ -659,6 +663,12 @@ function getSecondaryExportReportActions(report: Report, policy?: Policy, report options.push(CONST.REPORT.EXPORT_OPTIONS.DOWNLOAD_CSV, CONST.REPORT.EXPORT_OPTIONS.EXPENSE_LEVEL_EXPORT, CONST.REPORT.EXPORT_OPTIONS.REPORT_LEVEL_EXPORT); + if (integrationsExportTemplates && integrationsExportTemplates.length > 0) { + for (const template of integrationsExportTemplates) { + options.push(template.name); + } + } + return options; } diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index b01e304b3c0f..e1e2ec24381c 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -79,7 +79,7 @@ function SearchPage({route}: SearchPageProps) { const [newParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${newReport?.parentReportID}`, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: false}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); - + const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES, {canBeMissing: true}); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); const [isDeleteExpensesConfirmModalVisible, setIsDeleteExpensesConfirmModalVisible] = useState(false); @@ -198,6 +198,20 @@ function SearchPage({route}: SearchPageProps) { }); } + // If the user has any custom integration export templates, add them as export options + if (integrationsExportTemplates && integrationsExportTemplates.length > 0) { + for (const template of integrationsExportTemplates) { + exportOptions.push({ + text: template.name, + icon: Expensicons.Table, + onSelected: () => { + // Custom IS templates are not policy specific, so we don't need to pass a policyID + beginExportWithTemplate(template.name, CONST.EXPORT_TEMPLATE_TYPES.INTEGRATIONS, undefined); + }, + }); + } + } + return exportOptions; }; @@ -433,6 +447,7 @@ function SearchPage({route}: SearchPageProps) { styles.fontWeightNormal, styles.textWrap, beginExportWithTemplate, + integrationsExportTemplates, ]); const handleDeleteExpenses = () => { diff --git a/src/types/onyx/IntegrationServerExportTemplate.ts b/src/types/onyx/IntegrationServerExportTemplate.ts new file mode 100644 index 000000000000..4c354e1c0c17 --- /dev/null +++ b/src/types/onyx/IntegrationServerExportTemplate.ts @@ -0,0 +1,9 @@ +import type * as OnyxCommon from './OnyxCommon'; + +/** Information about integration server export templates */ +type IntegrationServerExportTemplate = OnyxCommon.OnyxValueWithOfflineFeedback<{ + /** Name of the template */ + name: string; +}>; + +export default IntegrationServerExportTemplate; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 873831f04141..4390afa64f46 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -34,6 +34,7 @@ import type {FundList} from './Fund'; import type Fund from './Fund'; import type HybridApp from './HybridApp'; import type ImportedSpreadsheet from './ImportedSpreadsheet'; +import type IntegrationServerExportTemplate from './IntegrationServerExportTemplate'; import type IntroSelected from './IntroSelected'; import type InvitedEmailsToAccountIDs from './InvitedEmailsToAccountIDs'; import type JoinablePolicies from './JoinablePolicies'; @@ -265,5 +266,6 @@ export type { ValidateUserAndGetAccessiblePolicies, VacationDelegate, BillingReceiptDetails, + IntegrationServerExportTemplate, HybridApp, };