Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ module.exports = {
'no-console': 'off',
'no-continue': 'off',
'no-loop-func': 'off',
// eslint-disable-next-line no-warning-comments
'no-undef': 'warn', // TODO: find how to remove "ESLint: 'JSX' is not defined." errors properly
'consistent-return': 'off',
'@typescript-eslint/no-unused-vars': 'warn',

Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"postinstall": "[ -d dist/ ] || npm run build"
},
"devDependencies": {
"@algolia/autocomplete-js": "1.0.0-alpha.44",
"@algolia/autocomplete-js": "1.0.0-alpha.45",
"@algolia/autocomplete-preset-algolia": "1.0.0-alpha.45",
"@algolia/autocomplete-theme-classic": "1.0.0-alpha.45",
"@algolia/transporter": "4.8.6",
Expand Down
66 changes: 4 additions & 62 deletions frontend/src/AutocompleteWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
import {
autocomplete,
highlightHit,
snippetHit,
} from '@algolia/autocomplete-js';
import { autocomplete } from '@algolia/autocomplete-js';
import type {
VNode,
AutocompleteApi,
AutocompleteSource,
SourceTemplates,
} from '@algolia/autocomplete-js';
import type { HighlightedHit } from '@algolia/autocomplete-preset-algolia';
import { getAlgoliaHits } from '@algolia/autocomplete-preset-algolia';
import type { Hit } from '@algolia/client-search';
import algoliasearch from 'algoliasearch/lite';
import type { SearchClient } from 'algoliasearch/lite';

// @ts-expect-error
import { version } from '../package.json';

import { templates } from './templates';
import type { Options, AlgoliaRecord, HighlightedHierarchy } from './types';
import type { Options, AlgoliaRecord } from './types';

class AutocompleteWrapper {
private options;
Expand Down Expand Up @@ -94,13 +88,8 @@ class AutocompleteWrapper {
header() {
return '';
},
item({ item }) {
return templates.item(
item,
highlightHit({ hit: item, attribute: 'title' }),
getSuggestionSnippet(item),
getHighlightedHierarchy(item)
);
item({ item, components }) {
return templates.item(item, components);
},
footer() {
if (poweredBy) {
Expand Down Expand Up @@ -155,53 +144,6 @@ class AutocompleteWrapper {
}
}

function getSuggestionSnippet(hit: Hit<AlgoliaRecord>): Array<string | VNode> {
// If they are defined as `searchableAttributes`, 'description' and 'content' are always
// present in the `_snippetResult`, even if they don't match.
// So we need to have 1 check on the presence and 1 check on the match
const description = hit._snippetResult?.description;
const content = hit._snippetResult?.content;

// Take in priority props that matches the search
if (description && description.matchLevel === 'full') {
return snippetHit({ hit, attribute: 'description' });
}
if (content && content.matchLevel === 'full') {
return snippetHit({ hit, attribute: 'content' });
}

// Otherwise take the prop that was at least correctly returned
if (description && !content) {
return snippetHit({ hit, attribute: 'description' });
}
if (content) {
return snippetHit({ hit, attribute: 'content' });
}

// Otherwise raw value or empty
const res = hit.description || hit.content || '';
return [res];
}

function getHighlightedHierarchy(
hit: Hit<AlgoliaRecord>
): HighlightedHierarchy | null {
if (!hit.hierarchy) {
return null;
}
const highlightedHierarchy: HighlightedHierarchy = {};
for (let i = 0; i <= 6; ++i) {
if (!hit.hierarchy[`lvl${i}`]) {
continue;
}
highlightedHierarchy[`lvl${i}`] = highlightHit({
hit,
attribute: ['hierarchy', `lvl${i}`],
});
}
return highlightedHierarchy;
}

function addCss(
css: string,
$mainStyle: HTMLElement | null = null
Expand Down
92 changes: 66 additions & 26 deletions frontend/src/templates.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { VNode } from '@algolia/autocomplete-js';
import type { AutocompleteComponents, VNode } from '@algolia/autocomplete-js';
import type { Hit } from '@algolia/client-search';

import type { AlgoliaRecord, HighlightedHierarchy } from './types';
import type { AlgoliaRecord } from './types';

export const templates = {
poweredBy: ({ hostname }: { hostname: string }): VNode => {
Expand All @@ -19,13 +20,11 @@ export const templates = {
},

item: (
record: AlgoliaRecord,
title: Array<string | VNode>,
description: Array<string | VNode>,
hierarchy: HighlightedHierarchy | null
): VNode => {
hit: AlgoliaRecord,
components: AutocompleteComponents
): JSX.Element => {
return (
<a href={record.url}>
<a href={hit.url}>
<div className="aa-ItemContent">
<div className="aa-ItemIcon">
<svg width="20" height="20" viewBox="0 0 20 20">
Expand All @@ -39,15 +38,19 @@ export const templates = {
</svg>
</div>
<div>
<div className="aa-ItemTitle">{hierarchy?.lvl0 ?? title}</div>
{hierarchy && (
<div className="aa-ItemTitle">
{hit.hierarchy?.lvl0 ?? (
<components.Highlight hit={hit} attribute="title" />
)}
</div>
{hit.hierarchy && (
<div className="aa-ItemHierarchy">
{hierarchyToBreadcrumbVNodes(hierarchy)}
{hierarchyToBreadcrumbs(hit, components)}
</div>
)}
{description && (
<div className="aa-ItemDescription">{description}</div>
)}
<div className="aa-ItemDescription">
{getSuggestionSnippet(hit, components)}
</div>
</div>
</div>
</a>
Expand All @@ -56,26 +59,63 @@ export const templates = {
};

/**
* Transform a highlighted hierarchy object into an array of VNode[].
* Transform a highlighted hierarchy object into an array of Highlighted elements.
* 3 levels max are returned.
*
* @param hierarchy - An highlighted hierarchy, i.e. { lvl0: (string | VNode)[], lvl1: (string | VNode)[], lvl2: (string | VNode)[], ... }.
* @returns An array of VNode[], representing of the highlighted hierarchy starting from lvl1.
* Between each VNode[] we insert a ' > ' character to render them as breadcrumbs eventually.
* @param hit - A record with a hierarchy field ( { lvl0: string, lvl1: string, lvl2: string, ... } ).
* @param components - Autocomplete components.
* @returns An array of JSX.Elements | string, representing of the highlighted hierarchy starting from lvl1.
* Between each element, we insert a ' > ' character to render them as breadcrumbs eventually.
*/
function hierarchyToBreadcrumbVNodes(
hierarchy: HighlightedHierarchy
): Array<string | Array<string | VNode>> {
const breadcrumbVNodeArray: Array<string | Array<string | VNode>> = [];
function hierarchyToBreadcrumbs(
hit: Hit<AlgoliaRecord>,
components: AutocompleteComponents
): Array<JSX.Element | string> {
const breadcrumbArray: Array<JSX.Element | string> = [];
let addedLevels = 0;
if (!hit.hierarchy) {
return breadcrumbArray;
}
for (let i = 1; i < 7 && addedLevels < 3; ++i) {
if (hierarchy[`lvl${i}`] && hierarchy[`lvl${i}`].length > 0) {
if (hit.hierarchy[`lvl${i}`] && hit.hierarchy[`lvl${i}`].length > 0) {
if (addedLevels > 0) {
breadcrumbVNodeArray.push(' > ');
breadcrumbArray.push(' > ');
}
breadcrumbVNodeArray.push(hierarchy[`lvl${i}`]);
breadcrumbArray.push(
<components.Highlight hit={hit} attribute="description" />
);
++addedLevels;
}
}
return breadcrumbVNodeArray;
return breadcrumbArray;
}

function getSuggestionSnippet(
hit: Hit<AlgoliaRecord>,
components: AutocompleteComponents
): JSX.Element | string {
// If they are defined as `searchableAttributes`, 'description' and 'content' are always
// present in the `_snippetResult`, even if they don't match.
// So we need to have 1 check on the presence and 1 check on the match
const description = hit._snippetResult?.description;
const content = hit._snippetResult?.content;

// Take in priority props that matches the search
if (description && description.matchLevel === 'full') {
return <components.Snippet hit={hit} attribute="description" />;
}
if (content && content.matchLevel === 'full') {
return <components.Snippet hit={hit} attribute="content" />;
}

// Otherwise take the prop that was at least correctly returned
if (description && !content) {
return <components.Snippet hit={hit} attribute="description" />;
}
if (content) {
return <components.Snippet hit={hit} attribute="content" />;
}

// Otherwise raw value or empty
return hit.description || hit.content || '';
}
3 changes: 0 additions & 3 deletions frontend/src/types/record.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { VNode } from '@algolia/autocomplete-js';

export type AlgoliaRecord = {
objectID: string;

Expand All @@ -25,4 +23,3 @@ export type AlgoliaRecord = {
};

export type Hierarchy = { [lvl: string]: string };
export type HighlightedHierarchy = { [lvl: string]: Array<string | VNode> };
36 changes: 12 additions & 24 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,30 @@
# yarn lockfile v1


"@algolia/[email protected].44":
version "1.0.0-alpha.44"
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.0.0-alpha.44.tgz#e626dba45f5f3950d6beb0ab055395ef0f7e8bb2"
integrity sha512-2iMXthldMIDXtlbg9omRKLgg1bLo2ZzINAEqwhNjUeyj1ceEyL1ck6FY0VnJpf2LsjmNthHCz2BuFk+nYUeDNA==
"@algolia/[email protected].45":
version "1.0.0-alpha.45"
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.0.0-alpha.45.tgz#424f8e2cfca6c4c3682fa02ce7d122ae2eb0f0d1"
integrity sha512-Lol3IZSscUmZgkrq6DCcvImj1YW4NNHr8IkcARHTDsJvKo+G+mu7LrBLjUD/XEQZy2MAE0JbxrkShecdEpdjTw==
dependencies:
"@algolia/autocomplete-shared" "1.0.0-alpha.44"
"@algolia/autocomplete-shared" "1.0.0-alpha.45"

"@algolia/[email protected].44":
version "1.0.0-alpha.44"
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-js/-/autocomplete-js-1.0.0-alpha.44.tgz#a252bdbf7ab662dedcc05cfe53e318d6becd7bee"
integrity sha512-KmWhIvO/T5yS+kelZQrVMgAGKZKozoFHQM8VMrXK3a77i1uqTYFkg70HFIsiQ9kRGjB/EA0exNtm3/BwGkIIkw==
"@algolia/[email protected].45":
version "1.0.0-alpha.45"
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-js/-/autocomplete-js-1.0.0-alpha.45.tgz#b3835cbec368438f37637f065a11b375a92e300f"
integrity sha512-HAN3HzIstVGoL+ghtXtbg0i81Wn6rXag+CCdmniUuPIVTxsGLeSu4DK6AkZRpxoR8ozW+OeUj7tMnjziUZdhqw==
dependencies:
"@algolia/autocomplete-core" "1.0.0-alpha.44"
"@algolia/autocomplete-preset-algolia" "1.0.0-alpha.44"
"@algolia/autocomplete-shared" "1.0.0-alpha.44"
"@algolia/autocomplete-core" "1.0.0-alpha.45"
"@algolia/autocomplete-preset-algolia" "1.0.0-alpha.45"
"@algolia/autocomplete-shared" "1.0.0-alpha.45"
preact "^10.0.0"

"@algolia/[email protected]":
version "1.0.0-alpha.44"
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.0.0-alpha.44.tgz#0ea0b255d0be10fbe262e281472dd6e4619b62ba"
integrity sha512-DCHwo5ovzg9k2ejUolGNTLFnIA7GpsrkbNJTy1sFbMnYfBmeK8egZPZnEl7lBTr27OaZu7IkWpTepLVSztZyng==
dependencies:
"@algolia/autocomplete-shared" "1.0.0-alpha.44"

"@algolia/[email protected]":
version "1.0.0-alpha.45"
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.0.0-alpha.45.tgz#da23c1cc799893a58b23726508080a9a3f845b0e"
integrity sha512-IrTwyNKAiwz59/IYsn57rR6/BD71T5xA9tZ2jeX0QMyMlMpz24wqIUe7JdGkAlhcxw3H1qundjdwFQ8kISjCDA==
dependencies:
"@algolia/autocomplete-shared" "1.0.0-alpha.45"

"@algolia/[email protected]":
version "1.0.0-alpha.44"
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.0.0-alpha.44.tgz#db13902ad1667e455711b77d08cae1a0feafaa48"
integrity sha512-2oQZPERYV+yNx/yoVWYjZZdOqsitJ5dfxXJjL18yczOXH6ujnsq+DTczSrX+RjzjQdVeJ1UAG053EJQF/FOiMg==

"@algolia/[email protected]":
version "1.0.0-alpha.45"
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.0.0-alpha.45.tgz#3540dbc31f3e6f0e976409d568939783d18b948e"
Expand Down