diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 98d1f6b90..455836c32 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -18,16 +18,16 @@ jobs: - name: Check out repo uses: actions/checkout@v2 - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: Cache build uses: actions/cache@v2.1.6 with: path: build key: build + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Cache node_modules uses: actions/cache@v2 id: yarn-cache @@ -57,6 +57,9 @@ jobs: name: Perform E2E Test in ${{ matrix.browser }} needs: build-frontend runs-on: ubuntu-latest + container: + image: cypress/browsers:node14.16.0-chrome90-ff88 + options: --user 1001 --shm-size=2g strategy: matrix: browser: [ 'chrome', 'firefox' ] diff --git a/public/locales/en.json b/public/locales/en.json index 808582e2e..7100f6b5f 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -461,6 +461,7 @@ "successfullyCopied": "Copied!", "copyError": "Error while copying!", "errorOccurred": "An error occurred", + "errorWhileLoadingLibrary": "An unexpected error occurred while loading '{{name}}'.\nCheck the browser console for more information.\nReport this error only if it comes up again.", "readForMoreInfo": "Read here for more information" }, "login": { diff --git a/src/components/editor-page/editor-pane/autocompletion/code-block.ts b/src/components/editor-page/editor-pane/autocompletion/code-block.ts index 052acc656..b1a3aeffa 100644 --- a/src/components/editor-page/editor-pane/autocompletion/code-block.ts +++ b/src/components/editor-page/editor-pane/autocompletion/code-block.ts @@ -5,45 +5,111 @@ */ import { Editor, Hint, Hints, Pos } from 'codemirror' -import { findWordAtCursor, Hinter, search } from './index' +import { findWordAtCursor, generateHintListByPrefix, Hinter } from './index' +import { DEFAULT_DURATION_IN_SECONDS, dispatchUiNotification } from '../../../../redux/ui-notifications/methods' +import i18n from 'i18next' + +type highlightJsImport = typeof import('../../../common/hljs/hljs') const wordRegExp = /^```((\w|-|_|\+)*)$/ let allSupportedLanguages: string[] = [] -const codeBlockHint = (editor: Editor): Promise => { - return import(/* webpackChunkName: "highlight.js" */ '../../../common/hljs/hljs').then( - (hljs) => - new Promise((resolve) => { - const searchTerm = findWordAtCursor(editor) - const searchResult = wordRegExp.exec(searchTerm.text) - if (searchResult === null) { - resolve(null) - return - } - const term = searchResult[1] - if (allSupportedLanguages.length === 0) { - allSupportedLanguages = hljs.default - .listLanguages() - .concat('csv', 'flow', 'html', 'js', 'markmap', 'abc', 'graphviz', 'mermaid', 'vega-lite') - } - const suggestions = search(term, allSupportedLanguages) - const cursor = editor.getCursor() - if (!suggestions) { - resolve(null) - } else { - resolve({ - list: suggestions.map( - (suggestion: string): Hint => ({ - text: '```' + suggestion + '\n\n```\n', - displayText: suggestion - }) - ), - from: Pos(cursor.line, searchTerm.start), - to: Pos(cursor.line, searchTerm.end) - }) - } +/** + * Fetches the highlight js chunk. + * @return the retrieved highlight js api + */ +const loadHighlightJs = async (): Promise => { + try { + return await import('../../../common/hljs/hljs') + } catch (error) { + dispatchUiNotification( + i18n.t('common.errorOccurred'), + i18n.t('common.errorWhileLoadingLibrary', { name: 'highlight.js' }), + DEFAULT_DURATION_IN_SECONDS, + 'exclamation-circle' + ) + console.error("can't load highlight js", error) + return null + } +} + +/** + * Extracts the language from the current line in the editor. + * + * @param editor The editor that contains the search time + * @return null if no search term could be found or the found word and the cursor position. + */ +const extractSearchTerm = ( + editor: Editor +): null | { + searchTerm: string + startIndex: number + endIndex: number +} => { + const searchTerm = findWordAtCursor(editor) + const searchResult = wordRegExp.exec(searchTerm.text) + if (searchResult === null) { + return null + } + + return { + searchTerm: searchResult[1], + startIndex: searchTerm.start, + endIndex: searchTerm.end + } +} + +/** + * Builds the list of languages that are supported by highlight js or custom embeddings. + * @return An array of language names + */ +const buildLanguageList = async (): Promise => { + const highlightJs = await loadHighlightJs() + + if (highlightJs === null) { + return [] + } + + if (allSupportedLanguages.length === 0) { + allSupportedLanguages = highlightJs.default + .listLanguages() + .concat('csv', 'flow', 'html', 'js', 'markmap', 'abc', 'graphviz', 'mermaid', 'vega-lite') + } + + return allSupportedLanguages +} + +/** + * Creates a codemirror autocompletion hint with supported highlight js languages. + * + * @param editor The codemirror editor that requested the autocompletion + * @return The generated {@link Hints} or null if no hints exist. + */ +const codeBlockHint = async (editor: Editor): Promise => { + const searchResult = extractSearchTerm(editor) + if (!searchResult) { + return null + } + + const languages = await buildLanguageList() + if (languages.length === 0) { + return null + } + const suggestions = generateHintListByPrefix(searchResult.searchTerm, languages) + if (!suggestions) { + return null + } + const lineIndex = editor.getCursor().line + return { + list: suggestions.map( + (suggestion: string): Hint => ({ + text: '```' + suggestion + '\n\n```\n', + displayText: suggestion }) - ) + ), + from: Pos(lineIndex, searchResult.startIndex), + to: Pos(lineIndex, searchResult.endIndex) + } } export const CodeBlockHinter: Hinter = { diff --git a/src/components/editor-page/editor-pane/autocompletion/header.ts b/src/components/editor-page/editor-pane/autocompletion/header.ts index f7650466f..9884136af 100644 --- a/src/components/editor-page/editor-pane/autocompletion/header.ts +++ b/src/components/editor-page/editor-pane/autocompletion/header.ts @@ -5,7 +5,7 @@ */ import { Editor, Hint, Hints, Pos } from 'codemirror' -import { findWordAtCursor, Hinter, search } from './index' +import { findWordAtCursor, generateHintListByPrefix, Hinter } from './index' const wordRegExp = /^(\s{0,3})(#{1,6})$/ const allSupportedHeaders = ['# h1', '## h2', '### h3', '#### h4', '##### h5', '###### h6', '###### tags: `example`'] @@ -24,7 +24,7 @@ const headerHint = (editor: Editor): Promise => { resolve(null) return } - const suggestions = search(term, allSupportedHeaders) + const suggestions = generateHintListByPrefix(term, allSupportedHeaders) const cursor = editor.getCursor() if (!suggestions) { resolve(null) diff --git a/src/components/editor-page/editor-pane/autocompletion/index.ts b/src/components/editor-page/editor-pane/autocompletion/index.ts index 2d650cf03..06ff5df3c 100644 --- a/src/components/editor-page/editor-pane/autocompletion/index.ts +++ b/src/components/editor-page/editor-pane/autocompletion/index.ts @@ -46,14 +46,15 @@ export const findWordAtCursor = (editor: Editor): findWordAtCursorResponse => { } } -export const search = (term: string, list: string[]): string[] => { - const suggestions: string[] = [] - list.forEach((item) => { - if (item.toLowerCase().startsWith(term.toLowerCase())) { - suggestions.push(item) - } - }) - return suggestions.slice(0, 7) +/** + * Generates a list (with max 8 entries) of hints for the autocompletion. + * + * @param prefix This is the case insensitive prefix that every hint must have + * @param hintCandidates The list of hint candidates + */ +export const generateHintListByPrefix = (prefix: string, hintCandidates: string[]): string[] => { + const searchTerm = prefix.toLowerCase() + return hintCandidates.filter((item) => item.toLowerCase().startsWith(searchTerm)).slice(0, 7) } export const allHinters: Hinter[] = [ diff --git a/src/components/notifications/ui-notification-toast.tsx b/src/components/notifications/ui-notification-toast.tsx index 7c91dfb5f..6923a2835 100644 --- a/src/components/notifications/ui-notification-toast.tsx +++ b/src/components/notifications/ui-notification-toast.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import React, { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { Button, ProgressBar, Toast } from 'react-bootstrap' import { UiNotification } from '../../redux/ui-notifications/types' import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon' @@ -88,6 +88,17 @@ export const UiNotificationToast: React.FC = ({ [buttons, dismissThisNotification] ) + const contentDom = useMemo(() => { + return content.split('\n').map((value) => { + return ( + + {value} +
+
+ ) + }) + }, [content]) + return ( @@ -99,7 +110,7 @@ export const UiNotificationToast: React.FC = ({ {date.toRelative({ style: 'short' })} - {content} + {contentDom}
{buttonsDom}