mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 17:26:29 -05:00
fix(frontend): make note details in redux optional
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
118f158ad1
commit
6698450461
50 changed files with 278 additions and 135 deletions
|
@ -71,7 +71,7 @@ describe('Revision modal', () => {
|
||||||
cy.getByCypressId('sidebar.revision.modal').should('be.visible')
|
cy.getByCypressId('sidebar.revision.modal').should('be.visible')
|
||||||
})
|
})
|
||||||
it('can download revisions', () => {
|
it('can download revisions', () => {
|
||||||
cy.intercept('GET', '/api/private/notes/mock-note/revisions/1', {
|
cy.intercept('GET', `/api/private/notes/${testNoteId}/revisions/1`, {
|
||||||
id: 1,
|
id: 1,
|
||||||
createdAt: defaultCreatedAt,
|
createdAt: defaultCreatedAt,
|
||||||
title: 'Features',
|
title: 'Features',
|
||||||
|
@ -86,7 +86,7 @@ describe('Revision modal', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const downloadFolder = Cypress.config('downloadsFolder')
|
const downloadFolder = Cypress.config('downloadsFolder')
|
||||||
const fileName = `mock-note-${defaultCreatedAt.replace(/:/g, '_')}.md`
|
const fileName = `${testNoteId}-${defaultCreatedAt.replace(/:/g, '_')}.md`
|
||||||
const filePath = join(downloadFolder, fileName)
|
const filePath = join(downloadFolder, fileName)
|
||||||
|
|
||||||
cy.getByCypressId('revision.modal.lists').contains(formattedDate).click()
|
cy.getByCypressId('revision.modal.lists').contains(formattedDate).click()
|
||||||
|
|
|
@ -15,6 +15,7 @@ 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 type { PropsWithChildren } from 'react'
|
import type { PropsWithChildren } from 'react'
|
||||||
import React, { useEffect, useMemo } from 'react'
|
import React, { useEffect, useMemo } from 'react'
|
||||||
|
import { unloadNote } from '../../../redux/note-details/methods'
|
||||||
|
|
||||||
const logger = new Logger('NoteLoadingBoundary')
|
const logger = new Logger('NoteLoadingBoundary')
|
||||||
|
|
||||||
|
@ -37,6 +38,13 @@ export const NoteLoadingBoundary: React.FC<PropsWithChildren<NoteIdProps>> = ({
|
||||||
loadNoteFromServer()
|
loadNoteFromServer()
|
||||||
}, [loadNoteFromServer])
|
}, [loadNoteFromServer])
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
unloadNote()
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const errorComponent = useMemo(() => {
|
const errorComponent = useMemo(() => {
|
||||||
if (error === undefined) {
|
if (error === undefined) {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -15,17 +15,18 @@ import { useMemo } from 'react'
|
||||||
*/
|
*/
|
||||||
export const useSendAdditionalConfigurationToRenderer = (rendererReady: boolean): void => {
|
export const useSendAdditionalConfigurationToRenderer = (rendererReady: boolean): void => {
|
||||||
const darkModePreference = useApplicationState((state) => state.darkMode.darkModePreference)
|
const darkModePreference = useApplicationState((state) => state.darkMode.darkModePreference)
|
||||||
const newlinesAreBreaks = useApplicationState((state) => state.noteDetails.frontmatter.newlinesAreBreaks)
|
const newlinesAreBreaks = useApplicationState((state) => state.noteDetails?.frontmatter.newlinesAreBreaks)
|
||||||
|
|
||||||
useSendToRenderer(
|
useSendToRenderer(
|
||||||
useMemo(
|
useMemo(() => {
|
||||||
() => ({
|
return newlinesAreBreaks === undefined
|
||||||
type: CommunicationMessageType.SET_ADDITIONAL_CONFIGURATION,
|
? undefined
|
||||||
darkModePreference: darkModePreference,
|
: {
|
||||||
newLinesAreBreaks: newlinesAreBreaks
|
type: CommunicationMessageType.SET_ADDITIONAL_CONFIGURATION,
|
||||||
}),
|
darkModePreference: darkModePreference,
|
||||||
[darkModePreference, newlinesAreBreaks]
|
newLinesAreBreaks: newlinesAreBreaks
|
||||||
),
|
}
|
||||||
|
}, [darkModePreference, newlinesAreBreaks]),
|
||||||
rendererReady
|
rendererReady
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,11 @@ export const DocumentInfobar: React.FC = () => {
|
||||||
|
|
||||||
// TODO Check permissions ("writability") of note and show edit link depending on that.
|
// TODO Check permissions ("writability") of note and show edit link depending on that.
|
||||||
const linkTitle = useTranslatedText('views.readOnly.editNote')
|
const linkTitle = useTranslatedText('views.readOnly.editNote')
|
||||||
|
|
||||||
|
if (noteDetails === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`d-flex flex-row my-3 ${styles['document-infobar']}`}>
|
<div className={`d-flex flex-row my-3 ${styles['document-infobar']}`}>
|
||||||
<div className={'col-md'}> </div>
|
<div className={'col-md'}> </div>
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { useCodemirrorReferenceContext } from '../../../change-content-context/c
|
||||||
import type { CursorSelection } from '../../tool-bar/formatters/types/cursor-selection'
|
import type { CursorSelection } from '../../tool-bar/formatters/types/cursor-selection'
|
||||||
import { useHandleUpload } from '../use-handle-upload'
|
import { useHandleUpload } from '../use-handle-upload'
|
||||||
import { findRegexMatchInText } from './find-regex-match-in-text'
|
import { findRegexMatchInText } from './find-regex-match-in-text'
|
||||||
import { Optional } from '@mrdrogdrog/optional'
|
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
const log = new Logger('useOnImageUpload')
|
const log = new Logger('useOnImageUpload')
|
||||||
|
@ -43,12 +42,7 @@ export const useOnImageUploadFromRenderer = (): void => {
|
||||||
.then((result) => result.blob())
|
.then((result) => result.blob())
|
||||||
.then((blob) => {
|
.then((blob) => {
|
||||||
const file = new File([blob], fileName, { type: blob.type })
|
const file = new File([blob], fileName, { type: blob.type })
|
||||||
const { cursorSelection, alt, title } = Optional.ofNullable(lineIndex)
|
const { cursorSelection, alt, title } = findPlaceholderInMarkdownContent(lineIndex, placeholderIndexInLine)
|
||||||
.flatMap((actualLineIndex) => {
|
|
||||||
const lineOffset = getGlobalState().noteDetails.startOfContentLineOffset
|
|
||||||
return findPlaceholderInMarkdownContent(actualLineIndex + lineOffset, placeholderIndexInLine)
|
|
||||||
})
|
|
||||||
.orElse({} as ExtractResult)
|
|
||||||
handleUpload(codeMirrorReference, file, cursorSelection, alt, title)
|
handleUpload(codeMirrorReference, file, cursorSelection, alt, title)
|
||||||
})
|
})
|
||||||
.catch((error) => log.error(error))
|
.catch((error) => log.error(error))
|
||||||
|
@ -71,11 +65,30 @@ export interface ExtractResult {
|
||||||
* @param replacementIndexInLine If multiple image placeholders are present in the target line then this number describes the index of the wanted placeholder.
|
* @param replacementIndexInLine If multiple image placeholders are present in the target line then this number describes the index of the wanted placeholder.
|
||||||
* @return the calculated start and end position or undefined if no position could be determined
|
* @return the calculated start and end position or undefined if no position could be determined
|
||||||
*/
|
*/
|
||||||
const findPlaceholderInMarkdownContent = (lineIndex: number, replacementIndexInLine = 0): Optional<ExtractResult> => {
|
const findPlaceholderInMarkdownContent = (
|
||||||
|
lineIndex: number | undefined,
|
||||||
|
replacementIndexInLine: number | undefined
|
||||||
|
): ExtractResult => {
|
||||||
|
if (lineIndex === undefined) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
const noteDetails = getGlobalState().noteDetails
|
const noteDetails = getGlobalState().noteDetails
|
||||||
|
if (!noteDetails) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
const currentMarkdownContentLines = noteDetails.markdownContent.lines
|
const currentMarkdownContentLines = noteDetails.markdownContent.lines
|
||||||
return Optional.ofNullable(noteDetails.markdownContent.lineStartIndexes[lineIndex]).map((startIndexOfLine) =>
|
const actualLineIndex = noteDetails.startOfContentLineOffset + lineIndex
|
||||||
findImagePlaceholderInLine(currentMarkdownContentLines[lineIndex], startIndexOfLine, replacementIndexInLine)
|
const lineStartIndex = noteDetails.markdownContent.lineStartIndexes[actualLineIndex]
|
||||||
|
if (lineStartIndex === undefined) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
findImagePlaceholderInLine(
|
||||||
|
currentMarkdownContentLines[actualLineIndex],
|
||||||
|
lineStartIndex,
|
||||||
|
replacementIndexInLine ?? 0
|
||||||
|
) ?? {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,10 @@ export const useHandleUpload = (): handleUploadSignature => {
|
||||||
: t('editor.upload.uploadFile.withoutDescription', { fileName: file.name })
|
: t('editor.upload.uploadFile.withoutDescription', { fileName: file.name })
|
||||||
|
|
||||||
const uploadPlaceholder = `![${uploadFileInfo}](upload-${randomId}${additionalUrlText ?? ''})`
|
const uploadPlaceholder = `![${uploadFileInfo}](upload-${randomId}${additionalUrlText ?? ''})`
|
||||||
const noteId = getGlobalState().noteDetails.id
|
const noteId = getGlobalState().noteDetails?.id
|
||||||
|
if (noteId === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
changeContent(({ currentSelection }) => {
|
changeContent(({ currentSelection }) => {
|
||||||
return replaceSelection(cursorSelection ?? currentSelection, uploadPlaceholder, false)
|
return replaceSelection(cursorSelection ?? currentSelection, uploadPlaceholder, false)
|
||||||
})
|
})
|
||||||
|
|
|
@ -24,24 +24,23 @@ const calculateLineBasedPosition = (absolutePosition: number, lineStartIndexes:
|
||||||
* Returns the line+character based position of the to-cursor, if available.
|
* Returns the line+character based position of the to-cursor, if available.
|
||||||
*/
|
*/
|
||||||
export const useLineBasedToPosition = (): LineBasedPosition | undefined => {
|
export const useLineBasedToPosition = (): LineBasedPosition | undefined => {
|
||||||
const lineStartIndexes = useApplicationState((state) => state.noteDetails.markdownContent.lineStartIndexes)
|
const lineStartIndexes = useApplicationState((state) => state.noteDetails?.markdownContent.lineStartIndexes ?? [])
|
||||||
const selection = useApplicationState((state) => state.noteDetails.selection)
|
const selectionTo = useApplicationState((state) => state.noteDetails?.selection.to)
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const to = selection.to
|
if (selectionTo === undefined) {
|
||||||
if (to === undefined) {
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return calculateLineBasedPosition(to, lineStartIndexes)
|
return calculateLineBasedPosition(selectionTo, lineStartIndexes)
|
||||||
}, [selection.to, lineStartIndexes])
|
}, [selectionTo, lineStartIndexes])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the line+character based position of the from-cursor.
|
* Returns the line+character based position of the from-cursor.
|
||||||
*/
|
*/
|
||||||
export const useLineBasedFromPosition = (): LineBasedPosition => {
|
export const useLineBasedFromPosition = (): LineBasedPosition => {
|
||||||
const lineStartIndexes = useApplicationState((state) => state.noteDetails.markdownContent.lineStartIndexes)
|
const lineStartIndexes = useApplicationState((state) => state.noteDetails?.markdownContent.lineStartIndexes ?? [])
|
||||||
const selection = useApplicationState((state) => state.noteDetails.selection)
|
const selection = useApplicationState((state) => state.noteDetails?.selection ?? { from: 0 })
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return calculateLineBasedPosition(selection.from, lineStartIndexes)
|
return calculateLineBasedPosition(selection.from, lineStartIndexes)
|
||||||
|
|
|
@ -32,7 +32,7 @@ export const useRealtimeConnection = (): MessageTransporter => {
|
||||||
if (isMockMode) {
|
if (isMockMode) {
|
||||||
logger.debug('Creating Loopback connection...')
|
logger.debug('Creating Loopback connection...')
|
||||||
messageTransporter.setAdapter(
|
messageTransporter.setAdapter(
|
||||||
new MockedBackendTransportAdapter(getGlobalState().noteDetails.markdownContent.plain)
|
new MockedBackendTransportAdapter(getGlobalState().noteDetails?.markdownContent.plain ?? '')
|
||||||
)
|
)
|
||||||
} else if (websocketUrl) {
|
} else if (websocketUrl) {
|
||||||
logger.debug(`Connecting to ${websocketUrl.toString()}`)
|
logger.debug(`Connecting to ${websocketUrl.toString()}`)
|
||||||
|
|
|
@ -4,9 +4,7 @@
|
||||||
* 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 { store } from '../../../../../redux'
|
import { resetRealtimeStatus, setRealtimeUsers } from '../../../../../redux/realtime/methods'
|
||||||
import { setRealtimeUsers } from '../../../../../redux/realtime/methods'
|
|
||||||
import { RealtimeStatusActionType } from '../../../../../redux/realtime/types'
|
|
||||||
import type { MessageTransporter } from '@hedgedoc/commons'
|
import type { MessageTransporter } from '@hedgedoc/commons'
|
||||||
import { MessageType } from '@hedgedoc/commons'
|
import { MessageType } from '@hedgedoc/commons'
|
||||||
import type { Listener } from 'eventemitter2'
|
import type { Listener } from 'eventemitter2'
|
||||||
|
@ -43,12 +41,5 @@ export const useReceiveRealtimeUsers = (messageTransporter: MessageTransporter):
|
||||||
}
|
}
|
||||||
}, [isConnected, messageTransporter])
|
}, [isConnected, messageTransporter])
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => () => resetRealtimeStatus(), [])
|
||||||
() => () => {
|
|
||||||
store.dispatch({
|
|
||||||
type: RealtimeStatusActionType.RESET_REALTIME_STATUS
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ const LOCAL_FALLBACK_URL = 'ws://localhost:8080/realtime/'
|
||||||
* Provides the URL for the realtime endpoint.
|
* Provides the URL for the realtime endpoint.
|
||||||
*/
|
*/
|
||||||
export const useWebsocketUrl = (): URL | undefined => {
|
export const useWebsocketUrl = (): URL | undefined => {
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.id)
|
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||||
const baseUrl = useBaseUrl()
|
const baseUrl = useBaseUrl()
|
||||||
|
|
||||||
const websocketUrl = useMemo(() => {
|
const websocketUrl = useMemo(() => {
|
||||||
|
@ -33,7 +33,7 @@ export const useWebsocketUrl = (): URL | undefined => {
|
||||||
}, [baseUrl])
|
}, [baseUrl])
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (noteId === '') {
|
if (noteId === '' || noteId === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const url = new URL(websocketUrl)
|
const url = new URL(websocketUrl)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
export const NumberOfLinesInDocumentInfo: React.FC = () => {
|
export const NumberOfLinesInDocumentInfo: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
const linesInDocument = useApplicationState((state) => state.noteDetails.markdownContent.lines.length)
|
const linesInDocument = useApplicationState((state) => state.noteDetails?.markdownContent.lines.length ?? 0)
|
||||||
const translationOptions = useMemo(() => ({ lines: linesInDocument }), [linesInDocument])
|
const translationOptions = useMemo(() => ({ lines: linesInDocument }), [linesInDocument])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const RemainingCharactersInfo: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const maxDocumentLength = useFrontendConfig().maxDocumentLength
|
const maxDocumentLength = useFrontendConfig().maxDocumentLength
|
||||||
const contentLength = useApplicationState((state) => state.noteDetails.markdownContent.plain.length)
|
const contentLength = useApplicationState((state) => state.noteDetails?.markdownContent.plain.length ?? 0)
|
||||||
const remainingCharacters = useMemo(() => maxDocumentLength - contentLength, [contentLength, maxDocumentLength])
|
const remainingCharacters = useMemo(() => maxDocumentLength - contentLength, [contentLength, maxDocumentLength])
|
||||||
|
|
||||||
const remainingCharactersClass = useMemo(() => {
|
const remainingCharactersClass = useMemo(() => {
|
||||||
|
|
|
@ -14,10 +14,10 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
export const SelectedCharacters: React.FC = () => {
|
export const SelectedCharacters: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
const selection = useApplicationState((state) => state.noteDetails.selection)
|
const selection = useApplicationState((state) => state.noteDetails?.selection)
|
||||||
const count = useMemo(
|
const count = useMemo(
|
||||||
() => (selection.to === undefined ? undefined : selection.to - selection.from),
|
() => (selection === undefined || selection.to === undefined ? undefined : selection.to - selection.from),
|
||||||
[selection.from, selection.to]
|
[selection]
|
||||||
)
|
)
|
||||||
const countTranslationOptions = useMemo(() => ({ count }), [count])
|
const countTranslationOptions = useMemo(() => ({ count }), [count])
|
||||||
|
|
||||||
|
|
|
@ -16,16 +16,16 @@ import { useEffect, useRef } from 'react'
|
||||||
* The entry is updated when the title or tags of the note change.
|
* The entry is updated when the title or tags of the note change.
|
||||||
*/
|
*/
|
||||||
export const useUpdateLocalHistoryEntry = (): void => {
|
export const useUpdateLocalHistoryEntry = (): void => {
|
||||||
const id = useApplicationState((state) => state.noteDetails.id)
|
const id = useApplicationState((state) => state.noteDetails?.id)
|
||||||
const userExists = useApplicationState((state) => !!state.user)
|
const userExists = useApplicationState((state) => !!state.user)
|
||||||
const currentNoteTitle = useApplicationState((state) => state.noteDetails.title)
|
const currentNoteTitle = useApplicationState((state) => state.noteDetails?.title ?? '')
|
||||||
const currentNoteTags = useApplicationState((state) => state.noteDetails.frontmatter.tags)
|
const currentNoteTags = useApplicationState((state) => state.noteDetails?.frontmatter.tags ?? [])
|
||||||
|
|
||||||
const lastNoteTitle = useRef('')
|
const lastNoteTitle = useRef('')
|
||||||
const lastNoteTags = useRef<string[]>([])
|
const lastNoteTags = useRef<string[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userExists) {
|
if (userExists || id === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (currentNoteTitle === lastNoteTitle.current && equal(currentNoteTags, lastNoteTags.current)) {
|
if (currentNoteTitle === lastNoteTitle.current && equal(currentNoteTags, lastNoteTags.current)) {
|
||||||
|
|
|
@ -19,8 +19,12 @@ export const useOnScrollWithLineOffset = (onScroll: ScrollCallback | undefined):
|
||||||
return undefined
|
return undefined
|
||||||
} else {
|
} else {
|
||||||
return (scrollState: ScrollState) => {
|
return (scrollState: ScrollState) => {
|
||||||
|
const noteDetails = getGlobalState().noteDetails
|
||||||
|
if (noteDetails === null) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
onScroll({
|
onScroll({
|
||||||
firstLineInView: scrollState.firstLineInView + getGlobalState().noteDetails.startOfContentLineOffset,
|
firstLineInView: scrollState.firstLineInView + noteDetails.startOfContentLineOffset,
|
||||||
scrolledPercentage: scrollState.scrolledPercentage
|
scrolledPercentage: scrollState.scrolledPercentage
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,9 @@ import { useMemo } from 'react'
|
||||||
* @return the adjusted scroll state without the line offset
|
* @return the adjusted scroll state without the line offset
|
||||||
*/
|
*/
|
||||||
export const useScrollStateWithoutLineOffset = (scrollState: ScrollState | undefined): ScrollState | undefined => {
|
export const useScrollStateWithoutLineOffset = (scrollState: ScrollState | undefined): ScrollState | undefined => {
|
||||||
const lineOffset = useApplicationState((state) => state.noteDetails.startOfContentLineOffset)
|
const lineOffset = useApplicationState((state) => state.noteDetails?.startOfContentLineOffset)
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return scrollState === undefined
|
return scrollState === undefined || lineOffset === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
firstLineInView: scrollState.firstLineInView - lineOffset,
|
firstLineInView: scrollState.firstLineInView - lineOffset,
|
||||||
|
|
|
@ -28,10 +28,14 @@ export type RendererPaneProps = Omit<
|
||||||
*/
|
*/
|
||||||
export const RendererPane: React.FC<RendererPaneProps> = ({ scrollState, onScroll, ...props }) => {
|
export const RendererPane: React.FC<RendererPaneProps> = ({ scrollState, onScroll, ...props }) => {
|
||||||
const trimmedContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter()
|
const trimmedContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter()
|
||||||
const noteType: NoteType = useApplicationState((state) => state.noteDetails.frontmatter.type)
|
const noteType = useApplicationState((state) => state.noteDetails?.frontmatter.type)
|
||||||
const adjustedOnScroll = useOnScrollWithLineOffset(onScroll)
|
const adjustedOnScroll = useOnScrollWithLineOffset(onScroll)
|
||||||
const adjustedScrollState = useScrollStateWithoutLineOffset(scrollState)
|
const adjustedScrollState = useScrollStateWithoutLineOffset(scrollState)
|
||||||
|
|
||||||
|
if (!noteType) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RendererIframe
|
<RendererIframe
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -24,13 +24,16 @@ const validAliasRegex = /^[a-z0-9_-]*$/
|
||||||
*/
|
*/
|
||||||
export const AliasesAddForm: React.FC = () => {
|
export const AliasesAddForm: React.FC = () => {
|
||||||
const { showErrorNotification } = useUiNotifications()
|
const { showErrorNotification } = useUiNotifications()
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.id)
|
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||||
const isOwner = useIsOwner()
|
const isOwner = useIsOwner()
|
||||||
const [newAlias, setNewAlias] = useState('')
|
const [newAlias, setNewAlias] = useState('')
|
||||||
|
|
||||||
const onAddAlias = useCallback(
|
const onAddAlias = useCallback(
|
||||||
(event: FormEvent<HTMLFormElement>) => {
|
(event: FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
if (noteId === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
addAlias(noteId, newAlias)
|
addAlias(noteId, newAlias)
|
||||||
.then(updateMetadata)
|
.then(updateMetadata)
|
||||||
.catch(showErrorNotification('editor.modal.aliases.errorAddingAlias'))
|
.catch(showErrorNotification('editor.modal.aliases.errorAddingAlias'))
|
||||||
|
|
|
@ -12,12 +12,14 @@ import React, { Fragment, useMemo } from 'react'
|
||||||
* Renders the list of aliases.
|
* Renders the list of aliases.
|
||||||
*/
|
*/
|
||||||
export const AliasesList: React.FC = () => {
|
export const AliasesList: React.FC = () => {
|
||||||
const aliases = useApplicationState((state: ApplicationState) => state.noteDetails.aliases)
|
const aliases = useApplicationState((state: ApplicationState) => state.noteDetails?.aliases)
|
||||||
|
|
||||||
const aliasesDom = useMemo(() => {
|
const aliasesDom = useMemo(() => {
|
||||||
return aliases
|
return aliases === undefined
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
? null
|
||||||
.map((alias) => <AliasesListEntry alias={alias} key={alias.name} />)
|
: aliases
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((alias) => <AliasesListEntry alias={alias} key={alias.name} />)
|
||||||
}, [aliases])
|
}, [aliases])
|
||||||
|
|
||||||
return <Fragment>{aliasesDom}</Fragment>
|
return <Fragment>{aliasesDom}</Fragment>
|
||||||
|
|
|
@ -26,11 +26,14 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
export const DeleteNoteSidebarEntry: React.FC<PropsWithChildren<SpecificSidebarEntryProps>> = ({ hide, className }) => {
|
export const DeleteNoteSidebarEntry: React.FC<PropsWithChildren<SpecificSidebarEntryProps>> = ({ hide, className }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.id)
|
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||||
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
||||||
const { showErrorNotification } = useUiNotifications()
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const deleteNoteAndCloseDialog = useCallback(() => {
|
const deleteNoteAndCloseDialog = useCallback(() => {
|
||||||
|
if (noteId === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
deleteNote(noteId)
|
deleteNote(noteId)
|
||||||
.then(() => router.push('/history'))
|
.then(() => router.push('/history'))
|
||||||
.catch(showErrorNotification('landing.history.error.deleteNote.text'))
|
.catch(showErrorNotification('landing.history.error.deleteNote.text'))
|
||||||
|
|
|
@ -20,7 +20,11 @@ export const ExportMarkdownSidebarEntry: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const markdownContent = useNoteMarkdownContent()
|
const markdownContent = useNoteMarkdownContent()
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
const sanitized = sanitize(getGlobalState().noteDetails.title)
|
const title = getGlobalState().noteDetails?.title
|
||||||
|
if (title === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sanitized = sanitize(title)
|
||||||
download(markdownContent, `${sanitized !== '' ? sanitized : t('editor.untitledNote')}.md`, 'text/markdown')
|
download(markdownContent, `${sanitized !== '' ? sanitized : t('editor.untitledNote')}.md`, 'text/markdown')
|
||||||
}, [markdownContent, t])
|
}, [markdownContent, t])
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { People as IconPeople } from 'react-bootstrap-icons'
|
||||||
* Renders an info line about the number of contributors for the note.
|
* Renders an info line about the number of contributors for the note.
|
||||||
*/
|
*/
|
||||||
export const NoteInfoLineContributors: React.FC = () => {
|
export const NoteInfoLineContributors: React.FC = () => {
|
||||||
const contributors = useApplicationState((state) => state.noteDetails.editedBy.length)
|
const contributors = useApplicationState((state) => state.noteDetails?.editedBy.length ?? 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.contributors'} icon={IconPeople}>
|
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.contributors'} icon={IconPeople}>
|
||||||
|
|
|
@ -16,10 +16,13 @@ import { useTranslation } from 'react-i18next'
|
||||||
*/
|
*/
|
||||||
export const NoteInfoLineCreatedAt: React.FC = () => {
|
export const NoteInfoLineCreatedAt: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const noteCreateTime = useApplicationState((state) => state.noteDetails.createdAt)
|
const noteCreateTime = useApplicationState((state) => state.noteDetails?.createdAt)
|
||||||
const noteCreateDateTime = useMemo(() => DateTime.fromSeconds(noteCreateTime), [noteCreateTime])
|
const noteCreateDateTime = useMemo(
|
||||||
|
() => (noteCreateTime === undefined ? undefined : DateTime.fromSeconds(noteCreateTime)),
|
||||||
|
[noteCreateTime]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return !noteCreateDateTime ? null : (
|
||||||
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.created'} icon={IconPlus}>
|
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.created'} icon={IconPlus}>
|
||||||
<TimeFromNow time={noteCreateDateTime} />
|
<TimeFromNow time={noteCreateDateTime} />
|
||||||
</SidebarMenuInfoEntry>
|
</SidebarMenuInfoEntry>
|
||||||
|
|
|
@ -16,10 +16,13 @@ import { useTranslation } from 'react-i18next'
|
||||||
*/
|
*/
|
||||||
export const NoteInfoLineUpdatedAt: React.FC = () => {
|
export const NoteInfoLineUpdatedAt: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const noteUpdateTime = useApplicationState((state) => state.noteDetails.updatedAt)
|
const noteUpdateTime = useApplicationState((state) => state.noteDetails?.updatedAt)
|
||||||
const noteUpdateDateTime = useMemo(() => DateTime.fromSeconds(noteUpdateTime), [noteUpdateTime])
|
const noteUpdateDateTime = useMemo(
|
||||||
|
() => (noteUpdateTime === undefined ? undefined : DateTime.fromSeconds(noteUpdateTime)),
|
||||||
|
[noteUpdateTime]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return !noteUpdateDateTime ? null : (
|
||||||
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.lastUpdated'} icon={IconPencil}>
|
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.lastUpdated'} icon={IconPencil}>
|
||||||
<TimeFromNow time={noteUpdateDateTime} />
|
<TimeFromNow time={noteUpdateDateTime} />
|
||||||
</SidebarMenuInfoEntry>
|
</SidebarMenuInfoEntry>
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
*/
|
*/
|
||||||
export const NoteInfoLineUpdatedBy: React.FC = () => {
|
export const NoteInfoLineUpdatedBy: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const noteUpdateUser = useApplicationState((state) => state.noteDetails.updateUsername)
|
const noteUpdateUser = useApplicationState((state) => state.noteDetails?.updateUsername)
|
||||||
|
|
||||||
const userBlock = useMemo(() => {
|
const userBlock = useMemo(() => {
|
||||||
if (!noteUpdateUser) {
|
if (!noteUpdateUser) {
|
||||||
|
|
|
@ -33,11 +33,14 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
||||||
type,
|
type,
|
||||||
disabled
|
disabled
|
||||||
}) => {
|
}) => {
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteId = useApplicationState((state) => state.noteDetails?.primaryAddress)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { showErrorNotification } = useUiNotifications()
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const onSetEntryReadOnly = useCallback(() => {
|
const onSetEntryReadOnly = useCallback(() => {
|
||||||
|
if (!noteId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
setGroupPermission(noteId, type, false)
|
setGroupPermission(noteId, type, false)
|
||||||
.then((updatedPermissions) => {
|
.then((updatedPermissions) => {
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
|
@ -46,6 +49,9 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
||||||
}, [noteId, showErrorNotification, type])
|
}, [noteId, showErrorNotification, type])
|
||||||
|
|
||||||
const onSetEntryWriteable = useCallback(() => {
|
const onSetEntryWriteable = useCallback(() => {
|
||||||
|
if (!noteId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
setGroupPermission(noteId, type, true)
|
setGroupPermission(noteId, type, true)
|
||||||
.then((updatedPermissions) => {
|
.then((updatedPermissions) => {
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
|
@ -54,6 +60,9 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
||||||
}, [noteId, showErrorNotification, type])
|
}, [noteId, showErrorNotification, type])
|
||||||
|
|
||||||
const onSetEntryDenied = useCallback(() => {
|
const onSetEntryDenied = useCallback(() => {
|
||||||
|
if (!noteId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
removeGroupPermission(noteId, type)
|
removeGroupPermission(noteId, type)
|
||||||
.then((updatedPermissions) => {
|
.then((updatedPermissions) => {
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
|
|
|
@ -31,10 +31,13 @@ export const PermissionEntryUser: React.FC<PermissionEntryUserProps & Permission
|
||||||
entry,
|
entry,
|
||||||
disabled
|
disabled
|
||||||
}) => {
|
}) => {
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteId = useApplicationState((state) => state.noteDetails?.primaryAddress)
|
||||||
const { showErrorNotification } = useUiNotifications()
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const onRemoveEntry = useCallback(() => {
|
const onRemoveEntry = useCallback(() => {
|
||||||
|
if (!noteId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
removeUserPermission(noteId, entry.username)
|
removeUserPermission(noteId, entry.username)
|
||||||
.then((updatedPermissions) => {
|
.then((updatedPermissions) => {
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
|
@ -43,6 +46,9 @@ export const PermissionEntryUser: React.FC<PermissionEntryUserProps & Permission
|
||||||
}, [noteId, entry.username, showErrorNotification])
|
}, [noteId, entry.username, showErrorNotification])
|
||||||
|
|
||||||
const onSetEntryReadOnly = useCallback(() => {
|
const onSetEntryReadOnly = useCallback(() => {
|
||||||
|
if (!noteId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
setUserPermission(noteId, entry.username, false)
|
setUserPermission(noteId, entry.username, false)
|
||||||
.then((updatedPermissions) => {
|
.then((updatedPermissions) => {
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
|
@ -51,6 +57,9 @@ export const PermissionEntryUser: React.FC<PermissionEntryUserProps & Permission
|
||||||
}, [noteId, entry.username, showErrorNotification])
|
}, [noteId, entry.username, showErrorNotification])
|
||||||
|
|
||||||
const onSetEntryWriteable = useCallback(() => {
|
const onSetEntryWriteable = useCallback(() => {
|
||||||
|
if (!noteId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
setUserPermission(noteId, entry.username, true)
|
setUserPermission(noteId, entry.username, true)
|
||||||
.then((updatedPermissions) => {
|
.then((updatedPermissions) => {
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
|
|
|
@ -26,9 +26,13 @@ export const PermissionOwnerInfo: React.FC<PermissionOwnerInfoProps & Permission
|
||||||
onEditOwner,
|
onEditOwner,
|
||||||
disabled
|
disabled
|
||||||
}) => {
|
}) => {
|
||||||
const noteOwner = useApplicationState((state) => state.noteDetails.permissions.owner)
|
const noteOwner = useApplicationState((state) => state.noteDetails?.permissions.owner)
|
||||||
const buttonTitle = useTranslatedText('editor.modal.permissions.ownerChange.button')
|
const buttonTitle = useTranslatedText('editor.modal.permissions.ownerChange.button')
|
||||||
|
|
||||||
|
if (!noteOwner) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<UserAvatarForUsername username={noteOwner} />
|
<UserAvatarForUsername username={noteOwner} />
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { Trans } from 'react-i18next'
|
||||||
* @param disabled If the user is not the owner, functionality is disabled.
|
* @param disabled If the user is not the owner, functionality is disabled.
|
||||||
*/
|
*/
|
||||||
export const PermissionSectionOwner: React.FC<PermissionDisabledProps> = ({ disabled }) => {
|
export const PermissionSectionOwner: React.FC<PermissionDisabledProps> = ({ disabled }) => {
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||||
const [changeOwner, setChangeOwner] = useState(false)
|
const [changeOwner, setChangeOwner] = useState(false)
|
||||||
const { showErrorNotification } = useUiNotifications()
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
|
@ -29,6 +29,9 @@ export const PermissionSectionOwner: React.FC<PermissionDisabledProps> = ({ disa
|
||||||
|
|
||||||
const onOwnerChange = useCallback(
|
const onOwnerChange = useCallback(
|
||||||
(newOwner: string) => {
|
(newOwner: string) => {
|
||||||
|
if (!noteId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
setNoteOwner(noteId, newOwner)
|
setNoteOwner(noteId, newOwner)
|
||||||
.then((updatedPermissions) => {
|
.then((updatedPermissions) => {
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
|
|
|
@ -18,10 +18,13 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
*/
|
*/
|
||||||
export const PermissionSectionSpecialGroups: React.FC<PermissionDisabledProps> = ({ disabled }) => {
|
export const PermissionSectionSpecialGroups: React.FC<PermissionDisabledProps> = ({ disabled }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const groupPermissions = useApplicationState((state) => state.noteDetails.permissions.sharedToGroups)
|
const groupPermissions = useApplicationState((state) => state.noteDetails?.permissions.sharedToGroups)
|
||||||
const isOwner = useIsOwner()
|
const isOwner = useIsOwner()
|
||||||
|
|
||||||
const specialGroupEntries = useMemo(() => {
|
const specialGroupEntries = useMemo(() => {
|
||||||
|
if (!groupPermissions) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const groupEveryone = groupPermissions.find((entry) => entry.groupName === (SpecialGroup.EVERYONE as string))
|
const groupEveryone = groupPermissions.find((entry) => entry.groupName === (SpecialGroup.EVERYONE as string))
|
||||||
const groupLoggedIn = groupPermissions.find((entry) => entry.groupName === (SpecialGroup.LOGGED_IN as string))
|
const groupLoggedIn = groupPermissions.find((entry) => entry.groupName === (SpecialGroup.LOGGED_IN as string))
|
||||||
|
|
||||||
|
@ -39,6 +42,10 @@ export const PermissionSectionSpecialGroups: React.FC<PermissionDisabledProps> =
|
||||||
}
|
}
|
||||||
}, [groupPermissions])
|
}, [groupPermissions])
|
||||||
|
|
||||||
|
if (!specialGroupEntries) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<h5 className={'my-3'}>
|
<h5 className={'my-3'}>
|
||||||
|
|
|
@ -20,11 +20,14 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
*/
|
*/
|
||||||
export const PermissionSectionUsers: React.FC<PermissionDisabledProps> = ({ disabled }) => {
|
export const PermissionSectionUsers: React.FC<PermissionDisabledProps> = ({ disabled }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const userPermissions = useApplicationState((state) => state.noteDetails.permissions.sharedToUsers)
|
const userPermissions = useApplicationState((state) => state.noteDetails?.permissions.sharedToUsers)
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||||
const { showErrorNotification } = useUiNotifications()
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const userEntries = useMemo(() => {
|
const userEntries = useMemo(() => {
|
||||||
|
if (!userPermissions) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return userPermissions.map((entry) => (
|
return userPermissions.map((entry) => (
|
||||||
<PermissionEntryUser key={entry.username} entry={entry} disabled={disabled} />
|
<PermissionEntryUser key={entry.username} entry={entry} disabled={disabled} />
|
||||||
))
|
))
|
||||||
|
@ -32,6 +35,9 @@ export const PermissionSectionUsers: React.FC<PermissionDisabledProps> = ({ disa
|
||||||
|
|
||||||
const onAddEntry = useCallback(
|
const onAddEntry = useCallback(
|
||||||
(username: string) => {
|
(username: string) => {
|
||||||
|
if (!noteId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
setUserPermission(noteId, username, false)
|
setUserPermission(noteId, username, false)
|
||||||
.then((updatedPermissions) => {
|
.then((updatedPermissions) => {
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
|
|
|
@ -22,21 +22,24 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
*/
|
*/
|
||||||
export const PinNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
|
export const PinNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const id = useApplicationState((state) => state.noteDetails.id)
|
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||||
const history = useApplicationState((state) => state.history)
|
const history = useApplicationState((state) => state.history)
|
||||||
const { showErrorNotification } = useUiNotifications()
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const isPinned = useMemo(() => {
|
const isPinned = useMemo(() => {
|
||||||
const entry = history.find((entry) => entry.identifier === id)
|
const entry = history.find((entry) => entry.identifier === noteId)
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return entry.pinStatus
|
return entry.pinStatus
|
||||||
}, [id, history])
|
}, [history, noteId])
|
||||||
|
|
||||||
const onPinClicked = useCallback(() => {
|
const onPinClicked = useCallback(() => {
|
||||||
toggleHistoryEntryPinning(id).catch(showErrorNotification('landing.history.error.updateEntry.text'))
|
if (!noteId) {
|
||||||
}, [id, showErrorNotification])
|
return
|
||||||
|
}
|
||||||
|
toggleHistoryEntryPinning(noteId).catch(showErrorNotification('landing.history.error.updateEntry.text'))
|
||||||
|
}, [noteId, showErrorNotification])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarButton
|
<SidebarButton
|
||||||
|
|
|
@ -21,9 +21,12 @@ import { Trans } from 'react-i18next'
|
||||||
*/
|
*/
|
||||||
export const RevisionDeleteModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
|
export const RevisionDeleteModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
|
||||||
const { showErrorNotification } = useUiNotifications()
|
const { showErrorNotification } = useUiNotifications()
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.id)
|
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||||
|
|
||||||
const deleteAllRevisions = useCallback(() => {
|
const deleteAllRevisions = useCallback(() => {
|
||||||
|
if (!noteId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
deleteRevisionsForNote(noteId).catch(showErrorNotification('editor.modal.deleteRevision.error')).finally(onHide)
|
deleteRevisionsForNote(noteId).catch(showErrorNotification('editor.modal.deleteRevision.error')).finally(onHide)
|
||||||
}, [noteId, onHide, showErrorNotification])
|
}, [noteId, onHide, showErrorNotification])
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,17 @@ export const RevisionModalBody = ({ onShowDeleteModal, onHide }: RevisionModal)
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const isOwner = useIsOwner()
|
const isOwner = useIsOwner()
|
||||||
const [selectedRevisionId, setSelectedRevisionId] = useState<number>()
|
const [selectedRevisionId, setSelectedRevisionId] = useState<number>()
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.id)
|
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||||
const { value: revisions, error, loading } = useAsync(() => getAllRevisions(noteIdentifier), [noteIdentifier])
|
const {
|
||||||
|
value: revisions,
|
||||||
|
error,
|
||||||
|
loading
|
||||||
|
} = useAsync(async () => {
|
||||||
|
if (!noteId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return getAllRevisions(noteId)
|
||||||
|
}, [noteId])
|
||||||
|
|
||||||
const revisionLength = revisions?.length ?? 0
|
const revisionLength = revisions?.length ?? 0
|
||||||
const enableDeleteRevisions = revisionLength > 1 && isOwner
|
const enableDeleteRevisions = revisionLength > 1 && isOwner
|
||||||
|
|
|
@ -37,7 +37,7 @@ export const RevisionModalFooter: React.FC<RevisionModalFooterProps> = ({
|
||||||
disableDeleteRevisions
|
disableDeleteRevisions
|
||||||
}) => {
|
}) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||||
const { showErrorNotification } = useUiNotifications()
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const onRevertToRevision = useCallback(() => {
|
const onRevertToRevision = useCallback(() => {
|
||||||
|
@ -47,15 +47,15 @@ export const RevisionModalFooter: React.FC<RevisionModalFooterProps> = ({
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onDownloadRevision = useCallback(() => {
|
const onDownloadRevision = useCallback(() => {
|
||||||
if (selectedRevisionId === undefined) {
|
if (selectedRevisionId === undefined || noteId === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
getRevision(noteIdentifier, selectedRevisionId)
|
getRevision(noteId, selectedRevisionId)
|
||||||
.then((revision) => {
|
.then((revision) => {
|
||||||
downloadRevision(noteIdentifier, revision)
|
downloadRevision(noteId, revision)
|
||||||
})
|
})
|
||||||
.catch(showErrorNotification(''))
|
.catch(showErrorNotification(''))
|
||||||
}, [noteIdentifier, selectedRevisionId, showErrorNotification])
|
}, [noteId, selectedRevisionId, showErrorNotification])
|
||||||
|
|
||||||
const openDeleteModal = useCallback(() => {
|
const openDeleteModal = useCallback(() => {
|
||||||
onHide?.()
|
onHide?.()
|
||||||
|
|
|
@ -25,16 +25,16 @@ export interface RevisionViewerProps {
|
||||||
* @param allRevisions List of metadata for all available revisions.
|
* @param allRevisions List of metadata for all available revisions.
|
||||||
*/
|
*/
|
||||||
export const RevisionViewer: React.FC<RevisionViewerProps> = ({ selectedRevisionId }) => {
|
export const RevisionViewer: React.FC<RevisionViewerProps> = ({ selectedRevisionId }) => {
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||||
const darkModeEnabled = useDarkModeState()
|
const darkModeEnabled = useDarkModeState()
|
||||||
|
|
||||||
const { value, error, loading } = useAsync(async () => {
|
const { value, error, loading } = useAsync(async () => {
|
||||||
if (selectedRevisionId === undefined) {
|
if (noteId === undefined || selectedRevisionId === undefined) {
|
||||||
throw new Error('No revision selected')
|
throw new Error('No revision selected')
|
||||||
} else {
|
} else {
|
||||||
return await getRevision(noteIdentifier, selectedRevisionId)
|
return await getRevision(noteId, selectedRevisionId)
|
||||||
}
|
}
|
||||||
}, [selectedRevisionId, noteIdentifier])
|
}, [selectedRevisionId, noteId])
|
||||||
|
|
||||||
const previousRevisionContent = useMemo(() => {
|
const previousRevisionContent = useMemo(() => {
|
||||||
return Optional.ofNullable(value)
|
return Optional.ofNullable(value)
|
||||||
|
|
|
@ -24,13 +24,16 @@ export interface LinkFieldProps {
|
||||||
*/
|
*/
|
||||||
export const NoteUrlField: React.FC<LinkFieldProps> = ({ type }) => {
|
export const NoteUrlField: React.FC<LinkFieldProps> = ({ type }) => {
|
||||||
const baseUrl = useBaseUrl()
|
const baseUrl = useBaseUrl()
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||||
|
|
||||||
const url = useMemo(() => {
|
const url = useMemo(() => {
|
||||||
|
if (noteId === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
const url = new URL(baseUrl)
|
const url = new URL(baseUrl)
|
||||||
url.pathname += `${type}/${noteIdentifier}`
|
url.pathname += `${type}/${noteId}`
|
||||||
return url.toString()
|
return url.toString()
|
||||||
}, [baseUrl, noteIdentifier, type])
|
}, [baseUrl, noteId, type])
|
||||||
|
|
||||||
return <CopyableField content={url} shareOriginUrl={url} />
|
return !url ? null : <CopyableField content={url} shareOriginUrl={url} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,11 @@ 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)
|
||||||
|
|
||||||
|
if (!noteFrontmatter) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
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'}>
|
||||||
|
|
|
@ -22,16 +22,17 @@ export const SlideShowPageContent: React.FC = () => {
|
||||||
const markdownContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter()
|
const markdownContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter()
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
const slideOptions = useApplicationState((state) => state.noteDetails.frontmatter.slideOptions)
|
const slideOptions = useApplicationState((state) => state.noteDetails?.frontmatter.slideOptions)
|
||||||
const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady)
|
const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady)
|
||||||
useSendToRenderer(
|
useSendToRenderer(
|
||||||
useMemo(
|
useMemo(() => {
|
||||||
() => ({
|
return !slideOptions
|
||||||
type: CommunicationMessageType.SET_SLIDE_OPTIONS,
|
? undefined
|
||||||
slideOptions
|
: {
|
||||||
}),
|
type: CommunicationMessageType.SET_SLIDE_OPTIONS,
|
||||||
[slideOptions]
|
slideOptions
|
||||||
),
|
}
|
||||||
|
}, [slideOptions]),
|
||||||
rendererReady
|
rendererReady
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,13 @@ export const useSetCheckboxInEditor = () => {
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
({ lineInMarkdown, newCheckedState }: TaskCheckedEventPayload): void => {
|
({ lineInMarkdown, newCheckedState }: TaskCheckedEventPayload): void => {
|
||||||
|
const noteDetails = store.getState().noteDetails
|
||||||
|
if (!noteDetails) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
changeEditorContent?.(({ markdownContent }) => {
|
changeEditorContent?.(({ markdownContent }) => {
|
||||||
const correctedLineIndex = lineInMarkdown + store.getState().noteDetails.startOfContentLineOffset
|
const correctedLineIndex = lineInMarkdown + noteDetails.startOfContentLineOffset
|
||||||
const edits = findCheckBox(markdownContent, correctedLineIndex)
|
const edits = findCheckBox(markdownContent, correctedLineIndex)
|
||||||
.map(([startIndex, endIndex]) => createCheckboxContentEdit(startIndex, endIndex, newCheckedState))
|
.map(([startIndex, endIndex]) => createCheckboxContentEdit(startIndex, endIndex, newCheckedState))
|
||||||
.orElse([])
|
.orElse([])
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from './use-application-state'
|
import { useApplicationState } from './use-application-state'
|
||||||
import type { NotePermissions } from '@hedgedoc/commons'
|
|
||||||
import { userIsOwner } from '@hedgedoc/commons'
|
import { userIsOwner } from '@hedgedoc/commons'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
@ -15,7 +14,7 @@ import { useMemo } from 'react'
|
||||||
*/
|
*/
|
||||||
export const useIsOwner = (): boolean => {
|
export const useIsOwner = (): boolean => {
|
||||||
const me: string | undefined = useApplicationState((state) => state.user?.username)
|
const me: string | undefined = useApplicationState((state) => state.user?.username)
|
||||||
const permissions: NotePermissions = useApplicationState((state) => state.noteDetails.permissions)
|
const permissions = useApplicationState((state) => state.noteDetails?.permissions)
|
||||||
|
|
||||||
return useMemo(() => userIsOwner(permissions, me), [permissions, me])
|
return useMemo(() => (permissions === undefined ? false : userIsOwner(permissions, me)), [permissions, me])
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from './use-application-state'
|
import { useApplicationState } from './use-application-state'
|
||||||
import type { NotePermissions } from '@hedgedoc/commons'
|
|
||||||
import { userCanEdit } from '@hedgedoc/commons'
|
import { userCanEdit } from '@hedgedoc/commons'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
@ -15,7 +14,7 @@ import { useMemo } from 'react'
|
||||||
*/
|
*/
|
||||||
export const useMayEdit = (): boolean => {
|
export const useMayEdit = (): boolean => {
|
||||||
const me: string | undefined = useApplicationState((state) => state.user?.username)
|
const me: string | undefined = useApplicationState((state) => state.user?.username)
|
||||||
const permissions: NotePermissions = useApplicationState((state) => state.noteDetails.permissions)
|
const permissions = useApplicationState((state) => state.noteDetails?.permissions)
|
||||||
|
|
||||||
return useMemo(() => userCanEdit(permissions, me), [permissions, me])
|
return useMemo(() => (!permissions ? false : userCanEdit(permissions, me)), [permissions, me])
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,5 +11,5 @@ import { useApplicationState } from './use-application-state'
|
||||||
* @return The markdown content of the note
|
* @return The markdown content of the note
|
||||||
*/
|
*/
|
||||||
export const useNoteMarkdownContent = (): string => {
|
export const useNoteMarkdownContent = (): string => {
|
||||||
return useApplicationState((state) => state.noteDetails.markdownContent.plain)
|
return useApplicationState((state) => state.noteDetails?.markdownContent.plain ?? '')
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { useMemo } from 'react'
|
||||||
*/
|
*/
|
||||||
export const useNoteTitle = (): string => {
|
export const useNoteTitle = (): string => {
|
||||||
const untitledNote = useTranslatedText('editor.untitledNote')
|
const untitledNote = useTranslatedText('editor.untitledNote')
|
||||||
const noteTitle = useApplicationState((state) => state.noteDetails.title)
|
const noteTitle = useApplicationState((state) => state.noteDetails?.title)
|
||||||
|
|
||||||
return useMemo(() => (noteTitle === '' ? untitledNote : noteTitle), [noteTitle, untitledNote])
|
return useMemo(() => (!noteTitle ? untitledNote : noteTitle), [noteTitle, untitledNote])
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,13 +14,23 @@ import { useMemo } from 'react'
|
||||||
*/
|
*/
|
||||||
export const useTrimmedNoteMarkdownContentWithoutFrontmatter = (): string[] => {
|
export const useTrimmedNoteMarkdownContentWithoutFrontmatter = (): string[] => {
|
||||||
const maxLength = useFrontendConfig().maxDocumentLength
|
const maxLength = useFrontendConfig().maxDocumentLength
|
||||||
const markdownContent = useApplicationState((state) => ({
|
const markdownContent = useApplicationState((state) => {
|
||||||
lines: state.noteDetails.markdownContent.lines,
|
const noteDetails = state.noteDetails
|
||||||
content: state.noteDetails.markdownContent.plain
|
if (!noteDetails) {
|
||||||
}))
|
return undefined
|
||||||
const lineOffset = useApplicationState((state) => state.noteDetails.startOfContentLineOffset)
|
} else {
|
||||||
|
return {
|
||||||
|
lines: noteDetails.markdownContent.lines,
|
||||||
|
content: noteDetails.markdownContent.plain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const lineOffset = useApplicationState((state) => state.noteDetails?.startOfContentLineOffset)
|
||||||
|
|
||||||
const trimmedLines = useMemo(() => {
|
const trimmedLines = useMemo(() => {
|
||||||
|
if (!markdownContent) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
if (markdownContent.content.length > maxLength) {
|
if (markdownContent.content.length > maxLength) {
|
||||||
return markdownContent.content.slice(0, maxLength).split('\n')
|
return markdownContent.content.slice(0, maxLength).split('\n')
|
||||||
} else {
|
} else {
|
||||||
|
@ -29,6 +39,6 @@ export const useTrimmedNoteMarkdownContentWithoutFrontmatter = (): string[] => {
|
||||||
}, [markdownContent, maxLength])
|
}, [markdownContent, maxLength])
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return trimmedLines.slice(lineOffset)
|
return trimmedLines === undefined || lineOffset === undefined ? [] : trimmedLines.slice(lineOffset)
|
||||||
}, [lineOffset, trimmedLines])
|
}, [lineOffset, trimmedLines])
|
||||||
}
|
}
|
||||||
|
|
4
frontend/src/redux/application-state.d.ts
vendored
4
frontend/src/redux/application-state.d.ts
vendored
|
@ -6,17 +6,17 @@
|
||||||
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'
|
||||||
import type { NoteDetails } from './note-details/types/note-details'
|
|
||||||
import type { RealtimeStatus } from './realtime/types'
|
import type { RealtimeStatus } from './realtime/types'
|
||||||
import type { RendererStatus } from './renderer-status/types'
|
import type { RendererStatus } from './renderer-status/types'
|
||||||
import type { OptionalUserState } from './user/types'
|
import type { OptionalUserState } from './user/types'
|
||||||
|
import type { OptionalNoteDetails } from './note-details/types/note-details'
|
||||||
|
|
||||||
export interface ApplicationState {
|
export interface ApplicationState {
|
||||||
user: OptionalUserState
|
user: OptionalUserState
|
||||||
history: HistoryEntryWithOrigin[]
|
history: HistoryEntryWithOrigin[]
|
||||||
editorConfig: EditorConfig
|
editorConfig: EditorConfig
|
||||||
darkMode: DarkModeConfig
|
darkMode: DarkModeConfig
|
||||||
noteDetails: NoteDetails
|
noteDetails: OptionalNoteDetails
|
||||||
rendererStatus: RendererStatus
|
rendererStatus: RendererStatus
|
||||||
realtimeStatus: RealtimeStatus
|
realtimeStatus: RealtimeStatus
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,9 +73,19 @@ export const updateCursorPositions = (selection: CursorSelection): void => {
|
||||||
* Updates the current note's metadata from the server.
|
* Updates the current note's metadata from the server.
|
||||||
*/
|
*/
|
||||||
export const updateMetadata = async (): Promise<void> => {
|
export const updateMetadata = async (): Promise<void> => {
|
||||||
const updatedMetadata = await getNoteMetadata(store.getState().noteDetails.id)
|
const noteDetails = store.getState().noteDetails
|
||||||
|
if (!noteDetails) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const updatedMetadata = await getNoteMetadata(noteDetails.id)
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: NoteDetailsActionType.UPDATE_METADATA,
|
type: NoteDetailsActionType.UPDATE_METADATA,
|
||||||
updatedMetadata
|
updatedMetadata
|
||||||
} as UpdateMetadataAction)
|
} as UpdateMetadataAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const unloadNote = (): void => {
|
||||||
|
store.dispatch({
|
||||||
|
type: NoteDetailsActionType.UNLOAD_NOTE
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { buildStateFromUpdatedMarkdownContent } from './build-state-from-updated-markdown-content'
|
import { buildStateFromUpdatedMarkdownContent } from './build-state-from-updated-markdown-content'
|
||||||
import { initialState } from './initial-state'
|
|
||||||
import { buildStateFromFirstHeadingUpdate } from './reducers/build-state-from-first-heading-update'
|
import { buildStateFromFirstHeadingUpdate } from './reducers/build-state-from-first-heading-update'
|
||||||
import { buildStateFromMetadataUpdate } from './reducers/build-state-from-metadata-update'
|
import { buildStateFromMetadataUpdate } from './reducers/build-state-from-metadata-update'
|
||||||
import { buildStateFromServerPermissions } from './reducers/build-state-from-server-permissions'
|
import { buildStateFromServerPermissions } from './reducers/build-state-from-server-permissions'
|
||||||
|
@ -12,13 +11,19 @@ import { buildStateFromServerDto } from './reducers/build-state-from-set-note-da
|
||||||
import { buildStateFromUpdateCursorPosition } from './reducers/build-state-from-update-cursor-position'
|
import { buildStateFromUpdateCursorPosition } from './reducers/build-state-from-update-cursor-position'
|
||||||
import type { NoteDetailsActions } from './types'
|
import type { NoteDetailsActions } from './types'
|
||||||
import { NoteDetailsActionType } from './types'
|
import { NoteDetailsActionType } from './types'
|
||||||
import type { NoteDetails } from './types/note-details'
|
import type { OptionalNoteDetails } from './types/note-details'
|
||||||
import type { Reducer } from 'redux'
|
import type { Reducer } from 'redux'
|
||||||
|
|
||||||
export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
|
export const NoteDetailsReducer: Reducer<OptionalNoteDetails, NoteDetailsActions> = (
|
||||||
state: NoteDetails = initialState,
|
state: OptionalNoteDetails = null,
|
||||||
action: NoteDetailsActions
|
action: NoteDetailsActions
|
||||||
) => {
|
) => {
|
||||||
|
if (action.type === NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER) {
|
||||||
|
return buildStateFromServerDto(action.noteFromServer)
|
||||||
|
}
|
||||||
|
if (state === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case NoteDetailsActionType.UPDATE_CURSOR_POSITION:
|
case NoteDetailsActionType.UPDATE_CURSOR_POSITION:
|
||||||
return buildStateFromUpdateCursorPosition(state, action.selection)
|
return buildStateFromUpdateCursorPosition(state, action.selection)
|
||||||
|
@ -28,10 +33,10 @@ export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
|
||||||
return buildStateFromServerPermissions(state, action.notePermissionsFromServer)
|
return buildStateFromServerPermissions(state, action.notePermissionsFromServer)
|
||||||
case NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING:
|
case NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING:
|
||||||
return buildStateFromFirstHeadingUpdate(state, action.firstHeading)
|
return buildStateFromFirstHeadingUpdate(state, action.firstHeading)
|
||||||
case NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER:
|
|
||||||
return buildStateFromServerDto(action.noteFromServer)
|
|
||||||
case NoteDetailsActionType.UPDATE_METADATA:
|
case NoteDetailsActionType.UPDATE_METADATA:
|
||||||
return buildStateFromMetadataUpdate(state, action.updatedMetadata)
|
return buildStateFromMetadataUpdate(state, action.updatedMetadata)
|
||||||
|
case NoteDetailsActionType.UNLOAD_NOTE:
|
||||||
|
return null
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,8 @@ export enum NoteDetailsActionType {
|
||||||
SET_NOTE_PERMISSIONS_FROM_SERVER = 'note-details/data/permissions/set',
|
SET_NOTE_PERMISSIONS_FROM_SERVER = 'note-details/data/permissions/set',
|
||||||
UPDATE_NOTE_TITLE_BY_FIRST_HEADING = 'note-details/update-note-title-by-first-heading',
|
UPDATE_NOTE_TITLE_BY_FIRST_HEADING = 'note-details/update-note-title-by-first-heading',
|
||||||
UPDATE_CURSOR_POSITION = 'note-details/updateCursorPosition',
|
UPDATE_CURSOR_POSITION = 'note-details/updateCursorPosition',
|
||||||
UPDATE_METADATA = 'note-details/update-metadata'
|
UPDATE_METADATA = 'note-details/update-metadata',
|
||||||
|
UNLOAD_NOTE = 'note-details/unload-note'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NoteDetailsActions =
|
export type NoteDetailsActions =
|
||||||
|
@ -24,6 +25,7 @@ export type NoteDetailsActions =
|
||||||
| UpdateNoteTitleByFirstHeadingAction
|
| UpdateNoteTitleByFirstHeadingAction
|
||||||
| UpdateCursorPositionAction
|
| UpdateCursorPositionAction
|
||||||
| UpdateMetadataAction
|
| UpdateMetadataAction
|
||||||
|
| UnloadNoteAction
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action for updating the document content of the currently loaded note.
|
* Action for updating the document content of the currently loaded note.
|
||||||
|
@ -69,3 +71,7 @@ export interface UpdateMetadataAction extends Action<NoteDetailsActionType> {
|
||||||
type: NoteDetailsActionType.UPDATE_METADATA
|
type: NoteDetailsActionType.UPDATE_METADATA
|
||||||
updatedMetadata: NoteMetadata
|
updatedMetadata: NoteMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UnloadNoteAction extends Action<NoteDetailsActionType> {
|
||||||
|
type: NoteDetailsActionType.UNLOAD_NOTE
|
||||||
|
}
|
||||||
|
|
|
@ -26,3 +26,5 @@ export interface NoteDetails extends Omit<NoteMetadata, UnnecessaryNoteAttribute
|
||||||
frontmatter: NoteFrontmatter
|
frontmatter: NoteFrontmatter
|
||||||
startOfContentLineOffset: number
|
startOfContentLineOffset: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OptionalNoteDetails = NoteDetails | null
|
||||||
|
|
Loading…
Reference in a new issue