Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4eb8b89
fix: 'input' type in pre engagement definitions
gpaoloni Feb 12, 2026
d435110
fix: 'input' type in pre engagement definitions
gpaoloni Feb 12, 2026
d913713
chore: implement form rendering based on definitions (WIP)
gpaoloni Feb 12, 2026
f8dc258
refactor: form input components cleanup
gpaoloni Feb 13, 2026
e3809f9
chore: lint
gpaoloni Feb 13, 2026
16c50fc
fix: tests
gpaoloni Feb 19, 2026
395bb62
fake commit
gpaoloni Feb 19, 2026
7bbeb1a
Revert "fake commit"
gpaoloni Feb 19, 2026
753627c
Merge remote-tracking branch 'origin/master' into gian_CHI-3696
gpaoloni Feb 19, 2026
5fe20e5
Merge remote-tracking branch 'origin/master' into gian_CHI-3696-2
gpaoloni Feb 20, 2026
1c5ec03
refactor: remove rhf, handle forms in Redux
gpaoloni Feb 24, 2026
4f41feb
chore: fix tests
gpaoloni Feb 24, 2026
1dccc7f
Merge remote-tracking branch 'origin/master' into gian_CHI-3696-2
gpaoloni Feb 24, 2026
af5e3c9
Merge remote-tracking branch 'origin/master' into gian_CHI-3696
gpaoloni Feb 24, 2026
325910c
Merge remote-tracking branch 'origin/gian_CHI-3696' into gian_CHI-3696-2
gpaoloni Feb 24, 2026
dd1602b
fix: dependent select reset value bug
gpaoloni Feb 24, 2026
d5ff36f
chore: update state onBlur
gpaoloni Feb 24, 2026
0815ead
Merge pull request #3957 from techmatters/gian_CHI-3696-2
gpaoloni Feb 24, 2026
f2fbc63
Initial plan
Copilot Feb 27, 2026
1d2ecf0
test: add unit test coverage for pre-engagement form components [CHI-…
Copilot Feb 27, 2026
0a0a239
Merge remote-tracking branch 'origin/master' into gian_CHI-3696
gpaoloni Feb 27, 2026
49e8a31
chore: add npm ci step for copilot
gpaoloni Feb 27, 2026
8a7f832
Merge remote-tracking branch 'origin/gian_CHI-3696' into copilot/sub-…
gpaoloni Feb 27, 2026
dde6822
revert: submit updates and tests
gpaoloni Feb 27, 2026
989af43
Merge pull request #3976 from techmatters/copilot/sub-pr-3897
gpaoloni Feb 27, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ excludeAgent: "code-review"

Always run the following checks and fix any issues they raise prior to requesting a review:

- Ensure the necessary dependencies are properly installed running `npm ci` in aselo-webchat-react-app/
- Ensure the typescript compiles by running the build command in aselo-webchat-react-app/package.json
- Ensure the unit tests pass by running the test script in aselo-webchat-react-app/package.json
- Ensure the code changes are correctly linted by running eslint using the configuration in the aselo-webchat-react-app directory
17 changes: 0 additions & 17 deletions aselo-webchat-react-app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions aselo-webchat-react-app/src/__mocks__/redux/mockRedux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const BASE_MOCK_REDUX: AppState = {
session: {
expanded: false,
currentPhase: EngagementPhase.Loading,
preEngagementData: {},
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const ConversationEnded = () => {

const handleStartNewChat = () => {
sessionDataHandler.clear();
dispatch(updatePreEngagementData({ email: '', name: '', query: '' }));
dispatch(updatePreEngagementData({}));
dispatch(changeEngagementPhase({ phase: EngagementPhase.PreEngagementForm }));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const MessagingCanvasPhase = () => {

useEffect(() => {
dispatch(removeNotification(notifications.failedToInitSessionNotification('ds').id));
sendInitialUserQuery(conversation as Conversation, preEngagmentData?.query);
sendInitialUserQuery(conversation as Conversation, 'TODO: trigger message');
}, [dispatch, conversation, preEngagmentData]);

const Wrapper = conversationState === 'active' ? AttachFileDropArea : Fragment;
Expand Down
83 changes: 25 additions & 58 deletions aselo-webchat-react-app/src/components/PreEngagementFormPhase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,44 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

import { Input } from '@twilio-paste/core/input';
import { Label } from '@twilio-paste/core/label';
import { Box } from '@twilio-paste/core/box';
import { TextArea } from '@twilio-paste/core/textarea';
import { FormEvent } from 'react';
import { Button } from '@twilio-paste/core/button';
import { useDispatch, useSelector } from 'react-redux';
import { Text } from '@twilio-paste/core/text';

import { sessionDataHandler } from '../sessionDataHandler';
import { addNotification, changeEngagementPhase, updatePreEngagementData } from '../store/actions/genericActions';
import { addNotification, changeEngagementPhase, updatePreEngagementDataField } from '../store/actions/genericActions';
import { initSession } from '../store/actions/initActions';
import { AppState, EngagementPhase } from '../store/definitions';
import { Header } from './Header';
import { notifications } from '../notifications';
import { NotificationBar } from './NotificationBar';
import { introStyles, fieldStyles, titleStyles, formStyles } from './styles/PreEngagementFormPhase.styles';
import { useSanitizer } from '../utils/useSanitizer';
import { fieldStyles, titleStyles, formStyles } from './styles/PreEngagementFormPhase.styles';
import LocalizedTemplate from '../localization/LocalizedTemplate';
import { generateForm } from './forms/formInputs';

export const PreEngagementFormPhase = () => {
const { name, email, query } = useSelector((state: AppState) => state.session.preEngagementData) || {};
const { preEngagementData } = useSelector((state: AppState) => state.session ?? {});
const { preEngagementFormDefinition } = useSelector((state: AppState) => state.config);
const dispatch = useDispatch();
const { onUserInputSubmit } = useSanitizer();

const { friendlyName } = preEngagementData;

const getItem = (inputName: string) => preEngagementData[inputName] ?? {};
const setItemValue = (payload: { name: string; value: string | boolean }) => {
dispatch(updatePreEngagementDataField(payload));
};
const handleChange = setItemValue;

const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
dispatch(changeEngagementPhase({ phase: EngagementPhase.Loading }));
try {
const data = await sessionDataHandler.fetchAndStoreNewSession({
formData: {
friendlyName: name && onUserInputSubmit(name, true),
email,
query: query && onUserInputSubmit(query),
...preEngagementData,
friendlyName,
},
});
dispatch(
Expand All @@ -61,65 +66,27 @@ export const PreEngagementFormPhase = () => {
}
};

const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit(e);
}
};
if (!preEngagementFormDefinition) {
return null;
}

const titleText = preEngagementFormDefinition.description ?? 'Hi there!';
const submitText = preEngagementFormDefinition.submitLabel ?? 'Start chat';

return (
<>
<Header />
<NotificationBar />
<Box as="form" data-test="pre-engagement-chat-form" onSubmit={handleSubmit} {...formStyles}>
<Text {...titleStyles} as="h3">
Hi there!
<LocalizedTemplate code={titleText} />
</Text>
<Text {...introStyles} as="p">
We&#39;re here to help. Please give us some info to get started.
</Text>
<Box {...fieldStyles}>
<Label htmlFor="name">Name</Label>
<Input
type="text"
placeholder="Please enter your name"
name="name"
data-test="pre-engagement-chat-form-name-input"
value={name}
onChange={e => dispatch(updatePreEngagementData({ name: e.target.value }))}
pattern="[A-Za-z0-9' ]{1,}"
required
/>
</Box>
<Box {...fieldStyles}>
<Label htmlFor="email">Email address</Label>
<Input
type="email"
placeholder="Please enter your email address"
name="email"
data-test="pre-engagement-chat-form-email-input"
value={email}
onChange={e => dispatch(updatePreEngagementData({ email: e.target.value }))}
required
/>
</Box>

<Box {...fieldStyles}>
<Label htmlFor="query">How can we help you?</Label>
<TextArea
placeholder="Ask a question"
name="query"
data-test="pre-engagement-chat-form-query-textarea"
value={query}
onChange={e => dispatch(updatePreEngagementData({ query: e.target.value }))}
onKeyDown={handleKeyPress}
required
/>
{generateForm({ form: preEngagementFormDefinition.fields, handleChange, getItem, setItemValue })}
</Box>

<Button variant="primary" type="submit" data-test="pre-engagement-start-chat-button">
Start chat
<LocalizedTemplate code={submitText} />
</Button>
</Box>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,6 @@ describe('Conversation Ended', () => {
const newChatButton = queryByText(newChatButtonText) as Element;
fireEvent.click(newChatButton);

expect(updatePreEngagementDataSpy).toHaveBeenCalledWith({ email: '', name: '', query: '' });
expect(updatePreEngagementDataSpy).toHaveBeenCalledWith({});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const conversationMock = {
getMessagesCount: jest.fn(),
prepareMessage: jest.fn(),
};
const TEST_QUERY = 'test query';
const TEST_QUERY = 'TODO: trigger message';
const baseReduxState = {
chat: { conversationState: 'closed', ...BASE_MOCK_REDUX.chat },
session: { token: 'token', ...BASE_MOCK_REDUX.session },
Expand Down Expand Up @@ -127,7 +127,7 @@ describe('Messaging Canvas Phase', () => {
resetMockRedux({
...baseReduxState,
chat: { ...baseReduxState.chat, conversation: conversationMock },
session: { ...baseReduxState.session, preEngagementData: { query: TEST_QUERY, email: '', name: '' } },
session: { ...baseReduxState.session, preEngagementData: {} },
});

await waitFor(() => render(<MessagingCanvasPhase />));
Expand All @@ -143,7 +143,7 @@ describe('Messaging Canvas Phase', () => {
resetMockRedux({
...baseReduxState,
chat: { ...baseReduxState.chat, conversationState: 'closed', conversation: conversationMock },
session: { ...baseReduxState.session, preEngagementData: { query: TEST_QUERY, email: '', name: '' } },
session: { ...baseReduxState.session, preEngagementData: {} },
});
conversationMock.getMessagesCount.mockResolvedValue(1);
await waitFor(() => render(<MessagingCanvasPhase />));
Expand All @@ -165,7 +165,7 @@ describe('Messaging Canvas Phase', () => {
it('Should not trigger conversation if conversation is not initialised', async () => {
resetMockRedux({
...baseReduxState,
session: { ...baseReduxState.session, preEngagementData: { query: TEST_QUERY, email: '', name: '' } },
session: { ...baseReduxState.session, preEngagementData: {} },
});
conversationMock.getMessagesCount.mockResolvedValue(1);
await waitFor(() => render(<MessagingCanvasPhase />));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

import { fireEvent, render, waitFor } from '@testing-library/react';
import { act, fireEvent, render, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Provider } from 'react-redux';
import { FormInputType } from 'hrm-form-definitions';

import { sessionDataHandler } from '../../sessionDataHandler';
import * as initAction from '../../store/actions/initActions';
import { EngagementPhase } from '../../store/definitions';
import { store } from '../../store/store';
import { AppState, EngagementPhase } from '../../store/definitions';
import { preloadStore } from '../../store/store';
import { PreEngagementFormPhase } from '../PreEngagementFormPhase';

const token = 'token';
Expand All @@ -42,8 +43,6 @@ jest.mock('../NotificationBar', () => ({
NotificationBar: () => <div title="NotificationBar" />,
}));

const withStore = (Component: React.ReactElement) => <Provider store={store}>{Component}</Provider>;

describe('Pre Engagement Form Phase', () => {
const namePlaceholderText = 'Please enter your name';
const emailPlaceholderText = 'Please enter your email address';
Expand All @@ -53,9 +52,35 @@ describe('Pre Engagement Form Phase', () => {
const queryLabelText = 'How can we help you?';

const name = 'John';
const email = 'email@email.email';
const email = 'email@email.com';
const query = 'Why is a potato?';

const preloadedState: Partial<AppState> = {
config: {
environment: 'test',
helplineCode: '',
quickExitUrl: 'https://',
translations: {},
defaultLocale: 'en-US',
deploymentKey: '',
aseloBackendUrl: '',
definitionVersion: '',
preEngagementFormDefinition: {
description: 'Description',
submitLabel: 'Submit Label',
fields: [
{ name: 'friendlyName', type: FormInputType.Input, label: nameLabelText, placeholder: namePlaceholderText },
{ name: 'email', type: FormInputType.Email, label: emailLabelText, placeholder: emailPlaceholderText },
{ name: 'query', type: FormInputType.Input, label: queryLabelText, placeholder: queryPlaceholderText },
],
},
},
};

const store = preloadStore(preloadedState);

const withStore = (Component: React.ReactElement) => <Provider store={store}>{Component}</Provider>;

beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(initAction, 'initSession').mockImplementation((data: any) => data);
Expand Down Expand Up @@ -150,30 +175,19 @@ describe('Pre Engagement Form Phase', () => {
const queryInput = getByPlaceholderText(queryPlaceholderText);

fireEvent.change(nameInput, { target: { value: name } });
fireEvent.blur(nameInput);
fireEvent.change(emailInput, { target: { value: email } });
fireEvent.blur(emailInput);
fireEvent.change(queryInput, { target: { value: query } });
fireEvent.blur(queryInput);
fireEvent.submit(formBox);

expect(fetchAndStoreNewSessionSpy).toHaveBeenCalledWith({ formData: { friendlyName: name, query, email } });
});

it('submits form on enter within textarea', () => {
const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession');
const { container } = render(withStore(<PreEngagementFormPhase />));

const textArea = container.querySelector('textarea') as Element;
fireEvent.keyDown(textArea, { key: 'Enter', code: 'Enter', charCode: 13, shiftKey: false });

expect(fetchAndStoreNewSessionSpy).toHaveBeenCalled();
});

it('does not submit form on shift+enter within textarea', () => {
const fetchAndStoreNewSessionSpy = jest.spyOn(sessionDataHandler, 'fetchAndStoreNewSession');
const { container } = render(withStore(<PreEngagementFormPhase />));

const textArea = container.querySelector('textarea') as Element;
fireEvent.keyDown(textArea, { key: 'Enter', code: 'Enter', charCode: 13, shiftKey: true });

expect(fetchAndStoreNewSessionSpy).not.toHaveBeenCalled();
expect(fetchAndStoreNewSessionSpy).toHaveBeenCalledWith({
formData: {
friendlyName: { value: name, error: null, dirty: true },
query: { value: query, error: null, dirty: true },
email: { value: email, error: null, dirty: true },
},
});
});
});
4 changes: 2 additions & 2 deletions aselo-webchat-react-app/src/components/endChat/EndChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default function EndChat(props: Props) {
setDisabled(true);
await configuredBackend('/endChat', { channelSid, token, language });
sessionDataHandler.clear();
dispatch(updatePreEngagementData({ email: '', name: '', query: '' }));
dispatch(updatePreEngagementData({}));
dispatch(changeEngagementPhase({ phase: EngagementPhase.PreEngagementForm }));
} catch (error) {
console.error(error);
Expand All @@ -72,7 +72,7 @@ export default function EndChat(props: Props) {
default:
if (confirm(configuredLocalizeKey('Header-CloseChatButtons-EndChatConfirmDialogMessageFromPreEngagement'))) {
sessionDataHandler.clear();
dispatch(updatePreEngagementData({ email: '', name: '', query: '' }));
dispatch(updatePreEngagementData({}));
dispatch(changeEngagementPhase({ phase: EngagementPhase.PreEngagementForm }));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default function QuickExit(props: Props) {
const handleExit = async () => {
// Clear chat history and open a new location
sessionDataHandler.clear();
dispatch(updatePreEngagementData({ email: '', name: '', query: '' }));
dispatch(updatePreEngagementData({}));
dispatch(changeEngagementPhase({ phase: EngagementPhase.PreEngagementForm }));
if (props.action === 'finishTask') {
// Only if we started a task
Expand Down
Loading
Loading