refactor(frontend): make terminology of cheatsheet more clear

Also add additional documentation to explain how cheatsheets work

Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Philip Molares 2023-06-28 22:44:09 +02:00
parent 81927b88f2
commit 7a365acdb9
11 changed files with 61 additions and 45 deletions

View file

@ -3,8 +3,8 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { CheatsheetEntry, CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension' import type { CheatsheetSingleEntry, CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
import { isCheatsheetGroup } from '../../cheatsheet/cheatsheet-extension' import { hasCheatsheetTopics } 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 { CheatsheetSearch } from './cheatsheet-search'
@ -20,11 +20,11 @@ import { Trans } from 'react-i18next'
export const CheatsheetContent: React.FC = () => { export const CheatsheetContent: React.FC = () => {
const [visibleExtensions, setVisibleExtensions] = useState<CheatsheetExtension[]>([]) const [visibleExtensions, setVisibleExtensions] = useState<CheatsheetExtension[]>([])
const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>() const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>()
const [selectedEntry, setSelectedEntry] = useState<CheatsheetEntry>() const [selectedEntry, setSelectedEntry] = useState<CheatsheetSingleEntry>()
const changeExtension = useCallback((value: CheatsheetExtension) => { const changeExtension = useCallback((value: CheatsheetExtension) => {
setSelectedExtension(value) setSelectedExtension(value)
setSelectedEntry(isCheatsheetGroup(value) ? value.entries[0] : value) setSelectedEntry(hasCheatsheetTopics(value) ? value.topics[0] : value)
}, []) }, [])
return ( return (
@ -46,7 +46,7 @@ export const CheatsheetContent: React.FC = () => {
/> />
{selectedEntry !== undefined ? ( {selectedEntry !== undefined ? (
<CheatsheetEntryPane <CheatsheetEntryPane
rootI18nKey={isCheatsheetGroup(selectedExtension) ? selectedExtension.i18nKey : undefined} rootI18nKey={hasCheatsheetTopics(selectedExtension) ? selectedExtension.i18nKey : undefined}
extension={selectedEntry} extension={selectedEntry}
/> />
) : ( ) : (

View file

@ -8,7 +8,7 @@ import { HtmlToReact } from '../../../common/html-to-react/html-to-react'
import { RendererIframe } from '../../../common/renderer-iframe/renderer-iframe' import { RendererIframe } from '../../../common/renderer-iframe/renderer-iframe'
import { ExtensionEventEmitterProvider } from '../../../markdown-renderer/hooks/use-extension-event-emitter' import { ExtensionEventEmitterProvider } from '../../../markdown-renderer/hooks/use-extension-event-emitter'
import { RendererType } from '../../../render-page/window-post-message-communicator/rendering-message' import { RendererType } from '../../../render-page/window-post-message-communicator/rendering-message'
import type { CheatsheetEntry } from '../../cheatsheet/cheatsheet-extension' import type { CheatsheetSingleEntry } from '../../cheatsheet/cheatsheet-extension'
import { EditorToRendererCommunicatorContextProvider } from '../../render-context/editor-to-renderer-communicator-context-provider' import { EditorToRendererCommunicatorContextProvider } from '../../render-context/editor-to-renderer-communicator-context-provider'
import { ReadMoreLinkItem } from './read-more-link-item' import { ReadMoreLinkItem } from './read-more-link-item'
import { useComponentsFromAppExtensions } from './use-components-from-app-extensions' import { useComponentsFromAppExtensions } from './use-components-from-app-extensions'
@ -19,7 +19,7 @@ import { Trans, useTranslation } from 'react-i18next'
interface CheatsheetRendererProps { interface CheatsheetRendererProps {
rootI18nKey?: string rootI18nKey?: string
extension: CheatsheetEntry extension: CheatsheetSingleEntry
} }
/** /**

View file

@ -7,6 +7,7 @@ import { allAppExtensions } from '../../../../extensions/all-app-extensions'
import type { SearchIndexEntry } from '../../../../hooks/common/use-document-search' import type { SearchIndexEntry } from '../../../../hooks/common/use-document-search'
import { useDocumentSearch } from '../../../../hooks/common/use-document-search' import { useDocumentSearch } from '../../../../hooks/common/use-document-search'
import { useOnInputChange } from '../../../../hooks/common/use-on-input-change' 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 { UiIcon } from '../../../common/icons/ui-icon'
import type { CheatsheetSingleEntry, CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension' import type { CheatsheetSingleEntry, CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
import { hasCheatsheetTopics } from '../../cheatsheet/cheatsheet-extension' import { hasCheatsheetTopics } from '../../cheatsheet/cheatsheet-extension'
@ -47,8 +48,8 @@ export const CheatsheetSearch: React.FC<CheatsheetSearchProps> = ({ setVisibleEx
() => allAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()), () => allAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()),
[] []
) )
const buildSearchIndexDocument = useCallback( const buildSearchIndexEntry = useCallback(
(entry: CheatsheetEntry, rootI18nKey: string | undefined = undefined): CheatsheetSearchIndexEntry => { (entry: CheatsheetSingleEntry, rootI18nKey: string | undefined = undefined): CheatsheetSearchIndexEntry => {
const rootI18nKeyWithDot = rootI18nKey ? `${rootI18nKey}.` : '' const rootI18nKeyWithDot = rootI18nKey ? `${rootI18nKey}.` : ''
return { return {
id: rootI18nKey ? rootI18nKey : entry.i18nKey, id: rootI18nKey ? rootI18nKey : entry.i18nKey,
@ -59,15 +60,16 @@ export const CheatsheetSearch: React.FC<CheatsheetSearchProps> = ({ setVisibleEx
}, },
[t] [t]
) )
const placeholderText = useTranslatedText('cheatsheet.search')
const cheatsheetSearchIndexEntries = useMemo( const cheatsheetSearchIndexEntries = useMemo(
() => () =>
allCheatsheetExtensions.flatMap((entry) => { allCheatsheetExtensions.flatMap((entry) => {
if (hasCheatsheetTopics(entry)) { if (hasCheatsheetTopics(entry)) {
return entry.topics.map((innerEntry) => buildSearchIndexEntry(innerEntry, entry.i18nKey)) return entry.topics.map((innerEntry) => buildSearchIndexEntry(innerEntry, entry.i18nKey))
} }
return buildSearchIndexDocument(entry) return buildSearchIndexEntry(entry)
}), }),
[buildSearchIndexDocument, allCheatsheetExtensions] [buildSearchIndexEntry, allCheatsheetExtensions]
) )
const searchResults = useDocumentSearch(cheatsheetSearchIndexEntries, searchOptions, searchTerm) const searchResults = useDocumentSearch(cheatsheetSearchIndexEntries, searchOptions, searchTerm)
useEffect(() => { useEffect(() => {
@ -81,21 +83,14 @@ export const CheatsheetSearch: React.FC<CheatsheetSearchProps> = ({ setVisibleEx
}) })
setVisibleExtensions(extensionResults) setVisibleExtensions(extensionResults)
}, [allCheatsheetExtensions, searchResults, searchTerm, setVisibleExtensions]) }, [allCheatsheetExtensions, searchResults, searchTerm, setVisibleExtensions])
const onChange = useOnInputChange((search) => { const onChange = useOnInputChange(setSearchTerm)
setSearchTerm(search)
})
const clearSearch = useCallback(() => { const clearSearch = useCallback(() => {
setSearchTerm('') setSearchTerm('')
}, [setSearchTerm]) }, [setSearchTerm])
return ( return (
<InputGroup className='mb-3'> <InputGroup className='mb-3'>
<FormControl <FormControl placeholder={placeholderText} aria-label={placeholderText} onChange={onChange} value={searchTerm} />
placeholder={t('cheatsheet.search') ?? undefined}
aria-label={t('cheatsheet.search') ?? undefined}
onChange={onChange}
value={searchTerm}
/>
<button className={styles.innerBtn} onClick={clearSearch}> <button className={styles.innerBtn} onClick={clearSearch}>
<UiIcon icon={X} /> <UiIcon icon={X} />
</button> </button>

View file

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

View file

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

View file

@ -9,19 +9,40 @@ export interface CheatsheetExtensionComponentProps {
setContent: (dispatcher: string | ((prevState: string) => string)) => void setContent: (dispatcher: string | ((prevState: string) => string)) => void
} }
export type CheatsheetExtension = CheatsheetEntry | CheatsheetGroup export type CheatsheetExtension = CheatsheetSingleEntry | CheatsheetEntryWithTopics
export const isCheatsheetGroup = (extension: CheatsheetExtension | undefined): extension is CheatsheetGroup => { /**
return (extension as CheatsheetGroup)?.entries !== undefined * Determine if a given {@link CheatsheetExtension} is a {@link CheatsheetEntryWithTopics} or just a {@link CheatsheetSingleEntry}.
*
* @param extension The extension in question
* @return boolean
*/
export const hasCheatsheetTopics = (
extension: CheatsheetExtension | undefined
): extension is CheatsheetEntryWithTopics => {
return (extension as CheatsheetEntryWithTopics)?.topics !== undefined
} }
export interface CheatsheetGroup { /**
* 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 {
i18nKey: string i18nKey: string
categoryI18nKey?: string categoryI18nKey?: string
entries: CheatsheetEntry[] topics: CheatsheetSingleEntry[]
} }
export interface CheatsheetEntry { /**
* This is an entry that describes something completely.
*
* In the translations you'll find both 'description' containing an explanation and 'example' containing a demonstration in markdown under the i18nKey.
* If this entry is a topic of some other entry the i18nKey needs to be prefixed with the i18nKey of the other entry.
*
* e.g 'basics.basicFormatting'
*/
export interface CheatsheetSingleEntry {
i18nKey: string i18nKey: string
categoryI18nKey?: string categoryI18nKey?: string
cheatsheetExtensionComponent?: React.FC<CheatsheetExtensionComponentProps> cheatsheetExtensionComponent?: React.FC<CheatsheetExtensionComponentProps>

View file

@ -33,7 +33,7 @@ export class BasicMarkdownSyntaxAppExtension extends AppExtension {
{ {
i18nKey: 'basics.headlines', i18nKey: 'basics.headlines',
categoryI18nKey: 'basic', categoryI18nKey: 'basic',
entries: [ topics: [
{ {
i18nKey: 'hashtag' i18nKey: 'hashtag'
}, },
@ -45,17 +45,17 @@ export class BasicMarkdownSyntaxAppExtension extends AppExtension {
{ {
i18nKey: 'basics.code', i18nKey: 'basics.code',
categoryI18nKey: 'basic', categoryI18nKey: 'basic',
entries: [{ i18nKey: 'inline' }, { i18nKey: 'block' }] topics: [{ i18nKey: 'inline' }, { i18nKey: 'block' }]
}, },
{ {
i18nKey: 'basics.lists', i18nKey: 'basics.lists',
categoryI18nKey: 'basic', categoryI18nKey: 'basic',
entries: [{ i18nKey: 'unordered' }, { i18nKey: 'ordered' }] topics: [{ i18nKey: 'unordered' }, { i18nKey: 'ordered' }]
}, },
{ {
i18nKey: 'basics.images', i18nKey: 'basics.images',
categoryI18nKey: 'basic', categoryI18nKey: 'basic',
entries: [{ i18nKey: 'basic' }, { i18nKey: 'size' }] topics: [{ i18nKey: 'basic' }, { i18nKey: 'size' }]
}, },
{ {
i18nKey: 'basics.links', i18nKey: 'basics.links',

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 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
*/ */
@ -22,7 +22,7 @@ export class BlockquoteAppExtension extends AppExtension {
} }
buildCheatsheetExtensions(): CheatsheetExtension[] { buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'blockquoteTags', entries: [{ i18nKey: 'name' }, { i18nKey: 'color' }, { i18nKey: 'time' }] }] return [{ i18nKey: 'blockquoteTags', topics: [{ i18nKey: 'name' }, { i18nKey: 'color' }, { i18nKey: 'time' }] }]
} }
buildAutocompletion(): CompletionSource[] { buildAutocompletion(): CompletionSource[] {

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 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
*/ */
@ -22,7 +22,7 @@ export class CsvTableAppExtension extends AppExtension {
} }
buildCheatsheetExtensions(): CheatsheetExtension[] { buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'csv', entries: [{ i18nKey: 'table' }, { i18nKey: 'header' }] }] return [{ i18nKey: 'csv', topics: [{ i18nKey: 'table' }, { i18nKey: 'header' }] }]
} }
buildAutocompletion(): CompletionSource[] { buildAutocompletion(): CompletionSource[] {

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 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
*/ */
@ -23,7 +23,7 @@ export class HighlightedCodeFenceAppExtension extends AppExtension {
return [ return [
{ {
i18nKey: 'codeHighlighting', i18nKey: 'codeHighlighting',
entries: [{ i18nKey: 'language' }, { i18nKey: 'lineNumbers' }, { i18nKey: 'lineWrapping' }] topics: [{ i18nKey: 'language' }, { i18nKey: 'lineNumbers' }, { i18nKey: 'lineWrapping' }]
} }
] ]
} }

View file

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