From 81927b88f26cce194a6d22cc9495a8f111236327 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Sat, 24 Jun 2023 15:14:49 +0200 Subject: [PATCH] feat(frontend): add search in cheatsheet view Signed-off-by: Philip Molares --- frontend/locales/en.json | 1 + .../app-bar/cheatsheet/category-accordion.tsx | 8 +- .../app-bar/cheatsheet/cheatsheet-content.tsx | 17 ++- .../app-bar/cheatsheet/cheatsheet-search.tsx | 104 ++++++++++++++++++ .../app-bar/cheatsheet/cheatsheet.module.scss | 28 ++++- 5 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-search.tsx diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 2217d8934..cc098f68a 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -645,6 +645,7 @@ }, "cheatsheet": { "button": "Open Cheatsheet", + "search": "Search for Cheatsheets", "modal": { "popup": "Open in new tab", "title": "Cheatsheet", diff --git a/frontend/src/components/editor-page/app-bar/cheatsheet/category-accordion.tsx b/frontend/src/components/editor-page/app-bar/cheatsheet/category-accordion.tsx index 5625ec3c2..e16a2d649 100644 --- a/frontend/src/components/editor-page/app-bar/cheatsheet/category-accordion.tsx +++ b/frontend/src/components/editor-page/app-bar/cheatsheet/category-accordion.tsx @@ -68,6 +68,12 @@ export const CategoryAccordion: React.FC = ({ extensions, s )) }, [groupEntries, onStateChange, selectedEntry]) + const defaultActiveKey = useMemo(() => { + if (groupEntries.length === 0) { + return '' + } + return groupEntries[0][0] + }, [groupEntries]) - return {elements} + return {elements} } diff --git a/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-content.tsx b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-content.tsx index 14d76ffbe..a2e1462d0 100644 --- a/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-content.tsx +++ b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-content.tsx @@ -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([]) const [selectedExtension, setSelectedExtension] = useState() const [selectedEntry, setSelectedEntry] = useState() @@ -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 ( - - + + + diff --git a/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-search.tsx b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-search.tsx new file mode 100644 index 000000000..b8b55c897 --- /dev/null +++ b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-search.tsx @@ -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 = { + document: { + id: 'id', + field: ['title', 'description', 'example'] + } +} + +export interface CheatsheetSearchProps { + setVisibleExtensions: React.Dispatch> +} + +/** + * 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 = ({ 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 ( + + + + + ) +} diff --git a/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet.module.scss b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet.module.scss index d7d7c02a7..2858a1bda 100644 --- a/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet.module.scss +++ b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet.module.scss @@ -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); +}