mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-12-22 12:31:40 +00: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')
|
||||
})
|
||||
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,
|
||||
createdAt: defaultCreatedAt,
|
||||
title: 'Features',
|
||||
|
@ -86,7 +86,7 @@ describe('Revision modal', () => {
|
|||
})
|
||||
|
||||
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)
|
||||
|
||||
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 type { PropsWithChildren } from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { unloadNote } from '../../../redux/note-details/methods'
|
||||
|
||||
const logger = new Logger('NoteLoadingBoundary')
|
||||
|
||||
|
@ -37,6 +38,13 @@ export const NoteLoadingBoundary: React.FC<PropsWithChildren<NoteIdProps>> = ({
|
|||
loadNoteFromServer()
|
||||
}, [loadNoteFromServer])
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
unloadNote()
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const errorComponent = useMemo(() => {
|
||||
if (error === undefined) {
|
||||
return null
|
||||
|
|
|
@ -15,17 +15,18 @@ import { useMemo } from 'react'
|
|||
*/
|
||||
export const useSendAdditionalConfigurationToRenderer = (rendererReady: boolean): void => {
|
||||
const darkModePreference = useApplicationState((state) => state.darkMode.darkModePreference)
|
||||
const newlinesAreBreaks = useApplicationState((state) => state.noteDetails.frontmatter.newlinesAreBreaks)
|
||||
const newlinesAreBreaks = useApplicationState((state) => state.noteDetails?.frontmatter.newlinesAreBreaks)
|
||||
|
||||
useSendToRenderer(
|
||||
useMemo(
|
||||
() => ({
|
||||
type: CommunicationMessageType.SET_ADDITIONAL_CONFIGURATION,
|
||||
darkModePreference: darkModePreference,
|
||||
newLinesAreBreaks: newlinesAreBreaks
|
||||
}),
|
||||
[darkModePreference, newlinesAreBreaks]
|
||||
),
|
||||
useMemo(() => {
|
||||
return newlinesAreBreaks === undefined
|
||||
? undefined
|
||||
: {
|
||||
type: CommunicationMessageType.SET_ADDITIONAL_CONFIGURATION,
|
||||
darkModePreference: darkModePreference,
|
||||
newLinesAreBreaks: newlinesAreBreaks
|
||||
}
|
||||
}, [darkModePreference, newlinesAreBreaks]),
|
||||
rendererReady
|
||||
)
|
||||
}
|
||||
|
|
|
@ -22,6 +22,11 @@ export const DocumentInfobar: React.FC = () => {
|
|||
|
||||
// TODO Check permissions ("writability") of note and show edit link depending on that.
|
||||
const linkTitle = useTranslatedText('views.readOnly.editNote')
|
||||
|
||||
if (noteDetails === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`d-flex flex-row my-3 ${styles['document-infobar']}`}>
|
||||
<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 { useHandleUpload } from '../use-handle-upload'
|
||||
import { findRegexMatchInText } from './find-regex-match-in-text'
|
||||
import { Optional } from '@mrdrogdrog/optional'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
const log = new Logger('useOnImageUpload')
|
||||
|
@ -43,12 +42,7 @@ export const useOnImageUploadFromRenderer = (): void => {
|
|||
.then((result) => result.blob())
|
||||
.then((blob) => {
|
||||
const file = new File([blob], fileName, { type: blob.type })
|
||||
const { cursorSelection, alt, title } = Optional.ofNullable(lineIndex)
|
||||
.flatMap((actualLineIndex) => {
|
||||
const lineOffset = getGlobalState().noteDetails.startOfContentLineOffset
|
||||
return findPlaceholderInMarkdownContent(actualLineIndex + lineOffset, placeholderIndexInLine)
|
||||
})
|
||||
.orElse({} as ExtractResult)
|
||||
const { cursorSelection, alt, title } = findPlaceholderInMarkdownContent(lineIndex, placeholderIndexInLine)
|
||||
handleUpload(codeMirrorReference, file, cursorSelection, alt, title)
|
||||
})
|
||||
.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.
|
||||
* @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
|
||||
if (!noteDetails) {
|
||||
return {}
|
||||
}
|
||||
const currentMarkdownContentLines = noteDetails.markdownContent.lines
|
||||
return Optional.ofNullable(noteDetails.markdownContent.lineStartIndexes[lineIndex]).map((startIndexOfLine) =>
|
||||
findImagePlaceholderInLine(currentMarkdownContentLines[lineIndex], startIndexOfLine, replacementIndexInLine)
|
||||
const actualLineIndex = noteDetails.startOfContentLineOffset + lineIndex
|
||||
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 })
|
||||
|
||||
const uploadPlaceholder = `![${uploadFileInfo}](upload-${randomId}${additionalUrlText ?? ''})`
|
||||
const noteId = getGlobalState().noteDetails.id
|
||||
const noteId = getGlobalState().noteDetails?.id
|
||||
if (noteId === undefined) {
|
||||
return
|
||||
}
|
||||
changeContent(({ currentSelection }) => {
|
||||
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.
|
||||
*/
|
||||
export const useLineBasedToPosition = (): LineBasedPosition | undefined => {
|
||||
const lineStartIndexes = useApplicationState((state) => state.noteDetails.markdownContent.lineStartIndexes)
|
||||
const selection = useApplicationState((state) => state.noteDetails.selection)
|
||||
const lineStartIndexes = useApplicationState((state) => state.noteDetails?.markdownContent.lineStartIndexes ?? [])
|
||||
const selectionTo = useApplicationState((state) => state.noteDetails?.selection.to)
|
||||
|
||||
return useMemo(() => {
|
||||
const to = selection.to
|
||||
if (to === undefined) {
|
||||
if (selectionTo === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return calculateLineBasedPosition(to, lineStartIndexes)
|
||||
}, [selection.to, lineStartIndexes])
|
||||
return calculateLineBasedPosition(selectionTo, lineStartIndexes)
|
||||
}, [selectionTo, lineStartIndexes])
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the line+character based position of the from-cursor.
|
||||
*/
|
||||
export const useLineBasedFromPosition = (): LineBasedPosition => {
|
||||
const lineStartIndexes = useApplicationState((state) => state.noteDetails.markdownContent.lineStartIndexes)
|
||||
const selection = useApplicationState((state) => state.noteDetails.selection)
|
||||
const lineStartIndexes = useApplicationState((state) => state.noteDetails?.markdownContent.lineStartIndexes ?? [])
|
||||
const selection = useApplicationState((state) => state.noteDetails?.selection ?? { from: 0 })
|
||||
|
||||
return useMemo(() => {
|
||||
return calculateLineBasedPosition(selection.from, lineStartIndexes)
|
||||
|
|
|
@ -32,7 +32,7 @@ export const useRealtimeConnection = (): MessageTransporter => {
|
|||
if (isMockMode) {
|
||||
logger.debug('Creating Loopback connection...')
|
||||
messageTransporter.setAdapter(
|
||||
new MockedBackendTransportAdapter(getGlobalState().noteDetails.markdownContent.plain)
|
||||
new MockedBackendTransportAdapter(getGlobalState().noteDetails?.markdownContent.plain ?? '')
|
||||
)
|
||||
} else if (websocketUrl) {
|
||||
logger.debug(`Connecting to ${websocketUrl.toString()}`)
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||
import { store } from '../../../../../redux'
|
||||
import { setRealtimeUsers } from '../../../../../redux/realtime/methods'
|
||||
import { RealtimeStatusActionType } from '../../../../../redux/realtime/types'
|
||||
import { resetRealtimeStatus, setRealtimeUsers } from '../../../../../redux/realtime/methods'
|
||||
import type { MessageTransporter } from '@hedgedoc/commons'
|
||||
import { MessageType } from '@hedgedoc/commons'
|
||||
import type { Listener } from 'eventemitter2'
|
||||
|
@ -43,12 +41,5 @@ export const useReceiveRealtimeUsers = (messageTransporter: MessageTransporter):
|
|||
}
|
||||
}, [isConnected, messageTransporter])
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
store.dispatch({
|
||||
type: RealtimeStatusActionType.RESET_REALTIME_STATUS
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
useEffect(() => () => resetRealtimeStatus(), [])
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ const LOCAL_FALLBACK_URL = 'ws://localhost:8080/realtime/'
|
|||
* Provides the URL for the realtime endpoint.
|
||||
*/
|
||||
export const useWebsocketUrl = (): URL | undefined => {
|
||||
const noteId = useApplicationState((state) => state.noteDetails.id)
|
||||
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||
const baseUrl = useBaseUrl()
|
||||
|
||||
const websocketUrl = useMemo(() => {
|
||||
|
@ -33,7 +33,7 @@ export const useWebsocketUrl = (): URL | undefined => {
|
|||
}, [baseUrl])
|
||||
|
||||
return useMemo(() => {
|
||||
if (noteId === '') {
|
||||
if (noteId === '' || noteId === undefined) {
|
||||
return
|
||||
}
|
||||
const url = new URL(websocketUrl)
|
||||
|
|
|
@ -13,7 +13,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||
export const NumberOfLinesInDocumentInfo: React.FC = () => {
|
||||
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])
|
||||
|
||||
return (
|
||||
|
|
|
@ -16,7 +16,7 @@ export const RemainingCharactersInfo: React.FC = () => {
|
|||
const { t } = useTranslation()
|
||||
|
||||
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 remainingCharactersClass = useMemo(() => {
|
||||
|
|
|
@ -14,10 +14,10 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||
export const SelectedCharacters: React.FC = () => {
|
||||
useTranslation()
|
||||
|
||||
const selection = useApplicationState((state) => state.noteDetails.selection)
|
||||
const selection = useApplicationState((state) => state.noteDetails?.selection)
|
||||
const count = useMemo(
|
||||
() => (selection.to === undefined ? undefined : selection.to - selection.from),
|
||||
[selection.from, selection.to]
|
||||
() => (selection === undefined || selection.to === undefined ? undefined : selection.to - selection.from),
|
||||
[selection]
|
||||
)
|
||||
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.
|
||||
*/
|
||||
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 currentNoteTitle = useApplicationState((state) => state.noteDetails.title)
|
||||
const currentNoteTags = useApplicationState((state) => state.noteDetails.frontmatter.tags)
|
||||
const currentNoteTitle = useApplicationState((state) => state.noteDetails?.title ?? '')
|
||||
const currentNoteTags = useApplicationState((state) => state.noteDetails?.frontmatter.tags ?? [])
|
||||
|
||||
const lastNoteTitle = useRef('')
|
||||
const lastNoteTags = useRef<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (userExists) {
|
||||
if (userExists || id === undefined) {
|
||||
return
|
||||
}
|
||||
if (currentNoteTitle === lastNoteTitle.current && equal(currentNoteTags, lastNoteTags.current)) {
|
||||
|
|
|
@ -19,8 +19,12 @@ export const useOnScrollWithLineOffset = (onScroll: ScrollCallback | undefined):
|
|||
return undefined
|
||||
} else {
|
||||
return (scrollState: ScrollState) => {
|
||||
const noteDetails = getGlobalState().noteDetails
|
||||
if (noteDetails === null) {
|
||||
return undefined
|
||||
}
|
||||
onScroll({
|
||||
firstLineInView: scrollState.firstLineInView + getGlobalState().noteDetails.startOfContentLineOffset,
|
||||
firstLineInView: scrollState.firstLineInView + noteDetails.startOfContentLineOffset,
|
||||
scrolledPercentage: scrollState.scrolledPercentage
|
||||
})
|
||||
}
|
||||
|
|
|
@ -14,9 +14,9 @@ import { useMemo } from 'react'
|
|||
* @return the adjusted scroll state without the line offset
|
||||
*/
|
||||
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 scrollState === undefined
|
||||
return scrollState === undefined || lineOffset === undefined
|
||||
? undefined
|
||||
: {
|
||||
firstLineInView: scrollState.firstLineInView - lineOffset,
|
||||
|
|
|
@ -28,10 +28,14 @@ export type RendererPaneProps = Omit<
|
|||
*/
|
||||
export const RendererPane: React.FC<RendererPaneProps> = ({ scrollState, onScroll, ...props }) => {
|
||||
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 adjustedScrollState = useScrollStateWithoutLineOffset(scrollState)
|
||||
|
||||
if (!noteType) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<RendererIframe
|
||||
{...props}
|
||||
|
|
|
@ -24,13 +24,16 @@ const validAliasRegex = /^[a-z0-9_-]*$/
|
|||
*/
|
||||
export const AliasesAddForm: React.FC = () => {
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
const noteId = useApplicationState((state) => state.noteDetails.id)
|
||||
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||
const isOwner = useIsOwner()
|
||||
const [newAlias, setNewAlias] = useState('')
|
||||
|
||||
const onAddAlias = useCallback(
|
||||
(event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
if (noteId === undefined) {
|
||||
return
|
||||
}
|
||||
addAlias(noteId, newAlias)
|
||||
.then(updateMetadata)
|
||||
.catch(showErrorNotification('editor.modal.aliases.errorAddingAlias'))
|
||||
|
|
|
@ -12,12 +12,14 @@ import React, { Fragment, useMemo } from 'react'
|
|||
* Renders the list of aliases.
|
||||
*/
|
||||
export const AliasesList: React.FC = () => {
|
||||
const aliases = useApplicationState((state: ApplicationState) => state.noteDetails.aliases)
|
||||
const aliases = useApplicationState((state: ApplicationState) => state.noteDetails?.aliases)
|
||||
|
||||
const aliasesDom = useMemo(() => {
|
||||
return aliases
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((alias) => <AliasesListEntry alias={alias} key={alias.name} />)
|
||||
return aliases === undefined
|
||||
? null
|
||||
: aliases
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((alias) => <AliasesListEntry alias={alias} key={alias.name} />)
|
||||
}, [aliases])
|
||||
|
||||
return <Fragment>{aliasesDom}</Fragment>
|
||||
|
|
|
@ -26,11 +26,14 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||
export const DeleteNoteSidebarEntry: React.FC<PropsWithChildren<SpecificSidebarEntryProps>> = ({ hide, className }) => {
|
||||
useTranslation()
|
||||
const router = useRouter()
|
||||
const noteId = useApplicationState((state) => state.noteDetails.id)
|
||||
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
|
||||
const deleteNoteAndCloseDialog = useCallback(() => {
|
||||
if (noteId === undefined) {
|
||||
return
|
||||
}
|
||||
deleteNote(noteId)
|
||||
.then(() => router.push('/history'))
|
||||
.catch(showErrorNotification('landing.history.error.deleteNote.text'))
|
||||
|
|
|
@ -20,7 +20,11 @@ export const ExportMarkdownSidebarEntry: React.FC = () => {
|
|||
const { t } = useTranslation()
|
||||
const markdownContent = useNoteMarkdownContent()
|
||||
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')
|
||||
}, [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.
|
||||
*/
|
||||
export const NoteInfoLineContributors: React.FC = () => {
|
||||
const contributors = useApplicationState((state) => state.noteDetails.editedBy.length)
|
||||
const contributors = useApplicationState((state) => state.noteDetails?.editedBy.length ?? 0)
|
||||
|
||||
return (
|
||||
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.contributors'} icon={IconPeople}>
|
||||
|
|
|
@ -16,10 +16,13 @@ import { useTranslation } from 'react-i18next'
|
|||
*/
|
||||
export const NoteInfoLineCreatedAt: React.FC = () => {
|
||||
useTranslation()
|
||||
const noteCreateTime = useApplicationState((state) => state.noteDetails.createdAt)
|
||||
const noteCreateDateTime = useMemo(() => DateTime.fromSeconds(noteCreateTime), [noteCreateTime])
|
||||
const noteCreateTime = useApplicationState((state) => state.noteDetails?.createdAt)
|
||||
const noteCreateDateTime = useMemo(
|
||||
() => (noteCreateTime === undefined ? undefined : DateTime.fromSeconds(noteCreateTime)),
|
||||
[noteCreateTime]
|
||||
)
|
||||
|
||||
return (
|
||||
return !noteCreateDateTime ? null : (
|
||||
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.created'} icon={IconPlus}>
|
||||
<TimeFromNow time={noteCreateDateTime} />
|
||||
</SidebarMenuInfoEntry>
|
||||
|
|
|
@ -16,10 +16,13 @@ import { useTranslation } from 'react-i18next'
|
|||
*/
|
||||
export const NoteInfoLineUpdatedAt: React.FC = () => {
|
||||
useTranslation()
|
||||
const noteUpdateTime = useApplicationState((state) => state.noteDetails.updatedAt)
|
||||
const noteUpdateDateTime = useMemo(() => DateTime.fromSeconds(noteUpdateTime), [noteUpdateTime])
|
||||
const noteUpdateTime = useApplicationState((state) => state.noteDetails?.updatedAt)
|
||||
const noteUpdateDateTime = useMemo(
|
||||
() => (noteUpdateTime === undefined ? undefined : DateTime.fromSeconds(noteUpdateTime)),
|
||||
[noteUpdateTime]
|
||||
)
|
||||
|
||||
return (
|
||||
return !noteUpdateDateTime ? null : (
|
||||
<SidebarMenuInfoEntry titleI18nKey={'editor.noteInfo.lastUpdated'} icon={IconPencil}>
|
||||
<TimeFromNow time={noteUpdateDateTime} />
|
||||
</SidebarMenuInfoEntry>
|
||||
|
|
|
@ -15,7 +15,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||
*/
|
||||
export const NoteInfoLineUpdatedBy: React.FC = () => {
|
||||
useTranslation()
|
||||
const noteUpdateUser = useApplicationState((state) => state.noteDetails.updateUsername)
|
||||
const noteUpdateUser = useApplicationState((state) => state.noteDetails?.updateUsername)
|
||||
|
||||
const userBlock = useMemo(() => {
|
||||
if (!noteUpdateUser) {
|
||||
|
|
|
@ -33,11 +33,14 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
|||
type,
|
||||
disabled
|
||||
}) => {
|
||||
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||
const noteId = useApplicationState((state) => state.noteDetails?.primaryAddress)
|
||||
const { t } = useTranslation()
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
|
||||
const onSetEntryReadOnly = useCallback(() => {
|
||||
if (!noteId) {
|
||||
return
|
||||
}
|
||||
setGroupPermission(noteId, type, false)
|
||||
.then((updatedPermissions) => {
|
||||
setNotePermissionsFromServer(updatedPermissions)
|
||||
|
@ -46,6 +49,9 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
|||
}, [noteId, showErrorNotification, type])
|
||||
|
||||
const onSetEntryWriteable = useCallback(() => {
|
||||
if (!noteId) {
|
||||
return
|
||||
}
|
||||
setGroupPermission(noteId, type, true)
|
||||
.then((updatedPermissions) => {
|
||||
setNotePermissionsFromServer(updatedPermissions)
|
||||
|
@ -54,6 +60,9 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
|||
}, [noteId, showErrorNotification, type])
|
||||
|
||||
const onSetEntryDenied = useCallback(() => {
|
||||
if (!noteId) {
|
||||
return
|
||||
}
|
||||
removeGroupPermission(noteId, type)
|
||||
.then((updatedPermissions) => {
|
||||
setNotePermissionsFromServer(updatedPermissions)
|
||||
|
|
|
@ -31,10 +31,13 @@ export const PermissionEntryUser: React.FC<PermissionEntryUserProps & Permission
|
|||
entry,
|
||||
disabled
|
||||
}) => {
|
||||
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||
const noteId = useApplicationState((state) => state.noteDetails?.primaryAddress)
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
|
||||
const onRemoveEntry = useCallback(() => {
|
||||
if (!noteId) {
|
||||
return
|
||||
}
|
||||
removeUserPermission(noteId, entry.username)
|
||||
.then((updatedPermissions) => {
|
||||
setNotePermissionsFromServer(updatedPermissions)
|
||||
|
@ -43,6 +46,9 @@ export const PermissionEntryUser: React.FC<PermissionEntryUserProps & Permission
|
|||
}, [noteId, entry.username, showErrorNotification])
|
||||
|
||||
const onSetEntryReadOnly = useCallback(() => {
|
||||
if (!noteId) {
|
||||
return
|
||||
}
|
||||
setUserPermission(noteId, entry.username, false)
|
||||
.then((updatedPermissions) => {
|
||||
setNotePermissionsFromServer(updatedPermissions)
|
||||
|
@ -51,6 +57,9 @@ export const PermissionEntryUser: React.FC<PermissionEntryUserProps & Permission
|
|||
}, [noteId, entry.username, showErrorNotification])
|
||||
|
||||
const onSetEntryWriteable = useCallback(() => {
|
||||
if (!noteId) {
|
||||
return
|
||||
}
|
||||
setUserPermission(noteId, entry.username, true)
|
||||
.then((updatedPermissions) => {
|
||||
setNotePermissionsFromServer(updatedPermissions)
|
||||
|
|
|
@ -26,9 +26,13 @@ export const PermissionOwnerInfo: React.FC<PermissionOwnerInfoProps & Permission
|
|||
onEditOwner,
|
||||
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')
|
||||
|
||||
if (!noteOwner) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<UserAvatarForUsername username={noteOwner} />
|
||||
|
|
|
@ -19,7 +19,7 @@ import { Trans } from 'react-i18next'
|
|||
* @param disabled If the user is not the owner, functionality is 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 { showErrorNotification } = useUiNotifications()
|
||||
|
||||
|
@ -29,6 +29,9 @@ export const PermissionSectionOwner: React.FC<PermissionDisabledProps> = ({ disa
|
|||
|
||||
const onOwnerChange = useCallback(
|
||||
(newOwner: string) => {
|
||||
if (!noteId) {
|
||||
return
|
||||
}
|
||||
setNoteOwner(noteId, newOwner)
|
||||
.then((updatedPermissions) => {
|
||||
setNotePermissionsFromServer(updatedPermissions)
|
||||
|
|
|
@ -18,10 +18,13 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||
*/
|
||||
export const PermissionSectionSpecialGroups: React.FC<PermissionDisabledProps> = ({ disabled }) => {
|
||||
useTranslation()
|
||||
const groupPermissions = useApplicationState((state) => state.noteDetails.permissions.sharedToGroups)
|
||||
const groupPermissions = useApplicationState((state) => state.noteDetails?.permissions.sharedToGroups)
|
||||
const isOwner = useIsOwner()
|
||||
|
||||
const specialGroupEntries = useMemo(() => {
|
||||
if (!groupPermissions) {
|
||||
return
|
||||
}
|
||||
const groupEveryone = groupPermissions.find((entry) => entry.groupName === (SpecialGroup.EVERYONE 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])
|
||||
|
||||
if (!specialGroupEntries) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<h5 className={'my-3'}>
|
||||
|
|
|
@ -20,11 +20,14 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||
*/
|
||||
export const PermissionSectionUsers: React.FC<PermissionDisabledProps> = ({ disabled }) => {
|
||||
useTranslation()
|
||||
const userPermissions = useApplicationState((state) => state.noteDetails.permissions.sharedToUsers)
|
||||
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||
const userPermissions = useApplicationState((state) => state.noteDetails?.permissions.sharedToUsers)
|
||||
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
|
||||
const userEntries = useMemo(() => {
|
||||
if (!userPermissions) {
|
||||
return null
|
||||
}
|
||||
return userPermissions.map((entry) => (
|
||||
<PermissionEntryUser key={entry.username} entry={entry} disabled={disabled} />
|
||||
))
|
||||
|
@ -32,6 +35,9 @@ export const PermissionSectionUsers: React.FC<PermissionDisabledProps> = ({ disa
|
|||
|
||||
const onAddEntry = useCallback(
|
||||
(username: string) => {
|
||||
if (!noteId) {
|
||||
return
|
||||
}
|
||||
setUserPermission(noteId, username, false)
|
||||
.then((updatedPermissions) => {
|
||||
setNotePermissionsFromServer(updatedPermissions)
|
||||
|
|
|
@ -22,21 +22,24 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||
*/
|
||||
export const PinNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
|
||||
useTranslation()
|
||||
const id = useApplicationState((state) => state.noteDetails.id)
|
||||
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||
const history = useApplicationState((state) => state.history)
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
|
||||
const isPinned = useMemo(() => {
|
||||
const entry = history.find((entry) => entry.identifier === id)
|
||||
const entry = history.find((entry) => entry.identifier === noteId)
|
||||
if (!entry) {
|
||||
return false
|
||||
}
|
||||
return entry.pinStatus
|
||||
}, [id, history])
|
||||
}, [history, noteId])
|
||||
|
||||
const onPinClicked = useCallback(() => {
|
||||
toggleHistoryEntryPinning(id).catch(showErrorNotification('landing.history.error.updateEntry.text'))
|
||||
}, [id, showErrorNotification])
|
||||
if (!noteId) {
|
||||
return
|
||||
}
|
||||
toggleHistoryEntryPinning(noteId).catch(showErrorNotification('landing.history.error.updateEntry.text'))
|
||||
}, [noteId, showErrorNotification])
|
||||
|
||||
return (
|
||||
<SidebarButton
|
||||
|
|
|
@ -21,9 +21,12 @@ import { Trans } from 'react-i18next'
|
|||
*/
|
||||
export const RevisionDeleteModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
const noteId = useApplicationState((state) => state.noteDetails.id)
|
||||
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||
|
||||
const deleteAllRevisions = useCallback(() => {
|
||||
if (!noteId) {
|
||||
return
|
||||
}
|
||||
deleteRevisionsForNote(noteId).catch(showErrorNotification('editor.modal.deleteRevision.error')).finally(onHide)
|
||||
}, [noteId, onHide, showErrorNotification])
|
||||
|
||||
|
|
|
@ -27,8 +27,17 @@ export const RevisionModalBody = ({ onShowDeleteModal, onHide }: RevisionModal)
|
|||
useTranslation()
|
||||
const isOwner = useIsOwner()
|
||||
const [selectedRevisionId, setSelectedRevisionId] = useState<number>()
|
||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.id)
|
||||
const { value: revisions, error, loading } = useAsync(() => getAllRevisions(noteIdentifier), [noteIdentifier])
|
||||
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||
const {
|
||||
value: revisions,
|
||||
error,
|
||||
loading
|
||||
} = useAsync(async () => {
|
||||
if (!noteId) {
|
||||
return []
|
||||
}
|
||||
return getAllRevisions(noteId)
|
||||
}, [noteId])
|
||||
|
||||
const revisionLength = revisions?.length ?? 0
|
||||
const enableDeleteRevisions = revisionLength > 1 && isOwner
|
||||
|
|
|
@ -37,7 +37,7 @@ export const RevisionModalFooter: React.FC<RevisionModalFooterProps> = ({
|
|||
disableDeleteRevisions
|
||||
}) => {
|
||||
useTranslation()
|
||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
|
||||
const onRevertToRevision = useCallback(() => {
|
||||
|
@ -47,15 +47,15 @@ export const RevisionModalFooter: React.FC<RevisionModalFooterProps> = ({
|
|||
}, [])
|
||||
|
||||
const onDownloadRevision = useCallback(() => {
|
||||
if (selectedRevisionId === undefined) {
|
||||
if (selectedRevisionId === undefined || noteId === undefined) {
|
||||
return
|
||||
}
|
||||
getRevision(noteIdentifier, selectedRevisionId)
|
||||
getRevision(noteId, selectedRevisionId)
|
||||
.then((revision) => {
|
||||
downloadRevision(noteIdentifier, revision)
|
||||
downloadRevision(noteId, revision)
|
||||
})
|
||||
.catch(showErrorNotification(''))
|
||||
}, [noteIdentifier, selectedRevisionId, showErrorNotification])
|
||||
}, [noteId, selectedRevisionId, showErrorNotification])
|
||||
|
||||
const openDeleteModal = useCallback(() => {
|
||||
onHide?.()
|
||||
|
|
|
@ -25,16 +25,16 @@ export interface RevisionViewerProps {
|
|||
* @param allRevisions List of metadata for all available revisions.
|
||||
*/
|
||||
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 { value, error, loading } = useAsync(async () => {
|
||||
if (selectedRevisionId === undefined) {
|
||||
if (noteId === undefined || selectedRevisionId === undefined) {
|
||||
throw new Error('No revision selected')
|
||||
} else {
|
||||
return await getRevision(noteIdentifier, selectedRevisionId)
|
||||
return await getRevision(noteId, selectedRevisionId)
|
||||
}
|
||||
}, [selectedRevisionId, noteIdentifier])
|
||||
}, [selectedRevisionId, noteId])
|
||||
|
||||
const previousRevisionContent = useMemo(() => {
|
||||
return Optional.ofNullable(value)
|
||||
|
|
|
@ -24,13 +24,16 @@ export interface LinkFieldProps {
|
|||
*/
|
||||
export const NoteUrlField: React.FC<LinkFieldProps> = ({ type }) => {
|
||||
const baseUrl = useBaseUrl()
|
||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||
const noteId = useApplicationState((state) => state.noteDetails?.id)
|
||||
|
||||
const url = useMemo(() => {
|
||||
if (noteId === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const url = new URL(baseUrl)
|
||||
url.pathname += `${type}/${noteIdentifier}`
|
||||
url.pathname += `${type}/${noteId}`
|
||||
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 }) => {
|
||||
useTranslation()
|
||||
const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter)
|
||||
const noteFrontmatter = useApplicationState((state) => state.noteDetails?.frontmatter)
|
||||
|
||||
if (!noteFrontmatter) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<CommonModal show={show} onHide={onHide} showCloseButton={true} titleI18nKey={'editor.modal.shareLink.title'}>
|
||||
|
|
|
@ -22,16 +22,17 @@ export const SlideShowPageContent: React.FC = () => {
|
|||
const markdownContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter()
|
||||
useTranslation()
|
||||
|
||||
const slideOptions = useApplicationState((state) => state.noteDetails.frontmatter.slideOptions)
|
||||
const slideOptions = useApplicationState((state) => state.noteDetails?.frontmatter.slideOptions)
|
||||
const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady)
|
||||
useSendToRenderer(
|
||||
useMemo(
|
||||
() => ({
|
||||
type: CommunicationMessageType.SET_SLIDE_OPTIONS,
|
||||
slideOptions
|
||||
}),
|
||||
[slideOptions]
|
||||
),
|
||||
useMemo(() => {
|
||||
return !slideOptions
|
||||
? undefined
|
||||
: {
|
||||
type: CommunicationMessageType.SET_SLIDE_OPTIONS,
|
||||
slideOptions
|
||||
}
|
||||
}, [slideOptions]),
|
||||
rendererReady
|
||||
)
|
||||
|
||||
|
|
|
@ -31,8 +31,13 @@ export const useSetCheckboxInEditor = () => {
|
|||
|
||||
return useCallback(
|
||||
({ lineInMarkdown, newCheckedState }: TaskCheckedEventPayload): void => {
|
||||
const noteDetails = store.getState().noteDetails
|
||||
if (!noteDetails) {
|
||||
return
|
||||
}
|
||||
|
||||
changeEditorContent?.(({ markdownContent }) => {
|
||||
const correctedLineIndex = lineInMarkdown + store.getState().noteDetails.startOfContentLineOffset
|
||||
const correctedLineIndex = lineInMarkdown + noteDetails.startOfContentLineOffset
|
||||
const edits = findCheckBox(markdownContent, correctedLineIndex)
|
||||
.map(([startIndex, endIndex]) => createCheckboxContentEdit(startIndex, endIndex, newCheckedState))
|
||||
.orElse([])
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useApplicationState } from './use-application-state'
|
||||
import type { NotePermissions } from '@hedgedoc/commons'
|
||||
import { userIsOwner } from '@hedgedoc/commons'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
|
@ -15,7 +14,7 @@ import { useMemo } from 'react'
|
|||
*/
|
||||
export const useIsOwner = (): boolean => {
|
||||
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
|
||||
*/
|
||||
import { useApplicationState } from './use-application-state'
|
||||
import type { NotePermissions } from '@hedgedoc/commons'
|
||||
import { userCanEdit } from '@hedgedoc/commons'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
|
@ -15,7 +14,7 @@ import { useMemo } from 'react'
|
|||
*/
|
||||
export const useMayEdit = (): boolean => {
|
||||
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
|
||||
*/
|
||||
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 => {
|
||||
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[] => {
|
||||
const maxLength = useFrontendConfig().maxDocumentLength
|
||||
const markdownContent = useApplicationState((state) => ({
|
||||
lines: state.noteDetails.markdownContent.lines,
|
||||
content: state.noteDetails.markdownContent.plain
|
||||
}))
|
||||
const lineOffset = useApplicationState((state) => state.noteDetails.startOfContentLineOffset)
|
||||
const markdownContent = useApplicationState((state) => {
|
||||
const noteDetails = state.noteDetails
|
||||
if (!noteDetails) {
|
||||
return undefined
|
||||
} else {
|
||||
return {
|
||||
lines: noteDetails.markdownContent.lines,
|
||||
content: noteDetails.markdownContent.plain
|
||||
}
|
||||
}
|
||||
})
|
||||
const lineOffset = useApplicationState((state) => state.noteDetails?.startOfContentLineOffset)
|
||||
|
||||
const trimmedLines = useMemo(() => {
|
||||
if (!markdownContent) {
|
||||
return undefined
|
||||
}
|
||||
if (markdownContent.content.length > maxLength) {
|
||||
return markdownContent.content.slice(0, maxLength).split('\n')
|
||||
} else {
|
||||
|
@ -29,6 +39,6 @@ export const useTrimmedNoteMarkdownContentWithoutFrontmatter = (): string[] => {
|
|||
}, [markdownContent, maxLength])
|
||||
|
||||
return useMemo(() => {
|
||||
return trimmedLines.slice(lineOffset)
|
||||
return trimmedLines === undefined || lineOffset === undefined ? [] : trimmedLines.slice(lineOffset)
|
||||
}, [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 { DarkModeConfig } from './dark-mode/types'
|
||||
import type { EditorConfig } from './editor/types'
|
||||
import type { NoteDetails } from './note-details/types/note-details'
|
||||
import type { RealtimeStatus } from './realtime/types'
|
||||
import type { RendererStatus } from './renderer-status/types'
|
||||
import type { OptionalUserState } from './user/types'
|
||||
import type { OptionalNoteDetails } from './note-details/types/note-details'
|
||||
|
||||
export interface ApplicationState {
|
||||
user: OptionalUserState
|
||||
history: HistoryEntryWithOrigin[]
|
||||
editorConfig: EditorConfig
|
||||
darkMode: DarkModeConfig
|
||||
noteDetails: NoteDetails
|
||||
noteDetails: OptionalNoteDetails
|
||||
rendererStatus: RendererStatus
|
||||
realtimeStatus: RealtimeStatus
|
||||
}
|
||||
|
|
|
@ -73,9 +73,19 @@ export const updateCursorPositions = (selection: CursorSelection): void => {
|
|||
* Updates the current note's metadata from the server.
|
||||
*/
|
||||
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({
|
||||
type: NoteDetailsActionType.UPDATE_METADATA,
|
||||
updatedMetadata
|
||||
} as UpdateMetadataAction)
|
||||
}
|
||||
|
||||
export const unloadNote = (): void => {
|
||||
store.dispatch({
|
||||
type: NoteDetailsActionType.UNLOAD_NOTE
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
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 { buildStateFromMetadataUpdate } from './reducers/build-state-from-metadata-update'
|
||||
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 type { NoteDetailsActions } 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'
|
||||
|
||||
export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
|
||||
state: NoteDetails = initialState,
|
||||
export const NoteDetailsReducer: Reducer<OptionalNoteDetails, NoteDetailsActions> = (
|
||||
state: OptionalNoteDetails = null,
|
||||
action: NoteDetailsActions
|
||||
) => {
|
||||
if (action.type === NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER) {
|
||||
return buildStateFromServerDto(action.noteFromServer)
|
||||
}
|
||||
if (state === null) {
|
||||
return null
|
||||
}
|
||||
switch (action.type) {
|
||||
case NoteDetailsActionType.UPDATE_CURSOR_POSITION:
|
||||
return buildStateFromUpdateCursorPosition(state, action.selection)
|
||||
|
@ -28,10 +33,10 @@ export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
|
|||
return buildStateFromServerPermissions(state, action.notePermissionsFromServer)
|
||||
case NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING:
|
||||
return buildStateFromFirstHeadingUpdate(state, action.firstHeading)
|
||||
case NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER:
|
||||
return buildStateFromServerDto(action.noteFromServer)
|
||||
case NoteDetailsActionType.UPDATE_METADATA:
|
||||
return buildStateFromMetadataUpdate(state, action.updatedMetadata)
|
||||
case NoteDetailsActionType.UNLOAD_NOTE:
|
||||
return null
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
|
|
@ -14,7 +14,8 @@ export enum NoteDetailsActionType {
|
|||
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_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 =
|
||||
|
@ -24,6 +25,7 @@ export type NoteDetailsActions =
|
|||
| UpdateNoteTitleByFirstHeadingAction
|
||||
| UpdateCursorPositionAction
|
||||
| UpdateMetadataAction
|
||||
| UnloadNoteAction
|
||||
|
||||
/**
|
||||
* 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
|
||||
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
|
||||
startOfContentLineOffset: number
|
||||
}
|
||||
|
||||
export type OptionalNoteDetails = NoteDetails | null
|
||||
|
|
Loading…
Reference in a new issue