feat(notes): redirect note to its primary address

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2023-03-24 16:43:50 +01:00
parent c0d4d233da
commit 4b82031464
No known key found for this signature in database
GPG key ID: DB99ADDDC5C0AF82
3 changed files with 53 additions and 3 deletions

View file

@ -45,6 +45,7 @@
"success": "Note has been created." "success": "Note has been created."
}, },
"error": { "error": {
"redirecting": "An error occurred while redirecting you to the primary address of this note",
"notFound": { "notFound": {
"title": "Note not found", "title": "Note not found",
"description": "The requested note doesn't exist." "description": "The requested note doesn't exist."

View file

@ -7,11 +7,13 @@ import { ApiError } from '../../../api/common/api-error'
import * as getNoteModule from '../../../api/notes' import * as getNoteModule from '../../../api/notes'
import type { Note } from '../../../api/notes/types' import type { Note } from '../../../api/notes/types'
import * as LoadingScreenModule from '../../../components/application-loader/loading-screen/loading-screen' import * as LoadingScreenModule from '../../../components/application-loader/loading-screen/loading-screen'
import * as useApplicationStateModule from '../../../hooks/common/use-application-state'
import * as useSingleStringUrlParameterModule from '../../../hooks/common/use-single-string-url-parameter' import * as useSingleStringUrlParameterModule from '../../../hooks/common/use-single-string-url-parameter'
import * as setNoteDataFromServerModule from '../../../redux/note-details/methods' import * as setNoteDataFromServerModule from '../../../redux/note-details/methods'
import { testId } from '../../../utils/test-id' import { testId } from '../../../utils/test-id'
import * as CommonErrorPageModule from '../../error-pages/common-error-page' import * as CommonErrorPageModule from '../../error-pages/common-error-page'
import { mockI18n } from '../../markdown-renderer/test-utils/mock-i18n' import { mockI18n } from '../../markdown-renderer/test-utils/mock-i18n'
import * as useUiNotificationsModule from '../../notifications/ui-notification-boundary'
import * as CreateNonExistingNoteHintModule from './create-non-existing-note-hint' import * as CreateNonExistingNoteHintModule from './create-non-existing-note-hint'
import { NoteLoadingBoundary } from './note-loading-boundary' import { NoteLoadingBoundary } from './note-loading-boundary'
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
@ -19,13 +21,20 @@ import { Fragment } from 'react'
import { Mock } from 'ts-mockery' import { Mock } from 'ts-mockery'
jest.mock('../../../hooks/common/use-single-string-url-parameter') jest.mock('../../../hooks/common/use-single-string-url-parameter')
jest.mock('../../../hooks/common/use-application-state')
jest.mock('../../../api/notes') jest.mock('../../../api/notes')
jest.mock('../../../redux/note-details/methods') jest.mock('../../../redux/note-details/methods')
jest.mock('../../error-pages/common-error-page', () => ({ jest.mock('../../error-pages/common-error-page', () => ({
CommonErrorPage: jest.fn() CommonErrorPage: jest.fn()
})) }))
jest.mock('../../../components/application-loader/loading-screen/loading-screen') jest.mock('../../../components/application-loader/loading-screen/loading-screen')
jest.mock('../../notifications/ui-notification-boundary')
jest.mock('./create-non-existing-note-hint') jest.mock('./create-non-existing-note-hint')
jest.mock('next/router', () => ({
useRouter: () => ({
push: jest.fn()
})
}))
describe('Note loading boundary', () => { describe('Note loading boundary', () => {
const mockedNoteId = 'mockedNoteId' const mockedNoteId = 'mockedNoteId'
@ -37,6 +46,7 @@ describe('Note loading boundary', () => {
beforeEach(async () => { beforeEach(async () => {
await mockI18n() await mockI18n()
jest.spyOn(useApplicationStateModule, 'useApplicationState').mockReturnValue(mockedNoteId)
jest.spyOn(CreateNonExistingNoteHintModule, 'CreateNonExistingNoteHint').mockImplementation(() => { jest.spyOn(CreateNonExistingNoteHintModule, 'CreateNonExistingNoteHint').mockImplementation(() => {
return ( return (
<Fragment> <Fragment>
@ -64,6 +74,11 @@ describe('Note loading boundary', () => {
</Fragment> </Fragment>
) )
}) })
jest.spyOn(useUiNotificationsModule, 'useUiNotifications').mockReturnValue({
showErrorNotification: jest.fn(),
dismissNotification: jest.fn(),
dispatchUiNotification: jest.fn()
})
mockGetNoteIdQueryParameter() mockGetNoteIdQueryParameter()
}) })

View file

@ -5,14 +5,18 @@
*/ */
import { ApiError } from '../../../api/common/api-error' import { ApiError } from '../../../api/common/api-error'
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 { useSingleStringUrlParameter } from '../../../hooks/common/use-single-string-url-parameter'
import { LoadingScreen } from '../../application-loader/loading-screen/loading-screen' import { LoadingScreen } from '../../application-loader/loading-screen/loading-screen'
import { CommonErrorPage } from '../../error-pages/common-error-page' import { CommonErrorPage } from '../../error-pages/common-error-page'
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
import { CustomAsyncLoadingBoundary } from '../async-loading-boundary/custom-async-loading-boundary' import { CustomAsyncLoadingBoundary } from '../async-loading-boundary/custom-async-loading-boundary'
import { ShowIf } from '../show-if/show-if' import { ShowIf } from '../show-if/show-if'
import { CreateNonExistingNoteHint } from './create-non-existing-note-hint' import { CreateNonExistingNoteHint } from './create-non-existing-note-hint'
import { useLoadNoteFromServer } from './hooks/use-load-note-from-server' import { useLoadNoteFromServer } from './hooks/use-load-note-from-server'
import { useRouter } from 'next/router'
import type { PropsWithChildren } from 'react' import type { PropsWithChildren } from 'react'
import React, { useEffect, useMemo } from 'react' import React, { useEffect, useMemo, useState } from 'react'
/** /**
* Loads the note identified by the note-id in the URL. * Loads the note identified by the note-id in the URL.
@ -23,10 +27,40 @@ import React, { useEffect, useMemo } from 'react'
*/ */
export const NoteLoadingBoundary: React.FC<PropsWithChildren> = ({ children }) => { export const NoteLoadingBoundary: React.FC<PropsWithChildren> = ({ children }) => {
const [{ error, loading, value }, loadNoteFromServer] = useLoadNoteFromServer() const [{ error, loading, value }, loadNoteFromServer] = useLoadNoteFromServer()
const noteId = useSingleStringUrlParameter('noteId', '')
const primaryNoteAddress = useApplicationState((state) => state.noteDetails.primaryAddress)
const router = useRouter()
const { showErrorNotification } = useUiNotifications()
const [primaryAddressChecked, setPrimaryAddressChecked] = useState(false)
useEffect(() => { useEffect(() => {
if (primaryAddressChecked) {
return
}
loadNoteFromServer() loadNoteFromServer()
}, [loadNoteFromServer]) }, [loadNoteFromServer, primaryAddressChecked])
useEffect(() => {
if (!value || primaryAddressChecked) {
return
}
if (noteId !== primaryNoteAddress) {
router
.replace(`/n/${primaryNoteAddress}`, undefined, { shallow: true })
.then(() => setPrimaryAddressChecked(true))
.catch(showErrorNotification('noteLoadingBoundary.error.redirecting'))
} else {
setPrimaryAddressChecked(true)
}
}, [
value,
primaryNoteAddress,
noteId,
router,
showErrorNotification,
primaryAddressChecked,
setPrimaryAddressChecked
])
const errorComponent = useMemo(() => { const errorComponent = useMemo(() => {
if (error === undefined) { if (error === undefined) {
@ -50,7 +84,7 @@ export const NoteLoadingBoundary: React.FC<PropsWithChildren> = ({ children }) =
return ( return (
<CustomAsyncLoadingBoundary <CustomAsyncLoadingBoundary
loading={loading || !value} loading={loading || !value || !primaryAddressChecked}
error={error} error={error}
errorComponent={errorComponent} errorComponent={errorComponent}
loadingComponent={<LoadingScreen />}> loadingComponent={<LoadingScreen />}>