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": {
|
"cheatsheet": {
|
||||||
"button": "Open Cheatsheet",
|
"button": "Open Cheatsheet",
|
||||||
|
"search": "Search for Cheatsheets",
|
||||||
"modal": {
|
"modal": {
|
||||||
"popup": "Open in new tab",
|
"popup": "Open in new tab",
|
||||||
"title": "Cheatsheet",
|
"title": "Cheatsheet",
|
||||||
|
|
|
@ -68,6 +68,12 @@ export const CategoryAccordion: React.FC<GroupAccordionProps> = ({ extensions, s
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
))
|
))
|
||||||
}, [groupEntries, onStateChange, selectedEntry])
|
}, [groupEntries, onStateChange, selectedEntry])
|
||||||
|
const defaultActiveKey = useMemo(() => {
|
||||||
|
if (groupEntries.length === 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return groupEntries[0][0]
|
||||||
|
}, [groupEntries])
|
||||||
|
|
||||||
return <Accordion defaultActiveKey={groupEntries[0][0]}>{elements}</Accordion>
|
return <Accordion defaultActiveKey={defaultActiveKey}>{elements}</Accordion>
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { allAppExtensions } from '../../../../extensions/all-app-extensions'
|
|
||||||
import type { CheatsheetEntry, CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
|
import type { CheatsheetEntry, CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
|
||||||
import { isCheatsheetGroup } from '../../cheatsheet/cheatsheet-extension'
|
import { isCheatsheetGroup } from '../../cheatsheet/cheatsheet-extension'
|
||||||
import { CategoryAccordion } from './category-accordion'
|
import { CategoryAccordion } from './category-accordion'
|
||||||
import { CheatsheetEntryPane } from './cheatsheet-entry-pane'
|
import { CheatsheetEntryPane } from './cheatsheet-entry-pane'
|
||||||
|
import { CheatsheetSearch } from './cheatsheet-search'
|
||||||
|
import styles from './cheatsheet.module.scss'
|
||||||
import { TopicSelection } from './topic-selection'
|
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 { Col, ListGroup, Row } from 'react-bootstrap'
|
||||||
import { Trans } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ import { Trans } from 'react-i18next'
|
||||||
* Renders the tab content for the cheatsheet.
|
* Renders the tab content for the cheatsheet.
|
||||||
*/
|
*/
|
||||||
export const CheatsheetContent: React.FC = () => {
|
export const CheatsheetContent: React.FC = () => {
|
||||||
|
const [visibleExtensions, setVisibleExtensions] = useState<CheatsheetExtension[]>([])
|
||||||
const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>()
|
const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>()
|
||||||
const [selectedEntry, setSelectedEntry] = useState<CheatsheetEntry>()
|
const [selectedEntry, setSelectedEntry] = useState<CheatsheetEntry>()
|
||||||
|
|
||||||
|
@ -25,12 +27,15 @@ export const CheatsheetContent: React.FC = () => {
|
||||||
setSelectedEntry(isCheatsheetGroup(value) ? value.entries[0] : value)
|
setSelectedEntry(isCheatsheetGroup(value) ? value.entries[0] : value)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const extensions = useMemo(() => allAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()), [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={`mt-2`}>
|
<Row className={`mt-2`}>
|
||||||
<Col xs={3}>
|
<Col xs={3} className={styles.sidebar}>
|
||||||
<CategoryAccordion extensions={extensions} selectedEntry={selectedExtension} onStateChange={changeExtension} />
|
<CheatsheetSearch setVisibleExtensions={setVisibleExtensions} />
|
||||||
|
<CategoryAccordion
|
||||||
|
extensions={visibleExtensions}
|
||||||
|
selectedEntry={selectedExtension}
|
||||||
|
onStateChange={changeExtension}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<ListGroup>
|
<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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -13,3 +13,27 @@
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
bottom: 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