-
+
-
+
\ No newline at end of file
diff --git a/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.js b/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.js
index 1bc1e63e32e..75daa7f1688 100644
--- a/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.js
+++ b/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.js
@@ -8,7 +8,15 @@ export default class ExampleQuanticNotifications extends LightningElement {
pageTitle = 'Quantic Notifications';
pageDescription =
'component is responsible for displaying notifications generated by the Coveo Search API.';
- options = [];
+ options = [
+ {
+ attribute: 'useCase',
+ label: 'Use Case',
+ description:
+ 'Define which use case to test. Possible values are: search, insights',
+ defaultValue: 'search',
+ },
+ ];
get notConfigured() {
return !this.isConfigured;
diff --git a/packages/quantic/force-app/main/default/lwc/quanticFacet/__tests__/quanticFacet.test.js b/packages/quantic/force-app/main/default/lwc/quanticFacet/__tests__/quanticFacet.test.js
new file mode 100644
index 00000000000..04ed94b5425
--- /dev/null
+++ b/packages/quantic/force-app/main/default/lwc/quanticFacet/__tests__/quanticFacet.test.js
@@ -0,0 +1,102 @@
+/* eslint-disable no-import-assign */
+// @ts-ignore
+import {createElement} from 'lwc';
+import QuanticFacet from 'c/quanticFacet';
+import * as mockHeadlessLoader from 'c/quanticHeadlessLoader';
+
+jest.mock('c/quanticHeadlessLoader');
+
+function createTestComponent(options = {}) {
+ prepareHeadlessState();
+
+ const element = createElement('c-quantic-facet', {
+ is: QuanticFacet,
+ });
+ for (const [key, value] of Object.entries(options)) {
+ element[key] = value;
+ }
+
+ document.body.appendChild(element);
+ return element;
+}
+
+const functionsMocks = {
+ buildFacet: jest.fn(() => ({
+ subscribe: jest.fn((callback) => callback()),
+ state: {
+ values: [],
+ },
+ })),
+ buildSearchStatus: jest.fn(() => ({
+ subscribe: jest.fn((callback) => callback()),
+ state: {},
+ })),
+};
+
+function prepareHeadlessState() {
+ // @ts-ignore
+ mockHeadlessLoader.getHeadlessBundle = () => {
+ return {
+ buildFacet: functionsMocks.buildFacet,
+ buildSearchStatus: functionsMocks.buildSearchStatus,
+ };
+ };
+}
+
+// Helper function to wait until the microtask queue is empty.
+function flushPromises() {
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
+ return new Promise((resolve) => setTimeout(resolve, 0));
+}
+
+const exampleEngine = {
+ id: 'dummy engine',
+};
+let isInitialized = false;
+
+function mockSuccessfulHeadlessInitialization() {
+ // @ts-ignore
+ mockHeadlessLoader.initializeWithHeadless = (element, _, initialize) => {
+ if (element instanceof QuanticFacet && !isInitialized) {
+ isInitialized = true;
+ initialize(exampleEngine);
+ }
+ };
+}
+
+function cleanup() {
+ // The jsdom instance is shared across test cases in a single file so reset the DOM
+ while (document.body.firstChild) {
+ document.body.removeChild(document.body.firstChild);
+ }
+ jest.clearAllMocks();
+ isInitialized = false;
+}
+
+describe('c-quantic-facet', () => {
+ beforeAll(() => {
+ mockSuccessfulHeadlessInitialization();
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ describe('controller initialization', () => {
+ it('should initialize the controller with the correct customSort value', async () => {
+ const exampleCustomSortValues = ['test'];
+ createTestComponent({customSort: exampleCustomSortValues});
+ await flushPromises();
+
+ expect(functionsMocks.buildFacet).toHaveBeenCalledTimes(1);
+ expect(functionsMocks.buildFacet).toHaveBeenCalledWith(
+ exampleEngine,
+ expect.objectContaining({
+ options: expect.objectContaining({
+ customSort: exampleCustomSortValues,
+ }),
+ })
+ );
+ });
+ });
+});
diff --git a/packages/quantic/force-app/main/default/lwc/quanticFacet/quanticFacet.js b/packages/quantic/force-app/main/default/lwc/quanticFacet/quanticFacet.js
index 7994301b74d..2443cb6dbca 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticFacet/quanticFacet.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticFacet/quanticFacet.js
@@ -129,6 +129,16 @@ export default class QuanticFacet extends LightningElement {
* @defaultValue `1000`
*/
@api injectionDepth = 1000;
+ /**
+ * Identifies the facet values that must appear at the top, in order.
+ * This parameter can be used in conjunction with the `sortCriteria` parameter.
+ * Facet values not part of the `customSort` list will be sorted according to the `sortCriteria`.
+ * The maximum amount of custom sort values is 25.
+ * The default value is `undefined`, and the facet values will be sorted using only the `sortCriteria`.
+ * @api
+ * @type {String[]}
+ */
+ @api customSort;
/**
* Whether the facet is collapsed.
* @api
@@ -154,6 +164,7 @@ export default class QuanticFacet extends LightningElement {
'displayValuesAs',
'noFilterFacetCount',
'injectionDepth',
+ 'customSort',
];
/** @type {FacetState} */
@@ -249,6 +260,9 @@ export default class QuanticFacet extends LightningElement {
facetId: this.facetId ?? this.field,
filterFacetCount: !this.noFilterFacetCount,
injectionDepth: Number(this.injectionDepth),
+ customSort: Array.isArray(this.customSort)
+ ? [...this.customSort]
+ : undefined,
};
this.facet = this.headless.buildFacet(engine, {options});
this.unsubscribe = this.facet.subscribe(() => this.updateState());
diff --git a/packages/quantic/force-app/main/default/lwc/quanticNotifications/__tests__/quanticNotifications.test.js b/packages/quantic/force-app/main/default/lwc/quanticNotifications/__tests__/quanticNotifications.test.js
new file mode 100644
index 00000000000..bd8e14e7d3b
--- /dev/null
+++ b/packages/quantic/force-app/main/default/lwc/quanticNotifications/__tests__/quanticNotifications.test.js
@@ -0,0 +1,227 @@
+/* eslint-disable no-import-assign */
+// @ts-ignore
+import QuanticNotifications from 'c/quanticNotifications';
+// @ts-ignore
+import {createElement} from 'lwc';
+import * as mockHeadlessLoader from 'c/quanticHeadlessLoader';
+import {AriaLiveRegion} from 'c/quanticUtils';
+
+jest.mock('c/quanticHeadlessLoader');
+jest.mock('c/quanticUtils');
+
+const exampleNotifications = ['notification1', 'notification2'];
+
+let notificationsState = {
+ notifications: exampleNotifications,
+};
+let isInitialized = false;
+
+const exampleEngine = {
+ id: 'mock engine',
+};
+
+const functionsMocks = {
+ buildNotifyTrigger: jest.fn(() => ({
+ state: notificationsState,
+ subscribe: functionsMocks.subscribe,
+ })),
+ dispatchMessage: jest.fn(() => {}),
+ subscribe: jest.fn((cb) => {
+ cb();
+ return functionsMocks.unsubscribe;
+ }),
+ unsubscribe: jest.fn(() => {}),
+};
+
+// @ts-ignore
+AriaLiveRegion.mockImplementation(() => {
+ return {
+ dispatchMessage: functionsMocks.dispatchMessage,
+ };
+});
+
+const selectors = {
+ notifications: '[data-test="notification"]',
+ initializationError: 'c-quantic-component-error',
+};
+
+const defaultOptions = {
+ engineId: 'exampleEngineId',
+};
+
+function createTestComponent(options = defaultOptions) {
+ prepareHeadlessState();
+
+ const element = createElement('c-quantic-notifications', {
+ is: QuanticNotifications,
+ });
+ for (const [key, value] of Object.entries(options)) {
+ element[key] = value;
+ }
+
+ document.body.appendChild(element);
+ return element;
+}
+
+function prepareHeadlessState() {
+ // @ts-ignore
+ mockHeadlessLoader.getHeadlessBundle = () => {
+ return {
+ buildNotifyTrigger: functionsMocks.buildNotifyTrigger,
+ };
+ };
+}
+
+// Helper function to wait until the microtask queue is empty.
+function flushPromises() {
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
+ return new Promise((resolve) => setTimeout(resolve, 0));
+}
+
+function mockSuccessfulHeadlessInitialization() {
+ // @ts-ignore
+ mockHeadlessLoader.initializeWithHeadless = (element, _, initialize) => {
+ if (element instanceof QuanticNotifications && !isInitialized) {
+ isInitialized = true;
+ initialize(exampleEngine);
+ }
+ };
+}
+
+function mockErroneousHeadlessInitialization() {
+ // @ts-ignore
+ mockHeadlessLoader.initializeWithHeadless = (element) => {
+ if (element instanceof QuanticNotifications) {
+ element.setInitializationError();
+ }
+ };
+}
+
+function cleanup() {
+ // The jsdom instance is shared across test cases in a single file so reset the DOM
+ while (document.body.firstChild) {
+ document.body.removeChild(document.body.firstChild);
+ }
+ jest.clearAllMocks();
+ isInitialized = false;
+}
+
+describe('c-quantic-notifications', () => {
+ beforeAll(() => {
+ mockSuccessfulHeadlessInitialization();
+ });
+
+ afterEach(() => {
+ cleanup();
+ notificationsState = {
+ notifications: exampleNotifications,
+ };
+ });
+
+ describe('when an error occurs during initialization', () => {
+ beforeEach(() => {
+ mockErroneousHeadlessInitialization();
+ });
+
+ afterAll(() => {
+ mockSuccessfulHeadlessInitialization();
+ });
+
+ it('should display the initialization error component', async () => {
+ const element = createTestComponent();
+ await flushPromises();
+
+ const initializationError = element.shadowRoot.querySelector(
+ selectors.initializationError
+ );
+
+ const notification = element.shadowRoot.querySelector(
+ selectors.notifications
+ );
+
+ expect(initializationError).not.toBeNull();
+ expect(notification).toBeNull();
+ });
+ });
+
+ describe('component initialization', () => {
+ it('should build the controller with the proper paramters', async () => {
+ createTestComponent();
+ await flushPromises();
+
+ expect(functionsMocks.buildNotifyTrigger).toHaveBeenCalledTimes(1);
+ expect(functionsMocks.buildNotifyTrigger).toHaveBeenCalledWith(
+ exampleEngine
+ );
+ });
+
+ it('should subscribe to the headless state changes', async () => {
+ createTestComponent();
+ await flushPromises();
+
+ expect(functionsMocks.subscribe).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call AriaLiveRegion with the right parameters', async () => {
+ await createTestComponent();
+ await flushPromises();
+
+ expect(AriaLiveRegion).toHaveBeenCalledTimes(1);
+ expect(AriaLiveRegion).toHaveBeenCalledWith(
+ 'notifications',
+ expect.anything()
+ );
+ });
+ });
+
+ describe('when the component is initialized successfully', () => {
+ describe('when some notifications are present in the state', () => {
+ it('should render the notifications component', async () => {
+ const element = createTestComponent();
+ await flushPromises();
+
+ const notifications = element.shadowRoot.querySelectorAll(
+ selectors.notifications
+ );
+
+ expect(notifications).not.toBeNull();
+ expect(notifications.length).toBe(exampleNotifications.length);
+ notifications.forEach((notification, index) => {
+ expect(notification.textContent).toEqual(exampleNotifications[index]);
+ });
+ });
+
+ it('should call dispatchMessage with the correct message', async () => {
+ await createTestComponent();
+ await flushPromises();
+
+ const expectedMessage =
+ ' Notification 1: notification1 Notification 2: notification2';
+
+ expect(functionsMocks.dispatchMessage).toHaveBeenCalledTimes(1);
+ expect(functionsMocks.dispatchMessage).toHaveBeenCalledWith(
+ expectedMessage
+ );
+ });
+ });
+
+ describe('when no notifications are present in the state', () => {
+ beforeEach(() => {
+ notificationsState = {
+ notifications: [],
+ };
+ });
+
+ it('should not render the notifications component', async () => {
+ const element = createTestComponent();
+ await flushPromises();
+
+ const notifications = element.shadowRoot.querySelectorAll(
+ selectors.notifications
+ );
+
+ expect(notifications.length).toEqual(0);
+ });
+ });
+ });
+});
diff --git a/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.html b/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.html
index 7a8742ef624..6b1621ec078 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.html
+++ b/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.html
@@ -5,7 +5,7 @@
-
+
diff --git a/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.js b/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.js
index d2b5329f6ce..d9f8302e01c 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.js
@@ -13,6 +13,7 @@ import {LightningElement, api} from 'lwc';
/**
* The `QuanticNotifications` component is responsible for displaying notifications generated by the Coveo Search API (see [Trigger](https://docs.coveo.com/en/1458)).
* @category Search
+ * @category Insight Panel
* @example
*
*/
@@ -47,8 +48,8 @@ export default class QuanticNotifications extends LightningElement {
initialize = (engine) => {
this.headless = getHeadlessBundle(this.engineId);
this.notifyTrigger = this.headless.buildNotifyTrigger(engine);
- this.unsubscribe = this.notifyTrigger.subscribe(() => this.updateState());
this.ariaLiveNotificationsRegion = AriaLiveRegion('notifications', this);
+ this.unsubscribe = this.notifyTrigger.subscribe(() => this.updateState());
};
disconnectedCallback() {
diff --git a/packages/quantic/force-app/main/default/lwc/quanticRefineModalContent/templates/disabledDynamicNavigation.html b/packages/quantic/force-app/main/default/lwc/quanticRefineModalContent/templates/disabledDynamicNavigation.html
index af19c4c8f75..944ad407846 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticRefineModalContent/templates/disabledDynamicNavigation.html
+++ b/packages/quantic/force-app/main/default/lwc/quanticRefineModalContent/templates/disabledDynamicNavigation.html
@@ -120,6 +120,7 @@
no-filter-facet-count={facet.noFilterFacetCount}
injection-depth={facet.injectionDepth}
key={facet.field}
+ custom-sort={facet.customSort}
is-collapsed
>
diff --git a/packages/quantic/force-app/main/default/lwc/quanticRefineModalContent/templates/dynamicNavigation.html b/packages/quantic/force-app/main/default/lwc/quanticRefineModalContent/templates/dynamicNavigation.html
index 058def95d12..0e0f37c4601 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticRefineModalContent/templates/dynamicNavigation.html
+++ b/packages/quantic/force-app/main/default/lwc/quanticRefineModalContent/templates/dynamicNavigation.html
@@ -121,6 +121,7 @@
no-filter-facet-count={facet.noFilterFacetCount}
injection-depth={facet.injectionDepth}
key={facet.field}
+ custom-sort={facet.customSort}
is-collapsed
>
diff --git a/packages/quantic/force-app/main/default/lwc/quanticRefineToggle/quanticRefineToggle.css b/packages/quantic/force-app/main/default/lwc/quanticRefineToggle/quanticRefineToggle.css
index ade5acd008e..ecd4ae08609 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticRefineToggle/quanticRefineToggle.css
+++ b/packages/quantic/force-app/main/default/lwc/quanticRefineToggle/quanticRefineToggle.css
@@ -21,6 +21,7 @@
width: 1.25rem;
height: 1.25rem;
font-weight: bold;
+ z-index: 1;
}
.refine-button {
diff --git a/packages/quantic/force-app/main/default/lwc/quanticSearchBox/__tests__/quanticSearchBox.test.js b/packages/quantic/force-app/main/default/lwc/quanticSearchBox/__tests__/quanticSearchBox.test.js
index 9cf2bc7bb9c..14925e30a05 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticSearchBox/__tests__/quanticSearchBox.test.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticSearchBox/__tests__/quanticSearchBox.test.js
@@ -1,14 +1,92 @@
+/* eslint-disable no-import-assign */
import QuanticSearchBox from 'c/quanticSearchBox';
// @ts-ignore
import {createElement} from 'lwc';
+import * as mockHeadlessLoader from 'c/quanticHeadlessLoader';
-describe('c-quantic-search-box', () => {
- function cleanup() {
- // The jsdom instance is shared across test cases in a single file so reset the DOM
- while (document.body.firstChild) {
- document.body.removeChild(document.body.firstChild);
+jest.mock('c/quanticHeadlessLoader');
+
+let isInitialized = false;
+
+const exampleEngine = {
+ id: 'dummy engine',
+};
+
+const functionsMocks = {
+ buildSearchBox: jest.fn(() => ({
+ state: {},
+ subscribe: functionsMocks.subscribe,
+ })),
+ loadQuerySuggestActions: jest.fn(() => {}),
+ subscribe: jest.fn((cb) => {
+ cb();
+ return functionsMocks.unsubscribe;
+ }),
+ unsubscribe: jest.fn(() => {}),
+};
+
+const defaultOptions = {
+ engineId: exampleEngine.id,
+ placeholder: null,
+ withoutSubmitButton: false,
+ numberOfSuggestions: 7,
+ textarea: false,
+ disableRecentQueries: false,
+ keepFiltersOnSearch: false,
+};
+
+function createTestComponent(options = defaultOptions) {
+ prepareHeadlessState();
+
+ const element = createElement('c-quantic-search-box', {
+ is: QuanticSearchBox,
+ });
+ for (const [key, value] of Object.entries(options)) {
+ element[key] = value;
+ }
+ document.body.appendChild(element);
+ return element;
+}
+
+function prepareHeadlessState() {
+ // @ts-ignore
+ mockHeadlessLoader.getHeadlessBundle = () => {
+ return {
+ buildSearchBox: functionsMocks.buildSearchBox,
+ loadQuerySuggestActions: functionsMocks.loadQuerySuggestActions,
+ };
+ };
+}
+
+// Helper function to wait until the microtask queue is empty.
+function flushPromises() {
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
+ return new Promise((resolve) => setTimeout(resolve, 0));
+}
+
+function mockSuccessfulHeadlessInitialization() {
+ // @ts-ignore
+ mockHeadlessLoader.initializeWithHeadless = (element, _, initialize) => {
+ if (element instanceof QuanticSearchBox && !isInitialized) {
+ isInitialized = true;
+ initialize(exampleEngine);
}
+ };
+}
+
+function cleanup() {
+ // The jsdom instance is shared across test cases in a single file so reset the DOM
+ while (document.body.firstChild) {
+ document.body.removeChild(document.body.firstChild);
}
+ jest.clearAllMocks();
+ isInitialized = false;
+}
+
+describe('c-quantic-search-box', () => {
+ beforeAll(() => {
+ mockSuccessfulHeadlessInitialization();
+ });
afterEach(() => {
cleanup();
@@ -19,4 +97,46 @@ describe('c-quantic-search-box', () => {
createElement('c-quantic-search-box', {is: QuanticSearchBox})
).not.toThrow();
});
+
+ describe('controller initialization', () => {
+ it('should subscribe to the headless state changes', async () => {
+ createTestComponent();
+ await flushPromises();
+
+ expect(functionsMocks.subscribe).toHaveBeenCalledTimes(1);
+ });
+
+ describe('when keepFiltersOnSearch is false (default)', () => {
+ it('should properly initialize the controller with clear filters enabled', async () => {
+ createTestComponent();
+ await flushPromises();
+
+ expect(functionsMocks.buildSearchBox).toHaveBeenCalledTimes(1);
+ expect(functionsMocks.buildSearchBox).toHaveBeenCalledWith(
+ exampleEngine,
+ expect.objectContaining({
+ options: expect.objectContaining({clearFilters: true}),
+ })
+ );
+ });
+ });
+
+ describe('when keepFiltersOnSearch is true', () => {
+ it('should properly initialize the controller with clear filters disabled', async () => {
+ createTestComponent({
+ ...defaultOptions,
+ keepFiltersOnSearch: true,
+ });
+ await flushPromises();
+
+ expect(functionsMocks.buildSearchBox).toHaveBeenCalledTimes(1);
+ expect(functionsMocks.buildSearchBox).toHaveBeenCalledWith(
+ exampleEngine,
+ expect.objectContaining({
+ options: expect.objectContaining({clearFilters: false}),
+ })
+ );
+ });
+ });
+ });
});
diff --git a/packages/quantic/force-app/main/default/lwc/quanticSearchBox/quanticSearchBox.js b/packages/quantic/force-app/main/default/lwc/quanticSearchBox/quanticSearchBox.js
index 6287c7970c8..94733d28516 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticSearchBox/quanticSearchBox.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticSearchBox/quanticSearchBox.js
@@ -66,6 +66,13 @@ export default class QuanticSearchBox extends LightningElement {
* @defaultValue false
*/
@api disableRecentQueries = false;
+ /**
+ * Whether to keep all active query filters when the end user submits a new query from the search box.
+ * @api
+ * @type {boolean}
+ * @defaultValue false
+ */
+ @api keepFiltersOnSearch = false;
/** @type {SearchBoxState} */
@track state;
@@ -100,6 +107,7 @@ export default class QuanticSearchBox extends LightningElement {
close: '',
},
},
+ clearFilters: !this.keepFiltersOnSearch,
},
});
diff --git a/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/templates/expandableSearchBoxInput.css b/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/templates/expandableSearchBoxInput.css
index bad3636a845..8b1297467e9 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/templates/expandableSearchBoxInput.css
+++ b/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/templates/expandableSearchBoxInput.css
@@ -37,6 +37,7 @@ textarea.searchbox__input {
border: none;
box-shadow: none;
overflow-x: clip;
+ overflow-y: hidden;
margin-right: 0.8rem;
}
diff --git a/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/__tests__/quanticStandaloneSearchBox.test.js b/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/__tests__/quanticStandaloneSearchBox.test.js
new file mode 100644
index 00000000000..a41ba41be23
--- /dev/null
+++ b/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/__tests__/quanticStandaloneSearchBox.test.js
@@ -0,0 +1,190 @@
+/* eslint-disable no-import-assign */
+import QuanticStandaloneSearchBox from 'c/quanticStandaloneSearchBox';
+// @ts-ignore
+import {createElement} from 'lwc';
+import * as mockHeadlessLoader from 'c/quanticHeadlessLoader';
+import {CurrentPageReference} from 'lightning/navigation';
+import getHeadlessConfiguration from '@salesforce/apex/HeadlessController.getHeadlessConfiguration';
+
+const nonStandaloneURL = 'https://www.example.com/global-search/%40uri';
+const defaultHeadlessConfiguration = JSON.stringify({
+ organization: 'testOrgId',
+ accessToken: 'testAccessToken',
+});
+
+jest.mock('c/quanticHeadlessLoader');
+
+jest.mock(
+ '@salesforce/apex/HeadlessController.getHeadlessConfiguration',
+ () => ({
+ default: jest.fn(),
+ }),
+ {virtual: true}
+);
+
+mockHeadlessLoader.loadDependencies = () =>
+ new Promise((resolve) => {
+ resolve();
+ });
+
+let isInitialized = false;
+
+const exampleEngine = {
+ id: 'engineId',
+};
+
+const functionsMocks = {
+ buildStandaloneSearchBox: jest.fn(() => ({
+ state: {},
+ subscribe: functionsMocks.subscribe,
+ })),
+ subscribe: jest.fn((cb) => {
+ cb();
+ return functionsMocks.unsubscribe;
+ }),
+ unsubscribe: jest.fn(() => {}),
+};
+
+const defaultOptions = {
+ engineId: exampleEngine.id,
+ placeholder: null,
+ withoutSubmitButton: false,
+ numberOfSuggestions: 7,
+ textarea: false,
+ disableRecentQueries: false,
+ keepFiltersOnSearch: false,
+ redirectUrl: '/global-search/%40uri',
+};
+
+function createTestComponent(options = defaultOptions) {
+ prepareHeadlessState();
+ const element = createElement('c-quantic-standalone-search-box', {
+ is: QuanticStandaloneSearchBox,
+ });
+ for (const [key, value] of Object.entries(options)) {
+ element[key] = value;
+ }
+ document.body.appendChild(element);
+ return element;
+}
+
+function prepareHeadlessState() {
+ // @ts-ignore
+ global.CoveoHeadless = {
+ buildStandaloneSearchBox: functionsMocks.buildStandaloneSearchBox,
+ };
+}
+
+// Helper function to wait until the microtask queue is empty.
+function flushPromises() {
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
+ return new Promise((resolve) => setTimeout(resolve, 0));
+}
+
+function mockSuccessfulHeadlessInitialization() {
+ // @ts-ignore
+ mockHeadlessLoader.initializeWithHeadless = (element, _, initialize) => {
+ if (element instanceof QuanticStandaloneSearchBox && !isInitialized) {
+ isInitialized = true;
+ initialize(exampleEngine);
+ }
+ };
+}
+
+function cleanup() {
+ // The jsdom instance is shared across test cases in a single file so reset the DOM
+ while (document.body.firstChild) {
+ document.body.removeChild(document.body.firstChild);
+ }
+ jest.clearAllMocks();
+ isInitialized = false;
+}
+
+describe('c-quantic-standalone-search-box', () => {
+ beforeEach(() => {
+ getHeadlessConfiguration.mockResolvedValue(defaultHeadlessConfiguration);
+ mockSuccessfulHeadlessInitialization();
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it('construct itself without throwing', () => {
+ expect(() => createTestComponent()).not.toThrow();
+ });
+
+ describe('controller initialization', () => {
+ it('should subscribe to the headless state changes', async () => {
+ createTestComponent();
+ await flushPromises();
+
+ expect(functionsMocks.subscribe).toHaveBeenCalledTimes(1);
+ });
+
+ describe('when the current page reference changes', () => {
+ beforeAll(() => {
+ // This is needed to mock the window.location.href property to test the keepFiltersOnSearch property in the quanticSearchBox.
+ // https://stackoverflow.com/questions/54021037/how-to-mock-window-location-href-with-jest-vuejs
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: {href: nonStandaloneURL},
+ });
+ });
+
+ it('should properly pass the keepFiltersOnSearch property to the quanticSearchBox', async () => {
+ const element = createTestComponent({
+ ...defaultOptions,
+ keepFiltersOnSearch: false,
+ });
+ // eslint-disable-next-line @lwc/lwc/no-unexpected-wire-adapter-usages
+ CurrentPageReference.emit({url: nonStandaloneURL});
+ await flushPromises();
+
+ const searchBox = element.shadowRoot.querySelector(
+ 'c-quantic-search-box'
+ );
+
+ expect(searchBox).not.toBeNull();
+ expect(searchBox.keepFiltersOnSearch).toEqual(false);
+ });
+ });
+
+ describe('when keepFiltersOnSearch is false (default)', () => {
+ it('should properly initialize the controller with clear filters enabled', async () => {
+ createTestComponent();
+ await flushPromises();
+
+ expect(functionsMocks.buildStandaloneSearchBox).toHaveBeenCalledTimes(
+ 1
+ );
+ expect(functionsMocks.buildStandaloneSearchBox).toHaveBeenCalledWith(
+ exampleEngine,
+ expect.objectContaining({
+ options: expect.objectContaining({clearFilters: true}),
+ })
+ );
+ });
+ });
+
+ describe('when keepFiltersOnSearch is true', () => {
+ it('should properly initialize the controller with clear filters disabled', async () => {
+ createTestComponent({
+ ...defaultOptions,
+ keepFiltersOnSearch: true,
+ });
+ await flushPromises();
+
+ expect(functionsMocks.buildStandaloneSearchBox).toHaveBeenCalledTimes(
+ 1
+ );
+ expect(functionsMocks.buildStandaloneSearchBox).toHaveBeenCalledWith(
+ exampleEngine,
+ expect.objectContaining({
+ options: expect.objectContaining({clearFilters: false}),
+ })
+ );
+ });
+ });
+ });
+});
diff --git a/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/quanticStandaloneSearchBox.js b/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/quanticStandaloneSearchBox.js
index 8d90e3748ba..80254aa7d36 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/quanticStandaloneSearchBox.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/quanticStandaloneSearchBox.js
@@ -55,6 +55,13 @@ export default class QuanticStandaloneSearchBox extends NavigationMixin(
* @defaultValue 5
*/
@api numberOfSuggestions = 5;
+ /**
+ * Whether to keep all active query filters when the end user submits a new query from the standalone search box.
+ * @api
+ * @type {boolean}
+ * @defaultValue false
+ */
+ @api keepFiltersOnSearch = false;
/**
* The url of the search page to redirect to when a query is made.
* The target search page should contain a `QuanticSearchInterface` with the same engine ID as the one specified for this component.
@@ -171,6 +178,7 @@ export default class QuanticStandaloneSearchBox extends NavigationMixin(
close: '',
},
},
+ clearFilters: !this.keepFiltersOnSearch,
redirectionUrl: 'http://placeholder.com',
},
});
diff --git a/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/templates/standaloneSearchBox.html b/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/templates/standaloneSearchBox.html
index f1671df53a8..ae1180513fa 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/templates/standaloneSearchBox.html
+++ b/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/templates/standaloneSearchBox.html
@@ -24,6 +24,7 @@
without-submit-button={withoutSubmitButton}
number-of-suggestions={numberOfSuggestions}
textarea={textarea}
+ keep-filters-on-search={keepFiltersOnSearch}
>
diff --git a/packages/quantic/force-app/main/default/lwc/quanticUserAction/__tests__/quanticUserAction.test.js b/packages/quantic/force-app/main/default/lwc/quanticUserAction/__tests__/quanticUserAction.test.js
index 491323c28ff..a3e87a080bc 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticUserAction/__tests__/quanticUserAction.test.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticUserAction/__tests__/quanticUserAction.test.js
@@ -49,6 +49,7 @@ const expectations = {
viewAction: {
iconName: 'utility:preview',
iconClass: 'user-action__view-action-icon',
+ titleClass: 'user-action__title',
},
};
@@ -364,15 +365,49 @@ describe('c-quantic-user-action', () => {
expect(icon.classList.contains(iconClass)).toBe(true);
});
- it('should properly display the action title', async () => {
- const element = createTestComponent({action: exampleAction});
- await flushPromises();
+ describe('when the contentIdKey of the action is clickable', () => {
+ it('should display the action title as a link', async () => {
+ const element = createTestComponent({
+ action: {
+ ...exampleAction,
+ document: {
+ ...exampleAction.document,
+ contentIdKey: '@clickableuri',
+ },
+ },
+ });
+ await flushPromises();
- const link = element.shadowRoot.querySelector(selectors.link);
+ const link = element.shadowRoot.querySelector(selectors.link);
+
+ expect(link).not.toBeNull();
+ expect(link.textContent).toBe(expectedTitle);
+ expect(link.href).toBe(expectedUrl);
+ });
+ });
- expect(link).not.toBeNull();
- expect(link.textContent).toBe(expectedTitle);
- expect(link.href).toBe(expectedUrl);
+ describe('when the contentIdKey of the action is not clickable', () => {
+ it('should display the action title as a text', async () => {
+ const element = createTestComponent({
+ action: {
+ ...exampleAction,
+ document: {
+ ...exampleAction.document,
+ contentIdKey: '@sfid',
+ },
+ },
+ });
+ await flushPromises();
+
+ const title = element.shadowRoot.querySelector(selectors.title);
+ const link = element.shadowRoot.querySelector(selectors.link);
+ const {titleClass} = expectations.viewAction;
+
+ expect(link).toBeNull();
+ expect(title).not.toBeNull();
+ expect(title.textContent).toBe(expectedTitle);
+ expect(title.classList.contains(titleClass)).toBe(true);
+ });
});
it('should properly display the action details', async () => {
diff --git a/packages/quantic/force-app/main/default/lwc/quanticUserAction/quanticUserAction.js b/packages/quantic/force-app/main/default/lwc/quanticUserAction/quanticUserAction.js
index 91a81486da5..1076c4aaeb9 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticUserAction/quanticUserAction.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticUserAction/quanticUserAction.js
@@ -33,6 +33,7 @@ export default class QuanticUserAction extends LightningElement {
ticketCreated,
emptySearch,
};
+ clickableContentIdKeys = ['@clickableuri'];
get iconName() {
return icons[this.action?.actionType];
@@ -74,6 +75,10 @@ export default class QuanticUserAction extends LightningElement {
return this.action?.document?.contentIdValue;
}
+ get contentIdKey() {
+ return this.action?.document?.contentIdKey;
+ }
+
get iconClass() {
switch (this.action?.actionType) {
case 'TICKET_CREATION':
@@ -97,7 +102,11 @@ export default class QuanticUserAction extends LightningElement {
}
render() {
- if (this.action?.actionType === 'VIEW') return viewActionTemplate;
+ const viewEventCanBeDisplayedAsLink = this.clickableContentIdKeys.includes(
+ this.contentIdKey
+ );
+ if (this.action?.actionType === 'VIEW' && viewEventCanBeDisplayedAsLink)
+ return viewActionTemplate;
return actionTemplate;
}
}
diff --git a/packages/quantic/force-app/main/default/lwc/quanticUserActionsToggle/quanticUserActionsToggle.js b/packages/quantic/force-app/main/default/lwc/quanticUserActionsToggle/quanticUserActionsToggle.js
index 953633db3c3..e466c3f0c43 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticUserActionsToggle/quanticUserActionsToggle.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticUserActionsToggle/quanticUserActionsToggle.js
@@ -77,8 +77,8 @@ export default class QuanticUserActionsToggle extends LightningElement {
this.userActions = this.headless.buildUserActions(engine, {
options: {
ticketCreationDate: this.ticketCreationDateTime,
- excludedCustomActions: this.excludedCustomActions?.length
- ? this.excludedCustomActions
+ excludedCustomActions: Array.isArray(this.excludedCustomActions)
+ ? [...this.excludedCustomActions]
: [],
},
});
diff --git a/packages/quantic/jest.config.js b/packages/quantic/jest.config.js
index d41019db77f..79116280322 100644
--- a/packages/quantic/jest.config.js
+++ b/packages/quantic/jest.config.js
@@ -29,6 +29,8 @@ module.exports = {
'/force-app/main/default/lwc/quanticResultActionStyles/quanticResultActionStyles',
'^c/searchBoxStyle$':
'/force-app/main/default/lwc/searchBoxStyle/searchBoxStyle',
+ '^c/quanticFacetStyles$':
+ '/force-app/main/default/lwc/quanticFacetStyles/quanticFacetStyles',
},
modulePathIgnorePatterns: ['.cache'],
// add any custom configurations here
diff --git a/packages/quantic/package.json b/packages/quantic/package.json
index 82d83d510dd..005f987e9f8 100644
--- a/packages/quantic/package.json
+++ b/packages/quantic/package.json
@@ -1,6 +1,6 @@
{
"name": "@coveo/quantic",
- "version": "3.2.1",
+ "version": "3.3.0",
"description": "A Salesforce Lightning Web Component (LWC) library for building modern UIs interfacing with the Coveo platform",
"author": "coveo.com",
"homepage": "https://coveo.com",
@@ -40,18 +40,18 @@
"promote:sfdx:ci": "npm run publish:sfdx -- --promote --ci",
"publish:npm": "npm run-script -w=@coveo/release npm-publish",
"publish:bump": "npm run-script -w=@coveo/release bump",
- "promote:npm:latest": "node ../../scripts/deploy/update-npm-tag.mjs latest",
+ "promote:npm:latest": "npm run-script -w=@coveo/release promote-npm-prod",
"preinstall": "node scripts/npm/check-sfdx-project.js",
"postinstall": "node scripts/npm/setup-quantic.js"
},
"dependencies": {
- "@coveo/bueno": "1.0.1",
- "@coveo/headless": "3.4.0",
+ "@coveo/bueno": "1.0.2",
+ "@coveo/headless": "3.5.0",
"dompurify": "3.1.6",
"marked": "12.0.2"
},
"engines": {
- "node": "^20.9.0"
+ "node": "^20.9.0 || ^22.11.0"
},
"devDependencies": {
"@ckeditor/jsdoc-plugins": "39.9.1",
diff --git a/packages/rollup-plugin-replace-with-ast/LICENSE b/packages/rollup-plugin-replace-with-ast/LICENSE
deleted file mode 100644
index 052e59b641a..00000000000
--- a/packages/rollup-plugin-replace-with-ast/LICENSE
+++ /dev/null
@@ -1,201 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright 2024 Coveo Solutions Inc.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
\ No newline at end of file
diff --git a/packages/rollup-plugin-replace-with-ast/package.json b/packages/rollup-plugin-replace-with-ast/package.json
deleted file mode 100644
index bad2213f1fb..00000000000
--- a/packages/rollup-plugin-replace-with-ast/package.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "name": "@coveo/rollup-plugin-replace-with-ast",
- "repository": {
- "type": "git",
- "url": "git+https://github.com/coveo/ui-kit.git",
- "directory": "packages/rollup-plugin-replace-with-ast"
- },
- "private": true,
- "main": "./dist/cjs/index.js",
- "module": "./dist/es/index.js",
- "exports": {
- "types": "./types/index.d.ts",
- "import": "./dist/es/index.js",
- "default": "./dist/cjs/index.js"
- },
- "engines": {
- "node": "^20.9.0"
- },
- "types": "./dist/definitions/index.d.ts",
- "license": "Apache-2.0",
- "version": "1.0.0",
- "files": [
- "dist/",
- "types/"
- ],
- "scripts": {
- "dev": "concurrently \"npm run build:definitions -- -w\" \"npm run build:bundles -- dev\"",
- "build": "nx build",
- "build:bundles": "rollup -c",
- "build:definitions": "tsc -d --emitDeclarationOnly --declarationDir dist/definitions",
- "clean": "rimraf -rf dist/*"
- },
- "peerDependencies": {
- "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
- },
- "peerDependenciesMeta": {
- "rollup": {
- "optional": true
- }
- },
- "devDependencies": {
- "@rollup/plugin-typescript": "11.1.6",
- "@rollup/pluginutils": "5.1.0",
- "acorn": "8.12.1",
- "magic-string": "0.30.11",
- "rollup": "4.0.0-24",
- "typescript": "4.8.3"
- }
-}
diff --git a/packages/rollup-plugin-replace-with-ast/project.json b/packages/rollup-plugin-replace-with-ast/project.json
deleted file mode 100644
index ea5f7373d7a..00000000000
--- a/packages/rollup-plugin-replace-with-ast/project.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "name": "rollup-plugin-replace-with-ast",
- "private": true,
- "$schema": "../../node_modules/nx/schemas/project-schema.json",
- "targets": {
- "cached:build": {
- "executor": "nx:run-commands",
- "options": {
- "commands": ["npm run build:bundles", "npm run build:definitions"],
- "parallel": true,
- "cwd": "packages/rollup-plugin-replace-with-ast"
- }
- },
- "build": {
- "dependsOn": ["cached:build"],
- "executor": "nx:noop"
- }
- }
-}
diff --git a/packages/rollup-plugin-replace-with-ast/rollup.config.mjs b/packages/rollup-plugin-replace-with-ast/rollup.config.mjs
deleted file mode 100644
index eb7b91fe275..00000000000
--- a/packages/rollup-plugin-replace-with-ast/rollup.config.mjs
+++ /dev/null
@@ -1,35 +0,0 @@
-import typescript from '@rollup/plugin-typescript';
-
-export default {
- input: 'src/index.ts',
-
- output: [
- {
- file: 'dist/cjs/index.js',
- format: 'cjs',
- exports: 'named',
- footer: 'module.exports = Object.assign(exports.default, exports);',
- sourcemap: true,
- },
- {
- file: './dist/es/index.js',
- format: 'es',
- sourcemap: true,
- plugins: [emitModulePackageFile()],
- },
- ],
- plugins: [typescript({sourceMap: true})],
-};
-
-export function emitModulePackageFile() {
- return {
- name: 'emit-module-package-file',
- generateBundle() {
- this.emitFile({
- type: 'asset',
- fileName: 'package.json',
- source: `{"type":"module"}`,
- });
- },
- };
-}
diff --git a/packages/rollup-plugin-replace-with-ast/src/index.ts b/packages/rollup-plugin-replace-with-ast/src/index.ts
deleted file mode 100644
index 174add14412..00000000000
--- a/packages/rollup-plugin-replace-with-ast/src/index.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import {createFilter} from '@rollup/pluginutils';
-import {
- parse,
- Node,
- ImportDeclaration,
- ExportNamedDeclaration,
- ExportAllDeclaration,
- Program,
-} from 'acorn';
-import MagicString from 'magic-string';
-
-interface PluginOptions {
- include?: string | string[];
- exclude?: string | string[];
- replacements?: Record;
-}
-
-function replaceWithASTPlugin(options: PluginOptions = {}) {
- const filter = createFilter(options.include, options.exclude);
- const replacements = options.replacements || {};
-
- return {
- name: 'replace-with-ast-plugin',
-
- transform(code: string, id: unknown) {
- if (!filter(id)) {
- return null;
- }
-
- let ast: Program;
- try {
- ast = parse(code, {ecmaVersion: 2020, sourceType: 'module'});
- } catch (error) {
- console.error(`Error parsing ${id}: ${(error as Error).message}`);
- return null;
- }
-
- const magicString = new MagicString(code);
-
- ast.body.forEach((node: Node) => {
- if (
- node.type === 'ImportDeclaration' ||
- node.type === 'ImportDefaultSpecifier' ||
- node.type === 'ImportSpecifier' ||
- node.type === 'ImportNamespaceSpecifier' ||
- node.type === 'ExportSpecifier' ||
- node.type === 'ExportDefaultSpecifier' ||
- node.type === 'ExportNamedDeclaration' ||
- node.type === 'ExportAllDeclaration'
- ) {
- const source = (
- node as
- | ImportDeclaration
- | ExportNamedDeclaration
- | ExportAllDeclaration
- ).source;
- if (
- source &&
- typeof source.value === 'string' &&
- replacements[source.value]
- ) {
- const start = source.start;
- const end = source.end;
- magicString.overwrite(
- start,
- end,
- JSON.stringify(replacements[source.value])
- );
- }
- }
- });
-
- return {
- code: magicString.toString(),
- map: magicString.generateMap({hires: true}),
- };
- },
- };
-}
-
-export default replaceWithASTPlugin;
diff --git a/packages/rollup-plugin-replace-with-ast/tsconfig.json b/packages/rollup-plugin-replace-with-ast/tsconfig.json
deleted file mode 100644
index 1c7b2d4da00..00000000000
--- a/packages/rollup-plugin-replace-with-ast/tsconfig.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "compilerOptions": {
- "allowSyntheticDefaultImports": true,
- "esModuleInterop": true,
- "lib": ["es6"],
- "module": "esnext",
- "moduleResolution": "node",
- "skipLibCheck": true,
- "noEmitOnError": false,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "pretty": true,
- "sourceMap": true,
- "strict": true,
- "target": "es2019"
- },
- "exclude": ["./dist/**", "./test/types/**"]
-}
diff --git a/packages/rollup-plugin-replace-with-ast/types/index.d.ts b/packages/rollup-plugin-replace-with-ast/types/index.d.ts
deleted file mode 100644
index a94814dbf52..00000000000
--- a/packages/rollup-plugin-replace-with-ast/types/index.d.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { Plugin } from 'rollup';
-
-export interface PluginOptions {
- include?: string | string[];
- exclude?: string | string[];
- replacements?: Record;
-}
-
-/**
- * A Rollup plugin for replacing import/export paths using AST.
- */
-export default function replaceWithASTPlugin(options?: PluginOptions): Plugin;
diff --git a/packages/samples/angular/package.json b/packages/samples/angular/package.json
index 6eeee45f6e6..8999f4b2fc7 100644
--- a/packages/samples/angular/package.json
+++ b/packages/samples/angular/package.json
@@ -19,7 +19,7 @@
"@angular/platform-browser": "17.3.12",
"@angular/platform-browser-dynamic": "17.3.12",
"@angular/router": "17.3.12",
- "@coveo/atomic-angular": "3.1.6",
+ "@coveo/atomic-angular": "3.2.0",
"rxjs": "7.8.1",
"tslib": "2.6.3",
"zone.js": "0.14.8"
diff --git a/packages/samples/atomic-next/package.json b/packages/samples/atomic-next/package.json
index dd0d67b5336..45b07f57185 100644
--- a/packages/samples/atomic-next/package.json
+++ b/packages/samples/atomic-next/package.json
@@ -4,9 +4,9 @@
"private": true,
"type": "module",
"dependencies": {
- "@coveo/atomic": "3.4.0",
- "@coveo/atomic-react": "3.1.6",
- "@coveo/headless": "3.4.0",
+ "@coveo/atomic": "3.7.0",
+ "@coveo/atomic-react": "3.2.0",
+ "@coveo/headless": "3.5.0",
"next": "14.2.5",
"react": "18.3.1",
"react-dom": "18.3.1"
diff --git a/packages/samples/atomic-react/package.json b/packages/samples/atomic-react/package.json
index 12edd1a08ff..8647b1b065d 100644
--- a/packages/samples/atomic-react/package.json
+++ b/packages/samples/atomic-react/package.json
@@ -4,9 +4,9 @@
"description": "Samples with atomic-react",
"private": true,
"dependencies": {
- "@coveo/atomic": "3.4.0",
- "@coveo/atomic-react": "3.1.6",
- "@coveo/headless": "3.4.0",
+ "@coveo/atomic": "3.7.0",
+ "@coveo/atomic-react": "3.2.0",
+ "@coveo/headless": "3.5.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
diff --git a/packages/samples/headless-commerce-react/package.json b/packages/samples/headless-commerce-react/package.json
index f663b12e7e5..c862c93458c 100644
--- a/packages/samples/headless-commerce-react/package.json
+++ b/packages/samples/headless-commerce-react/package.json
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"dependencies": {
- "@coveo/headless": "3.4.0",
+ "@coveo/headless": "3.5.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
diff --git a/packages/samples/headless-commerce-react/src/components/breadcrumb-manager/breadcrumb-manager.tsx b/packages/samples/headless-commerce-react/src/components/breadcrumb-manager/breadcrumb-manager.tsx
index 72cc88acbaf..5826c94404e 100644
--- a/packages/samples/headless-commerce-react/src/components/breadcrumb-manager/breadcrumb-manager.tsx
+++ b/packages/samples/headless-commerce-react/src/components/breadcrumb-manager/breadcrumb-manager.tsx
@@ -4,6 +4,7 @@ import {
NumericFacetValue,
DateFacetValue,
BreadcrumbManager as HeadlessBreadcrumbManager,
+ LocationFacetValue,
} from '@coveo/headless/commerce';
import {useEffect, useState} from 'react';
@@ -29,7 +30,8 @@ export default function BreadcrumbManager(props: BreadcrumbManagerProps) {
| CategoryFacetValue
| RegularFacetValue
| NumericFacetValue
- | DateFacetValue,
+ | DateFacetValue
+ | LocationFacetValue,
type: string
) => {
switch (type) {
@@ -50,6 +52,7 @@ export default function BreadcrumbManager(props: BreadcrumbManagerProps) {
(value as DateFacetValue).end
);
default:
+ // TODO COMHUB-292 add location facet example
return null;
}
};
diff --git a/packages/samples/headless-react/package.json b/packages/samples/headless-react/package.json
index cd47471f5f7..f7938ea35ea 100644
--- a/packages/samples/headless-react/package.json
+++ b/packages/samples/headless-react/package.json
@@ -5,11 +5,11 @@
"private": true,
"type": "module",
"engines": {
- "node": "^20.9.0"
+ "node": "^20.9.0 || ^22.11.0"
},
"dependencies": {
"@coveo/auth": "2.0.1",
- "@coveo/headless": "3.4.0",
+ "@coveo/headless": "3.5.0",
"@testing-library/jest-dom": "6.4.8",
"@testing-library/react": "14.3.1",
"@testing-library/user-event": "14.5.2",
@@ -21,7 +21,7 @@
"@types/react-router-dom": "5.3.3",
"dayjs": "1.11.12",
"escape-html": "1.0.3",
- "express": "4.19.2",
+ "express": "4.20.0",
"filesize": "10.1.4",
"react": "18.3.1",
"react-dom": "18.3.1",
diff --git a/packages/samples/headless-ssr-commerce/app/_components/breadcrumb-manager.tsx b/packages/samples/headless-ssr-commerce/app/_components/breadcrumb-manager.tsx
new file mode 100644
index 00000000000..6e4eef604ae
--- /dev/null
+++ b/packages/samples/headless-ssr-commerce/app/_components/breadcrumb-manager.tsx
@@ -0,0 +1,84 @@
+import {
+ BreadcrumbManagerState,
+ NumericFacetValue,
+ DateFacetValue,
+ CategoryFacetValue,
+ BreadcrumbManager as HeadlessBreadcrumbManager,
+ RegularFacetValue,
+ LocationFacetValue,
+} from '@coveo/headless/ssr-commerce';
+import {useEffect, useState} from 'react';
+
+interface BreadcrumbManagerProps {
+ staticState: BreadcrumbManagerState;
+ controller?: HeadlessBreadcrumbManager;
+}
+
+export default function BreadcrumbManager(props: BreadcrumbManagerProps) {
+ const {staticState, controller} = props;
+
+ const [state, setState] = useState(staticState);
+
+ useEffect(() => {
+ controller?.subscribe(() => setState(controller.state));
+ }, [controller]);
+
+ const renderBreadcrumbValue = (
+ value:
+ | CategoryFacetValue
+ | RegularFacetValue
+ | NumericFacetValue
+ | DateFacetValue
+ | LocationFacetValue,
+ type: string
+ ) => {
+ switch (type) {
+ case 'hierarchical':
+ return (value as CategoryFacetValue).path.join(' > ');
+ case 'regular':
+ return (value as RegularFacetValue).value;
+ case 'numericalRange':
+ return (
+ (value as NumericFacetValue).start +
+ ' - ' +
+ (value as NumericFacetValue).end
+ );
+ case 'dateRange':
+ return (
+ (value as DateFacetValue).start +
+ ' - ' +
+ (value as DateFacetValue).end
+ );
+ default:
+ // TODO COMHUB-291 support location breadcrumb
+ return null;
+ }
+ };
+
+ return (
+