mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 11:16:31 -05:00
feat: fetch frontend config in server side rendering
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
312d1adf6f
commit
24f1b2a361
41 changed files with 270 additions and 220 deletions
|
@ -1,10 +1,10 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
|
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
|
||||||
import type { Config } from './types'
|
import type { FrontendConfig } from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the frontend config from the backend.
|
* Fetches the frontend config from the backend.
|
||||||
|
@ -12,7 +12,7 @@ import type { Config } from './types'
|
||||||
* @return The frontend config.
|
* @return The frontend config.
|
||||||
* @throws {Error} when the api request wasn't successful.
|
* @throws {Error} when the api request wasn't successful.
|
||||||
*/
|
*/
|
||||||
export const getConfig = async (): Promise<Config> => {
|
export const getConfig = async (baseUrl?: string): Promise<FrontendConfig> => {
|
||||||
const response = await new GetApiRequestBuilder<Config>('config').sendRequest()
|
const response = await new GetApiRequestBuilder<FrontendConfig>('config', baseUrl).sendRequest()
|
||||||
return response.asParsedJsonObject()
|
return response.asParsedJsonObject()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface Config {
|
export interface FrontendConfig {
|
||||||
allowAnonymous: boolean
|
allowAnonymous: boolean
|
||||||
allowRegister: boolean
|
allowRegister: boolean
|
||||||
authProviders: AuthProvider[]
|
authProviders: AuthProvider[]
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { getConfig } from '../../../api/config'
|
|
||||||
import { setConfig } from '../../../redux/config/methods'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the {@link Config frontend config} and save it in the global application state.
|
|
||||||
*/
|
|
||||||
export const fetchFrontendConfig = async (): Promise<void> => {
|
|
||||||
const config = await getConfig()
|
|
||||||
if (!config) {
|
|
||||||
return Promise.reject(new Error('Config empty!'))
|
|
||||||
}
|
|
||||||
setConfig(config)
|
|
||||||
}
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -7,7 +7,6 @@ import { refreshHistoryState } from '../../../redux/history/methods'
|
||||||
import { Logger } from '../../../utils/logger'
|
import { Logger } from '../../../utils/logger'
|
||||||
import { isDevMode, isTestMode } from '../../../utils/test-modes'
|
import { isDevMode, isTestMode } from '../../../utils/test-modes'
|
||||||
import { fetchAndSetUser } from '../../login-page/auth/utils'
|
import { fetchAndSetUser } from '../../login-page/auth/utils'
|
||||||
import { fetchFrontendConfig } from './fetch-frontend-config'
|
|
||||||
import { loadDarkMode } from './load-dark-mode'
|
import { loadDarkMode } from './load-dark-mode'
|
||||||
import { setUpI18n } from './setupI18n'
|
import { setUpI18n } from './setupI18n'
|
||||||
|
|
||||||
|
@ -55,10 +54,6 @@ export const createSetUpTaskList = (): InitTask[] => {
|
||||||
name: 'Load Translations',
|
name: 'Load Translations',
|
||||||
task: setUpI18n
|
task: setUpI18n
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Load config',
|
|
||||||
task: fetchFrontendConfig
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Fetch user information',
|
name: 'Fetch user information',
|
||||||
task: fetchUserInformation
|
task: fetchUserInformation
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import React, { createContext, useState } from 'react'
|
|
||||||
import type { PropsWithChildren } from 'react'
|
import type { PropsWithChildren } from 'react'
|
||||||
|
import React, { createContext, useState } from 'react'
|
||||||
|
|
||||||
export interface BaseUrls {
|
export interface BaseUrls {
|
||||||
renderer: string
|
renderer: string
|
||||||
|
@ -28,9 +28,8 @@ export const BaseUrlContextProvider: React.FC<PropsWithChildren<BaseUrlContextPr
|
||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
const [baseUrlState] = useState<undefined | BaseUrls>(() => baseUrls)
|
const [baseUrlState] = useState<undefined | BaseUrls>(() => baseUrls)
|
||||||
|
|
||||||
return baseUrlState === undefined ? (
|
return baseUrlState === undefined ? (
|
||||||
<div className={'text-white'}>HedgeDoc is not configured correctly! Please check the server log.</div>
|
<span className={'text-white bg-dark'}>HedgeDoc is not configured correctly! Please check the server log.</span>
|
||||||
) : (
|
) : (
|
||||||
<baseUrlContext.Provider value={baseUrlState}>{children}</baseUrlContext.Provider>
|
<baseUrlContext.Provider value={baseUrlState}>{children}</baseUrlContext.Provider>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useFrontendConfig } from '../frontend-config-context/use-frontend-config'
|
||||||
import { ShowIf } from '../show-if/show-if'
|
import { ShowIf } from '../show-if/show-if'
|
||||||
import styles from './branding.module.scss'
|
import styles from './branding.module.scss'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
|
@ -21,7 +21,7 @@ export interface BrandingProps {
|
||||||
* @param delimiter If the delimiter between the HedgeDoc logo and the branding should be shown.
|
* @param delimiter If the delimiter between the HedgeDoc logo and the branding should be shown.
|
||||||
*/
|
*/
|
||||||
export const Branding: React.FC<BrandingProps> = ({ inline = false, delimiter = true }) => {
|
export const Branding: React.FC<BrandingProps> = ({ inline = false, delimiter = true }) => {
|
||||||
const branding = useApplicationState((state) => state.config.branding)
|
const branding = useFrontendConfig().branding
|
||||||
const showBranding = !!branding.name || !!branding.logo
|
const showBranding = !!branding.name || !!branding.logo
|
||||||
|
|
||||||
const brandingDom = useMemo(() => {
|
const brandingDom = useMemo(() => {
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import type { FrontendConfig } from '../../../api/config/types'
|
||||||
|
import { createContext } from 'react'
|
||||||
|
|
||||||
|
export const frontendConfigContext = createContext<FrontendConfig | undefined>(undefined)
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { getConfig } from '../../../api/config'
|
||||||
|
import type { FrontendConfig } from '../../../api/config/types'
|
||||||
|
import { useBaseUrl } from '../../../hooks/common/use-base-url'
|
||||||
|
import { Logger } from '../../../utils/logger'
|
||||||
|
import { frontendConfigContext } from './context'
|
||||||
|
import type { PropsWithChildren } from 'react'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
const logger = new Logger('FrontendConfigContextProvider')
|
||||||
|
|
||||||
|
interface FrontendConfigContextProviderProps extends PropsWithChildren {
|
||||||
|
config?: FrontendConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the given frontend configuration in a context or renders an error message otherwise.
|
||||||
|
*
|
||||||
|
* @param config the frontend config to provoide
|
||||||
|
* @param children the react elements to show if the config is valid
|
||||||
|
*/
|
||||||
|
export const FrontendConfigContextProvider: React.FC<FrontendConfigContextProviderProps> = ({ config, children }) => {
|
||||||
|
const [configState, setConfigState] = useState<undefined | FrontendConfig>(() => config)
|
||||||
|
|
||||||
|
const baseUrl = useBaseUrl()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (config === undefined && configState === undefined) {
|
||||||
|
logger.debug('Fetching Config client side')
|
||||||
|
getConfig(baseUrl)
|
||||||
|
.then((config) => setConfigState(config))
|
||||||
|
.catch((error) => logger.error(error))
|
||||||
|
}
|
||||||
|
}, [baseUrl, config, configState])
|
||||||
|
|
||||||
|
return configState === undefined ? (
|
||||||
|
<span className={'text-white bg-dark'}>No frontend config received! Please check the server log.</span>
|
||||||
|
) : (
|
||||||
|
<frontendConfigContext.Provider value={configState}>{children}</frontendConfigContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import type { FrontendConfig } from '../../../api/config/types'
|
||||||
|
import { frontendConfigContext } from './context'
|
||||||
|
import { Optional } from '@mrdrogdrog/optional'
|
||||||
|
import { useContext } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the current frontend config from the next react context.
|
||||||
|
*/
|
||||||
|
export const useFrontendConfig = (): FrontendConfig => {
|
||||||
|
return Optional.ofNullable(useContext(frontendConfigContext)).orElseThrow(
|
||||||
|
() => new Error('No frontend config context found. Did you forget to use the provider component?')
|
||||||
|
)
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ jest.mock('../../../hooks/common/use-base-url')
|
||||||
|
|
||||||
describe('motd modal', () => {
|
describe('motd modal', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
jest.spyOn(UseBaseUrlModule, 'useBaseUrl').mockImplementation(() => 'https://example.org')
|
jest.spyOn(UseBaseUrlModule, 'useBaseUrl').mockImplementation(() => new URL('https://example.org'))
|
||||||
await mockI18n()
|
await mockI18n()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
|
import { useBaseUrl } from '../../../../hooks/common/use-base-url'
|
||||||
|
import { CopyableField } from '../../../common/copyable/copyable-field/copyable-field'
|
||||||
|
import React, { useMemo } from 'react'
|
||||||
|
|
||||||
|
export enum LinkType {
|
||||||
|
EDITOR = 'n',
|
||||||
|
SLIDESHOW = 'p',
|
||||||
|
DOCUMENT = 's'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkFieldProps {
|
||||||
|
type: LinkType
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a specific URL to the current note.
|
||||||
|
* @param type defines the URL type. (editor, read only document, slideshow, etc.)
|
||||||
|
*/
|
||||||
|
export const NoteUrlField: React.FC<LinkFieldProps> = ({ type }) => {
|
||||||
|
const baseUrl = useBaseUrl()
|
||||||
|
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||||
|
|
||||||
|
const url = useMemo(() => {
|
||||||
|
const url = new URL(baseUrl)
|
||||||
|
url.pathname += `${type}/${noteIdentifier}`
|
||||||
|
return url.toString()
|
||||||
|
}, [baseUrl, noteIdentifier, type])
|
||||||
|
|
||||||
|
return <CopyableField content={url} shareOriginUrl={url} />
|
||||||
|
}
|
|
@ -4,11 +4,10 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
import { useBaseUrl } from '../../../../hooks/common/use-base-url'
|
|
||||||
import { CopyableField } from '../../../common/copyable/copyable-field/copyable-field'
|
|
||||||
import type { ModalVisibilityProps } from '../../../common/modals/common-modal'
|
import type { ModalVisibilityProps } from '../../../common/modals/common-modal'
|
||||||
import { CommonModal } from '../../../common/modals/common-modal'
|
import { CommonModal } from '../../../common/modals/common-modal'
|
||||||
import { ShowIf } from '../../../common/show-if/show-if'
|
import { ShowIf } from '../../../common/show-if/show-if'
|
||||||
|
import { LinkType, NoteUrlField } from './note-url-field'
|
||||||
import { NoteType } from '@hedgedoc/commons'
|
import { NoteType } from '@hedgedoc/commons'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Modal } from 'react-bootstrap'
|
import { Modal } from 'react-bootstrap'
|
||||||
|
@ -23,21 +22,19 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
export const ShareModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
|
export const ShareModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter)
|
const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter)
|
||||||
const baseUrl = useBaseUrl()
|
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal show={show} onHide={onHide} showCloseButton={true} titleI18nKey={'editor.modal.shareLink.title'}>
|
<CommonModal show={show} onHide={onHide} showCloseButton={true} titleI18nKey={'editor.modal.shareLink.title'}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Trans i18nKey={'editor.modal.shareLink.editorDescription'} />
|
<Trans i18nKey={'editor.modal.shareLink.editorDescription'} />
|
||||||
<CopyableField content={`${baseUrl}n/${noteIdentifier}`} shareOriginUrl={`${baseUrl}n/${noteIdentifier}`} />
|
<NoteUrlField type={LinkType.EDITOR} />
|
||||||
<ShowIf condition={noteFrontmatter.type === NoteType.SLIDE}>
|
<ShowIf condition={noteFrontmatter.type === NoteType.SLIDE}>
|
||||||
<Trans i18nKey={'editor.modal.shareLink.slidesDescription'} />
|
<Trans i18nKey={'editor.modal.shareLink.slidesDescription'} />
|
||||||
<CopyableField content={`${baseUrl}p/${noteIdentifier}`} shareOriginUrl={`${baseUrl}p/${noteIdentifier}`} />
|
<NoteUrlField type={LinkType.SLIDESHOW} />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={noteFrontmatter.type === NoteType.DOCUMENT}>
|
<ShowIf condition={noteFrontmatter.type === NoteType.DOCUMENT}>
|
||||||
<Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'} />
|
<Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'} />
|
||||||
<CopyableField content={`${baseUrl}s/${noteIdentifier}`} shareOriginUrl={`${baseUrl}s/${noteIdentifier}`} />
|
<NoteUrlField type={LinkType.DOCUMENT} />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</CommonModal>
|
</CommonModal>
|
||||||
|
|
|
@ -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 const useWebsocketUrl = (): URL | undefined => {
|
||||||
return LOCAL_FALLBACK_URL
|
return LOCAL_FALLBACK_URL
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const backendBaseUrlParsed = new URL(baseUrl, window.location.toString())
|
const backendBaseUrlParsed = new URL(baseUrl)
|
||||||
backendBaseUrlParsed.protocol = backendBaseUrlParsed.protocol === 'https:' ? 'wss:' : 'ws:'
|
backendBaseUrlParsed.protocol = backendBaseUrlParsed.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
backendBaseUrlParsed.pathname += 'realtime'
|
backendBaseUrlParsed.pathname += 'realtime'
|
||||||
return backendBaseUrlParsed.toString()
|
return backendBaseUrlParsed.toString()
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
|
import { useFrontendConfig } from '../../../common/frontend-config-context/use-frontend-config'
|
||||||
import type { ModalVisibilityProps } from '../../../common/modals/common-modal'
|
import type { ModalVisibilityProps } from '../../../common/modals/common-modal'
|
||||||
import { CommonModal } from '../../../common/modals/common-modal'
|
import { CommonModal } from '../../../common/modals/common-modal'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
@ -19,7 +19,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
*/
|
*/
|
||||||
export const MaxLengthWarningModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
|
export const MaxLengthWarningModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength)
|
const maxDocumentLength = useFrontendConfig().maxDocumentLength
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal
|
<CommonModal
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
|
||||||
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
||||||
import { useNoteMarkdownContent } from '../../../../hooks/common/use-note-markdown-content'
|
import { useNoteMarkdownContent } from '../../../../hooks/common/use-note-markdown-content'
|
||||||
|
import { useFrontendConfig } from '../../../common/frontend-config-context/use-frontend-config'
|
||||||
import { MaxLengthWarningModal } from './max-length-warning-modal'
|
import { MaxLengthWarningModal } from './max-length-warning-modal'
|
||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ import React, { useEffect, useRef } from 'react'
|
||||||
export const MaxLengthWarning: React.FC = () => {
|
export const MaxLengthWarning: React.FC = () => {
|
||||||
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
||||||
const maxLengthWarningAlreadyShown = useRef(false)
|
const maxLengthWarningAlreadyShown = useRef(false)
|
||||||
const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength)
|
const maxDocumentLength = useFrontendConfig().maxDocumentLength
|
||||||
const markdownContent = useNoteMarkdownContent()
|
const markdownContent = useNoteMarkdownContent()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
|
import { useFrontendConfig } from '../../../common/frontend-config-context/use-frontend-config'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
export const RemainingCharactersInfo: React.FC = () => {
|
export const RemainingCharactersInfo: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength)
|
const maxDocumentLength = useFrontendConfig().maxDocumentLength
|
||||||
const contentLength = useApplicationState((state) => state.noteDetails.markdownContent.plain.length)
|
const contentLength = useApplicationState((state) => state.noteDetails.markdownContent.plain.length)
|
||||||
const remainingCharacters = useMemo(() => maxDocumentLength - contentLength, [contentLength, maxDocumentLength])
|
const remainingCharacters = useMemo(() => maxDocumentLength - contentLength, [contentLength, maxDocumentLength])
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
|
||||||
import links from '../../../links.json'
|
import links from '../../../links.json'
|
||||||
|
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
||||||
import { ExternalLink } from '../../common/links/external-link'
|
import { ExternalLink } from '../../common/links/external-link'
|
||||||
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 { VersionInfoLink } from './version-info/version-info-link'
|
import { VersionInfoLink } from './version-info/version-info-link'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment, useMemo } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,8 +18,11 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
export const PoweredByLinks: React.FC = () => {
|
export const PoweredByLinks: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
const specialUrls: [string, string][] = useApplicationState((state) =>
|
const rawSpecialUrls = useFrontendConfig().specialUrls
|
||||||
Object.entries(state.config.specialUrls).map(([i18nkey, url]) => [i18nkey, String(url)])
|
|
||||||
|
const specialUrls = useMemo(
|
||||||
|
() => Object.entries(rawSpecialUrls).map(([i18nkey, url]) => [i18nkey, String(url)]),
|
||||||
|
[rawSpecialUrls]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { BackendVersion } from '../../../../api/config/types'
|
import type { BackendVersion } from '../../../../api/config/types'
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
|
||||||
import links from '../../../../links.json'
|
import links from '../../../../links.json'
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
import { CopyableField } from '../../../common/copyable/copyable-field/copyable-field'
|
import { CopyableField } from '../../../common/copyable/copyable-field/copyable-field'
|
||||||
|
import { useFrontendConfig } from '../../../common/frontend-config-context/use-frontend-config'
|
||||||
import { TranslatedExternalLink } from '../../../common/links/translated-external-link'
|
import { TranslatedExternalLink } from '../../../common/links/translated-external-link'
|
||||||
import type { CommonModalProps } from '../../../common/modals/common-modal'
|
import type { CommonModalProps } from '../../../common/modals/common-modal'
|
||||||
import { CommonModal } from '../../../common/modals/common-modal'
|
import { CommonModal } from '../../../common/modals/common-modal'
|
||||||
|
@ -22,7 +22,7 @@ import { Modal } from 'react-bootstrap'
|
||||||
* @param show If the modal should be shown.
|
* @param show If the modal should be shown.
|
||||||
*/
|
*/
|
||||||
export const VersionInfoModal: React.FC<CommonModalProps> = ({ onHide, show }) => {
|
export const VersionInfoModal: React.FC<CommonModalProps> = ({ onHide, show }) => {
|
||||||
const serverVersion: BackendVersion = useApplicationState((state) => state.config.version)
|
const serverVersion: BackendVersion = useFrontendConfig().version
|
||||||
const backendVersion = useMemo(() => {
|
const backendVersion = useMemo(() => {
|
||||||
const version = `${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`
|
const version = `${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
|
||||||
import { cypressId } from '../../../utils/cypress-attribute'
|
import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
|
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { filterOneClickProviders } from '../../login-page/auth/utils'
|
import { filterOneClickProviders } from '../../login-page/auth/utils'
|
||||||
import { getOneClickProviderMetadata } from '../../login-page/auth/utils/get-one-click-provider-metadata'
|
import { getOneClickProviderMetadata } from '../../login-page/auth/utils/get-one-click-provider-metadata'
|
||||||
|
@ -25,7 +25,7 @@ export type SignInButtonProps = Omit<ButtonProps, 'href'>
|
||||||
*/
|
*/
|
||||||
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
|
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const authProviders = useApplicationState((state) => state.config.authProviders)
|
const authProviders = useFrontendConfig().authProviders
|
||||||
|
|
||||||
const loginLink = useMemo(() => {
|
const loginLink = useMemo(() => {
|
||||||
const oneClickProviders = authProviders.filter(filterOneClickProviders)
|
const oneClickProviders = authProviders.filter(filterOneClickProviders)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import { doLocalLogin } from '../../../api/auth/local'
|
import { doLocalLogin } from '../../../api/auth/local'
|
||||||
import { ErrorToI18nKeyMapper } from '../../../api/common/error-to-i18n-key-mapper'
|
import { ErrorToI18nKeyMapper } from '../../../api/common/error-to-i18n-key-mapper'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
|
||||||
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
||||||
|
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { PasswordField } from './fields/password-field'
|
import { PasswordField } from './fields/password-field'
|
||||||
import { UsernameField } from './fields/username-field'
|
import { UsernameField } from './fields/username-field'
|
||||||
|
@ -25,7 +25,7 @@ export const ViaLocal: React.FC = () => {
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
const allowRegister = useApplicationState((state) => state.config.allowRegister)
|
const allowRegister = useFrontendConfig().allowRegister
|
||||||
|
|
||||||
const onLoginSubmit = useCallback(
|
const onLoginSubmit = useCallback(
|
||||||
(event: FormEvent) => {
|
(event: FormEvent) => {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import { getProxiedUrl } from '../../../../api/media'
|
import { getProxiedUrl } from '../../../../api/media'
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
|
||||||
import { Logger } from '../../../../utils/logger'
|
import { Logger } from '../../../../utils/logger'
|
||||||
|
import { useFrontendConfig } from '../../../common/frontend-config-context/use-frontend-config'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
const log = new Logger('ProxyImageFrame')
|
const log = new Logger('ProxyImageFrame')
|
||||||
|
@ -20,7 +20,7 @@ const log = new Logger('ProxyImageFrame')
|
||||||
*/
|
*/
|
||||||
export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = ({ src, title, alt, ...props }) => {
|
export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = ({ src, title, alt, ...props }) => {
|
||||||
const [imageUrl, setImageUrl] = useState('')
|
const [imageUrl, setImageUrl] = useState('')
|
||||||
const imageProxyEnabled = useApplicationState((state) => state.config.useImageProxy)
|
const imageProxyEnabled = useFrontendConfig().useImageProxy
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!imageProxyEnabled || !src) {
|
if (!imageProxyEnabled || !src) {
|
||||||
|
|
|
@ -3,18 +3,18 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import type { MarkdownRendererExtensionOptions } from '../../../../extensions/base/app-extension'
|
||||||
import { AppExtension } from '../../../../extensions/base/app-extension'
|
import { AppExtension } from '../../../../extensions/base/app-extension'
|
||||||
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
|
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
|
||||||
import { basicCompletion } from '../../../editor-page/editor-pane/autocompletions/basic-completion'
|
import { basicCompletion } from '../../../editor-page/editor-pane/autocompletions/basic-completion'
|
||||||
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
|
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
|
||||||
import { TableOfContentsMarkdownExtension } from './table-of-contents-markdown-extension'
|
import { TableOfContentsMarkdownExtension } from './table-of-contents-markdown-extension'
|
||||||
import type { CompletionSource } from '@codemirror/autocomplete'
|
import type { CompletionSource } from '@codemirror/autocomplete'
|
||||||
import type EventEmitter2 from 'eventemitter2'
|
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
|
|
||||||
export class TableOfContentsAppExtension extends AppExtension {
|
export class TableOfContentsAppExtension extends AppExtension {
|
||||||
buildMarkdownRendererExtensions(eventEmitter?: EventEmitter2): MarkdownRendererExtension[] {
|
buildMarkdownRendererExtensions(options: MarkdownRendererExtensionOptions): MarkdownRendererExtension[] {
|
||||||
return [new TableOfContentsMarkdownExtension(eventEmitter)]
|
return [new TableOfContentsMarkdownExtension(options.eventEmitter)]
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCheatsheetExtensions(): CheatsheetExtension[] {
|
buildCheatsheetExtensions(): CheatsheetExtension[] {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { optionalAppExtensions } from '../../../extensions/extra-integrations/optional-app-extensions'
|
import { optionalAppExtensions } from '../../../extensions/extra-integrations/optional-app-extensions'
|
||||||
|
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
||||||
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
|
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
|
||||||
import { DebuggerMarkdownExtension } from '../extensions/debugger-markdown-extension'
|
import { DebuggerMarkdownExtension } from '../extensions/debugger-markdown-extension'
|
||||||
import { ProxyImageMarkdownExtension } from '../extensions/image/proxy-image-markdown-extension'
|
import { ProxyImageMarkdownExtension } from '../extensions/image/proxy-image-markdown-extension'
|
||||||
|
@ -25,9 +26,15 @@ export const useMarkdownExtensions = (
|
||||||
additionalExtensions: MarkdownRendererExtension[]
|
additionalExtensions: MarkdownRendererExtension[]
|
||||||
): MarkdownRendererExtension[] => {
|
): MarkdownRendererExtension[] => {
|
||||||
const extensionEventEmitter = useExtensionEventEmitter()
|
const extensionEventEmitter = useExtensionEventEmitter()
|
||||||
|
const frontendConfig = useFrontendConfig()
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return [
|
return [
|
||||||
...optionalAppExtensions.flatMap((extension) => extension.buildMarkdownRendererExtensions(extensionEventEmitter)),
|
...optionalAppExtensions.flatMap((extension) =>
|
||||||
|
extension.buildMarkdownRendererExtensions({
|
||||||
|
frontendConfig: frontendConfig,
|
||||||
|
eventEmitter: extensionEventEmitter
|
||||||
|
})
|
||||||
|
),
|
||||||
...additionalExtensions,
|
...additionalExtensions,
|
||||||
new UploadIndicatingImageFrameMarkdownExtension(),
|
new UploadIndicatingImageFrameMarkdownExtension(),
|
||||||
new LinkAdjustmentMarkdownExtension(baseUrl),
|
new LinkAdjustmentMarkdownExtension(baseUrl),
|
||||||
|
@ -35,5 +42,5 @@ export const useMarkdownExtensions = (
|
||||||
new DebuggerMarkdownExtension(),
|
new DebuggerMarkdownExtension(),
|
||||||
new ProxyImageMarkdownExtension()
|
new ProxyImageMarkdownExtension()
|
||||||
]
|
]
|
||||||
}, [additionalExtensions, baseUrl, extensionEventEmitter])
|
}, [additionalExtensions, baseUrl, extensionEventEmitter, frontendConfig])
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
import { useFrontendConfig } from '../common/frontend-config-context/use-frontend-config'
|
||||||
import { TranslatedExternalLink } from '../common/links/translated-external-link'
|
import { TranslatedExternalLink } from '../common/links/translated-external-link'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
@ -14,7 +14,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
*/
|
*/
|
||||||
export const RegisterInfos: React.FC = () => {
|
export const RegisterInfos: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const specialUrls = useApplicationState((state) => state.config.specialUrls)
|
const specialUrls = useFrontendConfig().specialUrls
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowIf condition={!!specialUrls.termsOfUse || !!specialUrls.privacy}>
|
<ShowIf condition={!!specialUrls.termsOfUse || !!specialUrls.privacy}>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
|
import type { FrontendConfig } from '../../api/config/types'
|
||||||
import type { CheatsheetExtension } from '../../components/editor-page/cheatsheet/cheatsheet-extension'
|
import type { CheatsheetExtension } from '../../components/editor-page/cheatsheet/cheatsheet-extension'
|
||||||
import type { Linter } from '../../components/editor-page/editor-pane/linter/linter'
|
import type { Linter } from '../../components/editor-page/editor-pane/linter/linter'
|
||||||
import type { MarkdownRendererExtension } from '../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
|
import type { MarkdownRendererExtension } from '../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
|
||||||
|
@ -11,9 +12,14 @@ import type { EventEmitter2 } from 'eventemitter2'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
|
|
||||||
|
export interface MarkdownRendererExtensionOptions {
|
||||||
|
frontendConfig: FrontendConfig
|
||||||
|
eventEmitter?: EventEmitter2
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class AppExtension {
|
export abstract class AppExtension {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
public buildMarkdownRendererExtensions(eventEmitter?: EventEmitter2): MarkdownRendererExtension[] {
|
public buildMarkdownRendererExtensions(options: MarkdownRendererExtensionOptions): MarkdownRendererExtension[] {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -9,6 +9,7 @@ import {
|
||||||
codeFenceRegex
|
codeFenceRegex
|
||||||
} from '../../../components/editor-page/editor-pane/autocompletions/basic-completion'
|
} from '../../../components/editor-page/editor-pane/autocompletions/basic-completion'
|
||||||
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
|
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
|
||||||
|
import type { MarkdownRendererExtensionOptions } from '../../base/app-extension'
|
||||||
import { AppExtension } from '../../base/app-extension'
|
import { AppExtension } from '../../base/app-extension'
|
||||||
import { PlantumlMarkdownExtension } from './plantuml-markdown-extension'
|
import { PlantumlMarkdownExtension } from './plantuml-markdown-extension'
|
||||||
import type { CompletionSource } from '@codemirror/autocomplete'
|
import type { CompletionSource } from '@codemirror/autocomplete'
|
||||||
|
@ -19,8 +20,8 @@ import type { CompletionSource } from '@codemirror/autocomplete'
|
||||||
* @see https://plantuml.com
|
* @see https://plantuml.com
|
||||||
*/
|
*/
|
||||||
export class PlantumlAppExtension extends AppExtension {
|
export class PlantumlAppExtension extends AppExtension {
|
||||||
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
|
buildMarkdownRendererExtensions(options: MarkdownRendererExtensionOptions): MarkdownRendererExtension[] {
|
||||||
return [new PlantumlMarkdownExtension()]
|
return [new PlantumlMarkdownExtension(options.frontendConfig.plantumlServer)]
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCheatsheetExtensions(): CheatsheetExtension[] {
|
buildCheatsheetExtensions(): CheatsheetExtension[] {
|
||||||
|
|
|
@ -5,30 +5,17 @@
|
||||||
*/
|
*/
|
||||||
import { mockI18n } from '../../../components/markdown-renderer/test-utils/mock-i18n'
|
import { mockI18n } from '../../../components/markdown-renderer/test-utils/mock-i18n'
|
||||||
import { TestMarkdownRenderer } from '../../../components/markdown-renderer/test-utils/test-markdown-renderer'
|
import { TestMarkdownRenderer } from '../../../components/markdown-renderer/test-utils/test-markdown-renderer'
|
||||||
import * as reduxModule from '../../../redux'
|
|
||||||
import type { ApplicationState } from '../../../redux/application-state'
|
|
||||||
import { PlantumlMarkdownExtension } from './plantuml-markdown-extension'
|
import { PlantumlMarkdownExtension } from './plantuml-markdown-extension'
|
||||||
import { render } from '@testing-library/react'
|
import { render } from '@testing-library/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Mock } from 'ts-mockery'
|
|
||||||
|
|
||||||
jest.mock('../../../redux')
|
|
||||||
|
|
||||||
describe('PlantUML markdown extensions', () => {
|
describe('PlantUML markdown extensions', () => {
|
||||||
beforeAll(() => mockI18n())
|
beforeAll(() => mockI18n())
|
||||||
|
|
||||||
it('renders a plantuml codeblock', () => {
|
it('renders a plantuml codeblock', () => {
|
||||||
jest.spyOn(reduxModule, 'getGlobalState').mockReturnValue(
|
|
||||||
Mock.of<ApplicationState>({
|
|
||||||
config: {
|
|
||||||
plantumlServer: 'https://example.org'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const view = render(
|
const view = render(
|
||||||
<TestMarkdownRenderer
|
<TestMarkdownRenderer
|
||||||
extensions={[new PlantumlMarkdownExtension()]}
|
extensions={[new PlantumlMarkdownExtension('https://example.org')]}
|
||||||
content={'```plantuml\nclass Example\n```'}
|
content={'```plantuml\nclass Example\n```'}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -36,17 +23,9 @@ describe('PlantUML markdown extensions', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders an error if no server is defined', () => {
|
it('renders an error if no server is defined', () => {
|
||||||
jest.spyOn(reduxModule, 'getGlobalState').mockReturnValue(
|
|
||||||
Mock.of<ApplicationState>({
|
|
||||||
config: {
|
|
||||||
plantumlServer: undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const view = render(
|
const view = render(
|
||||||
<TestMarkdownRenderer
|
<TestMarkdownRenderer
|
||||||
extensions={[new PlantumlMarkdownExtension()]}
|
extensions={[new PlantumlMarkdownExtension(undefined)]}
|
||||||
content={'```plantuml\nclass Example\n```'}
|
content={'```plantuml\nclass Example\n```'}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
|
import { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
|
||||||
import type { ComponentReplacer } from '../../../components/markdown-renderer/replace-components/component-replacer'
|
import type { ComponentReplacer } from '../../../components/markdown-renderer/replace-components/component-replacer'
|
||||||
import { getGlobalState } from '../../../redux'
|
|
||||||
import { PlantumlNotConfiguredComponentReplacer } from './plantuml-not-configured-component-replacer'
|
import { PlantumlNotConfiguredComponentReplacer } from './plantuml-not-configured-component-replacer'
|
||||||
import { Optional } from '@mrdrogdrog/optional'
|
import { Optional } from '@mrdrogdrog/optional'
|
||||||
import type MarkdownIt from 'markdown-it'
|
import type MarkdownIt from 'markdown-it'
|
||||||
|
@ -20,6 +19,10 @@ import type Token from 'markdown-it/lib/token'
|
||||||
* @see https://plantuml.com
|
* @see https://plantuml.com
|
||||||
*/
|
*/
|
||||||
export class PlantumlMarkdownExtension extends MarkdownRendererExtension {
|
export class PlantumlMarkdownExtension extends MarkdownRendererExtension {
|
||||||
|
constructor(private plantumlServerUrl: string | undefined) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
private plantumlError(markdownIt: MarkdownIt): void {
|
private plantumlError(markdownIt: MarkdownIt): void {
|
||||||
const defaultRenderer: Renderer.RenderRule = markdownIt.renderer.rules.fence || (() => '')
|
const defaultRenderer: Renderer.RenderRule = markdownIt.renderer.rules.fence || (() => '')
|
||||||
markdownIt.renderer.rules.fence = (tokens: Token[], idx: number, options: Options, env, slf: Renderer) => {
|
markdownIt.renderer.rules.fence = (tokens: Token[], idx: number, options: Options, env, slf: Renderer) => {
|
||||||
|
@ -30,7 +33,7 @@ export class PlantumlMarkdownExtension extends MarkdownRendererExtension {
|
||||||
}
|
}
|
||||||
|
|
||||||
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||||
Optional.ofNullable(getGlobalState().config.plantumlServer)
|
Optional.ofNullable(this.plantumlServerUrl)
|
||||||
.map((plantumlServer) =>
|
.map((plantumlServer) =>
|
||||||
plantuml(markdownIt, {
|
plantuml(markdownIt, {
|
||||||
openMarker: '```plantuml',
|
openMarker: '```plantuml',
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
|
import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
|
||||||
|
import type { MarkdownRendererExtensionOptions } from '../../base/app-extension'
|
||||||
import { AppExtension } from '../../base/app-extension'
|
import { AppExtension } from '../../base/app-extension'
|
||||||
import { SetCheckboxInCheatsheet } from './set-checkbox-in-cheatsheet'
|
import { SetCheckboxInCheatsheet } from './set-checkbox-in-cheatsheet'
|
||||||
import { SetCheckboxInEditor } from './set-checkbox-in-editor'
|
import { SetCheckboxInEditor } from './set-checkbox-in-editor'
|
||||||
import { TaskListMarkdownExtension } from './task-list-markdown-extension'
|
import { TaskListMarkdownExtension } from './task-list-markdown-extension'
|
||||||
import type { EventEmitter2 } from 'eventemitter2'
|
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,8 +17,8 @@ import type React from 'react'
|
||||||
export class TaskListCheckboxAppExtension extends AppExtension {
|
export class TaskListCheckboxAppExtension extends AppExtension {
|
||||||
public static readonly EVENT_NAME = 'TaskListCheckbox'
|
public static readonly EVENT_NAME = 'TaskListCheckbox'
|
||||||
|
|
||||||
buildMarkdownRendererExtensions(eventEmitter: EventEmitter2): TaskListMarkdownExtension[] {
|
buildMarkdownRendererExtensions(options: MarkdownRendererExtensionOptions): TaskListMarkdownExtension[] {
|
||||||
return [new TaskListMarkdownExtension(eventEmitter)]
|
return [new TaskListMarkdownExtension(options.eventEmitter)]
|
||||||
}
|
}
|
||||||
|
|
||||||
buildEditorExtensionComponent(): React.FC {
|
buildEditorExtensionComponent(): React.FC {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from './use-application-state'
|
import { useFrontendConfig } from '../../components/common/frontend-config-context/use-frontend-config'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,7 +12,7 @@ import { useMemo } from 'react'
|
||||||
* @return The app title with branding.
|
* @return The app title with branding.
|
||||||
*/
|
*/
|
||||||
export const useAppTitle = (): string => {
|
export const useAppTitle = (): string => {
|
||||||
const brandingName = useApplicationState((state) => state.config.branding.name)
|
const brandingName = useFrontendConfig().branding.name
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return 'HedgeDoc' + (brandingName ? ` @ ${brandingName}` : '')
|
return 'HedgeDoc' + (brandingName ? ` @ ${brandingName}` : '')
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
|
import { useFrontendConfig } from '../../components/common/frontend-config-context/use-frontend-config'
|
||||||
import { useApplicationState } from './use-application-state'
|
import { useApplicationState } from './use-application-state'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
@ -12,7 +13,7 @@ import { useMemo } from 'react'
|
||||||
* @return The array of markdown content lines
|
* @return The array of markdown content lines
|
||||||
*/
|
*/
|
||||||
export const useTrimmedNoteMarkdownContentWithoutFrontmatter = (): string[] => {
|
export const useTrimmedNoteMarkdownContentWithoutFrontmatter = (): string[] => {
|
||||||
const maxLength = useApplicationState((state) => state.config.maxDocumentLength)
|
const maxLength = useFrontendConfig().maxDocumentLength
|
||||||
const markdownContent = useApplicationState((state) => ({
|
const markdownContent = useApplicationState((state) => ({
|
||||||
lines: state.noteDetails.markdownContent.lines,
|
lines: state.noteDetails.markdownContent.lines,
|
||||||
content: state.noteDetails.markdownContent.plain
|
content: state.noteDetails.markdownContent.plain
|
||||||
|
|
|
@ -5,9 +5,11 @@
|
||||||
*/
|
*/
|
||||||
import '../../global-styles/dark.scss'
|
import '../../global-styles/dark.scss'
|
||||||
import '../../global-styles/index.scss'
|
import '../../global-styles/index.scss'
|
||||||
|
import type { FrontendConfig } from '../api/config/types'
|
||||||
import { ApplicationLoader } from '../components/application-loader/application-loader'
|
import { ApplicationLoader } from '../components/application-loader/application-loader'
|
||||||
import { BaseUrlContextProvider } from '../components/common/base-url/base-url-context-provider'
|
|
||||||
import type { BaseUrls } from '../components/common/base-url/base-url-context-provider'
|
import type { BaseUrls } from '../components/common/base-url/base-url-context-provider'
|
||||||
|
import { BaseUrlContextProvider } from '../components/common/base-url/base-url-context-provider'
|
||||||
|
import { FrontendConfigContextProvider } from '../components/common/frontend-config-context/frontend-config-context-provider'
|
||||||
import { ErrorBoundary } from '../components/error-boundary/error-boundary'
|
import { ErrorBoundary } from '../components/error-boundary/error-boundary'
|
||||||
import { BaseHead } from '../components/layout/base-head'
|
import { BaseHead } from '../components/layout/base-head'
|
||||||
import { UiNotificationBoundary } from '../components/notifications/ui-notification-boundary'
|
import { UiNotificationBoundary } from '../components/notifications/ui-notification-boundary'
|
||||||
|
@ -16,6 +18,8 @@ import { BaseUrlFromEnvExtractor } from '../utils/base-url-from-env-extractor'
|
||||||
import { configureLuxon } from '../utils/configure-luxon'
|
import { configureLuxon } from '../utils/configure-luxon'
|
||||||
import { determineCurrentOrigin } from '../utils/determine-current-origin'
|
import { determineCurrentOrigin } from '../utils/determine-current-origin'
|
||||||
import { ExpectedOriginBoundary } from '../utils/expected-origin-boundary'
|
import { ExpectedOriginBoundary } from '../utils/expected-origin-boundary'
|
||||||
|
import { FrontendConfigFetcher } from '../utils/frontend-config-fetcher'
|
||||||
|
import { isTestMode } from '../utils/test-modes'
|
||||||
import type { AppContext, AppInitialProps, AppProps } from 'next/app'
|
import type { AppContext, AppInitialProps, AppProps } from 'next/app'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
@ -23,6 +27,7 @@ configureLuxon()
|
||||||
|
|
||||||
interface AppPageProps {
|
interface AppPageProps {
|
||||||
baseUrls: BaseUrls | undefined
|
baseUrls: BaseUrls | undefined
|
||||||
|
frontendConfig: FrontendConfig | undefined
|
||||||
currentOrigin: string | undefined
|
currentOrigin: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +38,7 @@ interface AppPageProps {
|
||||||
function HedgeDocApp({ Component, pageProps }: AppProps<AppPageProps>) {
|
function HedgeDocApp({ Component, pageProps }: AppProps<AppPageProps>) {
|
||||||
return (
|
return (
|
||||||
<BaseUrlContextProvider baseUrls={pageProps.baseUrls}>
|
<BaseUrlContextProvider baseUrls={pageProps.baseUrls}>
|
||||||
|
<FrontendConfigContextProvider config={pageProps.frontendConfig}>
|
||||||
<ExpectedOriginBoundary currentOrigin={pageProps.currentOrigin}>
|
<ExpectedOriginBoundary currentOrigin={pageProps.currentOrigin}>
|
||||||
<StoreProvider>
|
<StoreProvider>
|
||||||
<BaseHead />
|
<BaseHead />
|
||||||
|
@ -45,19 +51,23 @@ function HedgeDocApp({ Component, pageProps }: AppProps<AppPageProps>) {
|
||||||
</ApplicationLoader>
|
</ApplicationLoader>
|
||||||
</StoreProvider>
|
</StoreProvider>
|
||||||
</ExpectedOriginBoundary>
|
</ExpectedOriginBoundary>
|
||||||
|
</FrontendConfigContextProvider>
|
||||||
</BaseUrlContextProvider>
|
</BaseUrlContextProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
|
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
|
||||||
|
const frontendConfigFetcher = new FrontendConfigFetcher()
|
||||||
|
|
||||||
HedgeDocApp.getInitialProps = ({ ctx }: AppContext): AppInitialProps<AppPageProps> => {
|
HedgeDocApp.getInitialProps = async ({ ctx }: AppContext): Promise<AppInitialProps<AppPageProps>> => {
|
||||||
const baseUrls = baseUrlFromEnvExtractor.extractBaseUrls().orElse(undefined)
|
const baseUrls = baseUrlFromEnvExtractor.extractBaseUrls().orElse(undefined)
|
||||||
|
const frontendConfig = isTestMode ? undefined : await frontendConfigFetcher.fetch(baseUrls) //some tests mock the frontend config. Therefore it needs to be fetched in the browser.
|
||||||
const currentOrigin = determineCurrentOrigin(ctx)
|
const currentOrigin = determineCurrentOrigin(ctx)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageProps: {
|
pageProps: {
|
||||||
baseUrls,
|
baseUrls,
|
||||||
|
frontendConfig,
|
||||||
currentOrigin
|
currentOrigin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import type { Config } from '../../../api/config/types'
|
import type { FrontendConfig } from '../../../api/config/types'
|
||||||
import { AuthProviderType } from '../../../api/config/types'
|
import { AuthProviderType } from '../../../api/config/types'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../handler-utils/respond-to-matching-request'
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
respondToMatchingRequest<Config>(HttpMethod.GET, req, res, {
|
respondToMatchingRequest<FrontendConfig>(HttpMethod.GET, req, res, {
|
||||||
allowAnonymous: true,
|
allowAnonymous: true,
|
||||||
allowRegister: true,
|
allowRegister: true,
|
||||||
authProviders: [
|
authProviders: [
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
/*
|
/*
|
||||||
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
|
||||||
*/
|
*/
|
||||||
import type { AuthProviderWithCustomName } from '../api/config/types'
|
import type { AuthProviderWithCustomName } from '../api/config/types'
|
||||||
import { AuthProviderType } from '../api/config/types'
|
import { AuthProviderType } from '../api/config/types'
|
||||||
|
import { useFrontendConfig } from '../components/common/frontend-config-context/use-frontend-config'
|
||||||
import { RedirectBack } from '../components/common/redirect-back'
|
import { RedirectBack } from '../components/common/redirect-back'
|
||||||
import { ShowIf } from '../components/common/show-if/show-if'
|
import { ShowIf } from '../components/common/show-if/show-if'
|
||||||
import { LandingLayout } from '../components/landing-layout/landing-layout'
|
import { LandingLayout } from '../components/landing-layout/landing-layout'
|
||||||
|
@ -23,7 +24,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
*/
|
*/
|
||||||
export const LoginPage: React.FC = () => {
|
export const LoginPage: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const authProviders = useApplicationState((state) => state.config.authProviders)
|
const authProviders = useFrontendConfig().authProviders
|
||||||
const userLoggedIn = useApplicationState((state) => !!state.user)
|
const userLoggedIn = useApplicationState((state) => !!state.user)
|
||||||
|
|
||||||
const ldapProviders = useMemo(() => {
|
const ldapProviders = useMemo(() => {
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -9,6 +9,7 @@ import { DisplayNameField } from '../components/common/fields/display-name-field
|
||||||
import { NewPasswordField } from '../components/common/fields/new-password-field'
|
import { NewPasswordField } from '../components/common/fields/new-password-field'
|
||||||
import { PasswordAgainField } from '../components/common/fields/password-again-field'
|
import { PasswordAgainField } from '../components/common/fields/password-again-field'
|
||||||
import { UsernameField } from '../components/common/fields/username-field'
|
import { UsernameField } from '../components/common/fields/username-field'
|
||||||
|
import { useFrontendConfig } from '../components/common/frontend-config-context/use-frontend-config'
|
||||||
import { Redirect } from '../components/common/redirect'
|
import { Redirect } from '../components/common/redirect'
|
||||||
import { LandingLayout } from '../components/landing-layout/landing-layout'
|
import { LandingLayout } from '../components/landing-layout/landing-layout'
|
||||||
import { fetchAndSetUser } from '../components/login-page/auth/utils'
|
import { fetchAndSetUser } from '../components/login-page/auth/utils'
|
||||||
|
@ -30,7 +31,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
export const RegisterPage: NextPage = () => {
|
export const RegisterPage: NextPage = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const allowRegister = useApplicationState((state) => state.config.allowRegister)
|
const allowRegister = useFrontendConfig().allowRegister
|
||||||
const userExists = useApplicationState((state) => !!state.user)
|
const userExists = useApplicationState((state) => !!state.user)
|
||||||
|
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
|
|
2
frontend/src/redux/application-state.d.ts
vendored
2
frontend/src/redux/application-state.d.ts
vendored
|
@ -3,7 +3,6 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { Config } from '../api/config/types'
|
|
||||||
import type { HistoryEntryWithOrigin } from '../api/history/types'
|
import type { HistoryEntryWithOrigin } from '../api/history/types'
|
||||||
import type { DarkModeConfig } from './dark-mode/types'
|
import type { DarkModeConfig } from './dark-mode/types'
|
||||||
import type { EditorConfig } from './editor/types'
|
import type { EditorConfig } from './editor/types'
|
||||||
|
@ -14,7 +13,6 @@ import type { OptionalUserState } from './user/types'
|
||||||
|
|
||||||
export interface ApplicationState {
|
export interface ApplicationState {
|
||||||
user: OptionalUserState
|
user: OptionalUserState
|
||||||
config: Config
|
|
||||||
history: HistoryEntryWithOrigin[]
|
history: HistoryEntryWithOrigin[]
|
||||||
editorConfig: EditorConfig
|
editorConfig: EditorConfig
|
||||||
darkMode: DarkModeConfig
|
darkMode: DarkModeConfig
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { store } from '..'
|
|
||||||
import type { Config } from '../../api/config/types'
|
|
||||||
import type { SetConfigAction } from './types'
|
|
||||||
import { ConfigActionType } from './types'
|
|
||||||
|
|
||||||
export const setConfig = (state: Config): void => {
|
|
||||||
store.dispatch({
|
|
||||||
type: ConfigActionType.SET_CONFIG,
|
|
||||||
state: state
|
|
||||||
} as SetConfigAction)
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import type { Config } from '../../api/config/types'
|
|
||||||
import type { ConfigActions } from './types'
|
|
||||||
import { ConfigActionType } from './types'
|
|
||||||
import type { Reducer } from 'redux'
|
|
||||||
|
|
||||||
export const initialState: Config = {
|
|
||||||
allowAnonymous: true,
|
|
||||||
allowRegister: true,
|
|
||||||
authProviders: [],
|
|
||||||
branding: {
|
|
||||||
name: '',
|
|
||||||
logo: ''
|
|
||||||
},
|
|
||||||
useImageProxy: false,
|
|
||||||
specialUrls: {
|
|
||||||
privacy: undefined,
|
|
||||||
termsOfUse: undefined,
|
|
||||||
imprint: undefined
|
|
||||||
},
|
|
||||||
version: {
|
|
||||||
major: 0,
|
|
||||||
minor: 0,
|
|
||||||
patch: 0
|
|
||||||
},
|
|
||||||
plantumlServer: undefined,
|
|
||||||
maxDocumentLength: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ConfigReducer: Reducer<Config, ConfigActions> = (state: Config = initialState, action: ConfigActions) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case ConfigActionType.SET_CONFIG:
|
|
||||||
return action.state
|
|
||||||
default:
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import type { Config } from '../../api/config/types'
|
|
||||||
import type { Action } from 'redux'
|
|
||||||
|
|
||||||
export enum ConfigActionType {
|
|
||||||
SET_CONFIG = 'config/set'
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ConfigActions = SetConfigAction
|
|
||||||
|
|
||||||
export interface SetConfigAction extends Action<ConfigActionType> {
|
|
||||||
type: ConfigActionType.SET_CONFIG
|
|
||||||
state: Config
|
|
||||||
}
|
|
|
@ -1,10 +1,9 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
import type { ApplicationState } from './application-state'
|
import type { ApplicationState } from './application-state'
|
||||||
import { ConfigReducer } from './config/reducers'
|
|
||||||
import { DarkModeConfigReducer } from './dark-mode/reducers'
|
import { DarkModeConfigReducer } from './dark-mode/reducers'
|
||||||
import { EditorConfigReducer } from './editor/reducers'
|
import { EditorConfigReducer } from './editor/reducers'
|
||||||
import { HistoryReducer } from './history/reducers'
|
import { HistoryReducer } from './history/reducers'
|
||||||
|
@ -17,7 +16,6 @@ import { combineReducers } from 'redux'
|
||||||
|
|
||||||
export const allReducers: Reducer<ApplicationState> = combineReducers<ApplicationState>({
|
export const allReducers: Reducer<ApplicationState> = combineReducers<ApplicationState>({
|
||||||
user: UserReducer,
|
user: UserReducer,
|
||||||
config: ConfigReducer,
|
|
||||||
history: HistoryReducer,
|
history: HistoryReducer,
|
||||||
editorConfig: EditorConfigReducer,
|
editorConfig: EditorConfigReducer,
|
||||||
darkMode: DarkModeConfigReducer,
|
darkMode: DarkModeConfigReducer,
|
||||||
|
|
35
frontend/src/utils/frontend-config-fetcher.ts
Normal file
35
frontend/src/utils/frontend-config-fetcher.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { getConfig } from '../api/config'
|
||||||
|
import type { FrontendConfig } from '../api/config/types'
|
||||||
|
import type { BaseUrls } from '../components/common/base-url/base-url-context-provider'
|
||||||
|
import { Logger } from './logger'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches and caches the {@link FrontendConfig frontend config} from the backend.
|
||||||
|
*/
|
||||||
|
export class FrontendConfigFetcher {
|
||||||
|
private readonly logger = new Logger('Frontend config fetcher')
|
||||||
|
|
||||||
|
private frontendConfig: FrontendConfig | undefined = undefined
|
||||||
|
|
||||||
|
public async fetch(baseUrls: BaseUrls | undefined): Promise<FrontendConfig | undefined> {
|
||||||
|
if (!this.frontendConfig) {
|
||||||
|
if (baseUrls === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const baseUrl = baseUrls.editor.toString()
|
||||||
|
try {
|
||||||
|
this.frontendConfig = await getConfig(baseUrl)
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Couldn't fetch frontend configuration from ${baseUrl}`, error)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
this.logger.info(`Fetched frontend config from ${baseUrl}`)
|
||||||
|
}
|
||||||
|
return this.frontendConfig
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue