{this.renderSessionStartDate()}
- {this.areActionsAfterCaseCreationVisible &&
- this.renderActions(this.userActionsAfterCaseCreation)}
+ {this.areActionsAfterCaseCreationVisible && (
+
+ {this.renderActions(this.userActionsAfterCaseCreation)}
+
+ )}
{isShowMoreActionsButtonVisible && this.renderShowMoreActionsButton()}
diff --git a/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-session/user-action.tsx b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-session/user-action.tsx
index c3535a7dfcc..9e090f39e8a 100644
--- a/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-session/user-action.tsx
+++ b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-session/user-action.tsx
@@ -50,7 +50,7 @@ export const UserAction: FunctionalComponent
= ({
case 'TICKET_CREATION':
return (
- a{bindings.i18n.t('ticket-created')}
+ {bindings.i18n.t('ticket-created')}
);
case 'CUSTOM':
diff --git a/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/atomic-insight-user-actions-timeline.new.stories.tsx b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/atomic-insight-user-actions-timeline.new.stories.tsx
new file mode 100644
index 00000000000..ba3115d8205
--- /dev/null
+++ b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/atomic-insight-user-actions-timeline.new.stories.tsx
@@ -0,0 +1,26 @@
+import {parameters} from '@coveo/atomic/storybookUtils/common/common-meta-parameters';
+import {renderComponent} from '@coveo/atomic/storybookUtils/common/render-component';
+import type {Meta, StoryObj as Story} from '@storybook/web-components';
+import {wrapInInsightInterface} from '../../../../../storybookUtils/insight/insight-interface-wrapper';
+
+const {decorator, play} = wrapInInsightInterface();
+
+const meta: Meta = {
+ component: 'atomic-insight-user-actions-timeline',
+ title: 'Atomic/Insight/UserActionsTimelines',
+ id: 'atomic-insight-user-actions-timeline',
+
+ render: renderComponent,
+ decorators: [decorator],
+ parameters,
+ play,
+};
+
+export default meta;
+
+export const Default: Story = {
+ name: 'atomic-insight-user-actions-timeline',
+ play: async (context) => {
+ await play(context);
+ },
+};
diff --git a/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/atomic-insight-user-actions-timeline.pcss b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/atomic-insight-user-actions-timeline.pcss
new file mode 100644
index 00000000000..e0edebb1211
--- /dev/null
+++ b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/atomic-insight-user-actions-timeline.pcss
@@ -0,0 +1,6 @@
+@import '../../../../global/global.pcss';
+
+.separator {
+ height: 1px;
+ background-color: #e0e1dd;
+}
diff --git a/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/atomic-insight-user-actions-timeline.tsx b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/atomic-insight-user-actions-timeline.tsx
new file mode 100644
index 00000000000..dfdb30fdbbc
--- /dev/null
+++ b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/atomic-insight-user-actions-timeline.tsx
@@ -0,0 +1,228 @@
+import {Component, h, State, Prop} from '@stencil/core';
+import {
+ buildInsightUserActions,
+ InsightUserActions,
+ InsightUserActionsState,
+ InsightUserSession,
+} from '../..';
+import ArrowDownIcon from '../../../../images/big-arrow-down.svg';
+import ArrowUpIcon from '../../../../images/big-arrow-up.svg';
+import {
+ InitializableComponent,
+ InitializeBindings,
+ BindStateToController,
+} from '../../../../utils/initialization-utils';
+import {Button} from '../../../common/button';
+import {NoItemsContainer} from '../../../common/no-items/container';
+import {MagnifyingGlass} from '../../../common/no-items/magnifying-glass';
+import {InsightBindings} from '../../atomic-insight-interface/atomic-insight-interface';
+
+/**
+ * This component displays all the actions performed by a user around the time they created a case.
+ * The actions are grouped into multiple sessions, including the session during which the case was created,
+ * the sessions preceding the case creation and the sessions following the case creation.
+ *
+ * @component
+ * @example
+ *
+ *
+ */
+@Component({
+ tag: 'atomic-insight-user-actions-timeline',
+ styleUrl: 'atomic-insight-user-actions-timeline.pcss',
+ shadow: true,
+})
+export class AtomicInsightUserActionsTimeline
+ implements InitializableComponent
+{
+ @InitializeBindings() public bindings!: InsightBindings;
+ public userActions!: InsightUserActions;
+
+ @BindStateToController('userActions')
+ @State()
+ public userActionsState!: InsightUserActionsState;
+ public error!: Error;
+
+ /**
+ * The ID of the user whose actions are being displayed. For example in email format "someone@company.com".
+ */
+ @Prop() public userId!: string;
+ /**
+ * The date and time when the case was created. For example "2024-01-01T00:00:00Z"
+ */
+ @Prop() public ticketCreationDateTime!: string;
+
+ public initialize() {
+ this.userActions = buildInsightUserActions(this.bindings.engine, {
+ options: {ticketCreationDate: this.ticketCreationDateTime},
+ });
+
+ this.userActions.fetchUserActions(this.userId);
+ }
+
+ @State() followingSessionsAreVisible = false;
+ @State() precedingSessionsAreVisible = false;
+
+ private toggleFollowingSessions() {
+ this.followingSessionsAreVisible = !this.followingSessionsAreVisible;
+ }
+
+ private togglePrecedingSessions() {
+ this.precedingSessionsAreVisible = !this.precedingSessionsAreVisible;
+ }
+
+ private renderSessions(
+ sessions: Array | undefined,
+ renderSeparator?: Function,
+ testId?: string
+ ) {
+ return (
+
+ {sessions?.map(({actions, start}) => [
+
,
+ renderSeparator ? renderSeparator() : null,
+ ])}
+
+ );
+ }
+
+ private renderToggleFollowingSessionsButton() {
+ const btnClasses = 'flex items-center text-left p-2 text-sm max-w-full';
+ const iconClasses = 'h-3 w-3 mr-1';
+ const label = this.followingSessionsAreVisible
+ ? this.bindings.i18n.t('hide-following-sessions')
+ : this.bindings.i18n.t('show-following-sessions');
+ const icon = this.followingSessionsAreVisible ? ArrowDownIcon : ArrowUpIcon;
+
+ return (
+
+ );
+ }
+
+ private renderTogglePrecedingSessionsButton() {
+ const btnClasses = 'flex items-center text-left p-2 text-sm max-w-full';
+ const iconClasses = 'h-3 w-3 mr-1';
+ const label = this.precedingSessionsAreVisible
+ ? this.bindings.i18n.t('hide-preceding-sessions')
+ : this.bindings.i18n.t('show-preceding-sessions');
+ const icon = this.precedingSessionsAreVisible ? ArrowUpIcon : ArrowDownIcon;
+
+ return (
+
+ );
+ }
+
+ private renderFollowingSessionsSection() {
+ if (!this.userActionsState.timeline?.followingSessions?.length) {
+ return null;
+ }
+ return [
+ this.renderToggleFollowingSessionsButton(),
+ ,
+ this.followingSessionsAreVisible
+ ? this.renderSessions(
+ this.userActionsState.timeline?.followingSessions,
+ () => ,
+ 'following-session'
+ )
+ : null,
+ ];
+ }
+
+ private renderPrecedingSessionsSection() {
+ if (!this.userActionsState.timeline?.precedingSessions?.length) {
+ return null;
+ }
+ return [
+ ,
+ this.precedingSessionsAreVisible
+ ? this.renderSessions(
+ this.userActionsState.timeline?.precedingSessions,
+ () => ,
+ 'preceding-session'
+ )
+ : null,
+ this.renderTogglePrecedingSessionsButton(),
+ ];
+ }
+
+ private renderTimeline() {
+ return (
+
+ {this.renderFollowingSessionsSection()}
+
+ {this.renderPrecedingSessionsSection()}
+
+ );
+ }
+
+ private renderNoUserActionsScreen() {
+ return (
+
+
+
+
+ {this.bindings.i18n.t('no-user-actions-available')}
+
+
+ {this.bindings.i18n.t('no-user-actions-associated-with-params')}
+
+
+
+ );
+ }
+
+ render() {
+ const areUserActionsAvailable = this.userActionsState.timeline?.session;
+ const hasError = this.userActionsState.error;
+
+ if (areUserActionsAvailable && !hasError) {
+ return this.renderTimeline();
+ }
+ return this.renderNoUserActionsScreen();
+ }
+}
diff --git a/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/e2e/atomic-insight-user-actions-timeline.e2e.ts b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/e2e/atomic-insight-user-actions-timeline.e2e.ts
new file mode 100644
index 00000000000..24320069086
--- /dev/null
+++ b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/e2e/atomic-insight-user-actions-timeline.e2e.ts
@@ -0,0 +1,198 @@
+import {test, expect} from './fixture';
+
+const followingSessionsActions = [
+ {
+ name: 'CUSTOM',
+ value:
+ '{"event_type":"example","event_value":"exampleCustomAction","origin_level_1":"default"}',
+ time: new Date('2024-09-02T15:30:00Z').valueOf(),
+ },
+ {
+ name: 'CUSTOM',
+ value:
+ '{"event_type":"example","event_value":"exampleCustomAction","origin_level_1":"default"}',
+ time: new Date('2024-09-01T15:30:00Z').valueOf(),
+ },
+];
+
+const ticketCreationSessionActions = [
+ {
+ name: 'CUSTOM',
+ value:
+ '{"event_type":"errors","event_value":"One","origin_level_1":"default","origin_level_2":"default"}',
+ time: new Date('2024-08-30T00:10:00Z').valueOf(),
+ },
+ {
+ name: 'CUSTOM',
+ value:
+ '{"event_type":"errors","event_value":"Two","origin_level_1":"default","origin_level_2":"default"}',
+ time: new Date('2024-08-30T00:12:00Z').valueOf(),
+ },
+ {
+ name: 'CUSTOM',
+ value:
+ '{"event_type":"errors","event_value":"Three","origin_level_1":"default","origin_level_2":"default"}',
+ time: new Date('2024-08-29T23:45:00Z').valueOf(),
+ },
+];
+
+const precedingSessionsActions = [
+ {
+ name: 'CUSTOM',
+ value:
+ '{"event_type":"example","event_value":"exampleCustomAction","origin_level_1":"default"}',
+ time: new Date('2024-08-29T15:40:00Z').valueOf(),
+ },
+ {
+ name: 'CUSTOM',
+ value:
+ '{"event_type":"example","event_value":"exampleCustomAction","origin_level_1":"default"}',
+ time: new Date('2024-08-28T15:40:00Z').valueOf(),
+ },
+];
+
+const exampleUserActions = [
+ ...followingSessionsActions,
+ ...ticketCreationSessionActions,
+ ...precedingSessionsActions,
+];
+
+const exampleUserId = 'exampleUserId';
+const exampleTicketCreationDate = encodeURIComponent('2024-08-30');
+
+test.describe('user actions timeline', () => {
+ test.describe('when user actions data is found', () => {
+ test.beforeEach(async ({userActionsTimeline, page}) => {
+ await userActionsTimeline.load({
+ args: {
+ userId: exampleUserId,
+ ticketCreationDateTime: exampleTicketCreationDate,
+ },
+ });
+ await userActionsTimeline.mockUserActions(page, exampleUserActions);
+ });
+
+ test('should display the ticket creation session', async ({
+ userActionsTimeline,
+ }) => {
+ await expect(userActionsTimeline.activeSession).toBeVisible();
+ });
+
+ test('should display the show following sessions button', async ({
+ userActionsTimeline,
+ }) => {
+ await expect(
+ userActionsTimeline.showFollowingSessionsbutton
+ ).toBeVisible();
+ });
+
+ test('should display the show preceding sessions button', async ({
+ userActionsTimeline,
+ }) => {
+ await expect(
+ userActionsTimeline.showPrecedingSessionsbutton
+ ).toBeVisible();
+ });
+
+ test('should not display the preceding sessions automatically', async ({
+ userActionsTimeline,
+ }) => {
+ await expect(userActionsTimeline.precedingSession).not.toBeVisible();
+ });
+
+ test('should not display the following sessions automatically', async ({
+ userActionsTimeline,
+ }) => {
+ await expect(userActionsTimeline.followingSession).not.toBeVisible();
+ });
+
+ test.describe('when clicking the show more actions button', () => {
+ test('should properly show more actions', async ({
+ userActionsTimeline,
+ }) => {
+ await expect(userActionsTimeline.showMoreActionsButton).toBeVisible();
+ await expect(userActionsTimeline.moreActionsSection).not.toBeVisible();
+ await userActionsTimeline.showMoreActionsButton.click();
+ await userActionsTimeline.showMoreActionsButton.waitFor({
+ state: 'hidden',
+ });
+
+ await expect(userActionsTimeline.moreActionsSection).toBeVisible();
+ });
+ });
+
+ test.describe('when toggling the following sessions', () => {
+ test('should properly show and hide the following sessions', async ({
+ userActionsTimeline,
+ }) => {
+ const expectedFollowingSessionsCount = 2;
+
+ await userActionsTimeline.showFollowingSessionsbutton.click();
+ await userActionsTimeline.hideFollowingSessionsbutton.waitFor({
+ state: 'visible',
+ });
+
+ await expect(userActionsTimeline.followingSession).toHaveCount(
+ expectedFollowingSessionsCount
+ );
+
+ await userActionsTimeline.hideFollowingSessionsbutton.click();
+ await userActionsTimeline.showFollowingSessionsbutton.waitFor({
+ state: 'visible',
+ });
+
+ await expect(userActionsTimeline.followingSession).not.toBeVisible();
+ });
+ });
+
+ test.describe('when toggling the preceding sessions', () => {
+ test('should properly show and hide the preceding sessions', async ({
+ userActionsTimeline,
+ }) => {
+ const expectedPrecedingSessionsCount = 2;
+
+ await userActionsTimeline.showPrecedingSessionsbutton.click();
+ await userActionsTimeline.hidePrecedingSessionsbutton.waitFor({
+ state: 'visible',
+ });
+
+ await expect(userActionsTimeline.precedingSession).toHaveCount(
+ expectedPrecedingSessionsCount
+ );
+
+ await userActionsTimeline.hidePrecedingSessionsbutton.click();
+ await userActionsTimeline.showPrecedingSessionsbutton.waitFor({
+ state: 'visible',
+ });
+
+ await expect(userActionsTimeline.precedingSession).not.toBeVisible();
+ });
+ });
+ });
+
+ test.describe('when no user actions data is found', () => {
+ test.beforeEach(async ({userActionsTimeline, page}) => {
+ await userActionsTimeline.mockUserActions(page, []);
+ await userActionsTimeline.load();
+ });
+
+ test('should display the user actions error screen', async ({
+ userActionsTimeline,
+ }) => {
+ await expect(userActionsTimeline.userActionsError).toBeVisible();
+ });
+ });
+
+ test.describe('when an error occurs while fetching user actions', () => {
+ test.beforeEach(async ({userActionsTimeline, page}) => {
+ await userActionsTimeline.mockUserActionsError(page);
+ await userActionsTimeline.load();
+ });
+
+ test('should display the user actions error screen', async ({
+ userActionsTimeline,
+ }) => {
+ await expect(userActionsTimeline.userActionsError).toBeVisible();
+ });
+ });
+});
diff --git a/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/e2e/fixture.ts b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/e2e/fixture.ts
new file mode 100644
index 00000000000..2e05d942da9
--- /dev/null
+++ b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/e2e/fixture.ts
@@ -0,0 +1,21 @@
+import {test as base} from '@playwright/test';
+import {
+ AxeFixture,
+ makeAxeBuilder,
+} from '../../../../../../playwright-utils/base-fixture';
+import {UserActionsTimelinePageObject} from './page-object';
+
+type AtomicInsightUserActionsTimelineE2EFixtures = {
+ userActionsTimeline: UserActionsTimelinePageObject;
+};
+
+export const test = base.extend<
+ AtomicInsightUserActionsTimelineE2EFixtures & AxeFixture
+>({
+ makeAxeBuilder,
+ userActionsTimeline: async ({page}, use) => {
+ await use(new UserActionsTimelinePageObject(page));
+ },
+});
+
+export {expect} from '@playwright/test';
diff --git a/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/e2e/page-object.ts b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/e2e/page-object.ts
new file mode 100644
index 00000000000..f58bfc31e22
--- /dev/null
+++ b/packages/atomic/src/components/insight/user-actions/atomic-insight-user-actions-timeline/e2e/page-object.ts
@@ -0,0 +1,83 @@
+import {Page} from '@playwright/test';
+import {BasePageObject} from '../../../../../../playwright-utils/base-page-object';
+
+export class UserActionsTimelinePageObject extends BasePageObject<'atomic-insight-user-actions-timeline'> {
+ constructor(page: Page) {
+ super(page, 'atomic-insight-user-actions-timeline');
+ }
+
+ get activeSession() {
+ return this.page.locator('[data-testid="active-session"]');
+ }
+
+ get followingSession() {
+ return this.page.locator('[data-testid="following-session"]');
+ }
+
+ get precedingSession() {
+ return this.page.locator('[data-testid="preceding-session"]');
+ }
+
+ get showFollowingSessionsbutton() {
+ return this.page.getByLabel('Show following sessions');
+ }
+
+ get hideFollowingSessionsbutton() {
+ return this.page.getByLabel('Hide following sessions');
+ }
+
+ get showPrecedingSessionsbutton() {
+ return this.page.getByLabel('Show preceding sessions');
+ }
+
+ get hidePrecedingSessionsbutton() {
+ return this.page.getByLabel('Hide preceding sessions');
+ }
+
+ get userActionsError() {
+ return this.page.locator('[data-testid="user-actions-error"]');
+ }
+
+ get showMoreActionsButton() {
+ return this.page.locator('[data-testid="show-more-actions-button"] button');
+ }
+
+ get moreActionsSection() {
+ return this.page.locator('[data-testid="more-actions-section"]');
+ }
+
+ async mockUserActions(
+ page: Page,
+ userActions: Array<{name: string; value: string; time: number}>
+ ) {
+ await page.route('**/user/actions', async (route) => {
+ const body = {value: userActions};
+
+ await route.fulfill({
+ body: JSON.stringify(body),
+ status: 200,
+ headers: {
+ 'content-type': 'text/html',
+ },
+ });
+ });
+ }
+
+ async mockUserActionsError(page: Page) {
+ await page.route('**/user/actions', async (route) => {
+ const body = {
+ message: 'Access is denied.',
+ errorCode: 'ACCESS_DENIED',
+ requestID: '1486603b-db83-4dc2-9580-5f8e81c8e00c',
+ };
+
+ await route.fulfill({
+ body: JSON.stringify(body),
+ status: 403,
+ headers: {
+ 'content-type': 'text/html',
+ },
+ });
+ });
+ }
+}
diff --git a/packages/atomic/src/components/search/atomic-result/e2e/page-object.ts b/packages/atomic/src/components/search/atomic-result/e2e/page-object.ts
new file mode 100644
index 00000000000..0b14f29cd33
--- /dev/null
+++ b/packages/atomic/src/components/search/atomic-result/e2e/page-object.ts
@@ -0,0 +1,8 @@
+import type {Page} from '@playwright/test';
+import {BasePageObject} from '../../../../../playwright-utils/base-page-object';
+
+export class AtomicResultPageObject extends BasePageObject<'atomic-result'> {
+ constructor(page: Page) {
+ super(page, 'atomic-result');
+ }
+}
diff --git a/packages/atomic/src/components/search/result-lists/atomic-result-list/atomic-result-list.new.stories.tsx b/packages/atomic/src/components/search/result-lists/atomic-result-list/atomic-result-list.new.stories.tsx
index 92e21da9f21..691c1111b30 100644
--- a/packages/atomic/src/components/search/result-lists/atomic-result-list/atomic-result-list.new.stories.tsx
+++ b/packages/atomic/src/components/search/result-lists/atomic-result-list/atomic-result-list.new.stories.tsx
@@ -3,7 +3,16 @@ import {renderComponent} from '@coveo/atomic/storybookUtils/common/render-compon
import {wrapInSearchInterface} from '@coveo/atomic/storybookUtils/search/search-interface-wrapper';
import type {Meta, StoryObj as Story} from '@storybook/web-components';
-const {decorator, play} = wrapInSearchInterface();
+const {decorator, play} = wrapInSearchInterface({
+ search: {
+ preprocessSearchResponseMiddleware: (r) => {
+ const [result] = r.body.results;
+ result.title = 'Manage the Coveo In-Product Experiences (IPX)';
+ result.clickUri = 'https://docs.coveo.com/en/3160';
+ return r;
+ },
+ },
+});
const meta: Meta = {
component: 'atomic-result-list',
title: 'Atomic/ResultList',
@@ -18,5 +27,12 @@ const meta: Meta = {
export default meta;
export const Default: Story = {
- name: 'atomic-result-list',
+ name: 'List Display',
+};
+
+export const Grid: Story = {
+ name: 'Grid Display',
+ args: {
+ 'attributes-display': 'grid',
+ },
};
diff --git a/packages/atomic/src/components/search/result-lists/atomic-result-list/atomic-result-list.tsx b/packages/atomic/src/components/search/result-lists/atomic-result-list/atomic-result-list.tsx
index 20ce948563d..0692fcec22b 100644
--- a/packages/atomic/src/components/search/result-lists/atomic-result-list/atomic-result-list.tsx
+++ b/packages/atomic/src/components/search/result-lists/atomic-result-list/atomic-result-list.tsx
@@ -269,7 +269,10 @@ export class AtomicResultList implements InitializableComponent {
this.imageSize
),
content: this.itemTemplateProvider.getTemplateContent(result),
- linkContent: this.itemTemplateProvider.getLinkTemplateContent(result),
+ linkContent:
+ this.display === 'grid'
+ ? this.itemTemplateProvider.getLinkTemplateContent(result)
+ : this.itemTemplateProvider.getEmptyLinkTemplateContent(),
store: this.bindings.store,
density: this.density,
imageSize: this.imageSize,
diff --git a/packages/atomic/src/components/search/result-lists/atomic-result-list/e2e/atomic-result-list.e2e.ts b/packages/atomic/src/components/search/result-lists/atomic-result-list/e2e/atomic-result-list.e2e.ts
new file mode 100644
index 00000000000..721c6bc086b
--- /dev/null
+++ b/packages/atomic/src/components/search/result-lists/atomic-result-list/e2e/atomic-result-list.e2e.ts
@@ -0,0 +1,35 @@
+import {test, expect} from './fixture';
+
+test.describe('When using a list layout', () => {
+ test.beforeEach(async ({resultList}) => {
+ await resultList.load();
+ });
+
+ test.describe('when clicking a result', () => {
+ test.beforeEach(async ({result}) => {
+ await result.hydrated.first().click();
+ });
+
+ test('should not navigate', async ({page}) => {
+ expect(page.url()).toContain('http://localhost:4400/iframe.html');
+ });
+ });
+});
+
+test.describe('When using a grid layout', () => {
+ test.beforeEach(async ({resultList}) => {
+ await resultList.load({story: 'grid'});
+ });
+
+ test.describe('when clicking a result', () => {
+ test.beforeEach(async ({result}) => {
+ await result.hydrated.first().click();
+ });
+
+ test('should navigate', async ({page}) => {
+ await expect
+ .poll(() => page.url())
+ .toContain('https://docs.coveo.com/en/3160');
+ });
+ });
+});
diff --git a/packages/atomic/src/components/search/result-lists/atomic-result-list/e2e/fixture.ts b/packages/atomic/src/components/search/result-lists/atomic-result-list/e2e/fixture.ts
new file mode 100644
index 00000000000..6076f85fed5
--- /dev/null
+++ b/packages/atomic/src/components/search/result-lists/atomic-result-list/e2e/fixture.ts
@@ -0,0 +1,24 @@
+import {test as base} from '@playwright/test';
+import {
+ AxeFixture,
+ makeAxeBuilder,
+} from '../../../../../../playwright-utils/base-fixture';
+import {AtomicResultPageObject as Result} from '../../../atomic-result/e2e/page-object';
+import {AtomicResultListPageObject as ResultList} from './page-object';
+
+type Fixture = {
+ resultList: ResultList;
+ result: Result;
+};
+
+export const test = base.extend({
+ makeAxeBuilder,
+ resultList: async ({page}, use) => {
+ await use(new ResultList(page));
+ },
+ result: async ({page}, use) => {
+ await use(new Result(page));
+ },
+});
+
+export {expect} from '@playwright/test';
diff --git a/packages/atomic/src/components/search/result-lists/atomic-result-list/e2e/page-object.ts b/packages/atomic/src/components/search/result-lists/atomic-result-list/e2e/page-object.ts
new file mode 100644
index 00000000000..fcf5261b305
--- /dev/null
+++ b/packages/atomic/src/components/search/result-lists/atomic-result-list/e2e/page-object.ts
@@ -0,0 +1,8 @@
+import type {Page} from '@playwright/test';
+import {BasePageObject} from '../../../../../../playwright-utils/base-page-object';
+
+export class AtomicResultListPageObject extends BasePageObject<'atomic-result-list'> {
+ constructor(page: Page) {
+ super(page, 'atomic-result-list');
+ }
+}
diff --git a/packages/atomic/src/images/big-arrow-down.svg b/packages/atomic/src/images/big-arrow-down.svg
new file mode 100644
index 00000000000..a98f0eee699
--- /dev/null
+++ b/packages/atomic/src/images/big-arrow-down.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/atomic/src/images/big-arrow-up.svg b/packages/atomic/src/images/big-arrow-up.svg
new file mode 100644
index 00000000000..f409304c75d
--- /dev/null
+++ b/packages/atomic/src/images/big-arrow-up.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/atomic/src/index.ts b/packages/atomic/src/index.ts
index 1a12fa3ba36..cb28a35cbce 100644
--- a/packages/atomic/src/index.ts
+++ b/packages/atomic/src/index.ts
@@ -13,6 +13,7 @@ export {PopoverChildFacet} from './components/common/facets/popover/popover-type
export {resultContext} from './components/search/result-template-components/result-template-decorators';
export {productContext} from './components/commerce/product-template-components/product-template-decorators';
+export {SelectChildProductEventArgs} from './components/commerce/product-template-components/atomic-product-children/atomic-product-children';
export {
dispatchSearchBoxSuggestionsEvent,
SearchBoxSuggestionElement,
diff --git a/packages/atomic/src/locales.json b/packages/atomic/src/locales.json
index 7b2ca8d6e36..a4b17ee00e2 100644
--- a/packages/atomic/src/locales.json
+++ b/packages/atomic/src/locales.json
@@ -8263,6 +8263,168 @@
"zh-CN": "用户操作",
"zh-TW": "使用者操作"
},
+ "no-user-actions-available": {
+ "en": "No user actions available",
+ "fr": "Aucune action utilisateur disponible",
+ "cs": "Nejsou k dispozici žádné uživatelské akce",
+ "da": "Ingen brugerhandlinger tilgængelige",
+ "de": "Keine Benutzeraktionen verfügbar",
+ "el": "Δεν υπάρχουν διαθέσιμες ενέργειες χρήστη",
+ "es": "No hay acciones de usuario disponibles",
+ "fi": "Käyttäjätoimintoja ei ole saatavilla",
+ "hu": "Nincsenek elérhető user műveletek",
+ "id": "Tidak ada tindakan pengguna yang tersedia",
+ "it": "Nessuna azione utente disponibile",
+ "ja": "利用可能なユーザーアクションはありません",
+ "ko": "사용자 액션을 사용할 수 없습니다",
+ "nl": "Geen gebruikersacties beschikbaar",
+ "no": "Ingen brukerhandlinger tilgjengelig",
+ "pl": "Brak dostępnych akcji użytkownika",
+ "pt": "Nenhuma ação do usuário disponível",
+ "pt-BR": "Nenhuma ação do usuário disponível",
+ "ru": "Доступных действий пользователя нет",
+ "sv": "Inga användaråtgärder tillgängliga",
+ "th": "ไม่มีการดำเนินการของผู้ใช้ที่สามารถใช้งานได้",
+ "tr": "Kullanılabilir kullanıcı eylemi yok",
+ "zh": "没有可用的用户操作",
+ "zh-CN": "没有可用的用户操作",
+ "zh-TW": "沒有可用的用戶操作"
+ },
+ "no-user-actions-associated-with-params": {
+ "en": "There are no user actions associated with the user id or the case is too old to detect related actions.",
+ "fr": "Il n'y a pas d'actions utilisateur associées à l'identifiant utilisateur ou le cas est trop ancien pour détecter des actions liées.",
+ "cs": "K uživatelskému ID nejsou přiřazeny žádné uživatelské akce nebo je případ příliš starý na detekci souvisejících akcí.",
+ "da": "Der er ingen brugerhandlinger forbundet med bruger-id'et, eller sagen er for gammel til at opdage relaterede handlinger.",
+ "de": "Es gibt keine Benutzeraktionen, die mit der Benutzer-ID verbunden sind oder der Fall ist zu alt, um verwandte Aktionen zu erkennen.",
+ "el": "Δεν υπάρχουν ενέργειες χρήστη που σχετίζονται με το αναγνωριστικό χρήστη ή η περίπτωση είναι πολύ παλιά για να ανιχνεύσει σχετικές ενέργειες.",
+ "es": "No hay acciones de usuario asociadas con el ID de usuario o el caso es demasiado antiguo para detectar acciones relacionadas.",
+ "fi": "Käyttäjätunnukseen ei liity käyttäjätoimintoja tai tapaus on liian vanha havaittavaksi liittyviä toimeenpanovallan.",
+ "hu": "Nincsenek a felhasználói azonosítóhoz társított felhasználói műveletek vagy az eset túl régi a kapcsolódó műveletek észleléséhez.",
+ "id": "Tidak ada tindakan pengguna yang terkait dengan id pengguna atau kasusnya terlalu lama untuk mendeteksi tindakan terkait.",
+ "it": "Non ci sono azioni utente associate all'ID utente o il caso è troppo vecchio per rilevare azioni correlate.",
+ "ja": "ユーザーIDに関連付けられたユーザーアクションはない、またはケースが古すぎて関連アクションを検出できません。",
+ "ko": "사용자 ID에 연결된 사용자 작업이 없거나 사례가 너무 오래되어 관련 작업을 탐지할 수 없습니다.",
+ "nl": "Er zijn geen gebruikersacties geassocieerd met de gebruikers-id of de zaak is te oud om gerelateerde acties te detecteren.",
+ "no": "Det er ingen brukerhandlinger forbundet med bruker-IDen, eller saken er for gammel til å oppdage relaterte handlinger.",
+ "pl": "Brak akcji użytkownika powiązanych z identyfikatorem użytkownika lub sprawa jest zbyt stara, aby wykryć powiązane działania.",
+ "pt": "Não há ações do usuário associadas ao ID do usuário ou o caso é muito antigo para detectar ações relacionadas.",
+ "pt-BR": "Não há ações do usuário associadas ao ID do usuário ou o caso é muito antigo para detectar ações relacionadas.",
+ "ru": "Действий пользователя, связанных с идентификатором пользователя, нет, или дело слишком старое, чтобы обнаружить связанные действия.",
+ "sv": "Det finns inga användaråtgärder kopplade till användar-ID:t, eller fallet är för gammalt för att upptäcka relaterade åtgärder.",
+ "th": "ไม่มีการดำเนินการของผู้ใช้ที่เชื่อมโยงกับ ID ผู้ใช้หรือกรณีที่เก่าเกินไปในการตรวจสอบการดำเนินการที่เกี่ยวข้อง",
+ "tr": "Kullanıcı kimliği ile ilişkili kullanıcı işlemleri yok ya da durum, ilgili işlemleri tespit etmek için çok eski.",
+ "zh": "没有与用户id相关联的用户操作,或者案例太老无法检测到相关操作。",
+ "zh-CN": "没有与用户id相关联的用户操作,或者案例太老无法检测到相关操作。",
+ "zh-TW": "没有与用户id相关联的用户操作,或者案例太老无法检测到相关操作。"
+ },
+ "hide-preceding-sessions": {
+ "en": "Hide preceding sessions",
+ "fr": "Masquer les sessions précédentes",
+ "cs": "Skrýt předchozí relace",
+ "da": "Skjul forudgående sessioner",
+ "de": "Vorherige Sitzungen ausblenden",
+ "el": "Απόκρυψη προηγούμενων συνεδριών",
+ "es": "Ocultar sesiones anteriores",
+ "fi": "Piilota edeltävät istunnot",
+ "hu": "Előző munkamenetek elrejtése",
+ "id": "Sembunyikan sesi sebelumnya",
+ "it": "Nascondi le sessioni precedenti",
+ "ja": "前のセッションを隠す",
+ "ko": "이전 세션 숨기기",
+ "nl": "Vorige sessies verbergen",
+ "no": "Skjul forrige økter",
+ "pl": "Ukryj poprzednie sesje",
+ "pt": "Ocultar sessões anteriores",
+ "pt-BR": "Ocultar sessões anteriores",
+ "ru": "Скрыть предыдущие сессии",
+ "sv": "Dölj tidigare sessioner",
+ "th": "ซ่อนเซสชันก่อนหน้า",
+ "tr": "Önceki oturumları gizle",
+ "zh": "隐藏前面的会话",
+ "zh-CN": "隐藏先前的会话",
+ "zh-TW": "隱藏前面的會話"
+ },
+ "show-preceding-sessions": {
+ "en": "Show preceding sessions",
+ "fr": "Afficher les sessions précédentes",
+ "cs": "Zobrazit předchozí relace",
+ "da": "Vis foregående sessioner",
+ "de": "Vorangegangene Sitzungen anzeigen",
+ "el": "Εμφάνιση προηγούμενων συνεδριών",
+ "es": "Mostrar sesiones anteriores",
+ "fi": "Näytä edelliset istunnot",
+ "hu": "Előző munkamenetek megjelenítése",
+ "id": "Tampilkan sesi sebelumnya",
+ "it": "Mostra le sessioni precedenti",
+ "ja": "前のセッションを表示",
+ "ko": "이전 세션 보기",
+ "nl": "Vorige sessies tonen",
+ "no": "Vis forrige økter",
+ "pl": "Pokaż poprzednie sesje",
+ "pt": "Mostrar sessões anteriores",
+ "pt-BR": "Mostrar sessões anteriores",
+ "ru": "Показать предыдущие сессии",
+ "sv": "Visa föregående sessioner",
+ "th": "แสดงเซสชั่นก่อนหน้านี้",
+ "tr": "Önceki oturumları göster",
+ "zh": "显示前面的会议",
+ "zh-CN": "显示前面的会话",
+ "zh-TW": "顯示前面的會議"
+ },
+ "hide-following-sessions": {
+ "en": "Hide following sessions",
+ "fr": "Masquer les sessions suivantes",
+ "cs": "Skrýt následující relace",
+ "da": "Skjul følgende sessioner",
+ "de": "Folgende Sitzungen ausblenden",
+ "el": "Απόκρυψη των επόμενων συνεδριών",
+ "es": "Ocultar las sesiones siguientes",
+ "fi": "Piilota seuraavat istunnot",
+ "hu": "A következő munkamenetek elrejtése",
+ "id": "Sembunyikan sesi berikutnya",
+ "it": "Nascondi le sessioni seguenti",
+ "ja": "次のセッションを非表示",
+ "ko": "다음 세션 숨기기",
+ "nl": "Volgende sessies verbergen",
+ "no": "Skjul følgende økter",
+ "pl": "Ukryj następujące sesje",
+ "pt": "Ocultar sessões seguintes",
+ "pt-BR": "Ocultar as sessões seguintes",
+ "ru": "Скрыть следующие сессии",
+ "sv": "Dölj följande sessioner",
+ "th": "ซ่อนเซสชันถัดไป",
+ "tr": "İzleyen oturumları gizle",
+ "zh": "隐藏以下会话",
+ "zh-CN": "隐藏以下会话",
+ "zh-TW": "隱藏以下會議"
+ },
+ "show-following-sessions": {
+ "en": "Show following sessions",
+ "fr": "Afficher les sessions suivantes",
+ "cs": "Zobrazit následující relace",
+ "da": "Vis følgende sessioner",
+ "de": "Folgende Sitzungen anzeigen",
+ "el": "Εμφάνιση των ακόλουθων συνεδρίων",
+ "es": "Mostrar las siguientes sesiones",
+ "fi": "Näytä seuraavat istunnot",
+ "hu": "A következő munkamenetek megjelenítése",
+ "id": "Tampilkan sesi berikutnya",
+ "it": "Mostra le sessioni seguenti",
+ "ja": "次のセッションを表示",
+ "ko": "다음 세션 표시",
+ "nl": "De volgende sessies tonen",
+ "no": "Vis følgende økter",
+ "pl": "Pokaż następujące sesje",
+ "pt": "Mostrar as sessões seguintes",
+ "pt-BR": "Mostrar as sessões seguintes",
+ "ru": "Показать следующие сессии",
+ "sv": "Visa följande sessioner",
+ "th": "แสดงเซสชันถัดไป",
+ "tr": "Aşağıdaki oturumları göster",
+ "zh": "显示以下会议",
+ "zh-CN": "显示以下会议",
+ "zh-TW": "顯示以下會議"
+ },
"empty-search": {
"en": "Empty search",
"fr": "Recherche vide",
diff --git a/packages/atomic/storybookUtils/insight/insight-interface-wrapper.tsx b/packages/atomic/storybookUtils/insight/insight-interface-wrapper.tsx
new file mode 100644
index 00000000000..b24078c6b24
--- /dev/null
+++ b/packages/atomic/storybookUtils/insight/insight-interface-wrapper.tsx
@@ -0,0 +1,40 @@
+import {InsightEngineConfiguration} from '@coveo/headless/dist/definitions/insight.index';
+import {getSampleInsightEngineConfiguration} from '@coveo/headless/insight';
+import {within} from '@storybook/test';
+import {Decorator, StoryContext} from '@storybook/web-components';
+import {html} from 'lit/static-html.js';
+import type * as _ from '../../src/components';
+
+export const wrapInInsightInterface = (
+ config?: Partial,
+ skipFirstSearch = false
+): {
+ decorator: Decorator;
+ play: (context: StoryContext) => Promise;
+} => ({
+ decorator: (story) => html`
+
+ ${story()}
+
+ `,
+ play: async ({canvasElement, step}) => {
+ await customElements.whenDefined('atomic-insight-interface');
+ const canvas = within(canvasElement);
+ const insightInterface =
+ await canvas.findByTestId(
+ 'root-interface'
+ );
+ await step('Render the Insight Interface', async () => {
+ await insightInterface!.initialize({
+ ...getSampleInsightEngineConfiguration(),
+ ...config,
+ });
+ });
+ if (skipFirstSearch) {
+ return;
+ }
+ await step('Execute the first search', async () => {
+ await insightInterface!.executeFirstSearch();
+ });
+ },
+});
diff --git a/packages/atomic/tsconfig.storybook.json b/packages/atomic/tsconfig.storybook.json
index b8bf340425f..30d5ba207d2 100644
--- a/packages/atomic/tsconfig.storybook.json
+++ b/packages/atomic/tsconfig.storybook.json
@@ -3,7 +3,8 @@
"compilerOptions": {
"emitDecoratorMetadata": true,
"composite": true,
- "jsxFactory": "h"
+ "jsxFactory": "h",
+ "declaration": true
},
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
"include": [
diff --git a/packages/headless/esbuild.mjs b/packages/headless/esbuild.mjs
index 45b91b597a1..661ed911ce0 100644
--- a/packages/headless/esbuild.mjs
+++ b/packages/headless/esbuild.mjs
@@ -234,6 +234,7 @@ const nodeEsm = Object.entries(useCaseEntries).map((entry) => {
outfile,
format: 'esm',
external: ['pino'],
+ mainFields: ['module', 'main'],
},
dir
);
@@ -263,30 +264,6 @@ async function buildNodeConfig(options, outDir) {
return out;
}
-// https://github.com/coveo/ui-kit/issues/1616
-function adjustRequireImportsInNodeEsmBundles() {
- const paths = getNodeEsmBundlePaths();
-
- return paths.map(async (filePath) => {
- const resolvedPath = resolve(filePath);
-
- const content = await promises.readFile(resolvedPath, {
- encoding: 'utf-8',
- });
- const modified = content.replace(/__require\(/g, 'require(');
-
- await promises.writeFile(resolvedPath, modified);
- });
-}
-
-function getNodeEsmBundlePaths() {
- return Object.entries(useCaseEntries).map((entry) => {
- const [useCase] = entry;
- const dir = getUseCaseDir('dist/', useCase);
- return `${dir}/headless.esm.js`;
- });
-}
-
function outputMetafile(platform, outDir, metafile) {
const outFile = resolve(outDir, `${platform}.stats.json`);
writeFileSync(outFile, JSON.stringify(metafile));
@@ -301,7 +278,6 @@ async function main() {
...nodeCjs,
...quanticUmd,
]);
- await Promise.all(adjustRequireImportsInNodeEsmBundles());
}
main();
diff --git a/packages/headless/src/api/generated-answer/generated-answer-client.ts b/packages/headless/src/api/generated-answer/generated-answer-client.ts
index 9bdccd0113f..4a3ca6241ac 100644
--- a/packages/headless/src/api/generated-answer/generated-answer-client.ts
+++ b/packages/headless/src/api/generated-answer/generated-answer-client.ts
@@ -4,7 +4,6 @@ import {AsyncThunkOptions} from '../../app/async-thunk-options';
import {ClientThunkExtraArguments} from '../../app/thunk-extra-arguments';
import {GeneratedAnswerErrorPayload} from '../../features/generated-answer/generated-answer-actions';
import {SearchAppState} from '../../state/search-app-state';
-import {createAbortController} from '../../utils/abort-controller-polyfill';
import {URLPath} from '../../utils/url-utils';
import {resetTimeout} from '../../utils/utils';
import {GeneratedAnswerStreamEventData} from './generated-answer-event-payload';
@@ -99,7 +98,7 @@ export class GeneratedAnswerAPIClient {
timeoutStateManager.add(timeout);
};
- const abortController = createAbortController();
+ const abortController = new AbortController();
const stream = () =>
fetchEventSource(buildStreamingUrl(url, organizationId, streamId), {
diff --git a/packages/headless/src/api/search/api-calls-queue.ts b/packages/headless/src/api/search/api-calls-queue.ts
index 0d1a427b17b..aaf6afc4f69 100644
--- a/packages/headless/src/api/search/api-calls-queue.ts
+++ b/packages/headless/src/api/search/api-calls-queue.ts
@@ -1,5 +1,4 @@
import {Logger} from 'pino';
-import {createAbortController} from '../../utils/abort-controller-polyfill';
export class APICallsQueue {
private currentAbortController: AbortController | null = null;
@@ -13,7 +12,7 @@ export class APICallsQueue {
) {
const lastAbortController = this.currentAbortController;
const abortController = (this.currentAbortController =
- createAbortController());
+ new AbortController());
if (lastAbortController) {
if (options.warnOnAbort) {
options.logger.warn('Cancelling current pending search query');
diff --git a/packages/headless/src/app/insight-engine/insight-engine-configuration.ts b/packages/headless/src/app/insight-engine/insight-engine-configuration.ts
index 9e3353965dd..8f16040bebb 100644
--- a/packages/headless/src/app/insight-engine/insight-engine-configuration.ts
+++ b/packages/headless/src/app/insight-engine/insight-engine-configuration.ts
@@ -6,6 +6,7 @@ import {
import {
EngineConfiguration,
engineConfigurationDefinitions,
+ getSampleEngineConfiguration,
} from '../engine-configuration';
/**
@@ -49,3 +50,17 @@ export const insightEngineConfigurationSchema =
},
}),
});
+
+const sampleInsightId = '2729db39-d7fd-4504-a06e-668c64968c95';
+
+/**
+ * Creates a sample search engine configuration.
+ *
+ * @returns The sample search engine configuration.
+ */
+export function getSampleInsightEngineConfiguration(): InsightEngineConfiguration {
+ return {
+ ...getSampleEngineConfiguration(),
+ insightId: sampleInsightId,
+ };
+}
diff --git a/packages/headless/src/app/insight-engine/insight-engine.ts b/packages/headless/src/app/insight-engine/insight-engine.ts
index d18c542377b..ac3dcbf8396 100644
--- a/packages/headless/src/app/insight-engine/insight-engine.ts
+++ b/packages/headless/src/app/insight-engine/insight-engine.ts
@@ -30,12 +30,14 @@ import {
InsightEngineConfiguration,
insightEngineConfigurationSchema,
InsightEngineSearchConfigurationOptions,
+ getSampleInsightEngineConfiguration,
} from './insight-engine-configuration';
export type {
InsightEngineConfiguration,
InsightEngineSearchConfigurationOptions,
};
+export {getSampleInsightEngineConfiguration};
const insightEngineReducers = {
insightConfiguration,
diff --git a/packages/headless/src/insight.index.ts b/packages/headless/src/insight.index.ts
index 40a079af2a2..b31f69ce0af 100644
--- a/packages/headless/src/insight.index.ts
+++ b/packages/headless/src/insight.index.ts
@@ -11,7 +11,10 @@ export type {
InsightEngineConfiguration,
InsightEngineSearchConfigurationOptions,
} from './app/insight-engine/insight-engine';
-export {buildInsightEngine} from './app/insight-engine/insight-engine';
+export {
+ buildInsightEngine,
+ getSampleInsightEngineConfiguration,
+} from './app/insight-engine/insight-engine';
export type {CoreEngine, ExternalEngineOptions} from './app/engine';
export type {
diff --git a/packages/headless/src/utils/abort-controller-polyfill.ts b/packages/headless/src/utils/abort-controller-polyfill.ts
deleted file mode 100644
index 4f84ad5125f..00000000000
--- a/packages/headless/src/utils/abort-controller-polyfill.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export function createAbortController(): AbortController | null {
- // For nodejs environments only, we want to load the implementation of AbortController from node-abort-controller package.
- // For browser environments, we need to make sure that we don't use AbortController as it might not be available (Locker Service in Salesforce)
- // This is not something that can be polyfilled in a meaningful manner.
- // This is a low level browser API after all, and only JS code inside a polyfill cannot actually cancel network requests done by the browser.
-
- if (typeof window === 'undefined') {
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const {AbortController: nodeAbort} = require('node-abort-controller');
- return new nodeAbort() as AbortController;
- }
- return typeof AbortController === 'undefined' ? null : new AbortController();
-}
diff --git a/packages/quantic/cypress/e2e/default-2/recommendation-list/recommendation-list-expectations.ts b/packages/quantic/cypress/e2e/default-2/recommendation-list/recommendation-list-expectations.ts
index 9223fc6d006..28d4c848069 100644
--- a/packages/quantic/cypress/e2e/default-2/recommendation-list/recommendation-list-expectations.ts
+++ b/packages/quantic/cypress/e2e/default-2/recommendation-list/recommendation-list-expectations.ts
@@ -54,22 +54,22 @@ export function recommendationListExpectations(
},
recommendationsEqual: (recommendationsAlias: string) => {
- cy.get>(recommendationsAlias).then(
- (recommendations) => {
- selector
- .recommendationLinks()
- .then((elements) => {
- return Cypress.$.makeArray(elements).map(
- (element) => element.innerText
- );
- })
- .should(
- 'deep.equal',
- recommendations.map((result) => result.Title)
- )
- .logDetail('should render the received recommendations');
- }
- );
+ cy.get>(
+ recommendationsAlias
+ ).then((recommendations) => {
+ selector
+ .recommendationLinks()
+ .then((elements) => {
+ return Cypress.$.makeArray(elements).map(
+ (element) => element.innerText
+ );
+ })
+ .should(
+ 'deep.equal',
+ recommendations.map((result) => result.Title || result.clickUri)
+ )
+ .logDetail('should render the received recommendations');
+ });
},
correctFieldsIncluded: (expectedFieldsToInclude: string[]) => {
diff --git a/packages/quantic/cypress/e2e/default-2/result-list/result-list-expectations.ts b/packages/quantic/cypress/e2e/default-2/result-list/result-list-expectations.ts
index 77e41a92161..654c9f82929 100644
--- a/packages/quantic/cypress/e2e/default-2/result-list/result-list-expectations.ts
+++ b/packages/quantic/cypress/e2e/default-2/result-list/result-list-expectations.ts
@@ -18,20 +18,22 @@ export function resultListExpectations(selector: ResultListSelector) {
.logDetail(`${should(display)} display results`);
},
resultsEqual: (resultsAlias: string) => {
- cy.get(resultsAlias).then((results) => {
- selector
- .resultLinks()
- .then((elements) => {
- return Cypress.$.makeArray(elements).map(
- (element) => element.innerText
- );
- })
- .should(
- 'deep.equal',
- results.map((result) => result.Title)
- )
- .logDetail('should render the received results');
- });
+ cy.get>(resultsAlias).then(
+ (results) => {
+ selector
+ .resultLinks()
+ .then((elements) => {
+ return Cypress.$.makeArray(elements).map(
+ (element) => element.innerText
+ );
+ })
+ .should(
+ 'deep.equal',
+ results.map((result) => result.Title || result.clickUri)
+ )
+ .logDetail('should render the received results');
+ }
+ );
},
requestFields: (expectedFieldsToInclude: string[], useCase: string) => {
cy.wait(getQueryAlias(useCase))
diff --git a/packages/quantic/force-app/main/default/lwc/quanticCaseAssistInterface/quanticCaseAssistInterface.js b/packages/quantic/force-app/main/default/lwc/quanticCaseAssistInterface/quanticCaseAssistInterface.js
index ea5f348db3e..400f65b67b7 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticCaseAssistInterface/quanticCaseAssistInterface.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticCaseAssistInterface/quanticCaseAssistInterface.js
@@ -56,6 +56,7 @@ export default class QuanticCaseAssistInterface extends LightningElement {
caseAssistId: this.caseAssistId,
searchHub: this.searchHub,
analytics: {
+ analyticsMode: 'legacy',
...(document.referrer && {originLevel3: document.referrer}),
},
},
diff --git a/packages/quantic/force-app/main/default/lwc/quanticInsightInterface/quanticInsightInterface.js b/packages/quantic/force-app/main/default/lwc/quanticInsightInterface/quanticInsightInterface.js
index 801c559afd2..7da7d26e11e 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticInsightInterface/quanticInsightInterface.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticInsightInterface/quanticInsightInterface.js
@@ -69,6 +69,7 @@ export default class QuanticInsightInterface extends LightningElement {
locale: LOCALE,
},
analytics: {
+ analyticsMode: 'legacy',
...(document.referrer && {originLevel3: document.referrer}),
},
},
diff --git a/packages/quantic/force-app/main/default/lwc/quanticRecommendationInterface/quanticRecommendationInterface.js b/packages/quantic/force-app/main/default/lwc/quanticRecommendationInterface/quanticRecommendationInterface.js
index 28455d6f214..d4c627cb608 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticRecommendationInterface/quanticRecommendationInterface.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticRecommendationInterface/quanticRecommendationInterface.js
@@ -75,6 +75,7 @@ export default class QuanticRecommendationInterface extends LightningElement {
locale: LOCALE,
timezone: TIMEZONE,
analytics: {
+ analyticsMode: 'legacy',
originContext: this.analyticsOriginContext,
},
},
diff --git a/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/__tests__/quanticSearchBoxInput.test.js b/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/__tests__/quanticSearchBoxInput.test.js
index a75e345c78e..dfe2fa2867d 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/__tests__/quanticSearchBoxInput.test.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/__tests__/quanticSearchBoxInput.test.js
@@ -11,6 +11,8 @@ const functionsMocks = {
const defaultPlaceholder = 'Search...';
const mockInputValue = 'Test input value';
+const mockLongInputValue =
+ 'Test input value that is longer than the default input value length to test the textarea expanding feature';
const mockSuggestions = [
{key: '1', value: 'suggestion1', rawValue: 'suggestion1'},
{key: '2', value: 'suggestion2', rawValue: 'suggestion2'},
@@ -770,6 +772,36 @@ describe('c-quantic-search-box-input', () => {
});
});
});
+
+ describe('when clicking on the clear icon after typing something', () => {
+ it('should properly clear the input value', async () => {
+ const element = createTestComponent({
+ ...defaultOptions,
+ textarea: textareaValue,
+ });
+ await flushPromises();
+
+ element.inputValue = mockLongInputValue;
+ await flushPromises();
+
+ const clearIcon = element.shadowRoot.querySelector(
+ selectors.searchBoxClearIcon
+ );
+ const input = element.shadowRoot.querySelector(
+ textareaValue
+ ? selectors.searchBoxTextArea
+ : selectors.searchBoxInput
+ );
+
+ expect(input).not.toBeNull();
+ expect(input.value).toEqual(mockLongInputValue);
+
+ clearIcon.click();
+ expect(input.value).toEqual('');
+ const expectedCollapsedInputHeight = textareaValue ? '0px' : '';
+ expect(input.style.height).toEqual(expectedCollapsedInputHeight);
+ });
+ });
});
});
});
diff --git a/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/quanticSearchBoxInput.js b/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/quanticSearchBoxInput.js
index a172df4791a..485dae701cb 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/quanticSearchBoxInput.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/quanticSearchBoxInput.js
@@ -311,10 +311,20 @@ export default class QuanticSearchBoxInput extends LightningElement {
clearInput() {
this.sendInputValueChangeEvent('');
+ this.setDisplayedInputValue('');
+ this.input.removeAttribute('aria-activedescendant');
+ this.collapseTextArea();
this.input.focus();
- if (this.textarea) {
- this.adjustTextAreaHeight();
- }
+ }
+
+ /**
+ * Prevents the blur event from being triggered when clearing the input.
+ * This allows us to clear the input value before collapsing the input.
+ * @param {event} event
+ * @returns {void}
+ */
+ preventBlur(event) {
+ event.preventDefault();
}
handleSuggestionListEvent = (event) => {
diff --git a/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/templates/expandableSearchBoxInput.html b/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/templates/expandableSearchBoxInput.html
index 5cfbc30b0f4..797723c7fd3 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/templates/expandableSearchBoxInput.html
+++ b/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/templates/expandableSearchBoxInput.html
@@ -46,6 +46,7 @@
>