Feature/lazy load components (#590)

This commit is contained in:
mrdrogdrog 2020-09-26 09:54:17 +02:00 committed by GitHub
parent 9c38655a92
commit 101292da92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 261 additions and 248 deletions

View file

@ -1,5 +1,5 @@
import { RegisterError } from '../../components/register-page/register-page' import { RegisterError } from '../../components/register-page/register-page'
import { expectResponseCode, getApiUrl, defaultFetchConfig } from '../utils' import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
export const doInternalLogin = async (username: string, password: string): Promise<void> => { export const doInternalLogin = async (username: string, password: string): Promise<void> => {
const response = await fetch(getApiUrl() + '/auth/internal', { const response = await fetch(getApiUrl() + '/auth/internal', {

View file

@ -1,4 +1,4 @@
import { expectResponseCode, getApiUrl, defaultFetchConfig } from '../utils' import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
import { Config } from './types' import { Config } from './types'
export const getConfig = async (): Promise<Config> => { export const getConfig = async (): Promise<Config> => {

View file

@ -1,6 +1,5 @@
import { expectResponseCode, getApiUrl, defaultFetchConfig } from '../utils'
import { HistoryEntry } from '../../components/history-page/history-page' import { HistoryEntry } from '../../components/history-page/history-page'
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
export const getHistory = async (): Promise<HistoryEntry[]> => { export const getHistory = async (): Promise<HistoryEntry[]> => {
const response = await fetch(getApiUrl() + '/history') const response = await fetch(getApiUrl() + '/history')

View file

@ -1,5 +1,5 @@
import { UserResponse } from '../users/types' import { UserResponse } from '../users/types'
import { expectResponseCode, getApiUrl, defaultFetchConfig } from '../utils' import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
export const getMe = async (): Promise<UserResponse> => { export const getMe = async (): Promise<UserResponse> => {
const response = await fetch(getApiUrl() + '/me', { const response = await fetch(getApiUrl() + '/me', {

View file

@ -1,5 +1,5 @@
import { ImageProxyResponse } from '../../components/markdown-renderer/replace-components/image/types' import { ImageProxyResponse } from '../../components/markdown-renderer/replace-components/image/types'
import { expectResponseCode, getApiUrl, defaultFetchConfig } from '../utils' import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
export const getProxiedUrl = async (imageUrl: string): Promise<ImageProxyResponse> => { export const getProxiedUrl = async (imageUrl: string): Promise<ImageProxyResponse> => {
const response = await fetch(getApiUrl() + '/media/proxy', { const response = await fetch(getApiUrl() + '/media/proxy', {

View file

@ -1,8 +1,7 @@
import React, { Fragment, useCallback, useEffect, useState } from 'react' import React, { Suspense, useCallback, useEffect, useState } from 'react'
import { useLocation } from 'react-router' import { useLocation } from 'react-router'
import './application-loader.scss' import './application-loader.scss'
import { createSetUpTaskList, InitTask } from './initializers' import { createSetUpTaskList, InitTask } from './initializers'
import { LoadingScreen } from './loading-screen' import { LoadingScreen } from './loading-screen'
export const ApplicationLoader: React.FC = ({ children }) => { export const ApplicationLoader: React.FC = ({ children }) => {
@ -33,9 +32,13 @@ export const ApplicationLoader: React.FC = ({ children }) => {
} }
}, [initTasks, runTask]) }, [initTasks, runTask])
return ( const tasksAreRunning = doneTasks < initTasks.length || initTasks.length === 0
doneTasks < initTasks.length || initTasks.length === 0
? <LoadingScreen failedTitle={failedTitle}/> if (tasksAreRunning) {
: <Fragment>{children}</Fragment> return <LoadingScreen failedTitle={failedTitle}/>
) } else {
return <Suspense fallback={(<LoadingScreen/>)}>
{children}
</Suspense>
}
} }

View file

@ -4,7 +4,7 @@ import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
import { ShowIf } from '../common/show-if/show-if' import { ShowIf } from '../common/show-if/show-if'
export interface LoadingScreenProps { export interface LoadingScreenProps {
failedTitle: string failedTitle?: string
} }
export const LoadingScreen: React.FC<LoadingScreenProps> = ({ failedTitle }) => { export const LoadingScreen: React.FC<LoadingScreenProps> = ({ failedTitle }) => {
@ -14,7 +14,7 @@ export const LoadingScreen: React.FC<LoadingScreenProps> = ({ failedTitle }) =>
<ForkAwesomeIcon icon="file-text" size="5x" <ForkAwesomeIcon icon="file-text" size="5x"
className={failedTitle ? 'animation-shake' : 'animation-pulse'}/> className={failedTitle ? 'animation-shake' : 'animation-pulse'}/>
</div> </div>
<ShowIf condition={ failedTitle !== ''}> <ShowIf condition={ !!failedTitle}>
<Alert variant={'danger'}> <Alert variant={'danger'}>
The task '{failedTitle}' failed.<br/> The task '{failedTitle}' failed.<br/>
For further information look into the browser console. For further information look into the browser console.

View file

@ -1,4 +1,3 @@
import equal from 'fast-deep-equal' import equal from 'fast-deep-equal'
import React from 'react' import React from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'

View file

@ -1,4 +1,3 @@
import 'fork-awesome/css/fork-awesome.min.css'
import React from 'react' import React from 'react'
import { IconName, IconSize } from './types' import { IconName, IconSize } from './types'

View file

@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import { Button, ButtonProps } from 'react-bootstrap' import { Button, ButtonProps } from 'react-bootstrap'
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon' import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
import './icon-button.scss'
import { IconName } from '../fork-awesome/types' import { IconName } from '../fork-awesome/types'
import './icon-button.scss'
export interface IconButtonProps extends ButtonProps { export interface IconButtonProps extends ButtonProps {
icon: IconName icon: IconName

View file

@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import { Trans } from 'react-i18next' import { Trans } from 'react-i18next'
import './icon-button.scss'
import { IconButton, IconButtonProps } from './icon-button' import { IconButton, IconButtonProps } from './icon-button'
export interface TranslatedIconButton extends IconButtonProps { export interface TranslatedIconButton extends IconButtonProps {

View file

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { Modal, Button } from 'react-bootstrap' import { Button, Modal } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { CommonModal, CommonModalProps } from './common-modal' import { CommonModal, CommonModalProps } from './common-modal'

View file

@ -1,9 +1,9 @@
import equal from 'fast-deep-equal' import equal from 'fast-deep-equal'
import React from 'react' import React from 'react'
import { Alert, Button } from 'react-bootstrap'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ApplicationState } from '../../../redux' import { ApplicationState } from '../../../redux'
import { Alert, Button } from 'react-bootstrap'
import { setBanner } from '../../../redux/banner/methods' import { setBanner } from '../../../redux/banner/methods'
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon' import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
import { ShowIf } from '../show-if/show-if' import { ShowIf } from '../show-if/show-if'

View file

@ -9,12 +9,12 @@ import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { ShowIf } from '../../common/show-if/show-if' import { ShowIf } from '../../common/show-if/show-if'
import { SignInButton } from '../../landing-layout/navigation/sign-in-button' import { SignInButton } from '../../landing-layout/navigation/sign-in-button'
import { UserDropdown } from '../../landing-layout/navigation/user-dropdown' import { UserDropdown } from '../../landing-layout/navigation/user-dropdown'
import { SyncScrollButtons } from './sync-scroll-buttons/sync-scroll-buttons'
import { EditorPathParams } from '../editor' import { EditorPathParams } from '../editor'
import { DarkModeButton } from './dark-mode-button' import { DarkModeButton } from './dark-mode-button'
import { EditorViewMode } from './editor-view-mode' import { EditorViewMode } from './editor-view-mode'
import { HelpButton } from './help-button/help-button' import { HelpButton } from './help-button/help-button'
import { NavbarBranding } from './navbar-branding' import { NavbarBranding } from './navbar-branding'
import { SyncScrollButtons } from './sync-scroll-buttons/sync-scroll-buttons'
export const AppBar: React.FC = () => { export const AppBar: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()

View file

@ -2,10 +2,10 @@ import React from 'react'
import { Col, Row } from 'react-bootstrap' import { Col, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import links from '../../../../links.json'
import { ApplicationState } from '../../../../redux' import { ApplicationState } from '../../../../redux'
import { TranslatedExternalLink } from '../../../common/links/translated-external-link' import { TranslatedExternalLink } from '../../../common/links/translated-external-link'
import { TranslatedInternalLink } from '../../../common/links/translated-internal-link' import { TranslatedInternalLink } from '../../../common/links/translated-internal-link'
import links from '../../../../links.json'
export const Links: React.FC = () => { export const Links: React.FC = () => {
useTranslation() useTranslation()

View file

@ -3,7 +3,6 @@
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Before After
Before After

View file

@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import { Dropdown } from 'react-bootstrap' import { Dropdown } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import links from '../../../../links.json' import links from '../../../../links.json'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
const ExportMenu: React.FC = () => { const ExportMenu: React.FC = () => {
useTranslation() useTranslation()

View file

@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { Alert, Col, ListGroup, Modal, Row, Button } from 'react-bootstrap' import { Alert, Button, Col, ListGroup, Modal, Row } from 'react-bootstrap'
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer' import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'

View file

@ -1,13 +1,13 @@
import { Editor, Hint, Hints, Pos } from 'codemirror' import { Editor, Hint, Hints, Pos } from 'codemirror'
import hljs from 'highlight.js'
import { findWordAtCursor, Hinter, search } from './index' import { findWordAtCursor, Hinter, search } from './index'
const allowedChars = /[`\w-_+]/ const allowedChars = /[`\w-_+]/
const wordRegExp = /^```((\w|-|_|\+)*)$/ const wordRegExp = /^```((\w|-|_|\+)*)$/
const allSupportedLanguages = hljs.listLanguages().concat('csv', 'flow', 'html') let allSupportedLanguages: string[] = []
const codeBlockHint = (editor: Editor): Promise< Hints| null > => { const codeBlockHint = (editor: Editor): Promise< Hints| null > => {
return new Promise((resolve) => { return import(/* webpackChunkName: "highlight.js" */ 'highlight.js').then(hljs =>
new Promise((resolve) => {
const searchTerm = findWordAtCursor(editor, allowedChars) const searchTerm = findWordAtCursor(editor, allowedChars)
const searchResult = wordRegExp.exec(searchTerm.text) const searchResult = wordRegExp.exec(searchTerm.text)
if (searchResult === null) { if (searchResult === null) {
@ -15,6 +15,9 @@ const codeBlockHint = (editor: Editor): Promise< Hints| null > => {
return return
} }
const term = searchResult[1] const term = searchResult[1]
if (allSupportedLanguages.length === 0) {
allSupportedLanguages = hljs.listLanguages().concat('csv', 'flow', 'html')
}
const suggestions = search(term, allSupportedLanguages) const suggestions = search(term, allSupportedLanguages)
const cursor = editor.getCursor() const cursor = editor.getCursor()
if (!suggestions) { if (!suggestions) {
@ -29,7 +32,7 @@ const codeBlockHint = (editor: Editor): Promise< Hints| null > => {
to: Pos(cursor.line, searchTerm.end) to: Pos(cursor.line, searchTerm.end)
}) })
} }
}) }))
} }
export const CodeBlockHinter: Hinter = { export const CodeBlockHinter: Hinter = {

View file

@ -1,8 +1,8 @@
@import '../../../../node_modules/codemirror/lib/codemirror.css'; @import '../../../../node_modules/codemirror/lib/codemirror';
@import '../../../../node_modules/codemirror/addon/display/fullscreen.css'; @import '../../../../node_modules/codemirror/addon/display/fullscreen';
@import '../../../../node_modules/codemirror/addon/dialog/dialog.css'; @import '../../../../node_modules/codemirror/addon/dialog/dialog';
@import '../../../../node_modules/codemirror/theme/neat.css'; @import '../../../../node_modules/codemirror/theme/neat';
@import './one-dark.css'; @import './one-dark';
@import 'hints'; @import 'hints';
.CodeMirror { .CodeMirror {

View file

@ -3,8 +3,8 @@ import React, { Fragment, useState } from 'react'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon' import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
import { EmojiPicker } from './emoji-picker'
import { addEmoji } from '../utils/toolbarButtonUtils' import { addEmoji } from '../utils/toolbarButtonUtils'
import { EmojiPicker } from './emoji-picker'
export interface EmojiPickerButtonProps { export interface EmojiPickerButtonProps {
editor: CodeMirror.Editor editor: CodeMirror.Editor

View file

@ -1,3 +1,5 @@
@import '../../../../../../node_modules/emoji-mart/css/emoji-mart';
.emoji-mart { .emoji-mart {
position: absolute; position: absolute;
z-index: 10000; z-index: 10000;

View file

@ -1,12 +1,11 @@
import { CustomEmoji, Data, EmojiData, NimblePicker } from 'emoji-mart' import { CustomEmoji, Data, EmojiData, NimblePicker } from 'emoji-mart'
import 'emoji-mart/css/emoji-mart.css'
import emojiData from 'emoji-mart/data/twitter.json' import emojiData from 'emoji-mart/data/twitter.json'
import React, { useRef } from 'react' import React, { useRef } from 'react'
import { useClickAway } from 'react-use' import { useClickAway } from 'react-use'
import { ShowIf } from '../../../../common/show-if/show-if' import { ShowIf } from '../../../../common/show-if/show-if'
import './emoji-picker.scss' import './emoji-picker.scss'
import { ForkAwesomeIcons } from './icon-names'
import forkawesomeIcon from './forkawesome.png' import forkawesomeIcon from './forkawesome.png'
import { ForkAwesomeIcons } from './icon-names'
export interface EmojiPickerProps { export interface EmojiPickerProps {
show: boolean show: boolean

View file

@ -145,3 +145,5 @@ export const Editor: React.FC = () => {
</Fragment> </Fragment>
) )
} }
export default Editor

View file

@ -1,20 +1,5 @@
.history-menu { .history-menu {
.fa, &::after {
opacity: 0.5;
}
&:hover .fa, &:hover::after {
opacity: 1;
}
&.btn { &.btn {
padding: 0.6rem 0.65rem; padding: 0.6rem 0.65rem;
} }
} }
.dropup .dropdown-toggle, .dropdown-toggle {
&.no-arrow::after {
content: initial;
}
}

View file

@ -18,7 +18,7 @@ export interface EntryMenuProps {
className?: string className?: string
} }
const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, location, isDark, onRemove, onDelete, className }) => { export const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, location, isDark, onRemove, onDelete, className }) => {
useTranslation() useTranslation()
return ( return (
@ -54,5 +54,3 @@ const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, location, isDark, onRe
</Dropdown> </Dropdown>
) )
} }
export { EntryMenu }

View file

@ -189,8 +189,7 @@ export const HistoryPage: React.FC = () => {
sortAndFilterEntries(allEntries, toolbarState), sortAndFilterEntries(allEntries, toolbarState),
[allEntries, toolbarState]) [allEntries, toolbarState])
return ( return <Fragment>
<Fragment>
<ErrorModal show={error !== ''} onHide={resetError} <ErrorModal show={error !== ''} onHide={resetError}
titleI18nKey={error !== '' ? `landing.history.error.${error}.title` : ''}> titleI18nKey={error !== '' ? `landing.history.error.${error}.title` : ''}>
<h5> <h5>
@ -217,5 +216,6 @@ export const HistoryPage: React.FC = () => {
onDeleteClick={deleteNoteClick} onDeleteClick={deleteNoteClick}
/> />
</Fragment> </Fragment>
)
} }
export default HistoryPage

View file

@ -6,11 +6,10 @@ import { CoverButtons } from './cover-buttons/cover-buttons'
import { FeatureLinks } from './feature-links' import { FeatureLinks } from './feature-links'
import screenshot from './img/screenshot.png' import screenshot from './img/screenshot.png'
const IntroPage: React.FC = () => { export const IntroPage: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return <Fragment>
<Fragment>
<h1 dir='auto' className={'align-items-center d-flex justify-content-center'}> <h1 dir='auto' className={'align-items-center d-flex justify-content-center'}>
<ForkAwesomeIcon icon="file-text" className={'mr-2'}/> <ForkAwesomeIcon icon="file-text" className={'mr-2'}/>
<span>HedgeDoc</span> <span>HedgeDoc</span>
@ -25,7 +24,4 @@ const IntroPage: React.FC = () => {
<img alt={t('landing.intro.screenShotAltText')} src={screenshot} className="img-fluid mb-5"/> <img alt={t('landing.intro.screenShotAltText')} src={screenshot} className="img-fluid mb-5"/>
<FeatureLinks/> <FeatureLinks/>
</Fragment> </Fragment>
)
} }
export { IntroPage }

View file

@ -100,7 +100,7 @@ export interface ViaOneClickProps {
optionalName?: string; optionalName?: string;
} }
const ViaOneClick: React.FC<ViaOneClickProps> = ({ oneClickType, optionalName }) => { export const ViaOneClick: React.FC<ViaOneClickProps> = ({ oneClickType, optionalName }) => {
const backendUrl = useSelector((state: ApplicationState) => state.apiUrl.apiUrl) const backendUrl = useSelector((state: ApplicationState) => state.apiUrl.apiUrl)
const { name, icon, className, url } = getMetadata(backendUrl, oneClickType) const { name, icon, className, url } = getMetadata(backendUrl, oneClickType)
const text = optionalName || name const text = optionalName || name
@ -116,5 +116,3 @@ const ViaOneClick: React.FC<ViaOneClickProps> = ({ oneClickType, optionalName })
</SocialLinkButton> </SocialLinkButton>
) )
} }
export { ViaOneClick }

View file

@ -1,4 +1,4 @@
import React from 'react' import React, { Fragment } from 'react'
import { Card, Col, Row } from 'react-bootstrap' import { Card, Col, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
@ -10,6 +10,7 @@ import { ViaLdap } from './auth/via-ldap'
import { OneClickType, ViaOneClick } from './auth/via-one-click' import { OneClickType, ViaOneClick } from './auth/via-one-click'
import { ViaOpenId } from './auth/via-openid' import { ViaOpenId } from './auth/via-openid'
import equal from 'fast-deep-equal' import equal from 'fast-deep-equal'
export const LoginPage: React.FC = () => { export const LoginPage: React.FC = () => {
useTranslation() useTranslation()
const authProviders = useSelector((state: ApplicationState) => state.config.authProviders, equal) const authProviders = useSelector((state: ApplicationState) => state.config.authProviders, equal)
@ -38,7 +39,7 @@ export const LoginPage: React.FC = () => {
) )
} }
return ( return <Fragment>
<div className="my-3"> <div className="my-3">
<Row className="h-100 flex justify-content-center"> <Row className="h-100 flex justify-content-center">
<ShowIf condition={authProviders.internal || authProviders.ldap || authProviders.openid}> <ShowIf condition={authProviders.internal || authProviders.ldap || authProviders.openid}>
@ -76,5 +77,5 @@ export const LoginPage: React.FC = () => {
</ShowIf> </ShowIf>
</Row> </Row>
</div> </div>
) </Fragment>
} }

View file

@ -1,19 +1,21 @@
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
import { renderAbc } from 'abcjs'
export interface AbcFrameProps { export interface AbcFrameProps {
code: string code: string
} }
export const AbcFrame: React.FC<AbcFrameProps> = ({ code }) => { export const AbcFrame: React.FC<AbcFrameProps> = ({ code }) => {
const container = useRef<HTMLDivElement>(null) const container = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
if (container.current) { if (!container.current) {
renderAbc(container.current, code) return
} }
const actualContainer = container.current
import(/* webpackChunkName: "abc.js" */ 'abcjs').then((imp) => {
imp.renderAbc(actualContainer, code)
}).catch(() => { console.error('error while loading abcjs') })
}, [code]) }, [code])
return ( return <div ref={container} className={'bg-white text-center'}/>
<div ref={container} className={'bg-white text-center'}/>
)
} }

View file

@ -1,4 +1,3 @@
import { parse } from 'flowchart.js'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { Alert } from 'react-bootstrap' import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
@ -17,9 +16,11 @@ export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
if (diagramRef.current === null) { if (diagramRef.current === null) {
return return
} }
const parserOutput = parse(code) const currentDiagramRef = diagramRef.current
import(/* webpackChunkName: "flowchart.js" */ 'flowchart.js').then((imp) => {
const parserOutput = imp.parse(code)
try { try {
parserOutput.drawSVG(diagramRef.current, { parserOutput.drawSVG(currentDiagramRef, {
'line-width': 2, 'line-width': 2,
fill: 'none', fill: 'none',
'font-size': '16px', 'font-size': '16px',
@ -29,8 +30,7 @@ export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
} catch (error) { } catch (error) {
setError(true) setError(true)
} }
}).catch(() => { console.error('error while loading flowchart.js') })
const currentDiagramRef = diagramRef.current
return () => { return () => {
Array.from(currentDiagramRef.children).forEach(value => value.remove()) Array.from(currentDiagramRef.children).forEach(value => value.remove())
@ -41,8 +41,8 @@ export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
return ( return (
<Alert variant={'danger'}> <Alert variant={'danger'}>
<Trans i18nKey={'renderer.flowchart.invalidSyntax'}/> <Trans i18nKey={'renderer.flowchart.invalidSyntax'}/>
</Alert> </Alert>)
) } else {
}
return <div ref={diagramRef} className={'text-center'}/> return <div ref={diagramRef} className={'text-center'}/>
}
} }

View file

@ -1,7 +1,5 @@
import { graphviz } from 'd3-graphviz'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react' import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { Alert } from 'react-bootstrap' import { Alert } from 'react-bootstrap'
import '@hpcc-js/wasm'
import { ShowIf } from '../../../common/show-if/show-if' import { ShowIf } from '../../../common/show-if/show-if'
export interface GraphvizFrameProps { export interface GraphvizFrameProps {
@ -25,14 +23,18 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
if (!container.current) { if (!container.current) {
return return
} }
const actualContainer = container.current
Promise.all([import(/* webpackChunkName: "d3-graphviz" */ 'd3-graphviz'), import('@hpcc-js/wasm')]).then(([imp]) => {
try { try {
setError(undefined) setError(undefined)
graphviz(container.current, { useWorker: false, zoom: false }) imp.graphviz(actualContainer, { useWorker: false, zoom: false })
.onerror(showError) .onerror(showError)
.renderDot(code) .renderDot(code)
} catch (error) { } catch (error) {
showError(error) showError(error)
} }
}).catch(() => { console.error('error while loading graphviz') })
}, [code, error, showError]) }, [code, error, showError])
return <Fragment> return <Fragment>
@ -42,3 +44,5 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
<div className={'text-center'} ref={container} /> <div className={'text-center'} ref={container} />
</Fragment> </Fragment>
} }
export default GraphvizFrame

View file

@ -1,5 +1,4 @@
import hljs from 'highlight.js' import React, { Fragment, ReactElement, useEffect, useState } from 'react'
import React, { Fragment, useMemo } from 'react'
import ReactHtmlParser from 'react-html-parser' import ReactHtmlParser from 'react-html-parser'
import { CopyToClipboardButton } from '../../../../common/copyable/copy-to-clipboard-button/copy-to-clipboard-button' import { CopyToClipboardButton } from '../../../../common/copyable/copy-to-clipboard-button/copy-to-clipboard-button'
import '../../../utils/button-inside.scss' import '../../../utils/button-inside.scss'
@ -21,10 +20,6 @@ export const escapeHtml = (unsafe: string): string => {
.replace(/'/g, '&#039;') .replace(/'/g, '&#039;')
} }
const checkIfLanguageIsSupported = (language: string): boolean => {
return hljs.listLanguages().includes(language)
}
const correctLanguage = (language: string | undefined): string | undefined => { const correctLanguage = (language: string | undefined): string | undefined => {
switch (language) { switch (language) {
case 'html': case 'html':
@ -34,21 +29,21 @@ const correctLanguage = (language: string | undefined): string | undefined => {
} }
} }
export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language, startLineNumber, wrapLines }) => { const replaceCode = (code: string): ReactElement[][] => {
const highlightedCode = useMemo(() => { return code.split('\n')
const replacedLanguage = correctLanguage(language)
return ((!!replacedLanguage && checkIfLanguageIsSupported(replacedLanguage)) ? hljs.highlight(replacedLanguage, code).value : escapeHtml(code))
.split('\n')
.filter(line => !!line) .filter(line => !!line)
.map(line => ReactHtmlParser(line)) .map(line => ReactHtmlParser(line))
}, [code, language]) }
return ( export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language, startLineNumber, wrapLines }) => {
<Fragment> const [dom, setDom] = useState<ReactElement[]>()
<code className={`hljs ${startLineNumber !== undefined ? 'showGutter' : ''} ${wrapLines ? 'wrapLines' : ''}`}>
{ useEffect(() => {
highlightedCode import(/* webpackChunkName: "highlight.js" */ 'highlight.js').then((hljs) => {
.map((line, index) => ( const correctedLanguage = correctLanguage(language)
const languageSupported = (lang: string) => hljs.listLanguages().includes(lang)
const unreplacedCode = !!correctedLanguage && languageSupported(correctedLanguage) ? hljs.highlight(correctedLanguage, code).value : escapeHtml(code)
const replacedDom = replaceCode(unreplacedCode).map((line, index) => (
<Fragment key={index}> <Fragment key={index}>
<span className={'linenumber'} data-line-number={(startLineNumber || 1) + index}/> <span className={'linenumber'} data-line-number={(startLineNumber || 1) + index}/>
<div className={'codeline'}> <div className={'codeline'}>
@ -56,7 +51,14 @@ export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language
</div> </div>
</Fragment> </Fragment>
)) ))
} setDom(replacedDom)
}).catch(() => { console.error('error while loading highlight.js') })
}, [code, language, startLineNumber])
return (
<Fragment>
<code className={`hljs ${startLineNumber !== undefined ? 'showGutter' : ''} ${wrapLines ? 'wrapLines' : ''}`}>
{ dom }
</code> </code>
<div className={'text-right button-inside'}> <div className={'text-right button-inside'}>
<CopyToClipboardButton content={code}/> <CopyToClipboardButton content={code}/>

View file

@ -1,8 +1,7 @@
import { DomElement } from 'domhandler' import { DomElement } from 'domhandler'
import React from 'react' import React from 'react'
import 'katex/dist/katex.min.css'
import TeX from '@matejmazur/react-katex'
import { ComponentReplacer } from '../ComponentReplacer' import { ComponentReplacer } from '../ComponentReplacer'
import './katex.scss'
const getNodeIfKatexBlock = (node: DomElement): (DomElement|undefined) => { const getNodeIfKatexBlock = (node: DomElement): (DomElement|undefined) => {
if (node.name !== 'p' || !node.children || node.children.length === 0) { if (node.name !== 'p' || !node.children || node.children.length === 0) {
@ -17,13 +16,15 @@ const getNodeIfInlineKatex = (node: DomElement): (DomElement|undefined) => {
return (node.name === 'app-katex' && node.attribs?.inline !== undefined) ? node : undefined return (node.name === 'app-katex' && node.attribs?.inline !== undefined) ? node : undefined
} }
const KaTeX = React.lazy(() => import(/* webpackChunkName: "katex" */ '@matejmazur/react-katex'))
export class KatexReplacer extends ComponentReplacer { export class KatexReplacer extends ComponentReplacer {
public getReplacement (node: DomElement): React.ReactElement | undefined { public getReplacement (node: DomElement): React.ReactElement | undefined {
const katex = getNodeIfKatexBlock(node) || getNodeIfInlineKatex(node) const katex = getNodeIfKatexBlock(node) || getNodeIfInlineKatex(node)
if (katex?.children && katex.children[0]) { if (katex?.children && katex.children[0]) {
const mathJaxContent = katex.children[0]?.data as string const mathJaxContent = katex.children[0]?.data as string
const isInline = (katex.attribs?.inline) !== undefined const isInline = (katex.attribs?.inline) !== undefined
return <TeX block={!isInline} math={mathJaxContent} errorColor={'#cc0000'}/> return <KaTeX block={!isInline} math={mathJaxContent} errorColor={'#cc0000'}/>
} }
} }
} }

View file

@ -0,0 +1 @@
@import '../../../../../node_modules/katex/dist/katex.min';

View file

@ -1,5 +1,3 @@
import { transform } from 'markmap-lib/dist/transform'
import { Markmap } from 'markmap-lib/dist/view'
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
export interface MarkmapFrameProps { export interface MarkmapFrameProps {
@ -13,12 +11,16 @@ export const MarkmapFrame: React.FC<MarkmapFrameProps> = ({ code }) => {
if (!diagramContainer.current) { if (!diagramContainer.current) {
return return
} }
const actualContainer = diagramContainer.current
Promise.all([import(/* webpackChunkName: "markmap" */ 'markmap-lib/dist/transform'), import(/* webpackChunkName: "markmap" */ 'markmap-lib/dist/view')])
.then(([transform, view]) => {
const svg: SVGSVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg') const svg: SVGSVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('width', '100%') svg.setAttribute('width', '100%')
diagramContainer.current.querySelectorAll('svg').forEach(child => child.remove()) actualContainer.querySelectorAll('svg').forEach(child => child.remove())
diagramContainer.current.appendChild(svg) actualContainer.appendChild(svg)
const data = transform(code) const data = transform.transform(code)
Markmap.create(svg, {}, data) view.Markmap.create(svg, {}, data)
}).catch(() => { console.error('error while loading markmap') })
}, [code]) }, [code])
return <div className={'text-center'} ref={diagramContainer}/> return <div className={'text-center'} ref={diagramContainer}/>

View file

@ -1,7 +1,7 @@
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react' import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { Alert } from 'react-bootstrap' import { Alert } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import embed, { VisualizationSpec } from 'vega-embed' import { VisualizationSpec } from 'vega-embed'
import { ShowIf } from '../../../common/show-if/show-if' import { ShowIf } from '../../../common/show-if/show-if'
export interface VegaChartProps { export interface VegaChartProps {
@ -25,11 +25,14 @@ export const VegaChart: React.FC<VegaChartProps> = ({ code }) => {
if (!diagramContainer.current) { if (!diagramContainer.current) {
return return
} }
import(/* webpackChunkName: "vega" */ 'vega-embed').then((embed) => {
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment if (!diagramContainer.current) {
const spec: VisualizationSpec = JSON.parse(code) return
showError('') }
embed(diagramContainer.current, spec, {
const spec = JSON.parse(code) as VisualizationSpec
embed.default(diagramContainer.current, spec, {
actions: { actions: {
export: true, export: true,
source: false, source: false,
@ -41,11 +44,12 @@ export const VegaChart: React.FC<VegaChartProps> = ({ code }) => {
SVG_ACTION: t('renderer.vega-lite.svg') SVG_ACTION: t('renderer.vega-lite.svg')
} }
}) })
.then(result => console.log(result)) .then(() => showError(''))
.catch(err => showError(err)) .catch(err => showError(err))
} catch (err) { } catch (err) {
showError(t('renderer.vega-lite.errorJson')) showError(t('renderer.vega-lite.errorJson'))
} }
}).catch(() => { console.error('error while loading vega-light') })
}, [code, showError, t]) }, [code, showError, t])
return <Fragment> return <Fragment>

View file

@ -1,5 +1,5 @@
import { DomElement } from 'domhandler' import { DomElement } from 'domhandler'
import React, { Fragment, ReactElement } from 'react' import React, { ReactElement, Suspense } from 'react'
import { convertNodeToElement, Transform } from 'react-html-parser' import { convertNodeToElement, Transform } from 'react-html-parser'
import { import {
ComponentReplacer, ComponentReplacer,
@ -69,7 +69,9 @@ export const buildTransformer = (lineKeys: (LineKeys[] | undefined), allReplacer
} else if (tryReplacement === undefined) { } else if (tryReplacement === undefined) {
return nativeRenderer(node, key) return nativeRenderer(node, key)
} else { } else {
return <Fragment key={key}>{tryReplacement}</Fragment> return <Suspense key={key} fallback={<span>Loading...</span>}>
{ tryReplacement }
</Suspense>
} }
} }
return transform return transform

View file

@ -1,4 +1,4 @@
import React from 'react' import React, { Fragment } from 'react'
import { Col, Row } from 'react-bootstrap' import { Col, Row } from 'react-bootstrap'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { Redirect } from 'react-router' import { Redirect } from 'react-router'
@ -18,7 +18,7 @@ export const ProfilePage: React.FC = () => {
) )
} }
return ( return <Fragment>
<div className="my-3"> <div className="my-3">
<Row className="h-100 flex justify-content-center"> <Row className="h-100 flex justify-content-center">
<Col lg={6}> <Col lg={6}>
@ -30,5 +30,5 @@ export const ProfilePage: React.FC = () => {
</Col> </Col>
</Row> </Row>
</div> </div>
) </Fragment>
} }

View file

@ -1,4 +1,4 @@
import React, { FormEvent, useCallback, useEffect, useState } from 'react' import React, { FormEvent, Fragment, useCallback, useEffect, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { Redirect } from 'react-router' import { Redirect } from 'react-router'
import { doInternalRegister } from '../../api/auth' import { doInternalRegister } from '../../api/auth'
@ -53,7 +53,7 @@ export const RegisterPage: React.FC = () => {
) )
} }
return ( return <Fragment>
<div className='my-3'> <div className='my-3'>
<h1 className='mb-4'><Trans i18nKey='login.register.title'/></h1> <h1 className='mb-4'><Trans i18nKey='login.register.title'/></h1>
<Row className='h-100 d-flex justify-content-center'> <Row className='h-100 d-flex justify-content-center'>
@ -138,5 +138,5 @@ export const RegisterPage: React.FC = () => {
</Col> </Col>
</Row> </Row>
</div> </div>
) </Fragment>
} }

View file

@ -5,7 +5,6 @@ import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-d
import { ApplicationLoader } from './components/application-loader/application-loader' import { ApplicationLoader } from './components/application-loader/application-loader'
import { NotFoundErrorScreen } from './components/common/routing/not-found-error-screen' import { NotFoundErrorScreen } from './components/common/routing/not-found-error-screen'
import { Redirector } from './components/common/routing/redirector' import { Redirector } from './components/common/routing/redirector'
import { Editor } from './components/editor/editor'
import { ErrorBoundary } from './components/error-boundary/error-boundary' import { ErrorBoundary } from './components/error-boundary/error-boundary'
import { HistoryPage } from './components/history-page/history-page' import { HistoryPage } from './components/history-page/history-page'
import { IntroPage } from './components/intro-page/intro-page' import { IntroPage } from './components/intro-page/intro-page'
@ -15,8 +14,10 @@ import { ProfilePage } from './components/profile-page/profile-page'
import { RegisterPage } from './components/register-page/register-page' import { RegisterPage } from './components/register-page/register-page'
import { store } from './redux' import { store } from './redux'
import * as serviceWorker from './service-worker' import * as serviceWorker from './service-worker'
import './style/index.scss'
import './style/dark.scss' import './style/dark.scss'
import './style/index.scss'
const Editor = React.lazy(() => import(/* webpackPrefetch: true */ './components/editor/editor'))
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>

View file

@ -1,10 +1,5 @@
import { Reducer } from 'redux' import { Reducer } from 'redux'
import { import { BannerActions, BannerActionType, BannerState, SetBannerAction } from './types'
BannerActions,
BannerActionType,
BannerState,
SetBannerAction
} from './types'
export const initialState: BannerState = { export const initialState: BannerState = {
show: true, show: true,

View file

@ -1,5 +1,5 @@
import { Config } from '../../api/config/types'
import { store } from '..' import { store } from '..'
import { Config } from '../../api/config/types'
import { ConfigActionType, SetConfigAction } from './types' import { ConfigActionType, SetConfigAction } from './types'
export const setConfig = (state: Config): void => { export const setConfig = (state: Config): void => {

View file

@ -3,6 +3,7 @@
@import '../../node_modules/react-bootstrap-typeahead/css/Typeahead'; @import '../../node_modules/react-bootstrap-typeahead/css/Typeahead';
@import "fonts/source-code-pro/source-code-pro"; @import "fonts/source-code-pro/source-code-pro";
@import "fonts/twemoji/twemoji"; @import "fonts/twemoji/twemoji";
@import '../../node_modules/fork-awesome/css/fork-awesome.min';
.text-black, body.dark .text-black { .text-black, body.dark .text-black {
color: $black; color: $black;
@ -56,3 +57,19 @@ body {
.cursor-zoom-out { .cursor-zoom-out {
cursor: zoom-out; cursor: zoom-out;
} }
.faded-fa {
.fa, &::after {
opacity: 0.5;
}
&:hover .fa, &:hover::after {
opacity: 1;
}
}
.dropup .dropdown-toggle, .dropdown-toggle {
&.no-arrow::after {
content: initial;
}
}