Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support EXPORTINTEGRATION actions #44930

Merged
merged 27 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
844baf2
remove EXPORTINTEGRATION from OldDot actions list
arosiclair Jul 5, 2024
06f584c
add originalMessage for EXPORTINTEGRATION
arosiclair Jul 5, 2024
624f550
add copy
arosiclair Jul 5, 2024
3d9aa0f
add ExportIntegration action
arosiclair Jul 5, 2024
99731b3
add more integration labels
arosiclair Jul 5, 2024
9235c98
handle multiple non-reimbursable urls
arosiclair Jul 5, 2024
fdfb58b
calculate base62 reportID
arosiclair Jul 5, 2024
22ab047
allow multiple links
arosiclair Jul 8, 2024
854f5a7
remove trailing spaces from translations
arosiclair Jul 8, 2024
bb30de1
fix wrapping
arosiclair Jul 8, 2024
91f500a
Merge branch 'main' of github.com:Expensify/App into arosiclair-expor…
arosiclair Jul 8, 2024
8ed0db3
remove from OldDot actions
arosiclair Jul 8, 2024
71ceaaf
support pending action
arosiclair Jul 8, 2024
b0c573c
add key
arosiclair Jul 8, 2024
48d33a2
Merge branch 'main' of github.com:Expensify/App into arosiclair-expor…
arosiclair Jul 11, 2024
ceb811c
move to separate module to fix dependency cycle
arosiclair Jul 11, 2024
34fd955
move message building to ReportActionUtils
arosiclair Jul 11, 2024
48ca92b
use message util for LHN last message text
arosiclair Jul 11, 2024
24db1d4
Merge branch 'main' of github.com:Expensify/App into arosiclair-expor…
arosiclair Jul 12, 2024
fbd6b62
add spanish translations
arosiclair Jul 12, 2024
bb4bc42
use consts
arosiclair Jul 15, 2024
abd2288
handle salesforce URLs
arosiclair Jul 15, 2024
6a7c057
handle empty URLs
arosiclair Jul 15, 2024
0c49035
copy formatted message text to clipboard
arosiclair Jul 15, 2024
f409e36
copy formatted html to preserve links
arosiclair Jul 15, 2024
949bcec
Merge branch 'main' of github.com:Expensify/App into arosiclair-expor…
arosiclair Jul 16, 2024
730a715
use string type
arosiclair Jul 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2093,8 +2093,12 @@ const CONST = {
NAME_USER_FRIENDLY: {
netsuite: 'NetSuite',
quickbooksOnline: 'Quickbooks Online',
quickbooksDesktop: 'Quickbooks Desktop',
xero: 'Xero',
intacct: 'Sage Intacct',
financialForce: 'FinancialForce',
billCom: 'Bill.com',
zenefits: 'Zenefits',
},
SYNC_STAGE_NAME: {
STARTING_IMPORT_QBO: 'startingImportQBO',
Expand Down
48 changes: 48 additions & 0 deletions src/components/ReportActionItem/ExportIntegration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* eslint-disable react/no-array-index-key */
import React from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ReportActionUtils from '@libs/ReportActionsUtils';
import type {ReportAction} from '@src/types/onyx';

type ExportIntegrationProps = {
action: OnyxEntry<ReportAction>;
};

function ExportIntegration({action}: ExportIntegrationProps) {
const styles = useThemeStyles();
const fragments = ReportActionUtils.getExportIntegrationActionFragments(action);

return (
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.flexWrap]}>
{fragments.map((fragment, index) => {
if (!fragment.url) {
return (
<Text
key={index}
style={[styles.chatItemMessage, styles.colorMuted]}
>
{fragment.text}{' '}
</Text>
);
}

return (
<TextLink
key={index}
href={fragment.url}
>
{fragment.text}{' '}
</TextLink>
);
})}
</View>
);
}

ExportIntegration.displayName = 'ExportIntegration';

