mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 17:26:29 -05:00
feat(frontend): add search in cheatsheet view
Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
parent
1624ff9c3b
commit
81927b88f2
5 changed files with 149 additions and 9 deletions
|
@ -645,6 +645,7 @@
|
|||
},
|
||||
"cheatsheet": {
|
||||
"button": "Open Cheatsheet",
|
||||
"search": "Search for Cheatsheets",
|
||||
"modal": {
|
||||
"popup": "Open in new tab",
|
||||
"title": "Cheatsheet",
|
||||
|
|
|
@ -68,6 +68,12 @@ export const CategoryAccordion: React.FC<GroupAccordionProps> = ({ extensions, s
|
|||
</Accordion.Item>
|
||||
))
|
||||
}, [groupEntries, onStateChange, selectedEntry])
|
||||
|
||||
return <Accordion defaultActiveKey={groupEntries[0][0]}>{elements}</Accordion>
|
||||
const defaultActiveKey = useMemo(() => {
|
||||
if (groupEntries.length === 0) {
|
||||
return ''
|
||||
}
|
||||
return groupEntries[0][0]
|
||||
}, [groupEntries])
|
||||
|
||||
return <Accordion defaultActiveKey={defaultActiveKey}>{elements}</Accordion>
|
||||
}
|
||||
|
|
|
@ -3,13 +3,14 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { allAppExtensions } from '../../../../extensions/all-app-extensions'
|
||||
import type { CheatsheetEntry, CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
|
||||
import { isCheatsheetGroup } from '../../cheatsheet/cheatsheet-extension'
|
||||
import { CategoryAccordion } from './category-accordion'
|
||||
import { CheatsheetEntryPane } from './cheatsheet-entry-pane'
|
||||
import { CheatsheetSearch } from './cheatsheet-search'
|
||||
import styles from './cheatsheet.module.scss'
|
||||
import { TopicSelection } from './topic-selection'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Col, ListGroup, Row } from 'react-bootstrap'
|
||||
import { Trans } from 'react-i18next'
|
||||
|
||||
|
@ -17,6 +18,7 @@ import { Trans } from 'react-i18next'
|
|||
* Renders the tab content for the cheatsheet.
|
||||
*/
|
||||
export const CheatsheetContent: React.FC = () => {
|
||||
const [visibleExtensions, setVisibleExtensions] = useState<CheatsheetExtension[]>([])
|
||||
const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>()
|
||||
const [selectedEntry, setSelectedEntry] = useState<CheatsheetEntry>()
|
||||
|
||||
|
@ -25,12 +27,15 @@ export const CheatsheetContent: React.FC = () => {
|
|||
setSelectedEntry(isCheatsheetGroup(value) ? value.entries[0] : value)
|
||||
}, [])
|
||||
|
||||
const extensions = useMemo(() => allAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()), [])
|
||||
|
||||
return (
|
||||
<Row className={`mt-2`}>
|
||||
<Col xs={3}>
|
||||
<CategoryAccordion extensions={extensions} selectedEntry={selectedExtension} onStateChange={changeExtension} />
|
||||
<Col xs={3} className={styles.sidebar}>
|
||||
<CheatsheetSearch setVisibleExtensions={setVisibleExtensions} />
|
||||
<CategoryAccordion
|
||||
extensions={visibleExtensions}
|
||||
selectedEntry={selectedExtension}
|
||||
onStateChange={changeExtension}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
<ListGroup>
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* 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 { useOnInputChange } from '../../../../hooks/common/use-on-input-change'
|
||||
import { UiIcon } from '../../../common/icons/ui-icon'
|
||||
import type { CheatsheetSingleEntry, CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
|
||||
import { hasCheatsheetTopics } from '../../cheatsheet/cheatsheet-extension'
|
||||
import styles from './cheatsheet.module.scss'
|
||||
import type { IndexOptionsForDocumentSearch } 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> = {
|
||||
document: {
|
||||
id: 'id',
|
||||
field: ['title', 'description', 'example']
|
||||
}
|
||||
}
|
||||
|
||||
export interface CheatsheetSearchProps {
|
||||
setVisibleExtensions: React.Dispatch<React.SetStateAction<CheatsheetExtension[]>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a search field and handles the extension selection to display.
|
||||
* An empty search input leads to all extensions being set.
|
||||
*
|
||||
* @param setVisibleExtensions This sets the extensions that are displayed in the cheatsheet view.
|
||||
*/
|
||||
export const CheatsheetSearch: React.FC<CheatsheetSearchProps> = ({ setVisibleExtensions }) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const allCheatsheetExtensions = useMemo(
|
||||
() => allAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()),
|
||||
[]
|
||||
)
|
||||
const buildSearchIndexDocument = useCallback(
|
||||
(entry: CheatsheetEntry, rootI18nKey: string | undefined = undefined): CheatsheetSearchIndexEntry => {
|
||||
const rootI18nKeyWithDot = rootI18nKey ? `${rootI18nKey}.` : ''
|
||||
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`)
|
||||
}
|
||||
},
|
||||
[t]
|
||||
)
|
||||
const cheatsheetSearchIndexEntries = useMemo(
|
||||
() =>
|
||||
allCheatsheetExtensions.flatMap((entry) => {
|
||||
if (hasCheatsheetTopics(entry)) {
|
||||
return entry.topics.map((innerEntry) => buildSearchIndexEntry(innerEntry, entry.i18nKey))
|
||||
}
|
||||
return buildSearchIndexDocument(entry)
|
||||
}),
|
||||
[buildSearchIndexDocument, allCheatsheetExtensions]
|
||||
)
|
||||
const searchResults = useDocumentSearch(cheatsheetSearchIndexEntries, searchOptions, 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)
|
||||
})
|
||||
setVisibleExtensions(extensionResults)
|
||||
}, [allCheatsheetExtensions, searchResults, searchTerm, setVisibleExtensions])
|
||||
const onChange = useOnInputChange((search) => {
|
||||
setSearchTerm(search)
|
||||
})
|
||||
const clearSearch = useCallback(() => {
|
||||
setSearchTerm('')
|
||||
}, [setSearchTerm])
|
||||
|
||||
return (
|
||||
<InputGroup className='mb-3'>
|
||||
<FormControl
|
||||
placeholder={t('cheatsheet.search') ?? undefined}
|
||||
aria-label={t('cheatsheet.search') ?? undefined}
|
||||
onChange={onChange}
|
||||
value={searchTerm}
|
||||
/>
|
||||
<button className={styles.innerBtn} onClick={clearSearch}>
|
||||
<UiIcon icon={X} />
|
||||
</button>
|
||||
</InputGroup>
|
||||
)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -13,3 +13,27 @@
|
|||
top: 1rem;
|
||||
bottom: 1rem;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.innerBtn {
|
||||
position: absolute;
|
||||
font-weight: 550;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
align-self: center;
|
||||
left: calc(100% - 30px);
|
||||
z-index: 99999;
|
||||
border-radius: var(--bs-border-radius);
|
||||
filter: var(--bs-btn-close-white-filter);
|
||||
}
|
||||
|
||||
.innerBtn:hover {
|
||||
color: var(--bs-btn-close-color);
|
||||
text-decoration: none;
|
||||
opacity: var(--bs-btn-close-hover-opacity);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue