Skip to content

Commit 3ad2d46

Browse files
authored
[CSL-2867] Section configuration option updates (#112)
* Implementation * Update documentation * Change default section logic for recommendations * Add deprecation notice * Typo * Add explanation for recommendationsSection styles * Update docs * Forward indexSection * Add documentation for indexSection * Address comments * Simplify kebabcase implementation * Remove identifier * Rename indexSection * Add type guards * Lint * Update kebabcase * Typo * Update types
1 parent 1052c96 commit 3ad2d46

20 files changed

+362
-156
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ function YourComponent() {
7878
{isOpen && (
7979
<>
8080
{sections?.map((section) => (
81-
<div key={section.identifier} className={section.identifier}>
81+
<div key={section.indexSectionName} className={section.indexSectionName}>
8282
<div className='cio-section'>
8383
<div className='cio-sectionName'>
84-
{section?.displayName || section.identifier}
84+
{section?.displayName || section.indexSectionName}
8585
</div>
8686
<div className='cio-items'>
8787
{section?.data?.map((item) => (

src/components/Autocomplete/AutocompleteResults/AutocompleteResults.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { ReactNode, useContext } from 'react';
22
import { GetItemProps, Section } from '../../../types';
3+
import { toKebabCase } from '../../../utils';
34
import { CioAutocompleteContext } from '../CioAutocompleteProvider';
45
import SectionItemsList from '../SectionItemsList/SectionItemsList';
56

@@ -13,9 +14,27 @@ type AutocompleteResultsProps = {
1314
};
1415

1516
const DefaultRenderResults: RenderResults = ({ sections }) =>
16-
sections?.map((section: Section) => (
17-
<SectionItemsList section={section} key={section.identifier} />
18-
));
17+
sections?.map((section: Section) => {
18+
const { type } = section;
19+
let key = section.displayName;
20+
21+
switch (type) {
22+
case 'recommendations':
23+
key = section.podId;
24+
break;
25+
case 'custom':
26+
key = toKebabCase(section.displayName);
27+
break;
28+
case 'autocomplete':
29+
key = section.indexSectionName;
30+
break;
31+
default:
32+
key = section.indexSectionName;
33+
break;
34+
}
35+
36+
return <SectionItemsList section={section} key={key} />;
37+
});
1938

2039
export default function AutocompleteResults(props: AutocompleteResultsProps) {
2140
const { children = DefaultRenderResults } = props;

src/components/Autocomplete/SectionItemsList/SectionItemsList.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,32 @@ type SectionItemsListProps = {
1717
// eslint-disable-next-line func-names
1818
const DefaultRenderSectionItemsList: RenderSectionItemsList = function ({ section }) {
1919
const { getSectionProps } = useContext(CioAutocompleteContext);
20+
const { type, displayName } = section;
21+
let sectionTitle = displayName;
2022

21-
const sectionName = section?.displayName || section?.identifier;
23+
if (!sectionTitle) {
24+
switch (type) {
25+
case 'recommendations':
26+
sectionTitle = section.podId;
27+
break;
28+
case 'autocomplete':
29+
sectionTitle = section.indexSectionName;
30+
break;
31+
case 'custom':
32+
sectionTitle = section.displayName;
33+
break;
34+
default:
35+
sectionTitle = section.indexSectionName;
36+
break;
37+
}
38+
}
2239

2340
if (!section?.data?.length) return null;
2441

2542
return (
2643
<li {...getSectionProps(section)}>
2744
<h5 className='cio-sectionName' aria-hidden>
28-
{camelToStartCase(sectionName)}
45+
{camelToStartCase(sectionTitle)}
2946
</h5>
3047
<ul className='cio-section-items' role='none'>
3148
{section?.data?.map((item) => (

src/constants.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,24 +59,28 @@ The following stories show how different options affect the hook's behavior!
5959
// Storybook Pages
6060
/// //////////////////////////////
6161

62-
export const sectionsDescription = `- by default, typing a query will fetch data for search suggestions and Products
62+
export const sectionsDescription = `- by default, typing a query will fetch data for Search Suggestions and Products
6363
- to override this, pass an array of sections objects
6464
- the order of the objects in the \`sections\` array determines the order of the results
65-
- each section object must have an \`identifier\`
65+
- each autocomplete section object must have a \`indexSectionName\`
66+
- each recommendation section object must have a \`podId\`
67+
- each custom section object must have a \`displayName\`
6668
- each section object can specify a \`type\`
6769
- each section object can override the default \`numResults\` of 8
6870
71+
\`indexSectionName\` refers to a section under an index. The default sections are "Products" and "Search Suggestions". You can find all the sections that exist in your index under the "Indexes" tab of Constructor dashboard.
72+
6973
When no values are passed for the \`sections\` argument, the following defaults are used:
7074
7175
\`\`\`jsx
7276
[
7377
{
74-
identifier: 'Search Suggestions',
78+
indexSectionName: 'Search Suggestions',
7579
type: 'autocomplete',
7680
numResults: 8
7781
},
7882
{
79-
identifier: 'Products',
83+
indexSectionName: 'Products',
8084
type: 'autocomplete',
8185
numResults: 8
8286
}
@@ -98,7 +102,9 @@ export const zeroStateDescription = `- when the text input field has no text, we
98102
- when \`zeroStateSections\` has sections, the menu will open on user focus by default
99103
- set \`openOnFocus\` to false, to only show \`zeroStateSections\` after user has typed and then cleared the text input, instead of as soon as the user focuses on the text input
100104
- the order of the objects in the \`zeroStateSections\` array determines the order of the results
101-
- each section object must have an \`identifier\`
105+
- each autocomplete section object must have a \`indexSectionName\`
106+
- each recommendation section object must have a \`podId\`
107+
- each custom section object must have a \`displayName\`
102108
- each section object can specify a \`type\`
103109
- each section object can override the default \`numResults\` of 8`;
104110

src/hooks/useCioAutocomplete.ts

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,47 @@ import {
1414
getItemsForActiveSections,
1515
getSearchSuggestionFeatures,
1616
trackRecommendationView,
17+
toKebabCase,
1718
} from '../utils';
1819
import useConsoleErrors from './useConsoleErrors';
1920
import useSections from './useSections';
2021
import useRecommendationsObserver from './useRecommendationsObserver';
22+
import { isAutocompleteSection, isRecommendationsSection } from '../typeGuards';
2123

2224
export const defaultSections: UserDefinedSection[] = [
2325
{
24-
identifier: 'Search Suggestions',
26+
indexSectionName: 'Search Suggestions',
2527
type: 'autocomplete',
2628
},
2729
{
28-
identifier: 'Products',
30+
indexSectionName: 'Products',
2931
type: 'autocomplete',
3032
},
3133
];
3234

3335
export type UseCioAutocompleteOptions = Omit<CioAutocompleteProps, 'children'>;
3436

37+
const convertLegacyParametersAndAddDefaults = (sections: UserDefinedSection[]) =>
38+
sections.map((config) => {
39+
if (isRecommendationsSection(config)) {
40+
if (config.identifier && !config.podId) {
41+
return { ...config, podId: config.identifier };
42+
}
43+
44+
if (!config.indexSectionName) {
45+
return { ...config, indexSectionName: 'Products' };
46+
}
47+
}
48+
49+
if (isAutocompleteSection(config)) {
50+
if (config.identifier && !config.indexSectionName) {
51+
return { ...config, indexSectionName: config.identifier };
52+
}
53+
}
54+
55+
return config;
56+
});
57+
3558
const useCioAutocomplete = (options: UseCioAutocompleteOptions) => {
3659
const {
3760
onSubmit,
@@ -41,13 +64,29 @@ const useCioAutocomplete = (options: UseCioAutocompleteOptions) => {
4164
cioJsClient,
4265
cioJsClientOptions,
4366
placeholder = 'What can we help you find today?',
44-
sections = defaultSections,
45-
zeroStateSections,
4667
autocompleteClassName = 'cio-autocomplete',
4768
advancedParameters,
4869
defaultInput,
4970
} = options;
5071

72+
let { sections = defaultSections, zeroStateSections } = options;
73+
74+
sections = useMemo(() => {
75+
if (sections) {
76+
return convertLegacyParametersAndAddDefaults(sections);
77+
}
78+
79+
return sections;
80+
}, [sections]);
81+
82+
zeroStateSections = useMemo(() => {
83+
if (zeroStateSections) {
84+
return convertLegacyParametersAndAddDefaults(zeroStateSections);
85+
}
86+
87+
return zeroStateSections;
88+
}, [zeroStateSections]);
89+
5190
const [query, setQuery] = useState(defaultInput || '');
5291
const previousQuery = usePrevious(query);
5392
const cioClient = useCioClient({ apiKey, cioJsClient, cioJsClientOptions } as CioClientConfig);
@@ -171,18 +210,43 @@ const useCioAutocomplete = (options: UseCioAutocompleteOptions) => {
171210
'data-testid': 'cio-form',
172211
}),
173212
getSectionProps: (section: Section) => {
174-
const sectionName = section?.displayName || section?.identifier;
213+
const { type, displayName } = section;
214+
let sectionTitle = displayName;
215+
216+
// Add the indexSectionName as a class to the section container to make sure it gets the styles
217+
// Even if the section is a recommendation pod, if the results are "Products" or "Search Suggestions"
218+
// ... they should be styled accordingly
219+
const indexSectionName =
220+
type !== 'custom' && section.indexSectionName ? toKebabCase(section.indexSectionName) : '';
221+
222+
if (!sectionTitle) {
223+
switch (type) {
224+
case 'recommendations':
225+
sectionTitle = section.podId;
226+
break;
227+
case 'autocomplete':
228+
sectionTitle = section.indexSectionName;
229+
break;
230+
case 'custom':
231+
sectionTitle = section.displayName;
232+
break;
233+
default:
234+
sectionTitle = section.indexSectionName;
235+
break;
236+
}
237+
}
238+
175239
const attributes: HTMLPropsWithCioDataAttributes = {
176-
className: `${sectionName} cio-section`,
240+
className: `${sectionTitle} cio-section ${indexSectionName}`,
177241
ref: section.ref,
178242
role: 'none',
179243
'data-cnstrc-section': section.data[0]?.section,
180244
};
181245

182246
// Add data attributes for recommendations
183-
if (section.type === 'recommendations') {
247+
if (isRecommendationsSection(section)) {
184248
attributes['data-cnstrc-recommendations'] = true;
185-
attributes['data-cnstrc-recommendations-pod-id'] = section.identifier;
249+
attributes['data-cnstrc-recommendations-pod-id'] = section.podId;
186250
}
187251
return attributes;
188252
},

src/hooks/useDebouncedFetchSections.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ import ConstructorIOClient from '@constructor-io/constructorio-client-javascript
33
import {
44
Nullable,
55
IAutocompleteParameters,
6+
AutocompleteResponse,
67
} from '@constructor-io/constructorio-client-javascript/lib/types';
78
import useDebounce from './useDebounce';
89
import {
910
AutocompleteResultSections,
10-
UserDefinedSection,
1111
AdvancedParameters,
1212
AdvancedParametersBase,
1313
SectionsData,
14+
AutocompleteSectionConfiguration,
1415
} from '../types';
1516

16-
const transformResponse = (response, options) => {
17+
const transformResponse = (response: AutocompleteResponse, options) => {
1718
const { numTermsWithGroupSuggestions, numGroupsSuggestedPerTerm } = options;
1819
const newSectionsData: SectionsData = {};
1920
Object.keys(response?.sections || {}).forEach((section: string) => {
@@ -52,7 +53,7 @@ const transformResponse = (response, options) => {
5253
const useDebouncedFetchSection = (
5354
query: string,
5455
cioClient: Nullable<ConstructorIOClient>,
55-
autocompleteSections: UserDefinedSection[],
56+
autocompleteSections: AutocompleteSectionConfiguration[],
5657
advancedParameters?: AdvancedParameters
5758
) => {
5859
const [sectionsData, setSectionsData] = useState<AutocompleteResultSections>({
@@ -75,7 +76,7 @@ const useDebouncedFetchSection = (
7576
decoratedParameters.resultsPerSection = autocompleteSections.reduce(
7677
(acc, sectionConfig) => ({
7778
...acc,
78-
[sectionConfig.identifier]: sectionConfig?.numResults || 8,
79+
[sectionConfig.indexSectionName]: sectionConfig?.numResults || 8,
7980
}),
8081
{}
8182
);
@@ -92,11 +93,13 @@ const useDebouncedFetchSection = (
9293
debouncedSearchTerm,
9394
autocompleteParameters
9495
);
95-
const newSectionsData = transformResponse(response, {
96-
numTermsWithGroupSuggestions,
97-
numGroupsSuggestedPerTerm,
98-
});
99-
setSectionsData(newSectionsData);
96+
if (response) {
97+
const newSectionsData = transformResponse(response, {
98+
numTermsWithGroupSuggestions,
99+
numGroupsSuggestedPerTerm,
100+
});
101+
setSectionsData(newSectionsData);
102+
}
100103
} catch (error: any) {
101104
// eslint-disable-next-line no-console
102105
console.log(error);

src/hooks/useFetchRecommendationPod.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import { useEffect, useState } from 'react';
22
import ConstructorIOClient from '@constructor-io/constructorio-client-javascript';
33
import { Nullable } from '@constructor-io/constructorio-client-javascript/lib/types';
4-
import { RecommendationsSection, Item, SectionsData } from '../types';
4+
import { Item, SectionsData, RecommendationsSectionConfiguration } from '../types';
55

66
const useFetchRecommendationPod = (
77
cioClient: Nullable<ConstructorIOClient>,
8-
recommendationPods: RecommendationsSection[]
8+
recommendationPods: RecommendationsSectionConfiguration[]
99
) => {
1010
const [recommendationResults, setRecommendationResults] = useState<SectionsData>({});
1111

1212
useEffect(() => {
1313
if (!cioClient || !Array.isArray(recommendationPods) || recommendationPods.length === 0) return;
1414
const fetchRecommendationResults = async () => {
1515
const responses = await Promise.all(
16-
recommendationPods.map(({ identifier: podId, ...parameters }) =>
17-
cioClient.recommendations.getRecommendations(podId, parameters)
16+
recommendationPods.map(({ podId, indexSectionName, ...parameters }) =>
17+
cioClient.recommendations.getRecommendations(podId, {
18+
...parameters,
19+
section: indexSectionName,
20+
})
1821
)
1922
);
2023
const recommendationPodResults = {};
@@ -25,7 +28,7 @@ const useFetchRecommendationPod = (
2528
recommendationPodResults[pod.id] = results?.map((item: Item) => ({
2629
...item,
2730
id: item?.data?.id,
28-
section: recommendationPods[index]?.section || 'Products',
31+
section: recommendationPods[index]?.indexSectionName,
2932
podId: pod.id,
3033
}));
3134
}

src/hooks/useRecommendationsObserver.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
22
import { Nullable } from '@constructor-io/constructorio-client-javascript/lib/types';
33
import ConstructorIO from '@constructor-io/constructorio-client-javascript';
44
import { Section } from '../types';
5+
import { isRecommendationsSection } from '../typeGuards';
56

67
/**
78
* Custom hook that observes the visibility of recommendation sections and calls trackRecommendationView event.
@@ -27,7 +28,7 @@ function useRecommendationsObserver(
2728
) {
2829
// Get refs for each section
2930
const refs = sections
30-
.filter((section) => section.type === 'recommendations')
31+
.filter((section) => isRecommendationsSection(section))
3132
.map((section) => section.ref);
3233

3334
useEffect(() => {

0 commit comments

Comments
 (0)