export default ExportIntegration;
8 changes: 7 additions & 1 deletion src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3607,7 +3607,13 @@ export default {
changeType: ({oldType, newType}: ChangeTypeParams) => `changed type from ${oldType} to ${newType}`,
delegateSubmit: ({delegateUser, originalManager}: DelegateSubmitParams) => `sent this report to ${delegateUser} since ${originalManager} is on vacation`,
exportedToCSV: `exported this report to CSV`,
exportedToIntegration: ({label}: ExportedToIntegrationParams) => `exported this report to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[label] ?? label}`,
exportedToIntegration: {
automatic: ({label}: ExportedToIntegrationParams) => `exported this report to ${label}.`,
manual: ({label}: ExportedToIntegrationParams) => `marked this report as manually exported to ${label}.`,
reimburseableLink: 'View out of pocket expenses.',
nonReimbursableLink: 'View company card expenses.',
pending: ({label}: ExportedToIntegrationParams) => `started exporting this report to ${label}...`,
},
forwarded: ({amount, currency}: ForwardedParams) => `approved ${currency}${amount}`,
integrationsMessage: (errorMessage: string, label: string) => `failed to export this report to ${label} ("${errorMessage}").`,
managerAttachReceipt: `added a receipt`,
Expand Down
8 changes: 7 additions & 1 deletion src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3662,7 +3662,13 @@ export default {
changeType: ({oldType, newType}: ChangeTypeParams) => `cambió type de ${oldType} a ${newType}`,
delegateSubmit: ({delegateUser, originalManager}: DelegateSubmitParams) => `envié este informe a ${delegateUser} ya que ${originalManager} está de vacaciones`,
exportedToCSV: `exportó este informe a CSV`,
exportedToIntegration: ({label}: ExportedToIntegrationParams) => `exportó este informe a ${label}`,
exportedToIntegration: {
automatic: ({label}: ExportedToIntegrationParams) => `exportó este informe a ${label}.`,
manual: ({label}: ExportedToIntegrationParams) => `marcó este informe como exportado manualmente a ${label}.`,
reimburseableLink: 'Ver los gastos por cuenta propia.',
nonReimbursableLink: 'Ver los gastos de la tarjeta de empresa.',
pending: ({label}: ExportedToIntegrationParams) => `comenzó a exportar este informe a ${label}...`,
},
forwarded: ({amount, currency}: ForwardedParams) => `aprobado ${currency}${amount}`,
integrationsMessage: (errorMessage: string, label: string) => `no se pudo exportar este informe a ${label} ("${errorMessage}").`,
managerAttachReceipt: `agregó un recibo`,
Expand Down
2 changes: 2 additions & 0 deletions src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,8 @@ function getLastMessageTextForReport(report: OnyxEntry<Report>, lastActorDetails
lastMessageTextFromReport = ReportUtils.getIOUApprovedMessage(reportID);
} else if (ReportActionUtils.isActionableAddPaymentCard(lastReportAction)) {
lastMessageTextFromReport = ReportActionUtils.getReportActionMessageText(lastReportAction);
} else if (lastReportAction?.actionName === 'EXPORTINTEGRATION') {
lastMessageTextFromReport = ReportActionUtils.getExportIntegrationLastMessageText(lastReportAction);
} else if (lastReportAction?.actionName && ReportActionUtils.isOldDotReportAction(lastReportAction)) {
lastMessageTextFromReport = ReportActionUtils.getMessageOfOldDotReportAction(lastReportAction);
}
Expand Down
110 changes: 106 additions & 4 deletions src/libs/ReportActionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type {OnyxInputOrEntry} from '@src/types/onyx';
import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage';
import type {JoinWorkspaceResolution, OriginalMessageExportIntegration} from '@src/types/onyx/OriginalMessage';
import type Report from '@src/types/onyx/Report';
import type {Message, OldDotReportAction, OriginalMessage, ReportActions} from '@src/types/onyx/ReportAction';
import type ReportAction from '@src/types/onyx/ReportAction';
import type ReportActionName from '@src/types/onyx/ReportActionName';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import DateUtils from './DateUtils';
import * as Environment from './Environment/Environment';
import getBase62ReportID from './getBase62ReportID';
import isReportMessageAttachment from './isReportMessageAttachment';
import * as Localize from './Localize';
import Log from './Log';
Expand Down Expand Up @@ -82,6 +83,27 @@
let environmentURL: string;
Environment.getEnvironmentURL().then((url: string) => (environmentURL = url));

/*
* Url to the Xero non reimbursable expenses list
*/
const XERO_NON_REIMBURSABLE_EXPENSES_URL = 'https://go.xero.com/Bank/BankAccounts.aspx';

/*
* Url to the NetSuite global search, which should be suffixed with the reportID.
*/
const NETSUITE_NON_REIMBURSABLE_EXPENSES_URL_PREFIX =
'https://system.netsuite.com/app/common/search/ubersearchresults.nl?quicksearch=T&searchtype=Uber&frame=be&Uber_NAMEtype=KEYWORDSTARTSWITH&Uber_NAME=';

/*
* Url prefix to any Salesforce transaction or transaction list.
*/
const SALESFORCE_EXPENSES_URL_PREFIX = 'https://login.salesforce.com/';

/*
* Url to the QBO expenses list
*/
const QBO_EXPENSES_URL = 'https://qbo.intuit.com/app/expenses';

function isCreatedAction(reportAction: OnyxInputOrEntry<ReportAction>): boolean {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED;
}
Expand Down Expand Up @@ -1156,7 +1178,6 @@
CONST.REPORT.ACTIONS.TYPE.CHANGE_TYPE,
CONST.REPORT.ACTIONS.TYPE.DELEGATE_SUBMIT,
CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_CSV,
CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION,
CONST.REPORT.ACTIONS.TYPE.FORWARDED,
CONST.REPORT.ACTIONS.TYPE.INTEGRATIONS_MESSAGE,
CONST.REPORT.ACTIONS.TYPE.MANAGER_ATTACH_RECEIPT,
Expand Down Expand Up @@ -1220,8 +1241,6 @@
}
case CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_CSV:
return Localize.translateLocal('report.actions.type.exportedToCSV');
case CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION:
return Localize.translateLocal('report.actions.type.exportedToIntegration', {label: originalMessage.label});
case CONST.REPORT.ACTIONS.TYPE.INTEGRATIONS_MESSAGE: {
const {result, label} = originalMessage;
const errorMessage = result?.messages?.join(', ') ?? '';
Expand Down Expand Up @@ -1434,6 +1453,86 @@
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_ADD_PAYMENT_CARD;
}

function getExportIntegrationLastMessageText(reportAction: OnyxEntry<ReportAction>): string {
const fragments = getExportIntegrationActionFragments(reportAction);
return fragments.reduce((acc, fragment) => `${acc} ${fragment.text}`, '');
}

function getExportIntegrationMessageHTML(reportAction: OnyxEntry<ReportAction>): string {
const fragments = getExportIntegrationActionFragments(reportAction);
const htmlFragments = fragments.map((fragment) => (fragment.url ? `<a href="${fragment.url}">${fragment.text}</a>` : fragment.text));
return htmlFragments.join(' ');
}

function getExportIntegrationActionFragments(reportAction: OnyxEntry<ReportAction>): Array<{text: string; url: string}> {
if (reportAction?.actionName !== 'EXPORTINTEGRATION') {
throw Error(`received wrong action type. actionName: ${reportAction?.actionName}`);
}

const isPending = reportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD;
const originalMessage = (getOriginalMessage(reportAction) ?? {}) as OriginalMessageExportIntegration;
const {label, markedManually} = originalMessage;
const reimbursableUrls = originalMessage.reimbursableUrls ?? [];
const nonReimbursableUrls = originalMessage.nonReimbursableUrls ?? [];
const reportID = reportAction?.reportID ?? '';
const wasExportedAfterBase62 = (reportAction?.created ?? '') > '2022-11-14';
const base62ReportID = getBase62ReportID(Number(reportID));

const result: Array<{text: string; url: string}> = [];
if (isPending) {
result.push({
text: Localize.translateLocal('report.actions.type.exportedToIntegration.pending', {label}),

Check failure on line 1484 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / typecheck

Type 'string' is not assignable to type 'keyof Connections'.
url: '',
});
} else if (markedManually) {
result.push({
text: Localize.translateLocal('report.actions.type.exportedToIntegration.manual', {label}),

Check failure on line 1489 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / typecheck

Type 'string' is not assignable to type 'keyof Connections'.
url: '',
});
} else {
result.push({
text: Localize.translateLocal('report.actions.type.exportedToIntegration.automatic', {label}),

Check failure on line 1494 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / typecheck

Type 'string' is not assignable to type 'keyof Connections'.
url: '',
});
}

if (reimbursableUrls.length === 1) {
result.push({
text: Localize.translateLocal('report.actions.type.exportedToIntegration.reimburseableLink'),
url: reimbursableUrls[0],
});
}

if (nonReimbursableUrls.length) {
const text = Localize.translateLocal('report.actions.type.exportedToIntegration.nonReimbursableLink');
let url = '';

if (nonReimbursableUrls.length === 1) {
url = nonReimbursableUrls[0];
} else {
switch (label) {
case CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.xero:
url = XERO_NON_REIMBURSABLE_EXPENSES_URL;
break;
case CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.netsuite:
url = NETSUITE_NON_REIMBURSABLE_EXPENSES_URL_PREFIX;
url += wasExportedAfterBase62 ? base62ReportID : reportID;
break;
case CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.financialForce:
// The first three characters in a Salesforce ID is the expense type
url = nonReimbursableUrls[0].substring(0, SALESFORCE_EXPENSES_URL_PREFIX.length + 3);
break;
default:
url = QBO_EXPENSES_URL;
}
}

result.push({text, url});
}

return result;
}

export {
extractLinksFromMessageHtml,
getDismissedViolationMessageText,
Expand Down Expand Up @@ -1522,6 +1621,9 @@
getIOUActionForReportID,
getFilteredForOneTransactionView,
isActionableAddPaymentCard,
getExportIntegrationActionFragments,
getExportIntegrationLastMessageText,
getExportIntegrationMessageHTML,
};

export type {LastVisibleMessage};
19 changes: 19 additions & 0 deletions src/libs/getBase62ReportID.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Take an integer reportID and convert it to a string representing a Base-62 report ID.
*
* This is in it's own module to prevent a dependency cycle between libs/ReportUtils.ts and libs/ReportActionUtils.ts
*
* @return string The reportID in base 62-format, always 12 characters beginning with `R`.
*/
export default function getBase62ReportID(reportID: number): string {
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
let result = '';
let remainder = reportID;
while (remainder > 0) {
const currentVal = remainder % 62;
result = alphabet[currentVal] + result;
remainder = Math.floor(remainder / 62);
}

return `R${result.padStart(11, '0')}`;
}
2 changes: 2 additions & 0 deletions src/pages/home/report/ContextMenu/ContextMenuActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,8 @@ const ContextMenuActions: ContextMenuAction[] = [
const reason = originalMessage?.reason;
const violationName = originalMessage?.violationName;
Clipboard.setString(Localize.translateLocal(`violationDismissal.${violationName}.${reason}` as TranslationPaths));
} else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION) {
setClipboardMessage(ReportActionsUtils.getExportIntegrationMessageHTML(reportAction));
} else if (content) {
setClipboardMessage(
content.replace(/(<mention-user>)(.*?)(<\/mention-user>)/gi, (match, openTag: string, innerContent: string, closeTag: string): string => {
Expand Down
3 changes: 3 additions & 0 deletions src/pages/home/report/ReportActionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import RenderHTML from '@components/RenderHTML';
import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons';
import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons';
import ChronosOOOListActions from '@components/ReportActionItem/ChronosOOOListActions';
import ExportIntegration from '@components/ReportActionItem/ExportIntegration';
import MoneyRequestAction from '@components/ReportActionItem/MoneyRequestAction';
import RenameAction from '@components/ReportActionItem/RenameAction';
import ReportPreview from '@components/ReportActionItem/ReportPreview';
Expand Down Expand Up @@ -655,6 +656,8 @@ function ReportActionItem({
children = <ReportActionItemBasicMessage message={ReportActionsUtils.getDismissedViolationMessageText(ReportActionsUtils.getOriginalMessage(action))} />;
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_TAG) {
children = <ReportActionItemBasicMessage message={PolicyUtils.getCleanedTagName(ReportActionsUtils.getReportActionMessage(action)?.text ?? '')} />;
} else if (ReportActionsUtils.isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION)) {
children = <ExportIntegration action={action} />;
} else {
const hasBeenFlagged =
![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === moderationDecision) &&
Expand Down
38 changes: 37 additions & 1 deletion src/types/onyx/OriginalMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,41 @@ type OriginalMessageApproved = {
expenseReportID: string;
};

/**
*
*/
type OriginalMessageExportIntegration = {
/**
* Whether the export was done via an automation
*/
automaticAction: false;

/**
* The integration that was exported to (display text)
*/
label: string;

/**
*
*/
lastModified: string;

/**
* Whether the report was manually marked as exported
*/
markedManually: boolean;

/**
* An list of URLs to the report in the integration for company card expenses
*/
nonReimbursableUrls?: string[];

/**
* An list of URLs to the report in the integration for out of pocket expenses
*/
reimbursableUrls?: string[];
};

/** Model of `unapproved` report action */
type OriginalMessageUnapproved = {
/** Unapproved expense amount */
Expand Down Expand Up @@ -454,7 +489,7 @@ type OriginalMessageMap = {
/** */
[CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_CSV]: never;
/** */
[CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION]: never;
[CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION]: OriginalMessageExportIntegration;
/** */
[CONST.REPORT.ACTIONS.TYPE.FORWARDED]: never;
/** */
Expand Down Expand Up @@ -562,4 +597,5 @@ export type {
OriginalMessageChangeLog,
JoinWorkspaceResolution,
OriginalMessageModifiedExpense,
OriginalMessageExportIntegration,
};
Loading