From 917458844b51b93d9a62f2e100099a70e1ea4842 Mon Sep 17 00:00:00 2001 From: Oyelola Victoria <123843734+VriaA@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:33:05 +0100 Subject: [PATCH] Refactor email input handling to format comma-separated addresses (#193128) ## Summary This pull request fixes #189968 - Introduced `getFormattedEmailOptions` to split and trim comma-separated email values - Updated `EuiComboBox` to handle email entries for `to`, `cc`, and `bcc` fields https://github.com/user-attachments/assets/45a70132-8fd7-426e-81cf-62a6bf216408 --------- Signed-off-by: Oyelola Victoria Co-authored-by: Julian Gernun <17549662+jcger@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../email/email_params.test.tsx | 100 +++++++++++++++++- .../connector_types/email/email_params.tsx | 21 +++- 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.test.tsx index 76cc3b136455a2..3d772556e72629 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.test.tsx @@ -7,12 +7,13 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { render, fireEvent, screen } from '@testing-library/react'; +import { render, fireEvent, screen, within } from '@testing-library/react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks'; import EmailParamsFields from './email_params'; import { getIsExperimentalFeatureEnabled } from '../../common/get_experimental_features'; +import { getFormattedEmailOptions } from './email_params'; jest.mock('@kbn/kibana-react-plugin/public', () => ({ useKibana: jest.fn(), @@ -28,6 +29,24 @@ const mockKibana = () => { }); }; +const emailTestCases = [ + { + field: 'to', + fieldValue: 'new1@test.com, new2@test.com , new1@test.com, ', + expected: ['test@test.com', 'new1@test.com', 'new2@test.com'], + }, + { + field: 'cc', + fieldValue: 'newcc1@test.com, newcc2@test.com , newcc1@test.com, ', + expected: ['cc@test.com', 'newcc1@test.com', 'newcc2@test.com'], + }, + { + field: 'bcc', + fieldValue: 'newbcc1@test.com, newbcc2@test.com , newbcc1@test.com, ', + expected: ['bcc@test.com', 'newbcc1@test.com', 'newbcc2@test.com'], + }, +]; + describe('EmailParamsFields renders', () => { beforeEach(() => { jest.clearAllMocks(); @@ -62,6 +81,40 @@ describe('EmailParamsFields renders', () => { expect(await screen.findByTestId('messageTextArea')).toBeVisible(); }); + emailTestCases.forEach(({ field, fieldValue, expected }) => { + test(`"${field}" field value updates correctly when comma-separated emails are pasted`, async () => { + const actionParams = { + cc: ['cc@test.com'], + bcc: ['bcc@test.com'], + to: ['test@test.com'], + subject: 'test', + message: 'test message', + }; + + const editAction = jest.fn(); + + render( + + + + ); + + const euiComboBox = screen.getByTestId(`${field}EmailAddressInput`); + const input = within(euiComboBox).getByTestId('comboBoxSearchInput'); + fireEvent.change(input, { target: { value: fieldValue } }); + expect(input).toHaveValue(fieldValue); + + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); + expect(editAction).toHaveBeenCalledWith(field, expected, 0); + }); + }); + test('message param field is rendered with default value if not set', () => { const actionParams = { cc: [], @@ -234,3 +287,48 @@ describe('EmailParamsFields renders', () => { expect(editAction).not.toHaveBeenCalled(); }); }); + +describe('getFormattedEmailOptions', () => { + test('should return new options added to previous options', () => { + const searchValue = 'test@test.com, other@test.com'; + const previousOptions = [{ label: 'existing@test.com' }]; + const newOptions = getFormattedEmailOptions(searchValue, previousOptions); + + expect(newOptions).toEqual([ + { label: 'existing@test.com' }, + { label: 'test@test.com' }, + { label: 'other@test.com' }, + ]); + }); + + test('should trim extra spaces in search value', () => { + const searchValue = ' test@test.com , other@test.com , '; + const previousOptions: Array<{ label: string }> = []; + const newOptions = getFormattedEmailOptions(searchValue, previousOptions); + + expect(newOptions).toEqual([{ label: 'test@test.com' }, { label: 'other@test.com' }]); + }); + + test('should prevent duplicate email addresses', () => { + const searchValue = 'duplicate@test.com, duplicate@test.com'; + const previousOptions = [{ label: 'existing@test.com' }, { label: 'duplicate@test.com' }]; + const newOptions = getFormattedEmailOptions(searchValue, previousOptions); + + expect(newOptions).toEqual([{ label: 'existing@test.com' }, { label: 'duplicate@test.com' }]); + }); + + test('should return previous options if search value is empty', () => { + const searchValue = ''; + const previousOptions = [{ label: 'existing@test.com' }]; + const newOptions = getFormattedEmailOptions(searchValue, previousOptions); + expect(newOptions).toEqual([{ label: 'existing@test.com' }]); + }); + + test('should handle single email without comma', () => { + const searchValue = 'single@test.com'; + const previousOptions = [{ label: 'existing@test.com' }]; + const newOptions = getFormattedEmailOptions(searchValue, previousOptions); + + expect(newOptions).toEqual([{ label: 'existing@test.com' }, { label: 'single@test.com' }]); + }); +}); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.tsx index d9a4750cb4564d..745349ab5e8720 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.tsx @@ -18,6 +18,21 @@ import { EmailActionParams } from '../types'; const noop = () => {}; +export const getFormattedEmailOptions = ( + searchValue: string, + previousOptions: Array<{ label: string }> +): Array<{ label: string }> => { + if (!searchValue.trim()) return previousOptions; + const previousEmails: string[] = previousOptions.map((option) => option.label); + const allUniqueEmails: Set = new Set(previousEmails); + searchValue.split(',').forEach((email) => { + const trimmedEmail = email.trim(); + if (trimmedEmail) allUniqueEmails.add(trimmedEmail); + }); + const formattedOptions = Array.from(allUniqueEmails).map((email) => ({ label: email })); + return formattedOptions; +}; + export const EmailParamsFields = ({ actionParams, editAction, @@ -105,7 +120,7 @@ export const EmailParamsFields = ({ data-test-subj="toEmailAddressInput" selectedOptions={toOptions} onCreateOption={(searchValue: string) => { - const newOptions = [...toOptions, { label: searchValue }]; + const newOptions = getFormattedEmailOptions(searchValue, toOptions); editAction( 'to', newOptions.map((newOption) => newOption.label), @@ -148,7 +163,7 @@ export const EmailParamsFields = ({ data-test-subj="ccEmailAddressInput" selectedOptions={ccOptions} onCreateOption={(searchValue: string) => { - const newOptions = [...ccOptions, { label: searchValue }]; + const newOptions = getFormattedEmailOptions(searchValue, ccOptions); editAction( 'cc', newOptions.map((newOption) => newOption.label), @@ -192,7 +207,7 @@ export const EmailParamsFields = ({ data-test-subj="bccEmailAddressInput" selectedOptions={bccOptions} onCreateOption={(searchValue: string) => { - const newOptions = [...bccOptions, { label: searchValue }]; + const newOptions = getFormattedEmailOptions(searchValue, bccOptions); editAction( 'bcc', newOptions.map((newOption) => newOption.label),