fix(deps): replace flexsearch-ts with orama

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2024-01-15 22:35:55 +01:00 committed by David Mehren
parent 931ce68a32
commit 07162b7807
25 changed files with 266 additions and 215 deletions

View file

@ -681,7 +681,7 @@
"noSelection": "Select an entry on the left side to show the instructions."
},
"categories": {
"basic": "Basics",
"basics": "Basics",
"other": "Other",
"embedding": "Embedding",
"charts": "Charts & Diagrams"

View file

@ -19,9 +19,9 @@
"test:e2e:open": "cypress open",
"test:e2e": "cypress run --browser chrome",
"test:e2e:ci": "cypress run --browser chrome --record true --parallel --group \"chrome\"",
"test:watch": "cross-env NODE_ENV=test jest --watch",
"test:ci": "cross-env NODE_ENV=test jest --coverage",
"test": "cross-env NODE_ENV=test jest"
"test:watch": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" NODE_ENV=test jest --watch",
"test:ci": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" NODE_ENV=test jest --coverage",
"test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" NODE_ENV=test jest"
},
"browserslist": {
"production": [
@ -53,6 +53,7 @@
"@hedgedoc/html-to-react": "workspace:html-to-react",
"@hedgedoc/markdown-it-plugins": "workspace:markdown-it-plugins",
"@mrdrogdrog/optional": "1.2.1",
"@orama/orama": "2.0.0-beta.12",
"@react-hook/resize-observer": "1.2.6",
"@redux-devtools/core": "3.13.3",
"@reduxjs/toolkit": "1.9.7",
@ -75,7 +76,6 @@
"eventemitter2": "6.4.9",
"fast-deep-equal": "3.1.3",
"firacode": "6.2.0",
"flexsearch-ts": "0.7.35",
"flowchart.js": "1.17.1",
"highlight.js": "11.9.0",
"htmlparser2": "9.0.0",

View file

@ -5,8 +5,8 @@
*/
import { CategoryAccordion } from './category-accordion'
import { CheatsheetEntryPane } from './cheatsheet-entry-pane'
import type { CheatsheetSingleEntry, CheatsheetExtension } from './cheatsheet-extension'
import { hasCheatsheetTopics } from './cheatsheet-extension'
import type { CheatsheetExtension, CheatsheetEntry } from './cheatsheet-extension'
import { isCheatsheetMultiEntry } from './cheatsheet-extension'
import { CheatsheetSearch } from './cheatsheet-search'
import styles from './cheatsheet.module.scss'
import { TopicSelection } from './topic-selection'
@ -20,11 +20,11 @@ import { Trans } from 'react-i18next'
export const CheatsheetContent: React.FC = () => {
const [visibleExtensions, setVisibleExtensions] = useState<CheatsheetExtension[]>([])
const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>()
const [selectedEntry, setSelectedEntry] = useState<CheatsheetSingleEntry>()
const [selectedEntry, setSelectedEntry] = useState<CheatsheetEntry>()
const changeExtension = useCallback((value: CheatsheetExtension) => {
setSelectedExtension(value)
setSelectedEntry(hasCheatsheetTopics(value) ? value.topics[0] : value)
setSelectedEntry(isCheatsheetMultiEntry(value) ? value.topics[0] : value)
}, [])
return (
@ -44,10 +44,10 @@ export const CheatsheetContent: React.FC = () => {
selectedEntry={selectedEntry}
setSelectedEntry={setSelectedEntry}
/>
{selectedEntry !== undefined ? (
{selectedEntry !== undefined && selectedExtension !== undefined ? (
<CheatsheetEntryPane
rootI18nKey={hasCheatsheetTopics(selectedExtension) ? selectedExtension.i18nKey : undefined}
extension={selectedEntry}
i18nKeyPrefix={isCheatsheetMultiEntry(selectedExtension) ? selectedExtension.i18nKey : undefined}
entry={selectedEntry}
/>
) : (
<span>

View file

@ -9,7 +9,7 @@ import { RendererIframe } from '../common/renderer-iframe/renderer-iframe'
import { EditorToRendererCommunicatorContextProvider } from '../editor-page/render-context/editor-to-renderer-communicator-context-provider'
import { ExtensionEventEmitterProvider } from '../markdown-renderer/hooks/use-extension-event-emitter'
import { RendererType } from '../render-page/window-post-message-communicator/rendering-message'
import type { CheatsheetSingleEntry } from './cheatsheet-extension'
import type { CheatsheetEntry } from './cheatsheet-extension'
import { ReadMoreLinkItem } from './read-more-link-item'
import { useComponentsFromAppExtensions } from './use-components-from-app-extensions'
import MarkdownIt from 'markdown-it'
@ -18,8 +18,8 @@ import { ListGroupItem } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
interface CheatsheetRendererProps {
rootI18nKey?: string
extension: CheatsheetSingleEntry
i18nKeyPrefix?: string
entry: CheatsheetEntry
}
/**
@ -28,7 +28,7 @@ interface CheatsheetRendererProps {
* @param extension The extension to render
* @param rootI18nKey An additional i18n namespace
*/
export const CheatsheetEntryPane: React.FC<CheatsheetRendererProps> = ({ extension, rootI18nKey }) => {
export const CheatsheetEntryPane: React.FC<CheatsheetRendererProps> = ({ entry, i18nKeyPrefix }) => {
const { t } = useTranslation()
const [content, setContent] = useState('')
@ -36,13 +36,13 @@ export const CheatsheetEntryPane: React.FC<CheatsheetRendererProps> = ({ extensi
const lines = useMemo(() => content.split('\n'), [content])
const i18nPrefix = useMemo(
() => `cheatsheet.${rootI18nKey ? `${rootI18nKey}.` : ''}${extension.i18nKey}.`,
[extension.i18nKey, rootI18nKey]
() => `cheatsheet.${i18nKeyPrefix !== undefined ? i18nKeyPrefix + '.' : ''}${entry.i18nKey}.`,
[entry.i18nKey, i18nKeyPrefix]
)
useEffect(() => {
setContent(t(`${i18nPrefix}example`) ?? '')
}, [extension, i18nPrefix, t])
}, [entry, i18nPrefix, t])
const cheatsheetExtensionComponents = useComponentsFromAppExtensions(setContent)
@ -62,7 +62,7 @@ export const CheatsheetEntryPane: React.FC<CheatsheetRendererProps> = ({ extensi
</h4>
{descriptionElements}
</ListGroupItem>
<ReadMoreLinkItem url={extension.readMoreUrl}></ReadMoreLinkItem>
<ReadMoreLinkItem url={entry.readMoreUrl}></ReadMoreLinkItem>
<ListGroupItem>
<h4>
<Trans i18nKey={'cheatsheet.modal.headlines.exampleInput'} />

View file

@ -9,29 +9,28 @@ export interface CheatsheetExtensionComponentProps {
setContent: (dispatcher: string | ((prevState: string) => string)) => void
}
export type CheatsheetExtension = CheatsheetSingleEntry | CheatsheetEntryWithTopics
export type CheatsheetExtension = CheatsheetSingleEntry | CheatsheetMultiEntry
/**
* Determine if a given {@link CheatsheetExtension} is a {@link CheatsheetEntryWithTopics} or just a {@link CheatsheetSingleEntry}.
* Determine if a given {@link CheatsheetExtension} is a {@link CheatsheetSingleEntry} or a {@link CheatsheetMultiEntry}.
*
* @param extension The extension in question
* @return boolean
*/
export const hasCheatsheetTopics = (
export const isCheatsheetMultiEntry = (
extension: CheatsheetExtension | undefined
): extension is CheatsheetEntryWithTopics => {
return (extension as CheatsheetEntryWithTopics)?.topics !== undefined
): extension is CheatsheetMultiEntry => {
return extension !== undefined && typeof (extension as CheatsheetMultiEntry).topics === 'object'
}
/**
* This is an entry with just a name and a bunch of different topics to discuss.
*
* e.g 'basics.headlines' with the topics 'hashtag' and 'equal'
*/
export interface CheatsheetEntryWithTopics {
export interface CheatsheetMultiEntry {
i18nKey: string
categoryI18nKey?: string
topics: CheatsheetSingleEntry[]
categoryI18nKey: string
topics: CheatsheetEntry[]
}
export interface CheatsheetSingleEntry extends CheatsheetEntry {
categoryI18nKey: string
}
/**
@ -42,10 +41,8 @@ export interface CheatsheetEntryWithTopics {
*
* e.g 'basics.basicFormatting'
*/
export interface CheatsheetSingleEntry {
export interface CheatsheetEntry {
i18nKey: string
categoryI18nKey?: string
cheatsheetExtensionComponent?: React.FC<CheatsheetExtensionComponentProps>
readMoreUrl?: URL
}

View file

@ -4,33 +4,18 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { allAppExtensions } from '../../extensions/all-app-extensions'
import type { SearchIndexEntry } from '../../hooks/common/use-document-search'
import { useDocumentSearch } from '../../hooks/common/use-document-search'
import { useCheatsheetSearch } from '../../hooks/common/use-cheatsheet-search'
import { useOnInputChange } from '../../hooks/common/use-on-input-change'
import { useTranslatedText } from '../../hooks/common/use-translated-text'
import { UiIcon } from '../common/icons/ui-icon'
import type { CheatsheetSingleEntry, CheatsheetExtension } from './cheatsheet-extension'
import { hasCheatsheetTopics } from './cheatsheet-extension'
import type { CheatsheetExtension } from './cheatsheet-extension'
import { isCheatsheetMultiEntry } from './cheatsheet-extension'
import styles from './cheatsheet.module.scss'
import type { IndexOptionsForDocumentSearch, StoreOption } from 'flexsearch-ts'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { FormControl, InputGroup } from 'react-bootstrap'
import { X } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
interface CheatsheetSearchIndexEntry extends SearchIndexEntry {
title: string
description: string
example: string
}
const searchOptions: IndexOptionsForDocumentSearch<CheatsheetSearchIndexEntry, StoreOption> = {
document: {
id: 'id',
field: ['title', 'description', 'example']
}
}
export interface CheatsheetSearchProps {
setVisibleExtensions: React.Dispatch<React.SetStateAction<CheatsheetExtension[]>>
}
@ -48,38 +33,43 @@ export const CheatsheetSearch: React.FC<CheatsheetSearchProps> = ({ setVisibleEx
() => allAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()),
[]
)
const buildSearchIndexEntry = useCallback(
(entry: CheatsheetSingleEntry, rootI18nKey: string | undefined = undefined): CheatsheetSearchIndexEntry => {
const rootI18nKeyWithDot = rootI18nKey ? `${rootI18nKey}.` : ''
const placeholderText = useTranslatedText('cheatsheet.search')
const buildEntry = useCallback(
(i18nKey: string, extensionId: string) => {
return {
id: rootI18nKey ? rootI18nKey : entry.i18nKey,
title: t(`cheatsheet.${rootI18nKeyWithDot}${entry.i18nKey}.title`),
description: t(`cheatsheet.${rootI18nKeyWithDot}${entry.i18nKey}.description`),
example: t(`cheatsheet.${rootI18nKeyWithDot}${entry.i18nKey}.example`)
id: i18nKey,
extensionId: extensionId,
title: t(`cheatsheet.${i18nKey}.title`),
description: t(`cheatsheet.${i18nKey}.description`),
example: t(`cheatsheet.${i18nKey}.example`)
}
},
[t]
)
const placeholderText = useTranslatedText('cheatsheet.search')
const cheatsheetSearchIndexEntries = useMemo(
() =>
allCheatsheetExtensions.flatMap((entry) => {
if (hasCheatsheetTopics(entry)) {
return entry.topics.map((innerEntry) => buildSearchIndexEntry(innerEntry, entry.i18nKey))
allCheatsheetExtensions.flatMap((extension) => {
if (isCheatsheetMultiEntry(extension)) {
return extension.topics.map((entry) => {
return buildEntry(extension.i18nKey + '.' + entry.i18nKey, extension.i18nKey)
})
} else {
return buildEntry(extension.i18nKey, extension.i18nKey)
}
return buildSearchIndexEntry(entry)
}),
[buildSearchIndexEntry, allCheatsheetExtensions]
[allCheatsheetExtensions, buildEntry]
)
const searchResults = useDocumentSearch(cheatsheetSearchIndexEntries, searchOptions, searchTerm)
const searchResults = useCheatsheetSearch(cheatsheetSearchIndexEntries, searchTerm)
useEffect(() => {
if (searchTerm.trim() === '') {
setVisibleExtensions(allCheatsheetExtensions)
return
}
const mappedResults = searchResults.flatMap((result) => result.result)
const extensionResults = allCheatsheetExtensions.filter((extension) => {
return mappedResults.includes(extension.i18nKey)
return (
searchResults.find((result) => {
return result.extensionId === extension.i18nKey
}) !== undefined
)
})
setVisibleExtensions(extensionResults)
}, [allCheatsheetExtensions, searchResults, searchTerm, setVisibleExtensions])

View file

@ -3,16 +3,16 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { CheatsheetSingleEntry, CheatsheetExtension } from './cheatsheet-extension'
import { hasCheatsheetTopics } from './cheatsheet-extension'
import type { CheatsheetExtension, CheatsheetEntry } from './cheatsheet-extension'
import { isCheatsheetMultiEntry } from './cheatsheet-extension'
import React, { useMemo } from 'react'
import { Button, ButtonGroup, ListGroupItem } from 'react-bootstrap'
import { Trans } from 'react-i18next'
interface EntrySelectionProps {
extension: CheatsheetExtension | undefined
selectedEntry: CheatsheetSingleEntry | undefined
setSelectedEntry: (value: CheatsheetSingleEntry) => void
selectedEntry: CheatsheetEntry | undefined
setSelectedEntry: (value: CheatsheetEntry) => void
}
/**
@ -25,7 +25,7 @@ interface EntrySelectionProps {
*/
export const TopicSelection: React.FC<EntrySelectionProps> = ({ extension, selectedEntry, setSelectedEntry }) => {
const listItems = useMemo(() => {
if (!hasCheatsheetTopics(extension)) {
if (!isCheatsheetMultiEntry(extension)) {
return null
}
return extension.topics.map((entry) => (

View file

@ -5,7 +5,7 @@
*/
import { allAppExtensions } from '../../extensions/all-app-extensions'
import type { CheatsheetExtensionComponentProps } from './cheatsheet-extension'
import { hasCheatsheetTopics } from './cheatsheet-extension'
import { isCheatsheetMultiEntry } from './cheatsheet-extension'
import type { ReactElement } from 'react'
import React, { Fragment, useMemo } from 'react'
@ -20,7 +20,7 @@ export const useComponentsFromAppExtensions = (
<Fragment key={'app-extensions'}>
{allAppExtensions
.flatMap((extension) => extension.buildCheatsheetExtensions())
.flatMap((extension) => (hasCheatsheetTopics(extension) ? extension.topics : extension))
.flatMap((extension) => (isCheatsheetMultiEntry(extension) ? extension.topics : extension))
.map((extension) => {
if (extension.cheatsheetExtensionComponent) {
return React.createElement(extension.cheatsheetExtensionComponent, { key: extension.i18nKey, setContent })

View file

@ -22,7 +22,7 @@ export class AlertAppExtension extends AppExtension {
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'alert' }]
return [{ i18nKey: 'alert', categoryI18nKey: 'other' }]
}
buildAutocompletion(): CompletionSource[] {

View file

@ -20,19 +20,19 @@ export class BasicMarkdownSyntaxAppExtension extends AppExtension {
return [
{
i18nKey: 'basics.basicFormatting',
categoryI18nKey: 'basic'
categoryI18nKey: 'basics'
},
{
i18nKey: 'basics.abbreviation',
categoryI18nKey: 'basic'
categoryI18nKey: 'basics'
},
{
i18nKey: 'basics.footnote',
categoryI18nKey: 'basic'
categoryI18nKey: 'basics'
},
{
i18nKey: 'basics.headlines',
categoryI18nKey: 'basic',
categoryI18nKey: 'basics',
topics: [
{
i18nKey: 'hashtag'
@ -44,22 +44,43 @@ export class BasicMarkdownSyntaxAppExtension extends AppExtension {
},
{
i18nKey: 'basics.code',
categoryI18nKey: 'basic',
topics: [{ i18nKey: 'inline' }, { i18nKey: 'block' }]
categoryI18nKey: 'basics',
topics: [
{
i18nKey: 'inline'
},
{
i18nKey: 'block'
}
]
},
{
i18nKey: 'basics.lists',
categoryI18nKey: 'basic',
topics: [{ i18nKey: 'unordered' }, { i18nKey: 'ordered' }]
categoryI18nKey: 'basics',
topics: [
{
i18nKey: 'unordered'
},
{
i18nKey: 'ordered'
}
]
},
{
i18nKey: 'basics.images',
categoryI18nKey: 'basic',
topics: [{ i18nKey: 'basic' }, { i18nKey: 'size' }]
categoryI18nKey: 'basics',
topics: [
{
i18nKey: 'basic'
},
{
i18nKey: 'size'
}
]
},
{
i18nKey: 'basics.links',
categoryI18nKey: 'basic'
categoryI18nKey: 'basics'
}
]
}

View file

@ -22,7 +22,13 @@ export class BlockquoteAppExtension extends AppExtension {
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'blockquoteTags', topics: [{ i18nKey: 'name' }, { i18nKey: 'color' }, { i18nKey: 'time' }] }]
return [
{
i18nKey: 'blockquoteTags',
categoryI18nKey: 'other',
topics: [{ i18nKey: 'name' }, { i18nKey: 'color' }, { i18nKey: 'time' }]
}
]
}
buildAutocompletion(): CompletionSource[] {

View file

@ -20,7 +20,9 @@ export class BootstrapIconAppExtension extends AppExtension {
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'bootstrapIcon', readMoreUrl: new URL('https://icons.getbootstrap.com/') }]
return [
{ i18nKey: 'bootstrapIcon', categoryI18nKey: 'other', readMoreUrl: new URL('https://icons.getbootstrap.com/') }
]
}
buildAutocompletion(): CompletionSource[] {

View file

@ -22,7 +22,7 @@ export class CsvTableAppExtension extends AppExtension {
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'csv', topics: [{ i18nKey: 'table' }, { i18nKey: 'header' }] }]
return [{ i18nKey: 'csv', categoryI18nKey: 'other', topics: [{ i18nKey: 'table' }, { i18nKey: 'header' }] }]
}
buildAutocompletion(): CompletionSource[] {

View file

@ -21,6 +21,7 @@ export class EmojiAppExtension extends AppExtension {
return [
{
i18nKey: 'emoji',
categoryI18nKey: 'other',
readMoreUrl: new URL('https://twemoji.twitter.com/')
}
]

View file

@ -24,6 +24,7 @@ export class HighlightedCodeFenceAppExtension extends AppExtension {
return [
{
i18nKey: 'codeHighlighting',
categoryI18nKey: 'other',
topics: [{ i18nKey: 'language' }, { i18nKey: 'lineNumbers' }, { i18nKey: 'lineWrapping' }]
}
]

View file

@ -19,7 +19,8 @@ export class ImagePlaceholderAppExtension extends AppExtension {
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [
{
i18nKey: 'imagePlaceholder'
i18nKey: 'imagePlaceholder',
categoryI18nKey: 'other'
}
]
}

View file

@ -24,7 +24,7 @@ export class SpoilerAppExtension extends AppExtension {
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'spoiler' }]
return [{ i18nKey: 'spoiler', categoryI18nKey: 'other' }]
}
buildAutocompletion(): CompletionSource[] {

View file

@ -21,6 +21,7 @@ export class TableOfContentsAppExtension extends AppExtension {
return [
{
i18nKey: 'toc',
categoryI18nKey: 'other',
topics: [
{
i18nKey: 'basic'

View file

@ -26,6 +26,6 @@ export class TaskListCheckboxAppExtension extends AppExtension {
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'taskList', cheatsheetExtensionComponent: SetCheckboxInCheatsheet }]
return [{ i18nKey: 'taskList', categoryI18nKey: 'other', cheatsheetExtensionComponent: SetCheckboxInCheatsheet }]
}
}

View file

@ -19,6 +19,6 @@ export class KatexAppExtension extends AppExtension {
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'katex', readMoreUrl: new URL('https://katex.org/') }]
return [{ i18nKey: 'katex', categoryI18nKey: 'other', readMoreUrl: new URL('https://katex.org/') }]
}
}

View file

@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { CheatsheetSearchIndexEntry } from './use-cheatsheet-search'
import { useCheatsheetSearch } from './use-cheatsheet-search'
import { renderHook, waitFor } from '@testing-library/react'
describe('useDocumentSearch', () => {
const searchEntries: CheatsheetSearchIndexEntry[] = [
{
id: 'test1',
extensionId: 'test1',
title: 'title1',
description: 'description1',
example: 'example1'
},
{
id: 'test2',
extensionId: 'test2',
title: 'title2 sub',
description: 'description2',
example: 'example2'
},
{
id: 'test3',
extensionId: 'test3',
title: 'title3 sub',
description: 'description3',
example: 'example3'
}
]
it('returns all entries if no search term is given', async () => {
const { result } = renderHook((searchTerm: string) => useCheatsheetSearch(searchEntries, searchTerm), {
initialProps: ''
})
await waitFor(() => expect(result.current).toStrictEqual(searchEntries))
})
it('results no entries if nothing has been found', async () => {
const { result } = renderHook((searchTerm: string) => useCheatsheetSearch(searchEntries, searchTerm), {
initialProps: 'Foo'
})
await waitFor(() => expect(result.current).toHaveLength(0))
})
it('returns multiple entries if matching', async () => {
const { result } = renderHook((searchTerm: string) => useCheatsheetSearch(searchEntries, searchTerm), {
initialProps: 'sub'
})
await waitFor(() => expect(result.current).toHaveLength(2))
})
})

View file

@ -0,0 +1,80 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect, useState } from 'react'
import { create, insert, search } from '@orama/orama'
import { useAsync } from 'react-use'
import { Logger } from '../../utils/logger'
export interface CheatsheetSearchIndexEntry {
readonly id: string
readonly title: string
readonly description: string
readonly example: string
readonly extensionId: string
}
const logger = new Logger('Cheatsheet Search')
/**
* Generate document search index and provide functions to search.
*
* @param entries The list of entries to build the search index from
* @param searchTerm What to search for
* @return An array of the search results
*/
export const useCheatsheetSearch = (
entries: CheatsheetSearchIndexEntry[],
searchTerm: string
): CheatsheetSearchIndexEntry[] => {
const [results, setResults] = useState<CheatsheetSearchIndexEntry[]>([])
const {
value: searchIndex,
loading: searchIndexLoading,
error: searchIndexError
} = useAsync(async () => {
const db = await create({
schema: {
id: 'string',
title: 'string',
description: 'string',
example: 'string',
extensionId: 'string'
} as const
})
const adds = entries.map((entry) => {
logger.debug('Add to search entry:', entry)
return insert(db, entry)
})
await Promise.all(adds)
return db
}, [entries])
useEffect(() => {
if (searchIndexLoading || searchIndexError !== undefined || searchIndex === undefined || searchTerm === '') {
return setResults(entries)
}
search(searchIndex, {
term: searchTerm,
tolerance: 1,
properties: ['title', 'description', 'example'],
boost: {
title: 3,
description: 2,
example: 1
}
})
.then((results) => {
setResults(results.hits.map((entry) => entry.document))
})
.catch((error) => {
logger.error(error)
})
}, [entries, searchIndexError, searchIndexLoading, searchIndex, searchTerm])
return results
}

View file

@ -1,64 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { SearchIndexEntry } from './use-document-search'
import { useDocumentSearch } from './use-document-search'
import { renderHook } from '@testing-library/react'
describe('useDocumentSearch', () => {
interface TestSearchIndexEntry extends SearchIndexEntry {
name: string
text: string
}
const searchOptions = {
document: {
id: 'id',
field: ['name', 'text']
}
}
const searchEntries: TestSearchIndexEntry[] = [
{
id: 1,
name: 'Foo',
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean fermentum odio in bibendum venenatis. Cras aliquet ultrices finibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
},
{
id: 2,
name: 'Bar',
text: 'Vivamus sed mauris eget magna sodales blandit. Aliquam tincidunt nunc et sapien scelerisque placerat. Pellentesque a orci ac risus molestie suscipit id vel arcu.'
},
{
id: 3,
name: 'Cras',
text: 'Cras consectetur sit amet tortor eget sollicitudin. Ut convallis orci ipsum, eget dignissim nibh dignissim eget. Nunc commodo est neque, eget venenatis urna condimentum eget. Suspendisse dapibus ligula et enim venenatis hendrerit. '
}
]
it('results get populated', () => {
const { result, rerender } = renderHook(
(searchTerm: string) => useDocumentSearch(searchEntries, searchOptions, searchTerm),
{
initialProps: ''
}
)
rerender('Foo')
expect(result.current).toHaveLength(1)
expect(result.current[0]).toEqual({ field: 'name', result: [1] })
})
it('finds in multiple fields', () => {
const { result, rerender } = renderHook(
(searchTerm: string) => useDocumentSearch(searchEntries, searchOptions, searchTerm),
{
initialProps: ''
}
)
rerender('Cras')
expect(result.current).toHaveLength(2)
expect(result.current[0]).toEqual({ field: 'name', result: [3] })
expect(result.current[1]).toEqual({ field: 'text', result: [3, 1] })
})
})

View file

@ -1,43 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { IndexOptionsForDocumentSearch, Id, SimpleDocumentSearchResultSetUnit, StoreOption } from 'flexsearch-ts'
import { Document } from 'flexsearch-ts'
import { useEffect, useMemo, useState } from 'react'
export interface SearchIndexEntry {
id: Id
}
/**
* Generate document search index and provide functions to search.
*
* @param entries The list of entries to built the search index from
* @param options Options for the search index
* @param searchTerm What to search for
* @return An array of the search results
*/
export const useDocumentSearch = <T extends SearchIndexEntry>(
entries: Array<T>,
options: IndexOptionsForDocumentSearch<T, StoreOption>,
searchTerm: string
): SimpleDocumentSearchResultSetUnit[] => {
const [results, setResults] = useState<SimpleDocumentSearchResultSetUnit[]>([])
const searchIndex = useMemo(() => {
const index = new Document<T, StoreOption>({
tokenize: 'full',
...options
})
entries.forEach((entry) => {
index.add(entry)
})
return index
}, [entries, options])
useEffect(() => {
setResults(searchIndex.search(searchTerm))
}, [searchIndex, searchTerm])
return results
}

View file

@ -2441,6 +2441,7 @@ __metadata:
"@hedgedoc/markdown-it-plugins": "workspace:markdown-it-plugins"
"@mrdrogdrog/optional": "npm:1.2.1"
"@next/bundle-analyzer": "npm:13.4.19"
"@orama/orama": "npm:2.0.0-beta.12"
"@react-hook/resize-observer": "npm:1.2.6"
"@redux-devtools/core": "npm:3.13.3"
"@reduxjs/toolkit": "npm:1.9.7"
@ -2501,7 +2502,6 @@ __metadata:
eventemitter2: "npm:6.4.9"
fast-deep-equal: "npm:3.1.3"
firacode: "npm:6.2.0"
flexsearch-ts: "npm:0.7.35"
flowchart.js: "npm:1.17.1"
highlight.js: "npm:11.9.0"
htmlparser2: "npm:9.0.0"
@ -3786,6 +3786,13 @@ __metadata:
languageName: node
linkType: hard
"@orama/orama@npm:2.0.0-beta.12":
version: 2.0.0-beta.12
resolution: "@orama/orama@npm:2.0.0-beta.12"
checksum: 666a1d2326d181c384a302d77c45635e68ba26260b89ab022af264bfbd6bef4b1213f691fc9020fe570dd272be19208685707c588260072d46a633ccf5e0b73c
languageName: node
linkType: hard
"@pkgjs/parseargs@npm:^0.11.0":
version: 0.11.0
resolution: "@pkgjs/parseargs@npm:0.11.0"
@ -10020,13 +10027,6 @@ __metadata:
languageName: node
linkType: hard
"flexsearch-ts@npm:0.7.35":
version: 0.7.35
resolution: "flexsearch-ts@npm:0.7.35"
checksum: 0fc80ddecd016adbd477955d057f58fe3082aa57afe68c7274e4878a87995fae6fcb29287864e6895bdb012b81c02cff99e7894ef398a798eb2028abfa95e760
languageName: node
linkType: hard
"flowchart.js@npm:1.17.1":
version: 1.17.1
resolution: "flowchart.js@npm:1.17.1"