diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 99ca859f4030..cb27d465cddb 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1676,6 +1676,9 @@ const CONST = { ADD_EMPLOYEE: 'POLICYCHANGELOG_ADD_EMPLOYEE', ADD_CARD_FEED: 'POLICYCHANGELOG_ADD_CARD_FEED', ADD_EXPENSIFY_CARD_RULE: 'POLICYCHANGELOG_ADD_EXPENSIFY_CARD_RULE', + ADD_AGENT_RULE: 'POLICYCHANGELOG_ADD_AGENT_RULE', + UPDATE_AGENT_RULE: 'POLICYCHANGELOG_UPDATE_AGENT_RULE', + DELETE_AGENT_RULE: 'POLICYCHANGELOG_DELETE_AGENT_RULE', ADD_INTEGRATION: 'POLICYCHANGELOG_ADD_INTEGRATION', ADD_REPORT_FIELD: 'POLICYCHANGELOG_ADD_REPORT_FIELD', ADD_TAG: 'POLICYCHANGELOG_ADD_TAG', diff --git a/src/languages/de.ts b/src/languages/de.ts index 028b4400eb16..c29791b1b6c5 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -8679,6 +8679,12 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc updatedMccGroupCategory: ({mccGroupName, oldCategory, newCategory}: {mccGroupName: string; oldCategory: string; newCategory: string}) => `hat die Standardausgabenkategorie für „${mccGroupName}“ in „${newCategory}“ geändert (zuvor „${oldCategory}“)`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'aktiviert' : 'deaktiviert'} die Anforderung für Firmenkartenkäufe`, + agentRule: { + added: ({title, prompt}: {title: string; prompt: string}) => (title ? `hat die Agentenregel „${title}“ hinzugefügt: ${prompt}` : `hat eine Agentenregel hinzugefügt: ${prompt}`), + updated: ({title, prompt}: {title: string; prompt: string}) => + title ? `hat die Agentenregel „${title}“ aktualisiert zu: ${prompt}` : `hat eine Agentenregel aktualisiert zu: ${prompt}`, + deleted: ({title}: {title: string}) => (title ? `hat die Agentenregel „${title}“ entfernt` : 'hat eine Agentenregel entfernt'), + }, expensifyCardRule: { actionVerb: {block: 'blockiert', allow: 'erlaubt'}, amountOperator: {over: 'über', under: 'unter'}, diff --git a/src/languages/en.ts b/src/languages/en.ts index 6caf97e61d20..3ddd770c5899 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -8466,6 +8466,11 @@ const translations = { updatedCardFeedLiability: (feedName: string, enabled: boolean) => `${enabled ? 'enabled' : 'disabled'} cardholders to delete card transactions for card feed "${feedName}"`, updatedCardFeedStatementPeriod: (feedName: string, newValue?: string, previousValue?: string) => `changed card feed "${feedName}" statement period end day${newValue ? ` to "${newValue}"` : ''}${previousValue ? ` (previously "${previousValue}")` : ''}`, + agentRule: { + added: ({title, prompt}: {title: string; prompt: string}) => (title ? `added the agent rule "${title}": ${prompt}` : `added an agent rule: ${prompt}`), + updated: ({title, prompt}: {title: string; prompt: string}) => (title ? `updated the agent rule "${title}" to: ${prompt}` : `updated an agent rule to: ${prompt}`), + deleted: ({title}: {title: string}) => (title ? `removed the agent rule "${title}"` : 'removed an agent rule'), + }, expensifyCardRule: { actionVerb: { block: 'blocked', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2cbb85895718..1b14adf43b67 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -8159,6 +8159,11 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, `${enabled ? 'habilitó' : 'deshabilitó'} que los titulares de tarjetas eliminen transacciones de la fuente de tarjetas "${feedName}"`, updatedCardFeedStatementPeriod: (feedName: string, newValue?: string, previousValue?: string) => `cambió el día de cierre del período de estado de cuenta de la fuente de tarjetas "${feedName}"${newValue ? ` a "${newValue}"` : ''}${previousValue ? ` (previamente "${previousValue}")` : ''}`, + agentRule: { + added: ({title, prompt}: {title: string; prompt: string}) => (title ? `añadió la regla de agente «${title}»: ${prompt}` : `añadió una regla de agente: ${prompt}`), + updated: ({title, prompt}: {title: string; prompt: string}) => (title ? `actualizó la regla de agente «${title}» a: ${prompt}` : `actualizó una regla de agente a: ${prompt}`), + deleted: ({title}: {title: string}) => (title ? `eliminó la regla de agente «${title}»` : 'eliminó una regla de agente'), + }, expensifyCardRule: { actionVerb: {block: 'bloqueado', allow: 'permitido'}, amountOperator: {over: 'más de', under: 'debajo'}, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index b513f49e9009..19bc44e2e9f1 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -8713,6 +8713,12 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e updatedMccGroupCategory: ({mccGroupName, oldCategory, newCategory}: {mccGroupName: string; oldCategory: string; newCategory: string}) => `a modifié la catégorie de dépense par défaut pour « ${mccGroupName} » en « ${newCategory} » (auparavant « ${oldCategory} »)`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'activé' : 'désactivé'} l’exigence d’achats par carte d’entreprise`, + agentRule: { + added: ({title, prompt}: {title: string; prompt: string}) => (title ? `a ajouté la règle d’agent « ${title} » : ${prompt}` : `a ajouté une règle d’agent : ${prompt}`), + updated: ({title, prompt}: {title: string; prompt: string}) => + title ? `a mis à jour la règle d’agent « ${title} » en : ${prompt}` : `a mis à jour une règle d’agent en : ${prompt}`, + deleted: ({title}: {title: string}) => (title ? `a supprimé la règle d’agent « ${title} »` : 'a supprimé une règle d’agent'), + }, expensifyCardRule: { actionVerb: {block: 'bloqué', allow: 'autorisé'}, amountOperator: {over: 'terminé', under: 'sous'}, diff --git a/src/languages/it.ts b/src/languages/it.ts index a6d1f8586779..a3580d6e5e05 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -8652,6 +8652,13 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, updatedMccGroupCategory: ({mccGroupName, oldCategory, newCategory}: {mccGroupName: string; oldCategory: string; newCategory: string}) => `ha modificato la categoria di spesa predefinita per "${mccGroupName}" in "${newCategory}" (precedentemente "${oldCategory}")`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'abilitato' : 'disabilitato'} il requisito per gli acquisti con carta aziendale`, + agentRule: { + added: ({title, prompt}: {title: string; prompt: string}) => + title ? `ha aggiunto la regola dell’agente "${title}": ${prompt}` : `ha aggiunto una regola dell’agente: ${prompt}`, + updated: ({title, prompt}: {title: string; prompt: string}) => + title ? `ha aggiornato la regola dell’agente "${title}" in: ${prompt}` : `ha aggiornato una regola dell’agente in: ${prompt}`, + deleted: ({title}: {title: string}) => (title ? `ha rimosso la regola dell’agente "${title}"` : 'ha rimosso una regola dell’agente'), + }, expensifyCardRule: { actionVerb: {block: 'bloccato', allow: 'consentito'}, amountOperator: {over: 'terminato', under: 'sotto'}, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 912416c138b1..cb3b4ff8a0d9 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -8541,6 +8541,12 @@ ${reportName}`, updatedMccGroupCategory: ({mccGroupName, oldCategory, newCategory}: {mccGroupName: string; oldCategory: string; newCategory: string}) => `「${mccGroupName}」のデフォルト支出カテゴリーを「${newCategory}」に変更しました(以前は「${oldCategory}」)`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? '有効' : '無効'} の法人カード購入要件`, + agentRule: { + added: ({title, prompt}: {title: string; prompt: string}) => (title ? `エージェントルール「${title}」を追加しました:${prompt}` : `エージェントルールを追加しました:${prompt}`), + updated: ({title, prompt}: {title: string; prompt: string}) => + title ? `エージェントルール「${title}」を次の内容に更新しました:${prompt}` : `エージェントルールを次の内容に更新しました:${prompt}`, + deleted: ({title}: {title: string}) => (title ? `エージェントルール「${title}」を削除しました` : 'エージェントルールを削除しました'), + }, expensifyCardRule: { actionVerb: {block: 'ブロック済み', allow: '許可済み'}, amountOperator: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index d0ad03162ce4..ee926e7cfd00 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -8626,6 +8626,12 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, updatedMccGroupCategory: ({mccGroupName, oldCategory, newCategory}: {mccGroupName: string; oldCategory: string; newCategory: string}) => `heeft de standaarduitgavencategorie voor ‘${mccGroupName}’ gewijzigd in ‘${newCategory}’ (voorheen ‘${oldCategory}’)`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `vereiste ${enabled ? 'ingeschakeld' : 'uitgeschakeld'} voor bedrijfskaarttransacties`, + agentRule: { + added: ({title, prompt}: {title: string; prompt: string}) => (title ? `heeft de agentregel ‘${title}’ toegevoegd: ${prompt}` : `heeft een agentregel toegevoegd: ${prompt}`), + updated: ({title, prompt}: {title: string; prompt: string}) => + title ? `heeft de agentregel ‘${title}’ bijgewerkt naar: ${prompt}` : `heeft een agentregel bijgewerkt naar: ${prompt}`, + deleted: ({title}: {title: string}) => (title ? `heeft de agentregel ‘${title}’ verwijderd` : 'heeft een agentregel verwijderd'), + }, expensifyCardRule: { actionVerb: {block: 'geblokkeerd', allow: 'toegestaan'}, amountOperator: {over: 'over', under: 'onder'}, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 2ebb77a0f8be..2e463934f7ea 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -8606,6 +8606,11 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, updatedMccGroupCategory: ({mccGroupName, oldCategory, newCategory}: {mccGroupName: string; oldCategory: string; newCategory: string}) => `zmieniono domyślną kategorię wydatków dla „${mccGroupName}” na „${newCategory}” (poprzednio „${oldCategory}”)`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'włączone' : 'wyłączone'} wymóg dotyczący zakupów kartą służbową`, + agentRule: { + added: ({title, prompt}: {title: string; prompt: string}) => (title ? `dodał(a) regułę agenta „${title}”: ${prompt}` : `dodano regułę agenta: ${prompt}`), + updated: ({title, prompt}: {title: string; prompt: string}) => (title ? `zaktualizował(a) regułę agenta „${title}” na: ${prompt}` : `zaktualizowano regułę agenta na: ${prompt}`), + deleted: ({title}: {title: string}) => (title ? `usunął(-ęła) regułę agenta „${title}”` : 'usunięto regułę agenta'), + }, expensifyCardRule: { actionVerb: {block: 'zablokowane', allow: 'dozwolone'}, amountOperator: {over: 'ponad', under: 'pod'}, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 098ee4812d1e..1f36b4b809b7 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -8618,6 +8618,12 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, updatedMccGroupCategory: ({mccGroupName, oldCategory, newCategory}: {mccGroupName: string; oldCategory: string; newCategory: string}) => `alterou a categoria de gasto padrão de "${mccGroupName}" para "${newCategory}" (antes "${oldCategory}")`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'ativado' : 'desativado'} o requisito de compras com cartão corporativo`, + agentRule: { + added: ({title, prompt}: {title: string; prompt: string}) => (title ? `adicionou a regra de agente "${title}": ${prompt}` : `adicionou uma regra de agente: ${prompt}`), + updated: ({title, prompt}: {title: string; prompt: string}) => + title ? `atualizou a regra do agente "${title}" para: ${prompt}` : `atualizou uma regra de agente para: ${prompt}`, + deleted: ({title}: {title: string}) => (title ? `removeu a regra de agente "${title}"` : 'removeu uma regra de agente'), + }, expensifyCardRule: { actionVerb: {block: 'bloqueado', allow: 'permitido'}, amountOperator: {over: 'acima', under: 'abaixo'}, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 6ef0e723e500..f07f1aefb270 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -8336,6 +8336,11 @@ ${reportName}`, `已将“${mccGroupName}”的默认支出类别更改为“${newCategory}”(此前为“${oldCategory}”)`, addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `已添加 ${fieldType} 报告字段“${fieldName}”${defaultValue ? ` 默认值为“${defaultValue}”` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? '已启用' : '已禁用'} 公司商务卡消费要求`, + agentRule: { + added: ({title, prompt}: {title: string; prompt: string}) => (title ? `已添加代理规则“${title}”:${prompt}` : `已添加代理规则:${prompt}`), + updated: ({title, prompt}: {title: string; prompt: string}) => (title ? `已将代理规则“${title}”更新为:${prompt}` : `已将代理规则更新为:${prompt}`), + deleted: ({title}: {title: string}) => (title ? `已移除代理规则“${title}”` : '已移除代理规则'), + }, expensifyCardRule: { actionVerb: {block: '已阻止', allow: '允许'}, amountOperator: {over: '结束', under: '在……之下'}, diff --git a/src/libs/AgentRuleChangeLogUtils.ts b/src/libs/AgentRuleChangeLogUtils.ts new file mode 100644 index 000000000000..22a0b93f67dc --- /dev/null +++ b/src/libs/AgentRuleChangeLogUtils.ts @@ -0,0 +1,45 @@ +import type {LocalizedTranslate} from '@components/LocaleContextProvider'; + +import CONST from '@src/CONST'; +import type ReportAction from '@src/types/onyx/ReportAction'; + +import type {OnyxEntry} from 'react-native-onyx'; + +import {getOriginalMessage, isActionOfType} from './ReportActionsUtils'; + +/** + * Builds the #admins system message shown when an agent rule is added. Includes the rule's title and + * full prompt; the title is server-generated and best-effort, so it may be empty. + */ +function getAddAgentRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_AGENT_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + return translate('workspaceActions.agentRule.added', {title: message.ruleTitle ?? '', prompt: message.prompt ?? ''}); +} + +/** + * Builds the #admins system message shown when an agent rule's prompt is updated. The title is set + * once at creation and is not changed here. + */ +function getUpdateAgentRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AGENT_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + return translate('workspaceActions.agentRule.updated', {title: message.ruleTitle ?? '', prompt: message.prompt ?? ''}); +} + +/** + * Builds the #admins system message shown when an agent rule is deleted. Only the title is recorded. + */ +function getDeleteAgentRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_AGENT_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + return translate('workspaceActions.agentRule.deleted', {title: message.ruleTitle ?? ''}); +} + +export {getAddAgentRuleMessage, getUpdateAgentRuleMessage, getDeleteAgentRuleMessage}; diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 98abec83d6ff..7576bde4a501 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -4,6 +4,7 @@ import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleCon import type {PrivateIsArchivedMap} from '@hooks/usePrivateIsArchivedMap'; +import {getAddAgentRuleMessage, getDeleteAgentRuleMessage, getUpdateAgentRuleMessage} from '@libs/AgentRuleChangeLogUtils'; import {getEnabledCategoriesCount} from '@libs/CategoryUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import filterArrayByMatch from '@libs/filterArrayByMatch'; @@ -992,6 +993,15 @@ function getLastMessageTextForReport({ if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FEATURE_ENABLED)) { lastMessageTextFromReport = getWorkspaceFeatureEnabledMessage(translate, lastReportAction); } + if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_AGENT_RULE)) { + lastMessageTextFromReport = getAddAgentRuleMessage(translate, lastReportAction); + } + if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AGENT_RULE)) { + lastMessageTextFromReport = getUpdateAgentRuleMessage(translate, lastReportAction); + } + if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_AGENT_RULE)) { + lastMessageTextFromReport = getDeleteAgentRuleMessage(translate, lastReportAction); + } if (isPolicyCopyReportAction(lastReportAction)) { lastMessageTextFromReport = Parser.htmlToText(getPolicyChangeLogCopyMessage(translate, lastReportAction)); } diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index 8430e6ebd0a8..fc67898fb900 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -26,6 +26,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {Str} from 'expensify-common'; import Onyx from 'react-native-onyx'; +import {getAddAgentRuleMessage, getDeleteAgentRuleMessage, getUpdateAgentRuleMessage} from './AgentRuleChangeLogUtils'; import {convertToDisplayString} from './CurrencyUtils'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from './LocalePhoneNumber'; import {translateLocal} from './Localize'; @@ -857,6 +858,15 @@ function computeReportNameBasedOnReportAction( if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE)) { return getRemoveExpensifyCardRuleMessage(translate, parentReportAction); } + if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_AGENT_RULE)) { + return getAddAgentRuleMessage(translate, parentReportAction); + } + if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AGENT_RULE)) { + return getUpdateAgentRuleMessage(translate, parentReportAction); + } + if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_AGENT_RULE)) { + return getDeleteAgentRuleMessage(translate, parentReportAction); + } if (isPolicyCopyReportAction(parentReportAction)) { return Parser.htmlToText(getPolicyChangeLogCopyMessage(translate, parentReportAction)); } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 2966a2151b75..a835ef9aa13e 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -32,6 +32,7 @@ import {Str} from 'expensify-common'; import type {OptionData} from './ReportUtils'; import {isAnonymousUser} from './actions/Session'; +import {getAddAgentRuleMessage, getDeleteAgentRuleMessage, getUpdateAgentRuleMessage} from './AgentRuleChangeLogUtils'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from './LocalePhoneNumber'; import {formatList} from './Localize'; import { @@ -1250,6 +1251,12 @@ function getOptionData({ result.alternateText = getUpdateExpensifyCardRuleMessage(translate, lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE) { result.alternateText = getRemoveExpensifyCardRuleMessage(translate, lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_AGENT_RULE) { + result.alternateText = getAddAgentRuleMessage(translate, lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AGENT_RULE) { + result.alternateText = getUpdateAgentRuleMessage(translate, lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_AGENT_RULE) { + result.alternateText = getDeleteAgentRuleMessage(translate, lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MANUAL_APPROVAL_THRESHOLD) { result.alternateText = getUpdatedManualApprovalThresholdMessage(translate, lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_BUDGET) { diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index d17522ff6e1b..580bcb409106 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -9,6 +9,7 @@ import type {CurrencyListActionsContextType} from '@hooks/useCurrencyList'; import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import {getAddAgentRuleMessage, getDeleteAgentRuleMessage, getUpdateAgentRuleMessage} from '@libs/AgentRuleChangeLogUtils'; import {isMobileSafari} from '@libs/Browser'; import Clipboard from '@libs/Clipboard'; import getClipboardText from '@libs/Clipboard/getClipboardText'; @@ -1261,6 +1262,12 @@ const ContextMenuActions: ContextMenuAction[] = [ setClipboardMessage(getDeletedApprovalRuleMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_APPROVER_RULE)) { setClipboardMessage(getUpdatedApprovalRuleMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_AGENT_RULE)) { + setClipboardMessage(getAddAgentRuleMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AGENT_RULE)) { + setClipboardMessage(getUpdateAgentRuleMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_AGENT_RULE)) { + setClipboardMessage(getDeleteAgentRuleMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE)) { setClipboardMessage(getAddExpensifyCardRuleMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE)) { diff --git a/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx b/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx index b83427dc0059..da84c62a3331 100644 --- a/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx +++ b/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx @@ -4,6 +4,7 @@ import RenderHTML from '@components/RenderHTML'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import {getAddAgentRuleMessage, getDeleteAgentRuleMessage, getUpdateAgentRuleMessage} from '@libs/AgentRuleChangeLogUtils'; import {getCleanedTagName} from '@libs/PolicyUtils'; import { getAddedApprovalRuleMessage, @@ -181,6 +182,9 @@ const POLICY_CHANGE_LOG_RESOLVERS: Record = { [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE]: (translate, action) => getAddExpensifyCardRuleMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE]: (translate, action) => getUpdateExpensifyCardRuleMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE]: (translate, action) => getRemoveExpensifyCardRuleMessage(translate, action), + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_AGENT_RULE]: (translate, action) => getAddAgentRuleMessage(translate, action), + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AGENT_RULE]: (translate, action) => getUpdateAgentRuleMessage(translate, action), + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_AGENT_RULE]: (translate, action) => getDeleteAgentRuleMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_INTEGRATION]: (translate, action) => getAddedConnectionMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_INTEGRATION]: (translate, action) => getRemovedConnectionMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CARD_FEED]: (translate, action) => getAddedCardFeedMessage(translate, action), diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index ad58e5e4491f..aea207951c24 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -906,6 +906,21 @@ type OriginalMessageSpendRuleChangeLog = { currency?: string; }; +/** Model of a workspace agent rule change log action (add, update, or delete) */ +type OriginalMessageAgentRuleChangeLog = { + /** ID of the policy the agent rule belongs to */ + policyID?: string; + + /** ID of the agent rule that changed */ + ruleID?: string; + + /** Server-generated one-line title of the agent rule */ + ruleTitle?: string; + + /** Natural-language prompt of the agent rule (present for add/update, omitted for delete) */ + prompt?: string; +}; + /** Model of a policy copy change log action */ type OriginalMessagePolicyChangeCopyLog = { /** The ID of the source policy from which the user copied settings */ @@ -1774,6 +1789,9 @@ type OriginalMessageMap = { [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE]: OriginalMessageSpendRuleChangeLog; [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE]: OriginalMessageSpendRuleChangeLog; [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE]: OriginalMessageSpendRuleChangeLog; + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_AGENT_RULE]: OriginalMessageAgentRuleChangeLog; + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AGENT_RULE]: OriginalMessageAgentRuleChangeLog; + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_AGENT_RULE]: OriginalMessageAgentRuleChangeLog; } & Record, OriginalMessageChangeLog>; type OriginalMessage = T extends keyof OriginalMessageMap ? OriginalMessageMap[T] : never; diff --git a/tests/unit/AgentRuleChangeLogUtilsTest.ts b/tests/unit/AgentRuleChangeLogUtilsTest.ts new file mode 100644 index 000000000000..b092e9821f15 --- /dev/null +++ b/tests/unit/AgentRuleChangeLogUtilsTest.ts @@ -0,0 +1,139 @@ +import {getAddAgentRuleMessage, getDeleteAgentRuleMessage, getUpdateAgentRuleMessage} from '@libs/AgentRuleChangeLogUtils'; + +import CONST from '@src/CONST'; +import IntlStore from '@src/languages/IntlStore'; +import type {ReportAction} from '@src/types/onyx'; + +import {translateLocal} from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +describe('AgentRuleChangeLogUtils', () => { + beforeAll(() => { + IntlStore.load(CONST.LOCALES.DEFAULT); + return waitForBatchedUpdates(); + }); + + describe('getAddAgentRuleMessage', () => { + it('returns empty string for wrong action type', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: {}, + } as ReportAction; + expect(getAddAgentRuleMessage(translateLocal, action)).toBe(''); + }); + + it('includes the rule title and full prompt', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_AGENT_RULE, + reportActionID: '1', + created: '', + originalMessage: { + policyID: '1', + ruleID: '2', + ruleTitle: 'Receipts required', + prompt: 'Flag any expense over $25 that is missing a receipt', + }, + } as ReportAction; + expect(getAddAgentRuleMessage(translateLocal, action)).toBe('added the agent rule "Receipts required": Flag any expense over $25 that is missing a receipt'); + }); + + it('falls back to prompt-only when the title is empty', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_AGENT_RULE, + reportActionID: '1', + created: '', + originalMessage: { + policyID: '1', + ruleID: '2', + ruleTitle: '', + prompt: 'Reject any expense that includes alcohol', + }, + } as ReportAction; + expect(getAddAgentRuleMessage(translateLocal, action)).toBe('added an agent rule: Reject any expense that includes alcohol'); + }); + }); + + describe('getUpdateAgentRuleMessage', () => { + it('returns empty string for wrong action type', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_AGENT_RULE, + reportActionID: '1', + created: '', + originalMessage: {}, + } as ReportAction; + expect(getUpdateAgentRuleMessage(translateLocal, action)).toBe(''); + }); + + it('includes the preserved title and new prompt', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AGENT_RULE, + reportActionID: '1', + created: '', + originalMessage: { + policyID: '1', + ruleID: '2', + ruleTitle: 'Receipts required', + prompt: 'Reject any expense that includes alcohol', + }, + } as ReportAction; + expect(getUpdateAgentRuleMessage(translateLocal, action)).toBe('updated the agent rule "Receipts required" to: Reject any expense that includes alcohol'); + }); + + it('falls back to prompt-only when the title is empty', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AGENT_RULE, + reportActionID: '1', + created: '', + originalMessage: { + policyID: '1', + ruleID: '2', + ruleTitle: '', + prompt: 'Reject any expense that includes alcohol', + }, + } as ReportAction; + expect(getUpdateAgentRuleMessage(translateLocal, action)).toBe('updated an agent rule to: Reject any expense that includes alcohol'); + }); + }); + + describe('getDeleteAgentRuleMessage', () => { + it('returns empty string for wrong action type', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_AGENT_RULE, + reportActionID: '1', + created: '', + originalMessage: {}, + } as ReportAction; + expect(getDeleteAgentRuleMessage(translateLocal, action)).toBe(''); + }); + + it('includes only the rule title', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_AGENT_RULE, + reportActionID: '1', + created: '', + originalMessage: { + policyID: '1', + ruleID: '2', + ruleTitle: 'Receipts required', + }, + } as ReportAction; + expect(getDeleteAgentRuleMessage(translateLocal, action)).toBe('removed the agent rule "Receipts required"'); + }); + + it('falls back to a generic message when the title is empty', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_AGENT_RULE, + reportActionID: '1', + created: '', + originalMessage: { + policyID: '1', + ruleID: '2', + ruleTitle: '', + }, + } as ReportAction; + expect(getDeleteAgentRuleMessage(translateLocal, action)).toBe('removed an agent rule'); + }); + }); +}); diff --git a/tests/unit/ContextMenuActionsCopyMessageTest.ts b/tests/unit/ContextMenuActionsCopyMessageTest.ts index 76f716d8c2c6..3a79bb8ee35d 100644 --- a/tests/unit/ContextMenuActionsCopyMessageTest.ts +++ b/tests/unit/ContextMenuActionsCopyMessageTest.ts @@ -65,6 +65,21 @@ const createPayload = (selection: string): Record => ({ }, }); +const createReportActionPayload = (reportAction: Record): Record => ({ + reportAction, + selection: '', + report: {}, + originalReport: {}, + getLocalDateFromDatetime: jest.fn(), + policyTags: {}, + translate: (translateKey: string) => translateKey, + currentUserPersonalDetails: { + accountID: 1, + login: 'user@expensify.com', + email: 'user@expensify.com', + }, +}); + describe('ContextMenuActions copy message', () => { beforeEach(() => { jest.clearAllMocks(); @@ -101,4 +116,37 @@ describe('ContextMenuActions copy message', () => { expect(mockClipboard.setHtml).toHaveBeenCalledWith(selection, 'Expensify'); expect(mockClipboard.setString).not.toHaveBeenCalled(); }); + + it.each([ + [ + CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_AGENT_RULE, + {ruleTitle: 'Receipts required', prompt: 'Flag any expense over $25 that is missing a receipt'}, + 'workspaceActions.agentRule.added', + ], + [ + CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AGENT_RULE, + {ruleTitle: 'Receipts required', prompt: 'Reject any expense that includes alcohol'}, + 'workspaceActions.agentRule.updated', + ], + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_AGENT_RULE, {ruleTitle: 'Receipts required'}, 'workspaceActions.agentRule.deleted'], + ])('copies the localized message for a %s action', (actionName, originalMessage, expectedTranslationKey) => { + mockClipboard.canSetHtml.mockReturnValue(false); + mockGetClipboardText.mockReturnValue('mocked clipboard text'); + + if (!copyMessageAction?.onPress) { + throw new Error('Copy message context menu action was not found'); + } + + copyMessageAction.onPress( + false, + createReportActionPayload({ + actionName, + message: [{html: ''}], + originalMessage, + }), + ); + + expect(mockGetClipboardText).toHaveBeenCalledWith(expectedTranslationKey); + expect(mockClipboard.setString).toHaveBeenCalledWith('mocked clipboard text'); + }); }); diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 89314235b49c..cdd800e5b0ec 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -9,6 +9,7 @@ import OnyxListItemProvider from '@components/OnyxListItemProvider'; import type {PrivateIsArchivedMap} from '@hooks/usePrivateIsArchivedMap'; import useReportIsArchived from '@hooks/useReportIsArchived'; +import {getAddAgentRuleMessage, getDeleteAgentRuleMessage, getUpdateAgentRuleMessage} from '@libs/AgentRuleChangeLogUtils'; import DateUtils from '@libs/DateUtils'; import {translate} from '@libs/Localize'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; @@ -5446,6 +5447,78 @@ describe('OptionsListUtils', () => { }); expect(lastMessage).toBe(getCurrencyDefaultTaxUpdateMessage(translateLocal, action)); }); + it('ADD_AGENT_RULE action', async () => { + const report: Report = createRandomReport(0, undefined); + const action: ReportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_AGENT_RULE, + message: [{type: 'COMMENT', text: ''}], + originalMessage: {ruleTitle: 'Receipts required', prompt: 'Flag any expense over $25 that is missing a receipt'}, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [action.reportActionID]: action, + }); + const lastMessage = getLastMessageTextForReport({ + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + personalDetails: undefined, + translate: translateLocal, + report, + lastActorDetails: null, + policy: undefined, + isReportArchived: false, + + currentUserLogin: CURRENT_USER_EMAIL, + }); + expect(lastMessage).toBe(getAddAgentRuleMessage(translateLocal, action)); + }); + it('UPDATE_AGENT_RULE action', async () => { + const report: Report = createRandomReport(0, undefined); + const action: ReportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AGENT_RULE, + message: [{type: 'COMMENT', text: ''}], + originalMessage: {ruleTitle: 'Receipts required', prompt: 'Reject any expense that includes alcohol'}, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [action.reportActionID]: action, + }); + const lastMessage = getLastMessageTextForReport({ + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + personalDetails: undefined, + translate: translateLocal, + report, + lastActorDetails: null, + policy: undefined, + isReportArchived: false, + + currentUserLogin: CURRENT_USER_EMAIL, + }); + expect(lastMessage).toBe(getUpdateAgentRuleMessage(translateLocal, action)); + }); + it('DELETE_AGENT_RULE action', async () => { + const report: Report = createRandomReport(0, undefined); + const action: ReportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_AGENT_RULE, + message: [{type: 'COMMENT', text: ''}], + originalMessage: {ruleTitle: 'Receipts required'}, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [action.reportActionID]: action, + }); + const lastMessage = getLastMessageTextForReport({ + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + personalDetails: undefined, + translate: translateLocal, + report, + lastActorDetails: null, + policy: undefined, + isReportArchived: false, + + currentUserLogin: CURRENT_USER_EMAIL, + }); + expect(lastMessage).toBe(getDeleteAgentRuleMessage(translateLocal, action)); + }); it('UPDATE_FOREIGN_CURRENCY_DEFAULT_TAX action', async () => { const report: Report = createRandomReport(0, undefined); const action: ReportAction = {