@seqera/docusaurus-theme-seqera 1.0.25 → 1.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,836 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * Swizzled from docusaurus-theme-search-typesense to remove hardcoded
8
+ * group_by: 'url' so search page results match the search bar.
9
+ * Product routes are configured via themeConfig.typesense.productRoutes.
10
+ */
11
+ /* eslint-disable jsx-a11y/no-autofocus */
12
+ import React, {useEffect, useMemo, useState, useReducer, useRef} from 'react';
13
+ import clsx from 'clsx';
14
+ import algoliaSearchHelper from 'algoliasearch-helper';
15
+ import Head from '@docusaurus/Head';
16
+ import Link from '@docusaurus/Link';
17
+ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
18
+ import {
19
+ HtmlClassNameProvider,
20
+ usePluralForm,
21
+ isRegexpStringMatch,
22
+ useEvent,
23
+ // @ts-ignore
24
+ } from '@docusaurus/theme-common';
25
+ import {useSearchPage} from './useSearchPage';
26
+ import {useTitleFormatter} from './generalUtils';
27
+ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
28
+ import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
29
+ import Translate, {translate} from '@docusaurus/Translate';
30
+ import Layout from '@theme/Layout';
31
+ import styles from './styles.module.css';
32
+ import TypesenseInstantSearchAdapter from 'typesense-instantsearch-adapter';
33
+ // Non-content docusaurus_tag values to always exclude from search results.
34
+ // Verified against live Typesense facets — blog/doc-list tags have zero documents
35
+ // and are kept here defensively in case content is re-indexed with those tags.
36
+ const NON_CONTENT_TAGS = [
37
+ 'default',
38
+ 'doc_tag_doc_list',
39
+ 'blog_posts_list',
40
+ 'blog_tags_posts',
41
+ 'doc_tags_list',
42
+ 'blog_tags_list',
43
+ ];
44
+ // Custom dropdown for product/version filtering.
45
+ // A native <select> always closes and commits a value on click, making it impossible
46
+ // to let Platform Enterprise "expand" sub-options without immediately selecting it.
47
+ function FilterSelect({value, onChange, options}) {
48
+ const [isOpen, setIsOpen] = useState(false);
49
+ const [expandedId, setExpandedId] = useState(null);
50
+ const containerRef = useRef(null);
51
+ // Close when clicking outside
52
+ useEffect(() => {
53
+ if (!isOpen) return undefined;
54
+ function handleClickOutside(e) {
55
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
56
+ setIsOpen(false);
57
+ }
58
+ }
59
+ document.addEventListener('mousedown', handleClickOutside);
60
+ return () => document.removeEventListener('mousedown', handleClickOutside);
61
+ }, [isOpen]);
62
+ // Auto-expand the selected product's group when the dropdown opens
63
+ useEffect(() => {
64
+ if (!isOpen || !value) return;
65
+ const productId = value.includes('@') ? value.split('@')[0] : value;
66
+ const product = options.find((o) => o.id === productId);
67
+ if (product && product.versions.length > 1) {
68
+ setExpandedId(productId);
69
+ }
70
+ }, [isOpen]); // eslint-disable-line react-hooks/exhaustive-deps
71
+ function handleSelect(newValue) {
72
+ onChange(newValue);
73
+ setIsOpen(false);
74
+ setExpandedId(null);
75
+ }
76
+ // Build the trigger label from the current value
77
+ const triggerLabel = (() => {
78
+ if (!value)
79
+ return translate({
80
+ id: 'theme.SearchPage.allProductsOption',
81
+ message: 'All products',
82
+ });
83
+ const [productId, versionName] = value.includes('@')
84
+ ? value.split('@')
85
+ : [value, null];
86
+ const product = options.find((o) => o.id === productId);
87
+ if (!product)
88
+ return translate({
89
+ id: 'theme.SearchPage.allProductsOption',
90
+ message: 'All products',
91
+ });
92
+ if (versionName) {
93
+ const version = product.versions.find((v) => v.name === versionName);
94
+ return `${product.label} \u2013 ${version?.label || versionName}`;
95
+ }
96
+ if (product.versions.length > 1) {
97
+ const current =
98
+ product.versions.find((v) => v.isLast) || product.versions[0];
99
+ return `${product.label} \u2013 Current (${current.label})`;
100
+ }
101
+ return product.label;
102
+ })();
103
+ return (
104
+ <div className={styles.filterSelectWrapper} ref={containerRef}>
105
+ <div className={clsx(styles.filterBox, isOpen && styles.filterBoxOpen)}>
106
+ <button
107
+ type="button"
108
+ className={styles.filterTrigger}
109
+ onClick={() => setIsOpen((o) => !o)}
110
+ aria-haspopup="listbox"
111
+ aria-expanded={isOpen}>
112
+ <span>{triggerLabel}</span>
113
+ <svg
114
+ aria-hidden="true"
115
+ className={clsx(
116
+ styles.filterTriggerChevron,
117
+ isOpen
118
+ ? styles.filterTriggerChevronOpen
119
+ : styles.filterTriggerChevronClosed,
120
+ )}
121
+ xmlns="http://www.w3.org/2000/svg"
122
+ viewBox="4 4 16 16"
123
+ fill="currentColor">
124
+ <path d="M11.8152 13.1989L10.0167 11.1432C9.80447 10.9013 9.97697 10.5214 10.2991 10.5214H13.8961C13.9682 10.5214 14.0388 10.5421 14.0994 10.5811C14.16 10.6201 14.2081 10.6758 14.2379 10.7414C14.2677 10.8071 14.2779 10.8799 14.2674 10.9512C14.2569 11.0226 14.226 11.0893 14.1785 11.1435L12.38 13.1985C12.3448 13.2388 12.3014 13.2711 12.2527 13.2932C12.204 13.3153 12.1511 13.3268 12.0976 13.3268C12.0441 13.3268 11.9912 13.3153 11.9425 13.2932C11.8938 13.2711 11.8504 13.2388 11.8152 13.1985V13.1989Z" />
125
+ </svg>
126
+ </button>
127
+ </div>
128
+ {isOpen && (
129
+ <ul className={styles.filterDropdown} role="listbox">
130
+ <li
131
+ role="option"
132
+ aria-selected={!value}
133
+ className={clsx(
134
+ styles.filterOption,
135
+ !value && styles.filterOptionActive,
136
+ )}
137
+ onClick={() => handleSelect('')}>
138
+ {translate({
139
+ id: 'theme.SearchPage.allProductsOption',
140
+ message: 'All products',
141
+ })}
142
+ </li>
143
+ {options.map((option) => {
144
+ if (option.versions.length > 1) {
145
+ const isExpanded = expandedId === option.id;
146
+ const isActive =
147
+ value === option.id || value.startsWith(`${option.id}@`);
148
+ const currentVersion =
149
+ option.versions.find((v) => v.isLast) || option.versions[0];
150
+ const olderVersions = option.versions.filter((v) => !v.isLast);
151
+ return (
152
+ <React.Fragment key={option.id}>
153
+ <li
154
+ role="option"
155
+ aria-selected={false}
156
+ aria-expanded={isExpanded}
157
+ className={clsx(
158
+ styles.filterOption,
159
+ styles.filterOptionExpandable,
160
+ isActive && styles.filterOptionActive,
161
+ )}
162
+ onClick={() =>
163
+ setExpandedId(isExpanded ? null : option.id)
164
+ }>
165
+ <span>{option.label}</span>
166
+ <svg
167
+ aria-hidden="true"
168
+ className={clsx(
169
+ styles.filterExpandChevron,
170
+ isExpanded
171
+ ? styles.filterTriggerChevronOpen
172
+ : styles.filterTriggerChevronClosed,
173
+ )}
174
+ xmlns="http://www.w3.org/2000/svg"
175
+ viewBox="4 4 16 16"
176
+ fill="currentColor">
177
+ <path d="M11.8152 13.1989L10.0167 11.1432C9.80447 10.9013 9.97697 10.5214 10.2991 10.5214H13.8961C13.9682 10.5214 14.0388 10.5421 14.0994 10.5811C14.16 10.6201 14.2081 10.6758 14.2379 10.7414C14.2677 10.8071 14.2779 10.8799 14.2674 10.9512C14.2569 11.0226 14.226 11.0893 14.1785 11.1435L12.38 13.1985C12.3448 13.2388 12.3014 13.2711 12.2527 13.2932C12.204 13.3153 12.1511 13.3268 12.0976 13.3268C12.0441 13.3268 11.9912 13.3153 11.9425 13.2932C11.8938 13.2711 11.8504 13.2388 11.8152 13.1985V13.1989Z" />
178
+ </svg>
179
+ </li>
180
+ {isExpanded && (
181
+ <>
182
+ <li
183
+ role="option"
184
+ aria-selected={value === option.id}
185
+ className={clsx(
186
+ styles.filterOption,
187
+ styles.filterSubOption,
188
+ value === option.id && styles.filterOptionActive,
189
+ )}
190
+ onClick={() => handleSelect(option.id)}>
191
+ Current ({currentVersion.label})
192
+ </li>
193
+ {olderVersions.map((v, i) => (
194
+ <li
195
+ key={i}
196
+ role="option"
197
+ aria-selected={value === `${option.id}@${v.name}`}
198
+ className={clsx(
199
+ styles.filterOption,
200
+ styles.filterSubOption,
201
+ value === `${option.id}@${v.name}` &&
202
+ styles.filterOptionActive,
203
+ )}
204
+ onClick={() =>
205
+ handleSelect(`${option.id}@${v.name}`)
206
+ }>
207
+ {v.label}
208
+ </li>
209
+ ))}
210
+ </>
211
+ )}
212
+ </React.Fragment>
213
+ );
214
+ }
215
+ return (
216
+ <li
217
+ key={option.id}
218
+ role="option"
219
+ aria-selected={value === option.id}
220
+ className={clsx(
221
+ styles.filterOption,
222
+ value === option.id && styles.filterOptionActive,
223
+ )}
224
+ onClick={() => handleSelect(option.id)}>
225
+ {option.label}
226
+ </li>
227
+ );
228
+ })}
229
+ </ul>
230
+ )}
231
+ </div>
232
+ );
233
+ }
234
+ // Very simple pluralization: probably good enough for now
235
+ function useDocumentsFoundPlural() {
236
+ const {selectMessage} = usePluralForm();
237
+ return (count) =>
238
+ selectMessage(
239
+ count,
240
+ translate(
241
+ {
242
+ id: 'theme.SearchPage.documentsFound.plurals',
243
+ description:
244
+ 'Pluralized label for "{count} documents found". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
245
+ message: 'One document found|{count} documents found',
246
+ },
247
+ {count},
248
+ ),
249
+ );
250
+ }
251
+ function useDocsSearchVersionsHelpers() {
252
+ const allDocsData = useAllDocsData();
253
+ // State of the version select menus / algolia facet filters
254
+ // docsPluginId -> versionName map
255
+ const [searchVersions, setSearchVersions] = useState(() =>
256
+ Object.entries(allDocsData).reduce(
257
+ (acc, [pluginId, pluginData]) => ({
258
+ ...acc,
259
+ [pluginId]: pluginData.versions[0].name,
260
+ }),
261
+ {},
262
+ ),
263
+ );
264
+ // Set the value of a single select menu
265
+ const setSearchVersion = (pluginId, searchVersion) =>
266
+ setSearchVersions((s) => ({...s, [pluginId]: searchVersion}));
267
+ const versioningEnabled = Object.values(allDocsData).some(
268
+ (docsData) => docsData.versions.length > 1,
269
+ );
270
+ return {
271
+ allDocsData,
272
+ versioningEnabled,
273
+ searchVersions,
274
+ setSearchVersion,
275
+ };
276
+ }
277
+ function SearchPageContent() {
278
+ const {
279
+ siteConfig: {themeConfig},
280
+ i18n: {currentLocale},
281
+ } = useDocusaurusContext();
282
+ const {
283
+ typesense: {
284
+ typesenseCollectionName,
285
+ typesenseServerConfig,
286
+ typesenseSearchParameters,
287
+ contextualSearch,
288
+ externalUrlRegex,
289
+ productRoutes = [],
290
+ },
291
+ } = themeConfig;
292
+ const documentsFoundPlural = useDocumentsFoundPlural();
293
+ const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
294
+ // Maps URL path prefixes to product labels using the configured productRoutes.
295
+ function getProductLabel(pathname) {
296
+ const match = productRoutes.find(([prefix]) => pathname.startsWith(prefix));
297
+ return match ? match[1] : null;
298
+ }
299
+ // Compute tags for old versions of versioned plugins to exclude from results,
300
+ // so only the latest version of each plugin appears by default.
301
+ // The exclusion approach (rather than a whitelist) is used deliberately so that
302
+ // content accessible via URL rewrites or non-standard tags is not accidentally dropped.
303
+ const oldVersionTags = useMemo(() => {
304
+ const tags = [];
305
+ Object.entries(docsSearchVersionsHelpers.allDocsData).forEach(
306
+ ([pluginId, pluginData]) => {
307
+ if (pluginData.versions.length > 1) {
308
+ const latest =
309
+ pluginData.versions.find((v) => v.isLast) || pluginData.versions[0];
310
+ pluginData.versions
311
+ .filter((v) => v.name !== latest.name)
312
+ .forEach((v) => tags.push(`docs-${pluginId}-${v.name}`));
313
+ }
314
+ },
315
+ );
316
+ return tags;
317
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
318
+ const {searchQuery, setSearchQuery, selectedFilter, setSelectedFilter} =
319
+ useSearchPage();
320
+ // inputValue tracks the live input; searchQuery only updates on submit
321
+ const [inputValue, setInputValue] = useState(searchQuery);
322
+ // Sync inputValue when searchQuery is populated from the URL on hydration
323
+ useEffect(() => {
324
+ setInputValue(searchQuery);
325
+ }, [searchQuery]);
326
+ // Products available for filtering — plugin-based products (from allDocsData) and
327
+ // rewrite-based products with a known customTag (e.g. Nextflow → docs-default-current).
328
+ // Each entry has a stable `id` used as the select option value:
329
+ // - plugin-based: id = pluginId
330
+ // - rewrite-based: id = customTag (pluginId is null)
331
+ const productOptions = useMemo(
332
+ () =>
333
+ productRoutes
334
+ .filter(
335
+ ([, , pluginId, customTag]) =>
336
+ (pluginId && docsSearchVersionsHelpers.allDocsData[pluginId]) ||
337
+ customTag,
338
+ )
339
+ .map(([, label, pluginId, customTag]) => ({
340
+ id: pluginId || customTag,
341
+ label,
342
+ pluginId,
343
+ customTag,
344
+ versions:
345
+ pluginId && docsSearchVersionsHelpers.allDocsData[pluginId]
346
+ ? docsSearchVersionsHelpers.allDocsData[pluginId].versions
347
+ : [],
348
+ })),
349
+ [],
350
+ );
351
+ const initialSearchResultState = {
352
+ items: [],
353
+ query: null,
354
+ totalResults: null,
355
+ totalPages: null,
356
+ lastPage: null,
357
+ hasMore: null,
358
+ loading: null,
359
+ };
360
+ const [searchResultState, searchResultStateDispatcher] = useReducer(
361
+ (prevState, data) => {
362
+ switch (data.type) {
363
+ case 'reset': {
364
+ return initialSearchResultState;
365
+ }
366
+ case 'loading': {
367
+ return {...prevState, loading: true};
368
+ }
369
+ case 'update': {
370
+ if (searchQuery !== data.value.query) {
371
+ return prevState;
372
+ }
373
+ return {
374
+ ...data.value,
375
+ items:
376
+ data.value.lastPage === 0
377
+ ? data.value.items
378
+ : prevState.items.concat(data.value.items),
379
+ };
380
+ }
381
+ case 'advance': {
382
+ const hasMore =
383
+ (prevState.totalPages ?? 0) > (prevState.lastPage ?? 0) + 1;
384
+ return {
385
+ ...prevState,
386
+ lastPage: hasMore
387
+ ? (prevState.lastPage ?? 0) + 1
388
+ : prevState.lastPage,
389
+ hasMore,
390
+ };
391
+ }
392
+ default:
393
+ return prevState;
394
+ }
395
+ },
396
+ initialSearchResultState,
397
+ );
398
+ // Memoize the adapter and helper so they're only created once, not on every render.
399
+ // Creating a new TypesenseInstantSearchAdapter on every render causes repeated
400
+ // network activity and accumulates stale event listeners.
401
+ // eslint-disable-next-line react-hooks/exhaustive-deps
402
+ const typesenseInstantSearchAdapter = useMemo(() => {
403
+ // Parse selectedFilter: '' | 'productId' | 'productId@versionName'
404
+ const atIdx = selectedFilter.indexOf('@');
405
+ const filterId =
406
+ atIdx >= 0 ? selectedFilter.slice(0, atIdx) : selectedFilter;
407
+ const filterVersion = atIdx >= 0 ? selectedFilter.slice(atIdx + 1) : null;
408
+ const filterProduct = filterId
409
+ ? productOptions.find((p) => p.id === filterId)
410
+ : null;
411
+ let filterBy;
412
+ if (filterProduct) {
413
+ // Specific product selected: use an explicit inclusion filter so old versions
414
+ // are not accidentally blocked by the config-level exclusion list.
415
+ // We rebuild from NON_CONTENT_TAGS rather than typesenseSearchParameters.filter_by
416
+ // because the config filter already excludes old version tags, which would
417
+ // conflict when the user intentionally selects an older version.
418
+ let inclusionTag;
419
+ if (filterProduct.customTag) {
420
+ // Rewrite-based product (e.g. Nextflow): use its known docusaurus_tag directly
421
+ inclusionTag = filterProduct.customTag;
422
+ } else {
423
+ const targetVersionName =
424
+ filterVersion ||
425
+ (
426
+ filterProduct.versions.find((v) => v.isLast) ||
427
+ filterProduct.versions[0]
428
+ ).name;
429
+ inclusionTag = `docs-${filterProduct.pluginId}-${targetVersionName}`;
430
+ }
431
+ filterBy = [
432
+ `docusaurus_tag:!=[${NON_CONTENT_TAGS.join(',')}]`,
433
+ `docusaurus_tag:=[${inclusionTag}]`,
434
+ ].join(' && ');
435
+ } else {
436
+ // All products: use config filter + exclude remaining old version tags.
437
+ // Exclusion (rather than a whitelist) ensures content accessible via URL
438
+ // rewrites or non-standard tags is not accidentally dropped.
439
+ const versionExclusion =
440
+ oldVersionTags.length > 0
441
+ ? `docusaurus_tag:!=[${oldVersionTags.join(',')}]`
442
+ : null;
443
+ filterBy = [typesenseSearchParameters.filter_by, versionExclusion]
444
+ .filter(Boolean)
445
+ .join(' && ');
446
+ }
447
+ return new TypesenseInstantSearchAdapter({
448
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
449
+ server: typesenseServerConfig,
450
+ additionalSearchParameters: {
451
+ // Defaults matching typesense-docsearch-react (SearchBar) behaviour
452
+ query_by:
453
+ 'hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3,hierarchy.lvl4,hierarchy.lvl5,hierarchy.lvl6,content',
454
+ include_fields:
455
+ 'hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3,hierarchy.lvl4,hierarchy.lvl5,hierarchy.lvl6,content,anchor,url,type,id',
456
+ highlight_full_fields:
457
+ 'hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3,hierarchy.lvl4,hierarchy.lvl5,hierarchy.lvl6,content',
458
+ group_by: 'url',
459
+ group_limit: 1,
460
+ sort_by: 'item_priority:desc',
461
+ snippet_threshold: 8,
462
+ highlight_affix_num_tokens: 4,
463
+ ...typesenseSearchParameters,
464
+ filter_by: filterBy,
465
+ },
466
+ });
467
+ }, [selectedFilter]); // eslint-disable-line react-hooks/exhaustive-deps
468
+ // eslint-disable-next-line react-hooks/exhaustive-deps
469
+ const algoliaHelper = useMemo(
470
+ () =>
471
+ algoliaSearchHelper(
472
+ typesenseInstantSearchAdapter.searchClient,
473
+ typesenseCollectionName,
474
+ {
475
+ hitsPerPage: typesenseSearchParameters.per_page ?? 20,
476
+ advancedSyntax: true,
477
+ ...(contextualSearch && {
478
+ disjunctiveFacets: ['language', 'docusaurus_tag'],
479
+ }),
480
+ },
481
+ ),
482
+ [typesenseInstantSearchAdapter],
483
+ );
484
+ useEffect(() => {
485
+ const sanitizeValue = (value) =>
486
+ value.replace(
487
+ /algolia-docsearch-suggestion--highlight/g,
488
+ 'search-result-match',
489
+ );
490
+ function handleResult({results: {query, hits, page, nbHits, nbPages}}) {
491
+ if (query === '' || !Array.isArray(hits)) {
492
+ searchResultStateDispatcher({type: 'reset'});
493
+ return;
494
+ }
495
+ const items = hits.map((hit) => {
496
+ const {url, _highlightResult, _snippetResult: snippet = {}} = hit;
497
+ const parsedURL = new URL(url);
498
+ // Build levels using both raw and highlighted values.
499
+ // Raw values are plain text matching what the page breadcrumbs show.
500
+ // Highlighted values show which part of the hierarchy matched the query.
501
+ const levels = [0, 1, 2, 3, 4, 5, 6]
502
+ .map((lvl) => {
503
+ // Raw value: try dot-notation key first, then nested object
504
+ const raw =
505
+ hit[`hierarchy.lvl${lvl}`] || hit.hierarchy?.[`lvl${lvl}`] || '';
506
+ const h = _highlightResult[`hierarchy.lvl${lvl}`];
507
+ const highlighted = h ? sanitizeValue(h.value) : raw;
508
+ return {raw, highlighted};
509
+ })
510
+ .filter((l) => l.raw);
511
+ // Last level is the page/section title; remainder are breadcrumbs
512
+ const titleLevel = levels.pop();
513
+ const product = getProductLabel(parsedURL.pathname);
514
+ // Replace lvl0 ("Documentation") with the product label
515
+ if (product && levels.length > 0) {
516
+ levels[0] = {raw: product, highlighted: product};
517
+ } else if (product) {
518
+ levels.unshift({raw: product, highlighted: product});
519
+ }
520
+ const resultUrl = isRegexpStringMatch(externalUrlRegex, parsedURL.href)
521
+ ? parsedURL.href
522
+ : parsedURL.pathname + parsedURL.hash;
523
+ return {
524
+ title: titleLevel?.highlighted || '',
525
+ url: resultUrl,
526
+ summary: snippet.content?.value
527
+ ? `${sanitizeValue(snippet.content.value)}...`
528
+ : '',
529
+ // Include all levels (parent categories + current page/section)
530
+ // so the breadcrumb matches the full path shown on the page.
531
+ breadcrumbs: [...levels, ...(titleLevel ? [titleLevel] : [])].map(
532
+ (l) => l.raw,
533
+ ),
534
+ };
535
+ });
536
+ searchResultStateDispatcher({
537
+ type: 'update',
538
+ value: {
539
+ items,
540
+ query,
541
+ totalResults: nbHits,
542
+ totalPages: nbPages,
543
+ lastPage: page,
544
+ hasMore: nbPages > page + 1,
545
+ loading: false,
546
+ },
547
+ });
548
+ }
549
+ function handleError(e) {
550
+ console.error(e);
551
+ }
552
+ algoliaHelper.on('result', handleResult);
553
+ // @ts-ignore — 'error' is a valid event but missing from type definitions
554
+ algoliaHelper.on('error', handleError);
555
+ return () => {
556
+ algoliaHelper.removeAllListeners('result');
557
+ algoliaHelper.removeAllListeners('error');
558
+ };
559
+ }, [algoliaHelper]); // algoliaHelper is stable (useMemo with []), so this runs once
560
+ const [loaderRef, setLoaderRef] = useState(null);
561
+ const prevY = useRef(0);
562
+ const observer = useRef(
563
+ ExecutionEnvironment.canUseIntersectionObserver &&
564
+ new IntersectionObserver(
565
+ (entries) => {
566
+ const entry = entries[0];
567
+ if (!entry) return;
568
+ const {
569
+ isIntersecting,
570
+ boundingClientRect: {y: currentY},
571
+ } = entry;
572
+ if (isIntersecting && prevY.current > currentY) {
573
+ searchResultStateDispatcher({type: 'advance'});
574
+ }
575
+ prevY.current = currentY;
576
+ },
577
+ {threshold: 1},
578
+ ),
579
+ );
580
+ const getTitle = () =>
581
+ searchQuery
582
+ ? translate(
583
+ {
584
+ id: 'theme.SearchPage.existingResultsTitle',
585
+ message: 'Search results for "{query}"',
586
+ description: 'The search page title for non-empty query',
587
+ },
588
+ {query: searchQuery},
589
+ )
590
+ : translate({
591
+ id: 'theme.SearchPage.emptyResultsTitle',
592
+ message: 'Search the documentation',
593
+ description: 'The search page title for empty query',
594
+ });
595
+ const makeSearch = useEvent((page = 0) => {
596
+ if (contextualSearch) {
597
+ algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
598
+ algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale);
599
+ Object.entries(docsSearchVersionsHelpers.searchVersions).forEach(
600
+ ([pluginId, searchVersion]) => {
601
+ algoliaHelper.addDisjunctiveFacetRefinement(
602
+ 'docusaurus_tag',
603
+ `docs-${pluginId}-${searchVersion}`,
604
+ );
605
+ },
606
+ );
607
+ }
608
+ algoliaHelper.setQuery(searchQuery).setPage(page).search();
609
+ });
610
+ useEffect(() => {
611
+ if (!loaderRef) {
612
+ return undefined;
613
+ }
614
+ const currentObserver = observer.current;
615
+ if (currentObserver) {
616
+ currentObserver.observe(loaderRef);
617
+ return () => currentObserver.unobserve(loaderRef);
618
+ }
619
+ return () => true;
620
+ }, [loaderRef]);
621
+ useEffect(() => {
622
+ searchResultStateDispatcher({type: 'reset'});
623
+ if (searchQuery) {
624
+ searchResultStateDispatcher({type: 'loading'});
625
+ makeSearch();
626
+ }
627
+ }, [
628
+ searchQuery,
629
+ docsSearchVersionsHelpers.searchVersions,
630
+ makeSearch,
631
+ selectedFilter,
632
+ ]);
633
+ useEffect(() => {
634
+ if (!searchResultState.lastPage || searchResultState.lastPage === 0) {
635
+ return;
636
+ }
637
+ makeSearch(searchResultState.lastPage);
638
+ }, [makeSearch, searchResultState.lastPage]);
639
+ return (
640
+ <Layout>
641
+ <Head>
642
+ <title>{useTitleFormatter(getTitle())}</title>
643
+ {/*
644
+ We should not index search pages
645
+ See https://github.com/facebook/docusaurus/pull/3233
646
+ */}
647
+ <meta property="robots" content="noindex, follow" />
648
+ </Head>
649
+
650
+ <div className="container margin-vert--lg">
651
+ <h1>{getTitle()}</h1>
652
+
653
+ <form
654
+ onSubmit={(e) => {
655
+ e.preventDefault();
656
+ setSearchQuery(inputValue);
657
+ }}>
658
+ <div className={styles.searchQueryColumn}>
659
+ <div className={styles.searchInputRow}>
660
+ <input
661
+ type="search"
662
+ name="q"
663
+ className={styles.searchQueryInput}
664
+ placeholder={translate({
665
+ id: 'theme.SearchPage.inputPlaceholder',
666
+ message: 'Type your search here',
667
+ description: 'The placeholder for search page input',
668
+ })}
669
+ aria-label={translate({
670
+ id: 'theme.SearchPage.inputLabel',
671
+ message: 'Search',
672
+ description: 'The ARIA label for search page input',
673
+ })}
674
+ onChange={(e) => setInputValue(e.target.value)}
675
+ value={inputValue}
676
+ autoComplete="off"
677
+ autoFocus
678
+ />
679
+ {productOptions.length > 0 && (
680
+ <FilterSelect
681
+ value={selectedFilter}
682
+ onChange={setSelectedFilter}
683
+ options={productOptions}
684
+ />
685
+ )}
686
+ <button
687
+ type="submit"
688
+ className={styles.searchQueryButton}
689
+ aria-label={translate({
690
+ id: 'theme.SearchPage.searchButtonLabel',
691
+ message: 'Search',
692
+ description: 'The ARIA label for the search button',
693
+ })}>
694
+ <Translate
695
+ id="theme.SearchPage.searchButton"
696
+ description="The label for the search button">
697
+ Search
698
+ </Translate>
699
+ </button>
700
+ </div>
701
+ </div>
702
+ </form>
703
+
704
+ <div className="row">
705
+ <div className={clsx('col', 'col--8', styles.searchResultsColumn)}>
706
+ {!!searchResultState.totalResults &&
707
+ documentsFoundPlural(searchResultState.totalResults)}
708
+ </div>
709
+
710
+ <div
711
+ className={clsx(
712
+ 'col',
713
+ 'col--4',
714
+ 'text--right',
715
+ styles.searchLogoColumn,
716
+ )}>
717
+ <a
718
+ target="_blank"
719
+ rel="noopener noreferrer"
720
+ href={`https://typesense.org/?utm_medium=referral&utm_content=powered_by&utm_campaign=docsearch`}
721
+ aria-label={translate({
722
+ id: 'theme.SearchPage.typesenseLabel',
723
+ message: 'Search by Typesense',
724
+ description: 'The ARIA label for Typesense mention',
725
+ })}>
726
+ <svg
727
+ fill="none"
728
+ height="21"
729
+ viewBox="0 0 141 21"
730
+ width="141"
731
+ xmlns="http://www.w3.org/2000/svg">
732
+ <clipPath id="a">
733
+ <path d="m0 0h141v21h-141z" />
734
+ </clipPath>
735
+ <g clipPath="url(#a)">
736
+ <g fill="#1035bc">
737
+ <path d="m62.0647 6.453c.0371.19.0557.37367.0557.551 0 .16467-.0186.342-.0557.532l-2.3561-.019v6.384c0 .532.2412.798.7235.798h1.41c.0866.2153.1299.4307.1299.646s-.0124.3483-.0371.399c-.569.076-1.1564.114-1.7625.114-1.1997 0-1.7995-.5257-1.7995-1.577v-6.764l-1.3172.019c-.0371-.19-.0557-.36733-.0557-.532 0-.17733.0186-.361.0557-.551l1.3172.019v-1.995c0-.342.0494-.58267.1484-.722.0989-.152.2906-.228.5751-.228h.5009l.1113.114v2.85z" />
738
+ <path d="m71.0419 6.548-2.5416 8.911c-.47 1.634-.9709 2.7867-1.5027 3.458s-1.3296 1.007-2.3932 1.007c-.5442 0-1.0452-.0823-1.5028-.247-.0371-.3547.0619-.6967.2969-1.026.3834.1393.7915.209 1.2244.209.6555 0 1.1564-.228 1.5027-.684s.6617-1.1653.9462-2.128l.0556-.19c-.3215-.0253-.5689-.1013-.742-.228-.1608-.1267-.2969-.361-.4082-.703l-2.5973-8.36c.3834-.16467.6555-.247.8163-.247.3587 0 .5999.22167.7235.665l1.4657 4.769c.0494.152.3339 1.14.8534 2.964.0247.0887.0865.133.1855.133l2.2633-8.398c.1608-.05067.3711-.076.6308-.076.2721 0 .5009.038.6864.114z" />
739
+ <path d="m74.6067 15.155v3.762c0 .342-.0495.5827-.1484.722-.0989.152-.2968.228-.5937.228h-.5009l-.1113-.114v-13.243l.1113-.114h.4824c.2968 0 .4947.08233.5936.247.1114.152.167.40533.167.76v.095c.7421-.84867 1.6264-1.273 2.653-1.273 1.0513 0 1.8428.437 2.3746 1.311.5319.86133.7978 2.05834.7978 3.591 0 .7473-.099 1.4187-.2968 2.014-.1856.5953-.4391 1.102-.7607 1.52-.3092.4053-.6679.722-1.076.95-.4082.2153-.8287.323-1.2616.323-.8534 0-1.6635-.2597-2.4303-.779zm0-6.175v4.883c.7545.57 1.4656.855 2.1335.855s1.2183-.304 1.6512-.912c.4328-.608.6493-1.5263.6493-2.755 0-.608-.0557-1.13366-.167-1.577-.0989-.456-.235-.82967-.4081-1.121-.1732-.304-.3773-.52567-.6123-.665-.2226-.152-.4638-.228-.7235-.228-.4947 0-.9647.133-1.41.399-.4452.266-.8163.63967-1.1131 1.121z" />
740
+ <path d="m89.8263 11.545h-5.7512c.0619 2.1533.8596 3.23 2.3932 3.23.8411 0 1.7378-.266 2.6901-.798.2721.2533.4391.5763.5009.969-1.0142.7093-2.152 1.064-3.4136 1.064-.6431 0-1.1935-.1203-1.6511-.361-.4576-.2533-.8349-.5953-1.1317-1.026-.2845-.4433-.4947-.9627-.6308-1.558-.136-.5953-.204-1.2477-.204-1.957 0-.722.0803-1.38067.2411-1.976.1732-.59533.4205-1.10833.7421-1.539s.705-.76633 1.1503-1.007c.4576-.24067.977-.361 1.5583-.361.569 0 1.0761.10767 1.5213.323.4576.20267.8349.48767 1.1317.855.3092.35467.5442.78533.705 1.292.1608.494.2411 1.026.2411 1.596 0 .228-.0123.4497-.0371.665-.0123.2027-.0309.399-.0556.589zm-5.7512-1.083h4.4525v-.247c0-.874-.1793-1.577-.538-2.109s-.8967-.798-1.614-.798c-.705 0-1.2554.285-1.6512.855-.3834.57-.5998 1.33633-.6493 2.299z" />
741
+ <path d="m91.7359 15.117c.0123-.2787.0865-.5827.2226-.912.1484-.342.3154-.608.5009-.798.9771.5447 1.8367.817 2.5787.817.4082 0 .7359-.0823.9833-.247.2597-.1647.3896-.3863.3896-.665 0-.4433-.3339-.798-1.0018-1.064l-1.0389-.399c-1.5584-.5827-2.3376-1.5137-2.3376-2.793 0-.456.0804-.86133.2412-1.216.1731-.36733.4081-.67767.705-.931.3092-.266.674-.46867 1.0945-.608s.8905-.209 1.41-.209c.235 0 .4947.019.7792.057.2968.038.5937.095.8905.171.2968.06333.5813.13933.8534.228s.5071.18367.705.285c0 .31667-.0619.646-.1856.988-.1236.342-.2906.59533-.5009.76-.977-.44333-1.8243-.665-2.5416-.665-.3216 0-.5751.08233-.7606.247-.1856.152-.2783.35467-.2783.608 0 .39267.3092.703.9276.931l1.1317.418c.8163.2913 1.4223.6903 1.8181 1.197.3957.5067.5936 1.0957.5936 1.767 0 .8993-.3277 1.6213-.9832 2.166-.6555.532-1.5955.798-2.8199.798-1.1998 0-2.3253-.3103-3.3765-.931z" />
742
+ <path d="m107.996 11.868h-5.121c.037.6967.192 1.2477.464 1.653.284.3927.773.589 1.466.589.717 0 1.539-.2153 2.467-.646.359.38.587.8803.686 1.501-.989.722-2.176 1.083-3.562 1.083-1.311 0-2.306-.4117-2.987-1.235-.667-.836-1.001-2.071-1.001-3.705 0-.76.086-1.444.259-2.052.174-.62067.427-1.14633.761-1.577.334-.44333.742-.78533 1.224-1.026.483-.24067 1.033-.361 1.652-.361.63 0 1.187.10133 1.669.304.483.19.891.46867 1.225.836.334.35467.581.779.742 1.273.173.494.26 1.03233.26 1.615 0 .3167-.019.6207-.056.912-.037.2787-.087.5573-.148.836zm-3.581-3.99c-.965 0-1.484.74733-1.558 2.242h3.079v-.228c0-.608-.123-1.09567-.371-1.463-.247-.36733-.631-.551-1.15-.551z" />
743
+ <path d="m118.163 9.436v4.142c0 .8107.13 1.4123.39 1.805-.396.3547-.872.532-1.429.532-.532 0-.897-.1203-1.095-.361-.197-.2533-.296-.646-.296-1.178v-4.427c0-.57-.068-.969-.204-1.197-.137-.228-.39-.342-.761-.342-.656 0-1.268.304-1.837.912v6.46c-.185.038-.383.0633-.593.076-.198.0127-.402.019-.613.019-.21 0-.42-.0063-.63-.019-.198-.0127-.39-.038-.576-.076v-9.405l.112-.133h.927c.693 0 1.126.38 1.299 1.14.903-.798 1.8-1.197 2.69-1.197.891 0 1.546.29767 1.967.893.433.58267.649 1.368.649 2.356z" />
744
+ <path d="m120.109 15.117c.012-.2787.087-.5827.223-.912.148-.342.315-.608.501-.798.977.5447 1.836.817 2.578.817.408 0 .736-.0823.984-.247.259-.1647.389-.3863.389-.665 0-.4433-.334-.798-1.002-1.064l-1.039-.399c-1.558-.5827-2.337-1.5137-2.337-2.793 0-.456.08-.86133.241-1.216.173-.36733.408-.67767.705-.931.309-.266.674-.46867 1.095-.608.42-.13933.89-.209 1.41-.209.235 0 .494.019.779.057.297.038.593.095.89.171.297.06333.582.13933.854.228s.507.18367.705.285c0 .31667-.062.646-.186.988s-.291.59533-.501.76c-.977-.44333-1.824-.665-2.541-.665-.322 0-.576.08233-.761.247-.186.152-.278.35467-.278.608 0 .39267.309.703.927.931l1.132.418c.816.2913 1.422.6903 1.818 1.197s.594 1.0957.594 1.767c0 .8993-.328 1.6213-.984 2.166-.655.532-1.595.798-2.819.798-1.2 0-2.326-.3103-3.377-.931z" />
745
+ <path d="m136.369 11.868h-5.121c.037.6967.192 1.2477.464 1.653.285.3927.773.589 1.466.589.717 0 1.54-.2153 2.467-.646.359.38.588.8803.687 1.501-.99.722-2.177 1.083-3.562 1.083-1.311 0-2.307-.4117-2.987-1.235-.668-.836-1.002-2.071-1.002-3.705 0-.76.086-1.444.26-2.052.173-.62067.426-1.14633.76-1.577.334-.44333.742-.78533 1.225-1.026.482-.24067 1.032-.361 1.651-.361.631 0 1.187.10133 1.67.304.482.19.89.46867 1.224.836.334.35467.581.779.742 1.273.173.494.26 1.03233.26 1.615 0 .3167-.019.6207-.056.912-.037.2787-.086.5573-.148.836zm-3.581-3.99c-.965 0-1.484.74733-1.558 2.242h3.079v-.228c0-.608-.123-1.09567-.371-1.463-.247-.36733-.63-.551-1.15-.551z" />
746
+ <path d="m139.245 18.442v-17.385c.186-.038.396-.057.631-.057.247 0 .476.019.686.057v17.385c-.21.038-.439.057-.686.057-.235 0-.445-.019-.631-.057z" />
747
+ </g>
748
+ <path
749
+ d="m2.648 14.604c.216.144.556.272 1.02.384s.872.168 1.224.168c.592 0 1.104-.092 1.536-.276.44-.184.772-.436.996-.756.232-.32.348-.688.348-1.104 0-.384-.08-.712-.24-.984-.16-.28-.396-.528-.708-.744-.304-.216-.708-.44-1.212-.672-.56-.256-.984-.468-1.272-.636s-.512-.352-.672-.552c-.152-.208-.228-.456-.228-.744 0-.384.156-.684.468-.9.32-.216.744-.324 1.272-.324.352 0 .648.036.888.108.248.072.52.176.816.312l.324-.732c-.28-.144-.604-.264-.972-.36-.36-.096-.732-.144-1.116-.144-.52 0-.98.092-1.38.276-.392.176-.696.42-.912.732-.208.312-.312.66-.312 1.044 0 .544.172 1.004.516 1.38.352.376.9.724 1.644 1.044.52.224.928.424 1.224.6.304.168.536.36.696.576.16.208.24.452.24.732 0 .392-.172.712-.516.96-.336.24-.816.36-1.44.36-.352 0-.712-.048-1.08-.144-.36-.104-.668-.22-.924-.348zm11.0963-2.364c0-.96-.204-1.736-.612-2.328-.4-.592-1.024-.888-1.872-.888-.56 0-1.048.132-1.46396.396-.408.256-.72.616-.936 1.08-.208.456-.312.98-.312 1.572 0 .936.26 1.684.78 2.244s1.27596.84 2.26796.84c.4 0 .764-.052 1.092-.156.328-.112.656-.26.984-.444l-.3-.696c-.36.176-.672.304-.936.384-.256.08-.54.12-.852.12-.688 0-1.2-.188-1.536-.564-.32796-.384-.50396-.904-.52796-1.56zm-4.19996-.648c.056-.544.224-.972.50396-1.284.288-.32.68-.48 1.176-.48.92 0 1.448.588 1.584 1.764zm5.84426-1.344c.288-.128.552-.224.792-.288.248-.072.544-.108.888-.108.44 0 .76.124.96.372.208.248.312.588.312 1.02v.324h-1.5c-.632 0-1.14.156-1.524.468s-.576.748-.576 1.308c0 .536.168.972.504 1.308.344.336.84.504 1.488.504.304 0 .616-.072.936-.216.32-.152.588-.328.804-.528l.12.588h.708v-4.02c0-.376-.096-.712-.288-1.008s-.46-.528-.804-.696-.736-.252-1.176-.252c-.264 0-.6.048-1.008.144-.4.096-.704.216-.912.36zm.228 3.096c0-.32.104-.588.312-.804.216-.224.512-.336.888-.336h1.536v1.32c-.216.256-.468.464-.756.624-.28.16-.576.24-.888.24-.36 0-.632-.104-.816-.312s-.276-.452-.276-.732zm6.0874-2.352c.272-.28.604-.524.996-.732.392-.216.748-.324 1.068-.324l-.228-.756c-.28 0-.604.1-.972.3s-.684.412-.948.636l-.3-.936h-.564v5.82h.948zm6.9986 2.952c-.28.144-.532.248-.756.312s-.512.096-.864.096c-.584 0-1.036-.212-1.356-.636s-.48-.976-.48-1.656c.008-.648.18-1.18.516-1.596.336-.424.792-.636 1.368-.636.336 0 .608.032.816.096.216.064.484.164.804.3l.288-.672c-.232-.152-.54-.276-.924-.372-.376-.104-.696-.156-.96-.156-.576 0-1.08.132-1.512.396-.432.256-.768.616-1.008 1.08-.232.456-.348.98-.348 1.572 0 .6.112 1.136.336 1.608.224.464.548.828.972 1.092.424.256.924.384 1.5.384.264 0 .588-.052.972-.156.384-.096.696-.22.936-.372zm4.6201-4.944c.616 0 1.072.188 1.368.564.304.368.456.864.456 1.488v3.936h-.948v-3.804c0-.432-.08-.768-.24-1.008s-.428-.36-.804-.36c-.288 0-.616.1-.984.3-.36.2-.68.452-.96.756v4.128h-.948v-8.352l.948-.12v3.396c.28-.272.612-.492.996-.66.392-.176.764-.264 1.116-.264zm8.5136.024c.864 0 1.54.284 2.028.852.496.56.744 1.304.744 2.232 0 .592-.116 1.12-.348 1.584-.224.456-.548.816-.972 1.08-.416.256-.908.384-1.476.384-.24 0-.496-.052-.768-.156-.264-.096-.512-.236-.744-.42l-.204.42h-.564v-8.352l.948-.12v2.94c.216-.144.444-.252.684-.324.24-.08.464-.12.672-.12zm0 5.328c.576 0 1.02-.208 1.332-.624s.472-.952.48-1.608c0-.688-.156-1.24-.468-1.656-.304-.424-.748-.636-1.332-.636-.288 0-.54.044-.756.132s-.42.224-.612.408v3.468c.192.168.396.296.612.384s.464.132.744.132zm5.0915 1.608c-.088.24-.224.42-.408.54-.176.128-.452.28-.828.456l.288.684c.424-.088.796-.26 1.116-.516.328-.256.56-.564.696-.924l2.568-7.02h-.96l-1.668 4.5-1.764-4.68-.84.36 2.16 5.604z"
750
+ fill="#000"
751
+ fillOpacity=".25"
752
+ />
753
+ </g>
754
+ </svg>
755
+ </a>
756
+ </div>
757
+ </div>
758
+
759
+ {searchResultState.items.length > 0 ? (
760
+ <main>
761
+ {searchResultState.items.map(
762
+ ({title, url, summary, breadcrumbs}, i) => (
763
+ <article key={i} className={styles.searchResultItem}>
764
+ <h2 className={styles.searchResultItemHeading}>
765
+ <Link to={url} dangerouslySetInnerHTML={{__html: title}} />
766
+ </h2>
767
+
768
+ {breadcrumbs.length > 0 && (
769
+ <nav aria-label="breadcrumbs">
770
+ <ul
771
+ className={clsx(
772
+ 'breadcrumbs',
773
+ styles.searchResultItemPath,
774
+ )}>
775
+ {breadcrumbs.map((html, index) => (
776
+ <li
777
+ key={index}
778
+ className="breadcrumbs__item"
779
+ // Developer provided the HTML, so assume it's safe.
780
+ // eslint-disable-next-line react/no-danger
781
+ dangerouslySetInnerHTML={{__html: html}}
782
+ />
783
+ ))}
784
+ </ul>
785
+ </nav>
786
+ )}
787
+
788
+ {summary && (
789
+ <p
790
+ className={styles.searchResultItemSummary}
791
+ // Developer provided the HTML, so assume it's safe.
792
+ // eslint-disable-next-line react/no-danger
793
+ dangerouslySetInnerHTML={{__html: summary}}
794
+ />
795
+ )}
796
+ </article>
797
+ ),
798
+ )}
799
+ </main>
800
+ ) : (
801
+ [
802
+ searchQuery && !searchResultState.loading && (
803
+ <p key="no-results">
804
+ <Translate
805
+ id="theme.SearchPage.noResultsText"
806
+ description="The paragraph for empty search result">
807
+ No results were found
808
+ </Translate>
809
+ </p>
810
+ ),
811
+ !!searchResultState.loading && (
812
+ <div key="spinner" className={styles.loadingSpinner} />
813
+ ),
814
+ ]
815
+ )}
816
+
817
+ {searchResultState.hasMore && (
818
+ <div className={styles.loader} ref={setLoaderRef}>
819
+ <Translate
820
+ id="theme.SearchPage.fetchingNewResults"
821
+ description="The paragraph for fetching new search results">
822
+ Fetching new results...
823
+ </Translate>
824
+ </div>
825
+ )}
826
+ </div>
827
+ </Layout>
828
+ );
829
+ }
830
+ export default function SearchPage() {
831
+ return (
832
+ <HtmlClassNameProvider className="search-page-wrapper">
833
+ <SearchPageContent />
834
+ </HtmlClassNameProvider>
835
+ );
836
+ }