mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-09 13:51:57 -04:00
feat: migrate frontend app to nextjs app router
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
5b5dabc84e
commit
8602645bea
108 changed files with 893 additions and 1188 deletions
|
@ -1,10 +1,11 @@
|
|||
'use client'
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import React, { createContext, useState } from 'react'
|
||||
import React, { createContext } from 'react'
|
||||
|
||||
export interface BaseUrls {
|
||||
renderer: string
|
||||
|
@ -27,10 +28,9 @@ export const BaseUrlContextProvider: React.FC<PropsWithChildren<BaseUrlContextPr
|
|||
baseUrls,
|
||||
children
|
||||
}) => {
|
||||
const [baseUrlState] = useState<undefined | BaseUrls>(() => baseUrls)
|
||||
return baseUrlState === undefined ? (
|
||||
return baseUrls === undefined ? (
|
||||
<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={baseUrls}>{children}</baseUrlContext.Provider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { isClientSideRendering } from '../../../../utils/is-client-side-rendering'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import { UiIcon } from '../../icons/ui-icon'
|
||||
import { ShowIf } from '../../show-if/show-if'
|
||||
|
@ -30,7 +29,7 @@ export const CopyableField: React.FC<CopyableFieldProps> = ({ content, shareOrig
|
|||
useTranslation()
|
||||
|
||||
const sharingSupported = useMemo(
|
||||
() => shareOriginUrl !== undefined && isClientSideRendering() && typeof navigator.share === 'function',
|
||||
() => shareOriginUrl !== undefined && typeof navigator.share === 'function',
|
||||
[shareOriginUrl]
|
||||
)
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { isClientSideRendering } from '../../../../utils/is-client-side-rendering'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import { ShowIf } from '../../show-if/show-if'
|
||||
import type { ReactElement, RefObject } from 'react'
|
||||
|
@ -45,11 +44,6 @@ export const useCopyOverlay = (
|
|||
}, [reset, showState])
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
if (!isClientSideRendering()) {
|
||||
setShowState(SHOW_STATE.ERROR)
|
||||
log.error('Clipboard not available in server side rendering')
|
||||
return
|
||||
}
|
||||
if (typeof navigator.clipboard === 'undefined') {
|
||||
setShowState(SHOW_STATE.ERROR)
|
||||
return
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
'use client'
|
||||
/*
|
||||
* 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')
|
||||
import React from 'react'
|
||||
|
||||
interface FrontendConfigContextProviderProps extends PropsWithChildren {
|
||||
config?: FrontendConfig
|
||||
|
@ -24,22 +20,9 @@ interface FrontendConfigContextProviderProps extends PropsWithChildren {
|
|||
* @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 ? (
|
||||
return config === undefined ? (
|
||||
<span className={'text-white bg-dark'}>No frontend config received! Please check the server log.</span>
|
||||
) : (
|
||||
<frontendConfigContext.Provider value={configState}>{children}</frontendConfigContext.Provider>
|
||||
<frontendConfigContext.Provider value={config}>{children}</frontendConfigContext.Provider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ exports[`create non existing note hint renders an button as initial state 1`] =
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`create non existing note hint renders nothing if no note id has been provided 1`] = `<div />`;
|
||||
|
||||
exports[`create non existing note hint shows an error message if note couldn't be created 1`] = `
|
||||
<div>
|
||||
<div
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
import * as createNoteWithPrimaryAliasModule from '../../../api/notes'
|
||||
import type { Note, NoteMetadata } from '../../../api/notes/types'
|
||||
import * as useSingleStringUrlParameterModule from '../../../hooks/common/use-single-string-url-parameter'
|
||||
import { mockI18n } from '../../../test-utils/mock-i18n'
|
||||
import { CreateNonExistingNoteHint } from './create-non-existing-note-hint'
|
||||
import { waitForOtherPromisesToFinish } from '@hedgedoc/commons'
|
||||
|
@ -18,14 +17,6 @@ jest.mock('../../../hooks/common/use-single-string-url-parameter')
|
|||
describe('create non existing note hint', () => {
|
||||
const mockedNoteId = 'mockedNoteId'
|
||||
|
||||
const mockGetNoteIdQueryParameter = () => {
|
||||
const expectedQueryParameter = 'noteId'
|
||||
jest.spyOn(useSingleStringUrlParameterModule, 'useSingleStringUrlParameter').mockImplementation((parameter) => {
|
||||
expect(parameter).toBe(expectedQueryParameter)
|
||||
return mockedNoteId
|
||||
})
|
||||
}
|
||||
|
||||
const mockCreateNoteWithPrimaryAlias = () => {
|
||||
jest
|
||||
.spyOn(createNoteWithPrimaryAliasModule, 'createNoteWithPrimaryAlias')
|
||||
|
@ -59,14 +50,24 @@ describe('create non existing note hint', () => {
|
|||
jest.resetModules()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetNoteIdQueryParameter()
|
||||
it('renders nothing if no note id has been provided', async () => {
|
||||
const onNoteCreatedCallback = jest.fn()
|
||||
const view = render(
|
||||
<CreateNonExistingNoteHint noteId={undefined} onNoteCreated={onNoteCreatedCallback}></CreateNonExistingNoteHint>
|
||||
)
|
||||
await waitForOtherPromisesToFinish()
|
||||
expect(onNoteCreatedCallback).not.toBeCalled()
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders an button as initial state', async () => {
|
||||
mockCreateNoteWithPrimaryAlias()
|
||||
const onNoteCreatedCallback = jest.fn()
|
||||
const view = render(<CreateNonExistingNoteHint onNoteCreated={onNoteCreatedCallback}></CreateNonExistingNoteHint>)
|
||||
const view = render(
|
||||
<CreateNonExistingNoteHint
|
||||
noteId={mockedNoteId}
|
||||
onNoteCreated={onNoteCreatedCallback}></CreateNonExistingNoteHint>
|
||||
)
|
||||
await screen.findByTestId('createNoteMessage')
|
||||
await waitForOtherPromisesToFinish()
|
||||
expect(onNoteCreatedCallback).not.toBeCalled()
|
||||
|
@ -76,7 +77,11 @@ describe('create non existing note hint', () => {
|
|||
it('renders a waiting message when button is clicked', async () => {
|
||||
mockCreateNoteWithPrimaryAlias()
|
||||
const onNoteCreatedCallback = jest.fn()
|
||||
const view = render(<CreateNonExistingNoteHint onNoteCreated={onNoteCreatedCallback}></CreateNonExistingNoteHint>)
|
||||
const view = render(
|
||||
<CreateNonExistingNoteHint
|
||||
noteId={mockedNoteId}
|
||||
onNoteCreated={onNoteCreatedCallback}></CreateNonExistingNoteHint>
|
||||
)
|
||||
const button = await screen.findByTestId('createNoteButton')
|
||||
await act<void>(() => {
|
||||
button.click()
|
||||
|
@ -92,7 +97,11 @@ describe('create non existing note hint', () => {
|
|||
it('shows success message when the note has been created', async () => {
|
||||
mockCreateNoteWithPrimaryAlias()
|
||||
const onNoteCreatedCallback = jest.fn()
|
||||
const view = render(<CreateNonExistingNoteHint onNoteCreated={onNoteCreatedCallback}></CreateNonExistingNoteHint>)
|
||||
const view = render(
|
||||
<CreateNonExistingNoteHint
|
||||
noteId={mockedNoteId}
|
||||
onNoteCreated={onNoteCreatedCallback}></CreateNonExistingNoteHint>
|
||||
)
|
||||
const button = await screen.findByTestId('createNoteButton')
|
||||
await act<void>(() => {
|
||||
button.click()
|
||||
|
@ -108,7 +117,11 @@ describe('create non existing note hint', () => {
|
|||
it("shows an error message if note couldn't be created", async () => {
|
||||
mockFailingCreateNoteWithPrimaryAlias()
|
||||
const onNoteCreatedCallback = jest.fn()
|
||||
const view = render(<CreateNonExistingNoteHint onNoteCreated={onNoteCreatedCallback}></CreateNonExistingNoteHint>)
|
||||
const view = render(
|
||||
<CreateNonExistingNoteHint
|
||||
noteId={mockedNoteId}
|
||||
onNoteCreated={onNoteCreatedCallback}></CreateNonExistingNoteHint>
|
||||
)
|
||||
const button = await screen.findByTestId('createNoteButton')
|
||||
await act<void>(() => {
|
||||
button.click()
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { createNoteWithPrimaryAlias } from '../../../api/notes'
|
||||
import { useSingleStringUrlParameter } from '../../../hooks/common/use-single-string-url-parameter'
|
||||
import { testId } from '../../../utils/test-id'
|
||||
import { UiIcon } from '../icons/ui-icon'
|
||||
import { ShowIf } from '../show-if/show-if'
|
||||
|
@ -20,6 +19,7 @@ import { useAsyncFn } from 'react-use'
|
|||
|
||||
export interface CreateNonExistingNoteHintProps {
|
||||
onNoteCreated: () => void
|
||||
noteId: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,17 +27,16 @@ export interface CreateNonExistingNoteHintProps {
|
|||
* When the button was clicked it also shows the progress.
|
||||
*
|
||||
* @param onNoteCreated A function that will be called after the note was created.
|
||||
* @param noteId The wanted id for the note to create
|
||||
*/
|
||||
export const CreateNonExistingNoteHint: React.FC<CreateNonExistingNoteHintProps> = ({ onNoteCreated }) => {
|
||||
export const CreateNonExistingNoteHint: React.FC<CreateNonExistingNoteHintProps> = ({ onNoteCreated, noteId }) => {
|
||||
useTranslation()
|
||||
const noteIdFromUrl = useSingleStringUrlParameter('noteId', undefined)
|
||||
|
||||
const [returnState, createNote] = useAsyncFn(async () => {
|
||||
if (noteIdFromUrl === undefined) {
|
||||
throw new Error('Note id not set')
|
||||
if (noteId !== undefined) {
|
||||
return await createNoteWithPrimaryAlias('', noteId)
|
||||
}
|
||||
return await createNoteWithPrimaryAlias('', noteIdFromUrl)
|
||||
}, [noteIdFromUrl])
|
||||
}, [noteId])
|
||||
|
||||
const onClickHandler = useCallback(() => {
|
||||
void createNote()
|
||||
|
@ -49,7 +48,7 @@ export const CreateNonExistingNoteHint: React.FC<CreateNonExistingNoteHintProps>
|
|||
}
|
||||
}, [onNoteCreated, returnState.value])
|
||||
|
||||
if (noteIdFromUrl === undefined) {
|
||||
if (noteId === undefined) {
|
||||
return null
|
||||
} else if (returnState.value) {
|
||||
return (
|
||||
|
@ -76,7 +75,7 @@ export const CreateNonExistingNoteHint: React.FC<CreateNonExistingNoteHintProps>
|
|||
return (
|
||||
<Alert variant={'info'} {...testId('createNoteMessage')} className={'mt-5'}>
|
||||
<span>
|
||||
<Trans i18nKey={'noteLoadingBoundary.createNote.question'} values={{ aliasName: noteIdFromUrl }} />
|
||||
<Trans i18nKey={'noteLoadingBoundary.createNote.question'} values={{ aliasName: noteId }} />
|
||||
</span>
|
||||
<div className={'mt-3'}>
|
||||
<Button
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { getNote } from '../../../../api/notes'
|
||||
import { useSingleStringUrlParameter } from '../../../../hooks/common/use-single-string-url-parameter'
|
||||
import { setNoteDataFromServer } from '../../../../redux/note-details/methods'
|
||||
import { useAsyncFn } from 'react-use'
|
||||
import type { AsyncState } from 'react-use/lib/useAsyncFn'
|
||||
|
@ -14,15 +13,13 @@ import type { AsyncState } from 'react-use/lib/useAsyncFn'
|
|||
*
|
||||
* @return An {@link AsyncState async state} that represents the current state of the loading process.
|
||||
*/
|
||||
export const useLoadNoteFromServer = (): [AsyncState<boolean>, () => void] => {
|
||||
const id = useSingleStringUrlParameter('noteId', undefined)
|
||||
|
||||
export const useLoadNoteFromServer = (noteId: string | undefined): [AsyncState<boolean>, () => void] => {
|
||||
return useAsyncFn(async (): Promise<boolean> => {
|
||||
if (id === undefined) {
|
||||
if (noteId === undefined) {
|
||||
throw new Error('Invalid id')
|
||||
}
|
||||
const noteFromServer = await getNote(id)
|
||||
const noteFromServer = await getNote(noteId)
|
||||
setNoteDataFromServer(noteFromServer)
|
||||
return true
|
||||
}, [id])
|
||||
}, [noteId])
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import { ApiError } from '../../../api/common/api-error'
|
|||
import * as getNoteModule from '../../../api/notes'
|
||||
import type { Note } from '../../../api/notes/types'
|
||||
import * as LoadingScreenModule from '../../../components/application-loader/loading-screen/loading-screen'
|
||||
import * as useSingleStringUrlParameterModule from '../../../hooks/common/use-single-string-url-parameter'
|
||||
import * as setNoteDataFromServerModule from '../../../redux/note-details/methods'
|
||||
import { mockI18n } from '../../../test-utils/mock-i18n'
|
||||
import { testId } from '../../../utils/test-id'
|
||||
|
@ -64,17 +63,8 @@ describe('Note loading boundary', () => {
|
|||
</Fragment>
|
||||
)
|
||||
})
|
||||
mockGetNoteIdQueryParameter()
|
||||
})
|
||||
|
||||
const mockGetNoteIdQueryParameter = () => {
|
||||
const expectedQueryParameter = 'noteId'
|
||||
jest.spyOn(useSingleStringUrlParameterModule, 'useSingleStringUrlParameter').mockImplementation((parameter) => {
|
||||
expect(parameter).toBe(expectedQueryParameter)
|
||||
return mockedNoteId
|
||||
})
|
||||
}
|
||||
|
||||
const mockGetNoteApiCall = (returnValue: Note) => {
|
||||
jest.spyOn(getNoteModule, 'getNote').mockImplementation((id) => {
|
||||
expect(id).toBe(mockedNoteId)
|
||||
|
@ -105,7 +95,7 @@ describe('Note loading boundary', () => {
|
|||
const setNoteInReduxFunctionMock = mockSetNoteInRedux(mockedNote)
|
||||
|
||||
const view = render(
|
||||
<NoteLoadingBoundary>
|
||||
<NoteLoadingBoundary noteId={mockedNoteId}>
|
||||
<span data-testid={'success'}>success!</span>
|
||||
</NoteLoadingBoundary>
|
||||
)
|
||||
|
@ -121,7 +111,7 @@ describe('Note loading boundary', () => {
|
|||
const setNoteInReduxFunctionMock = mockSetNoteInRedux(mockedNote)
|
||||
|
||||
const view = render(
|
||||
<NoteLoadingBoundary>
|
||||
<NoteLoadingBoundary noteId={mockedNoteId}>
|
||||
<span data-testid={'success'}>success!</span>
|
||||
</NoteLoadingBoundary>
|
||||
)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use client'
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
|
@ -17,15 +18,20 @@ import React, { useEffect, useMemo } from 'react'
|
|||
|
||||
const logger = new Logger('NoteLoadingBoundary')
|
||||
|
||||
export interface NoteIdProps {
|
||||
noteId: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the note identified by the note-id in the URL.
|
||||
* During the loading a {@link LoadingScreen loading screen} will be rendered instead of the child elements.
|
||||
* The boundary also shows errors that occur during the loading process.
|
||||
*
|
||||
* @param children The react elements that will be shown when the loading was successful.
|
||||
* @param children The react elements that will be shown when the loading was successful
|
||||
* @param noteId the id of the note to load
|
||||
*/
|
||||
export const NoteLoadingBoundary: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const [{ error, loading, value }, loadNoteFromServer] = useLoadNoteFromServer()
|
||||
export const NoteLoadingBoundary: React.FC<PropsWithChildren<NoteIdProps>> = ({ children, noteId }) => {
|
||||
const [{ error, loading, value }, loadNoteFromServer] = useLoadNoteFromServer(noteId)
|
||||
|
||||
useEffect(() => {
|
||||
loadNoteFromServer()
|
||||
|
@ -46,11 +52,11 @@ export const NoteLoadingBoundary: React.FC<PropsWithChildren> = ({ children }) =
|
|||
titleI18nKey={`${errorI18nKeyPrefix}.title`}
|
||||
descriptionI18nKey={`${errorI18nKeyPrefix}.description`}>
|
||||
<ShowIf condition={error instanceof ApiError && error.statusCode === 404}>
|
||||
<CreateNonExistingNoteHint onNoteCreated={loadNoteFromServer} />
|
||||
<CreateNonExistingNoteHint onNoteCreated={loadNoteFromServer} noteId={noteId} />
|
||||
</ShowIf>
|
||||
</CommonErrorPage>
|
||||
)
|
||||
}, [error, loadNoteFromServer])
|
||||
}, [error, loadNoteFromServer, noteId])
|
||||
|
||||
return (
|
||||
<CustomAsyncLoadingBoundary
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useRouter } from 'next/router'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Logger } from '../../utils/logger'
|
||||
import { testId } from '../../utils/test-id'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
export interface RedirectProps {
|
||||
|
@ -14,8 +13,6 @@ export interface RedirectProps {
|
|||
replace?: boolean
|
||||
}
|
||||
|
||||
const logger = new Logger('Redirect')
|
||||
|
||||
/**
|
||||
* Redirects the user to another URL. Can be external or internal.
|
||||
*
|
||||
|
@ -26,9 +23,7 @@ export const Redirect: React.FC<RedirectProps> = ({ to, replace }) => {
|
|||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
;(replace ? router.replace(to) : router.push(to)).catch((error: Error) => {
|
||||
logger.error(`Error while redirecting to ${to}`, error)
|
||||
})
|
||||
replace ? router.replace(to) : router.push(to)
|
||||
}, [replace, router, to])
|
||||
|
||||
return (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue