Skip to content

Commit

Permalink
Merge branch 'master' into KIT-3416
Browse files Browse the repository at this point in the history
  • Loading branch information
jpmarceau authored Jul 23, 2024
2 parents 5a2ded8 + 0eddd7f commit 44c1b1d
Show file tree
Hide file tree
Showing 50 changed files with 2,283 additions and 439 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {tabSetReducer as tabSet} from '../../../features/tab-set/tab-set-slice';
import {
buildMockSearchEngine,
MockedSearchEngine,
} from '../../../test/mock-engine-v2';
import {createMockState} from '../../../test/mock-state';
import {buildCoreTabManager} from './headless-core-tab-manager';

jest.mock('../../../features/tab-set/tab-set-actions');

describe('Core Tab Manager', () => {
let engine: MockedSearchEngine;

function initTabManager() {
buildCoreTabManager(engine);
}

beforeEach(() => {
engine = buildMockSearchEngine(createMockState());
initTabManager();
});

it('returns the current active tab', () => {
expect(engine.state.tabSet).toEqual({});

const tabId = '1';
const expectedTabSet = {
[tabId]: {
id: tabId,
expression: 'test',
isActive: true,
},
};

engine.state.tabSet = expectedTabSet;

expect(engine.state.tabSet).toEqual(expectedTabSet);
});

it('adds the correct reducers to engine', () => {
expect(engine.addReducers).toHaveBeenCalledWith({
tabSet,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {createSelector} from '@reduxjs/toolkit';
import {CoreEngine} from '../../../app/engine';
import {tabSetReducer as tabSet} from '../../../features/tab-set/tab-set-slice';
import {TabSection} from '../../../state/state-sections';
import {loadReducerError} from '../../../utils/errors';
import {
buildController,
Controller,
} from '../../controller/headless-controller';

export interface TabManager extends Controller {
/**
* The state of the `TabManager` controller.
*/
state: TabManagerState;
}

/**
* A scoped and simplified part of the headless state that is relevant to the `TabManager` controller.
*/
export interface TabManagerState {
activeTab: string;
}

/**
* @internal
*/
export function buildCoreTabManager(engine: CoreEngine): TabManager {
if (!loadTabReducers(engine)) {
throw loadReducerError;
}

const controller = buildController(engine);

const currentTab = createSelector(
(state: typeof engine.state) => state.tabSet,
(state) => {
const activeTab = Object.values(state).find((tab) => tab.isActive);
return activeTab?.id ?? '';
}
);

return {
...controller,

get state() {
return {activeTab: currentTab(engine.state)};
},
};
}

function loadTabReducers(engine: CoreEngine): engine is CoreEngine<TabSection> {
engine.addReducers({tabSet});
return true;
}
17 changes: 15 additions & 2 deletions packages/headless/src/controllers/core/tab/headless-core-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {BooleanValue, Schema} from '@coveo/bueno';
import {configuration} from '../../../app/common-reducers';
import {CoreEngine} from '../../../app/engine';
import {getConfigurationInitialState} from '../../../features/configuration/configuration-state';
import {prepareForSearchWithQuery} from '../../../features/search/search-actions';
import {
registerTab,
updateActiveTab,
Expand Down Expand Up @@ -30,6 +31,11 @@ export interface TabOptions {
*/
expression: string;

/**
* Whether to clear the state when the active tab changes.
*/
clearFiltersOnTabChange?: boolean;

/**
* A unique identifier for the tab. The value will be used as the originLevel2 when the tab is active.
*/
Expand All @@ -47,6 +53,7 @@ export interface TabInitialState {
const optionsSchema = new Schema<Required<TabOptions>>({
expression: requiredEmptyAllowedString,
id: requiredNonEmptyString,
clearFiltersOnTabChange: new BooleanValue(),
});

const initialStateSchema = new Schema({
Expand All @@ -72,7 +79,6 @@ export interface Tab extends Controller {
* Activates the tab.
*/
select(): void;

/**
* The state of the `Tab` controller.
*/
Expand Down Expand Up @@ -105,7 +111,6 @@ export function buildCoreTab(engine: CoreEngine, props: TabProps): Tab {

const controller = buildController(engine);
const {dispatch} = engine;

validateOptions(engine, optionsSchema, props.options, 'buildTab');
const initialState = validateInitialState(
engine,
Expand All @@ -126,6 +131,14 @@ export function buildCoreTab(engine: CoreEngine, props: TabProps): Tab {
...controller,

select() {
if (props.options.clearFiltersOnTabChange) {
dispatch(
prepareForSearchWithQuery({
q: '',
clearFilters: true,
})
);
}
dispatch(updateActiveTab(id));
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
import {
logClearRecentQueries,
logRecentQueryClick,
recentQueryClick,
} from '../../features/recent-queries/recent-queries-analytics-actions';
import {recentQueriesReducer as recentQueries} from '../../features/recent-queries/recent-queries-slice';
import {
Expand Down Expand Up @@ -218,7 +217,6 @@ export function buildRecentQueriesList(
dispatch(
executeSearch({
legacy: logRecentQueryClick(),
next: recentQueryClick(),
})
);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {SearchEngine} from '../../app/search-engine/search-engine';
import {ControllerDefinitionWithoutProps} from '../../app/ssr-engine/types/common';
import {TabManager, buildTabManager} from './headless-tab-manager';

export * from './headless-tab-manager';

export interface TabManagerDefinition
extends ControllerDefinitionWithoutProps<SearchEngine, TabManager> {}

/**
* Defines a `TabManager` controller instance.
*
* @returns The `TabManager` controller definition.
* */
export function defineTabManager(): TabManagerDefinition {
return {
build: (engine) => buildTabManager(engine),
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
MockedSearchEngine,
buildMockSearchEngine,
} from '../../test/mock-engine-v2';
import {createMockState} from '../../test/mock-state';
import {buildTabManager, TabManager} from './headless-tab-manager';

jest.mock('../../features/search/search-actions');

describe('Tab', () => {
let engine: MockedSearchEngine;
let tabManager: TabManager;

function initTabManager() {
tabManager = buildTabManager(engine);
}

beforeEach(() => {
engine = buildMockSearchEngine(createMockState());

initTabManager();
});

it('initializes', () => {
expect(tabManager).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {SearchEngine} from '../../app/search-engine/search-engine';
import {
TabManager,
TabManagerState,
buildCoreTabManager,
} from '../core/tab-manager/headless-core-tab-manager';

export type {TabManager, TabManagerState};

/**
* Creates a `Tab Manager` controller instance.
*
* @param engine - The headless engine.
* @param props - The configurable `Tab Manager` properties.
* @returns A `Tab Manager` controller instance.
*/
export function buildTabManager(engine: SearchEngine): TabManager {
const tabManager = buildCoreTabManager(engine);

return {
...tabManager,

get state() {
return tabManager.state;
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ export enum SearchPageEvents {
/**
* Identifies the search event that gets logged when `enableAutoCorrection: true` and the query is automatically corrected.
*/
didyoumeanAutomatic = 'didyoumeanAutomatic',
didYouMeanAutomatic = 'didYouMeanAutomatic',
/**
* Identifies the search event that gets logged when the query suggestion with the corrected term is selected and successfully updates the query.
*/
didyoumeanClick = 'didyoumeanClick',
didYouMeanClick = 'didYouMeanClick',
/**
* Identifies the search event that gets logged when a sorting method is selected.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ export const logDidYouMeanAutomatic = (): LegacySearchAction =>
);

export const didYouMeanClick = (): SearchAction => ({
actionCause: SearchPageEvents.didyoumeanClick,
actionCause: SearchPageEvents.didYouMeanClick,
});

export const didYouMeanAutomatic = (): SearchAction => ({
actionCause: SearchPageEvents.didyoumeanAutomatic,
actionCause: SearchPageEvents.didYouMeanAutomatic,
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {SearchPageEvents} from '../analytics/search-action-cause';
import {getCaseContextAnalyticsMetadata} from '../case-context/case-context-state';

export const logDidYouMeanClick = (): InsightAction =>
makeInsightAnalyticsActionFactory(SearchPageEvents.didyoumeanClick)(
makeInsightAnalyticsActionFactory(SearchPageEvents.didYouMeanClick)(
'analytics/didyoumean/click',
(client, state) =>
client.logDidYouMeanClick(
Expand All @@ -15,7 +15,7 @@ export const logDidYouMeanClick = (): InsightAction =>
);

export const logDidYouMeanAutomatic = (): InsightAction =>
makeInsightAnalyticsActionFactory(SearchPageEvents.didyoumeanAutomatic)(
makeInsightAnalyticsActionFactory(SearchPageEvents.didYouMeanAutomatic)(
'analytics/didyoumean/automatic',
(client, state) =>
client.logDidYouMeanAutomatic(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import {
CustomAction,
LegacySearchAction,
} from '../analytics/analytics-utils';
import {SearchPageEvents} from '../analytics/search-action-cause';
import {SearchAction} from '../search/search-actions';

export const logClearRecentQueries = (): CustomAction =>
makeAnalyticsAction('analytics/recentQueries/clear', (client) => {
Expand All @@ -16,7 +14,3 @@ export const logRecentQueryClick = (): LegacySearchAction =>
makeAnalyticsAction('analytics/recentQueries/click', (client) => {
return client.makeRecentQueryClick();
});

export const recentQueryClick = (): SearchAction => ({
actionCause: SearchPageEvents.recentQueryClick,
});
11 changes: 5 additions & 6 deletions packages/headless/src/features/search/search-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,13 @@ export const executeSearch = createAsyncThunk<
'search/executeSearch',
async (searchAction: TransitiveSearchAction, config) => {
const state = config.getState();
if (
state.configuration.analytics.analyticsMode === 'legacy' ||
!searchAction.next
) {
if (state.configuration.analytics.analyticsMode === 'legacy') {
return legacyExecuteSearch(state, config, searchAction.legacy);
}
addEntryInActionsHistory(state);
const analyticsAction = buildSearchReduxAction(searchAction.next);
const analyticsAction = searchAction.next
? buildSearchReduxAction(searchAction.next)
: undefined;

const request = await buildSearchRequest(
state,
Expand All @@ -127,7 +126,7 @@ export const executeSearch = createAsyncThunk<

const processor = new AsyncSearchThunkProcessor<
ReturnType<typeof config.rejectWithValue>
>({...config, analyticsAction});
>({...config, analyticsAction: analyticsAction ?? {}});

const fetched = await processor.fetchFromAPI(request, {
origin: 'mainSearch',
Expand Down
6 changes: 6 additions & 0 deletions packages/headless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,12 @@ export type {
} from './controllers/tab/headless-tab';
export {buildTab} from './controllers/tab/headless-tab';

export type {
TabManagerState,
TabManager,
} from './controllers/tab-manager/headless-tab-manager';
export {buildTabManager} from './controllers/tab-manager/headless-tab-manager';

export type {
FacetManagerPayload,
FacetManagerState,
Expand Down
Loading

0 comments on commit 44c1b1d

Please sign in to comment.