From 669845046164a40c86df3bff895434f8c273572a Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Wed, 6 Sep 2023 22:18:38 +0200 Subject: [PATCH] fix(frontend): make note details in redux optional Signed-off-by: Tilman Vatteroth --- frontend/cypress/e2e/revision.spec.ts | 4 +-- .../note-loading-boundary.tsx | 8 +++++ ...nd-additional-configuration-to-renderer.ts | 19 ++++++----- .../document-infobar.tsx | 5 +++ .../use-on-image-upload-from-renderer.ts | 33 +++++++++++++------ .../editor-pane/hooks/use-handle-upload.tsx | 5 ++- .../hooks/use-line-based-position.ts | 15 ++++----- .../hooks/yjs/use-realtime-connection.ts | 2 +- .../hooks/yjs/use-receive-realtime-users.ts | 13 ++------ .../hooks/yjs/use-websocket-url.ts | 4 +-- .../number-of-lines-in-document-info.tsx | 2 +- .../status-bar/remaining-characters-info.tsx | 2 +- .../status-bar/selected-characters.tsx | 6 ++-- .../hooks/use-update-local-history-entry.ts | 8 ++--- .../hooks/use-on-scroll-with-line-offset.ts | 6 +++- .../use-scroll-state-without-line-offset.ts | 4 +-- .../renderer-pane/renderer-pane.tsx | 6 +++- .../aliases-modal/aliases-add-form.tsx | 5 ++- .../aliases-modal/aliases-list.tsx | 10 +++--- .../delete-note-sidebar-entry.tsx | 5 ++- .../export-markdown-sidebar-entry.tsx | 6 +++- .../note-info-line-contributors.tsx | 2 +- .../note-info-line-created-at.tsx | 9 +++-- .../note-info-line-updated-at.tsx | 9 +++-- .../note-info-line-updated-by.tsx | 2 +- .../permission-entry-special-group.tsx | 11 ++++++- .../permission-entry-user.tsx | 11 ++++++- .../permission-owner-info.tsx | 6 +++- .../permission-section-owner.tsx | 5 ++- .../permission-section-special-groups.tsx | 9 ++++- .../permission-section-users.tsx | 10 ++++-- .../pin-note-sidebar-entry.tsx | 13 +++++--- .../revisions-modal/delete-revision-modal.tsx | 5 ++- .../revisions-modal/revision-modal-body.tsx | 13 ++++++-- .../revisions-modal/revision-modal-footer.tsx | 10 +++--- .../revisions-modal/revision-viewer.tsx | 8 ++--- .../share-modal/note-url-field.tsx | 11 ++++--- .../share-modal/share-modal.tsx | 6 +++- .../slide-show-page-content.tsx | 17 +++++----- .../task-list/set-checkbox-in-editor.tsx | 7 +++- frontend/src/hooks/common/use-is-owner.ts | 5 ++- frontend/src/hooks/common/use-may-edit.ts | 5 ++- .../hooks/common/use-note-markdown-content.ts | 2 +- frontend/src/hooks/common/use-note-title.ts | 4 +-- ...te-markdown-content-without-frontmatter.ts | 22 +++++++++---- frontend/src/redux/application-state.d.ts | 4 +-- frontend/src/redux/note-details/methods.ts | 12 ++++++- frontend/src/redux/note-details/reducer.ts | 17 ++++++---- frontend/src/redux/note-details/types.ts | 8 ++++- .../redux/note-details/types/note-details.ts | 2 ++ 50 files changed, 278 insertions(+), 135 deletions(-) diff --git a/frontend/cypress/e2e/revision.spec.ts b/frontend/cypress/e2e/revision.spec.ts index 12eb76106..329b0d55d 100644 --- a/frontend/cypress/e2e/revision.spec.ts +++ b/frontend/cypress/e2e/revision.spec.ts @@ -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() diff --git a/frontend/src/components/common/note-loading-boundary/note-loading-boundary.tsx b/frontend/src/components/common/note-loading-boundary/note-loading-boundary.tsx index 8e4d09917..37891660f 100644 --- a/frontend/src/components/common/note-loading-boundary/note-loading-boundary.tsx +++ b/frontend/src/components/common/note-loading-boundary/note-loading-boundary.tsx @@ -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> = ({ loadNoteFromServer() }, [loadNoteFromServer]) + useEffect( + () => () => { + unloadNote() + }, + [] + ) + const errorComponent = useMemo(() => { if (error === undefined) { return null diff --git a/frontend/src/components/common/renderer-iframe/hooks/use-send-additional-configuration-to-renderer.ts b/frontend/src/components/common/renderer-iframe/hooks/use-send-additional-configuration-to-renderer.ts index 897aa266e..c4428c79c 100644 --- a/frontend/src/components/common/renderer-iframe/hooks/use-send-additional-configuration-to-renderer.ts +++ b/frontend/src/components/common/renderer-iframe/hooks/use-send-additional-configuration-to-renderer.ts @@ -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 ) } diff --git a/frontend/src/components/document-read-only-page/document-infobar.tsx b/frontend/src/components/document-read-only-page/document-infobar.tsx index 4cefe2036..4f4c70367 100644 --- a/frontend/src/components/document-read-only-page/document-infobar.tsx +++ b/frontend/src/components/document-read-only-page/document-infobar.tsx @@ -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 (
 
diff --git a/frontend/src/components/editor-page/editor-pane/hooks/image-upload-from-renderer/use-on-image-upload-from-renderer.ts b/frontend/src/components/editor-page/editor-pane/hooks/image-upload-from-renderer/use-on-image-upload-from-renderer.ts index 5c8cc2098..124acbd02 100644 --- a/frontend/src/components/editor-page/editor-pane/hooks/image-upload-from-renderer/use-on-image-upload-from-renderer.ts +++ b/frontend/src/components/editor-page/editor-pane/hooks/image-upload-from-renderer/use-on-image-upload-from-renderer.ts @@ -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 => { +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 + ) ?? {} ) } diff --git a/frontend/src/components/editor-page/editor-pane/hooks/use-handle-upload.tsx b/frontend/src/components/editor-page/editor-pane/hooks/use-handle-upload.tsx index 8e2839938..21f7f2959 100644 --- a/frontend/src/components/editor-page/editor-pane/hooks/use-handle-upload.tsx +++ b/frontend/src/components/editor-page/editor-pane/hooks/use-handle-upload.tsx @@ -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) }) diff --git a/frontend/src/components/editor-page/editor-pane/hooks/use-line-based-position.ts b/frontend/src/components/editor-page/editor-pane/hooks/use-line-based-position.ts index cd8815e36..09bf2b0a4 100644 --- a/frontend/src/components/editor-page/editor-pane/hooks/use-line-based-position.ts +++ b/frontend/src/components/editor-page/editor-pane/hooks/use-line-based-position.ts @@ -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) diff --git a/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-realtime-connection.ts b/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-realtime-connection.ts index 2eef1df9c..79813c2a2 100644 --- a/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-realtime-connection.ts +++ b/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-realtime-connection.ts @@ -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()}`) diff --git a/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-receive-realtime-users.ts b/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-receive-realtime-users.ts index 5b3fa15f1..4161dda66 100644 --- a/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-receive-realtime-users.ts +++ b/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-receive-realtime-users.ts @@ -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(), []) } diff --git a/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-websocket-url.ts b/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-websocket-url.ts index 3083a85c2..6ce35d265 100644 --- a/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-websocket-url.ts +++ b/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-websocket-url.ts @@ -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) diff --git a/frontend/src/components/editor-page/editor-pane/status-bar/number-of-lines-in-document-info.tsx b/frontend/src/components/editor-page/editor-pane/status-bar/number-of-lines-in-document-info.tsx index f5e323a05..9984febf9 100644 --- a/frontend/src/components/editor-page/editor-pane/status-bar/number-of-lines-in-document-info.tsx +++ b/frontend/src/components/editor-page/editor-pane/status-bar/number-of-lines-in-document-info.tsx @@ -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 ( diff --git a/frontend/src/components/editor-page/editor-pane/status-bar/remaining-characters-info.tsx b/frontend/src/components/editor-page/editor-pane/status-bar/remaining-characters-info.tsx index 7aff674a5..16595b7c1 100644 --- a/frontend/src/components/editor-page/editor-pane/status-bar/remaining-characters-info.tsx +++ b/frontend/src/components/editor-page/editor-pane/status-bar/remaining-characters-info.tsx @@ -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(() => { diff --git a/frontend/src/components/editor-page/editor-pane/status-bar/selected-characters.tsx b/frontend/src/components/editor-page/editor-pane/status-bar/selected-characters.tsx index d2e9f3626..f57fe10ac 100644 --- a/frontend/src/components/editor-page/editor-pane/status-bar/selected-characters.tsx +++ b/frontend/src/components/editor-page/editor-pane/status-bar/selected-characters.tsx @@ -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]) diff --git a/frontend/src/components/editor-page/hooks/use-update-local-history-entry.ts b/frontend/src/components/editor-page/hooks/use-update-local-history-entry.ts index a69585eac..55fc596a5 100644 --- a/frontend/src/components/editor-page/hooks/use-update-local-history-entry.ts +++ b/frontend/src/components/editor-page/hooks/use-update-local-history-entry.ts @@ -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([]) useEffect(() => { - if (userExists) { + if (userExists || id === undefined) { return } if (currentNoteTitle === lastNoteTitle.current && equal(currentNoteTags, lastNoteTags.current)) { diff --git a/frontend/src/components/editor-page/renderer-pane/hooks/use-on-scroll-with-line-offset.ts b/frontend/src/components/editor-page/renderer-pane/hooks/use-on-scroll-with-line-offset.ts index 00a3ba8b7..279b3af22 100644 --- a/frontend/src/components/editor-page/renderer-pane/hooks/use-on-scroll-with-line-offset.ts +++ b/frontend/src/components/editor-page/renderer-pane/hooks/use-on-scroll-with-line-offset.ts @@ -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 }) } diff --git a/frontend/src/components/editor-page/renderer-pane/hooks/use-scroll-state-without-line-offset.ts b/frontend/src/components/editor-page/renderer-pane/hooks/use-scroll-state-without-line-offset.ts index 02647b2cf..777fc4ba3 100644 --- a/frontend/src/components/editor-page/renderer-pane/hooks/use-scroll-state-without-line-offset.ts +++ b/frontend/src/components/editor-page/renderer-pane/hooks/use-scroll-state-without-line-offset.ts @@ -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, diff --git a/frontend/src/components/editor-page/renderer-pane/renderer-pane.tsx b/frontend/src/components/editor-page/renderer-pane/renderer-pane.tsx index 7fca53790..75f0ec398 100644 --- a/frontend/src/components/editor-page/renderer-pane/renderer-pane.tsx +++ b/frontend/src/components/editor-page/renderer-pane/renderer-pane.tsx @@ -28,10 +28,14 @@ export type RendererPaneProps = Omit< */ export const RendererPane: React.FC = ({ 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 ( { 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) => { event.preventDefault() + if (noteId === undefined) { + return + } addAlias(noteId, newAlias) .then(updateMetadata) .catch(showErrorNotification('editor.modal.aliases.errorAddingAlias')) diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list.tsx index de14d33f6..acd883ff8 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list.tsx @@ -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) => ) + return aliases === undefined + ? null + : aliases + .sort((a, b) => a.name.localeCompare(b.name)) + .map((alias) => ) }, [aliases]) return {aliasesDom} diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/delete-note-sidebar-entry/delete-note-sidebar-entry.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/delete-note-sidebar-entry/delete-note-sidebar-entry.tsx index f5c94be8b..3be779df2 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/delete-note-sidebar-entry/delete-note-sidebar-entry.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/delete-note-sidebar-entry/delete-note-sidebar-entry.tsx @@ -26,11 +26,14 @@ import { Trans, useTranslation } from 'react-i18next' export const DeleteNoteSidebarEntry: React.FC> = ({ 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')) diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/export-markdown-sidebar-entry.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/export-markdown-sidebar-entry.tsx index 45aab361a..0f4bf28c9 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/export-markdown-sidebar-entry.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/export-markdown-sidebar-entry.tsx @@ -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]) diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-contributors.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-contributors.tsx index a131cab14..6c9dd3195 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-contributors.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-contributors.tsx @@ -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 ( diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-created-at.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-created-at.tsx index e99d48919..f9be610d0 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-created-at.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-created-at.tsx @@ -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 : ( diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-updated-at.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-updated-at.tsx index f5a6a61f5..e5c22254a 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-updated-at.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-updated-at.tsx @@ -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 : ( diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-updated-by.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-updated-by.tsx index 541886237..8a88588d9 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-updated-by.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-updated-by.tsx @@ -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) { diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-entry-special-group.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-entry-special-group.tsx index 1fe74624a..354d4ec5c 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-entry-special-group.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-entry-special-group.tsx @@ -33,11 +33,14 @@ export const PermissionEntrySpecialGroup: React.FC { - 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 { + if (!noteId) { + return + } setGroupPermission(noteId, type, true) .then((updatedPermissions) => { setNotePermissionsFromServer(updatedPermissions) @@ -54,6 +60,9 @@ export const PermissionEntrySpecialGroup: React.FC { + if (!noteId) { + return + } removeGroupPermission(noteId, type) .then((updatedPermissions) => { setNotePermissionsFromServer(updatedPermissions) diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-entry-user.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-entry-user.tsx index 80fe884d2..70184af1c 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-entry-user.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-entry-user.tsx @@ -31,10 +31,13 @@ export const PermissionEntryUser: React.FC { - 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 { + if (!noteId) { + return + } setUserPermission(noteId, entry.username, false) .then((updatedPermissions) => { setNotePermissionsFromServer(updatedPermissions) @@ -51,6 +57,9 @@ export const PermissionEntryUser: React.FC { + if (!noteId) { + return + } setUserPermission(noteId, entry.username, true) .then((updatedPermissions) => { setNotePermissionsFromServer(updatedPermissions) diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-owner-info.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-owner-info.tsx index fb9ff2ec6..0353de0d5 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-owner-info.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-owner-info.tsx @@ -26,9 +26,13 @@ export const PermissionOwnerInfo: React.FC { - 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 ( diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-section-owner.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-section-owner.tsx index 3a9fdfd1d..0da3a8cc3 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-section-owner.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-section-owner.tsx @@ -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 = ({ 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 = ({ disa const onOwnerChange = useCallback( (newOwner: string) => { + if (!noteId) { + return + } setNoteOwner(noteId, newOwner) .then((updatedPermissions) => { setNotePermissionsFromServer(updatedPermissions) diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-section-special-groups.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-section-special-groups.tsx index dfa5f4c52..a50159fc2 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-section-special-groups.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-section-special-groups.tsx @@ -18,10 +18,13 @@ import { Trans, useTranslation } from 'react-i18next' */ export const PermissionSectionSpecialGroups: React.FC = ({ 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 = } }, [groupPermissions]) + if (!specialGroupEntries) { + return null + } + return (
diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-section-users.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-section-users.tsx index 1e05d4d96..de2ce8954 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-section-users.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-section-users.tsx @@ -20,11 +20,14 @@ import { Trans, useTranslation } from 'react-i18next' */ export const PermissionSectionUsers: React.FC = ({ 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) => ( )) @@ -32,6 +35,9 @@ export const PermissionSectionUsers: React.FC = ({ disa const onAddEntry = useCallback( (username: string) => { + if (!noteId) { + return + } setUserPermission(noteId, username, false) .then((updatedPermissions) => { setNotePermissionsFromServer(updatedPermissions) diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/pin-note-sidebar-entry/pin-note-sidebar-entry.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/pin-note-sidebar-entry/pin-note-sidebar-entry.tsx index 991de5b82..5e2bdff9e 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/pin-note-sidebar-entry/pin-note-sidebar-entry.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/pin-note-sidebar-entry/pin-note-sidebar-entry.tsx @@ -22,21 +22,24 @@ import { Trans, useTranslation } from 'react-i18next' */ export const PinNoteSidebarEntry: React.FC = ({ 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 ( = ({ 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]) diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-modal-body.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-modal-body.tsx index d9cf78b24..f7844a953 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-modal-body.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-modal-body.tsx @@ -27,8 +27,17 @@ export const RevisionModalBody = ({ onShowDeleteModal, onHide }: RevisionModal) useTranslation() const isOwner = useIsOwner() const [selectedRevisionId, setSelectedRevisionId] = useState() - 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 diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-modal-footer.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-modal-footer.tsx index d4f69c0be..f4794f333 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-modal-footer.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-modal-footer.tsx @@ -37,7 +37,7 @@ export const RevisionModalFooter: React.FC = ({ 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 = ({ }, []) 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?.() diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-viewer.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-viewer.tsx index 699a28c50..a7374f1c9 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-viewer.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/revisions-sidebar-entry/revisions-modal/revision-viewer.tsx @@ -25,16 +25,16 @@ export interface RevisionViewerProps { * @param allRevisions List of metadata for all available revisions. */ export const RevisionViewer: React.FC = ({ 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) diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/share-note-sidebar-entry/share-modal/note-url-field.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/share-note-sidebar-entry/share-modal/note-url-field.tsx index cc4f3452b..70fcd03fb 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/share-note-sidebar-entry/share-modal/note-url-field.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/share-note-sidebar-entry/share-modal/note-url-field.tsx @@ -24,13 +24,16 @@ export interface LinkFieldProps { */ export const NoteUrlField: React.FC = ({ 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 + return !url ? null : } diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/share-note-sidebar-entry/share-modal/share-modal.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/share-note-sidebar-entry/share-modal/share-modal.tsx index 0f2dfe5c4..248a83382 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/share-note-sidebar-entry/share-modal/share-modal.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/share-note-sidebar-entry/share-modal/share-modal.tsx @@ -21,7 +21,11 @@ import { Trans, useTranslation } from 'react-i18next' */ export const ShareModal: React.FC = ({ show, onHide }) => { useTranslation() - const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter) + const noteFrontmatter = useApplicationState((state) => state.noteDetails?.frontmatter) + + if (!noteFrontmatter) { + return null + } return ( diff --git a/frontend/src/components/slide-show-page/slide-show-page-content.tsx b/frontend/src/components/slide-show-page/slide-show-page-content.tsx index 96e75548e..a329da6d2 100644 --- a/frontend/src/components/slide-show-page/slide-show-page-content.tsx +++ b/frontend/src/components/slide-show-page/slide-show-page-content.tsx @@ -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 ) diff --git a/frontend/src/extensions/essential-app-extensions/task-list/set-checkbox-in-editor.tsx b/frontend/src/extensions/essential-app-extensions/task-list/set-checkbox-in-editor.tsx index 979939188..ce0103c14 100644 --- a/frontend/src/extensions/essential-app-extensions/task-list/set-checkbox-in-editor.tsx +++ b/frontend/src/extensions/essential-app-extensions/task-list/set-checkbox-in-editor.tsx @@ -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([]) diff --git a/frontend/src/hooks/common/use-is-owner.ts b/frontend/src/hooks/common/use-is-owner.ts index bfa209cb1..7cfbb76df 100644 --- a/frontend/src/hooks/common/use-is-owner.ts +++ b/frontend/src/hooks/common/use-is-owner.ts @@ -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]) } diff --git a/frontend/src/hooks/common/use-may-edit.ts b/frontend/src/hooks/common/use-may-edit.ts index 756d2fe47..f45fe63f6 100644 --- a/frontend/src/hooks/common/use-may-edit.ts +++ b/frontend/src/hooks/common/use-may-edit.ts @@ -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]) } diff --git a/frontend/src/hooks/common/use-note-markdown-content.ts b/frontend/src/hooks/common/use-note-markdown-content.ts index ae7945582..9aa02a19e 100644 --- a/frontend/src/hooks/common/use-note-markdown-content.ts +++ b/frontend/src/hooks/common/use-note-markdown-content.ts @@ -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 ?? '') } diff --git a/frontend/src/hooks/common/use-note-title.ts b/frontend/src/hooks/common/use-note-title.ts index 6b317aa8b..79decf728 100644 --- a/frontend/src/hooks/common/use-note-title.ts +++ b/frontend/src/hooks/common/use-note-title.ts @@ -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]) } diff --git a/frontend/src/hooks/common/use-trimmed-note-markdown-content-without-frontmatter.ts b/frontend/src/hooks/common/use-trimmed-note-markdown-content-without-frontmatter.ts index 26f27a21b..9fee3ba39 100644 --- a/frontend/src/hooks/common/use-trimmed-note-markdown-content-without-frontmatter.ts +++ b/frontend/src/hooks/common/use-trimmed-note-markdown-content-without-frontmatter.ts @@ -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]) } diff --git a/frontend/src/redux/application-state.d.ts b/frontend/src/redux/application-state.d.ts index 1861375a5..3b71843ec 100644 --- a/frontend/src/redux/application-state.d.ts +++ b/frontend/src/redux/application-state.d.ts @@ -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 } diff --git a/frontend/src/redux/note-details/methods.ts b/frontend/src/redux/note-details/methods.ts index cff1e6357..52113656a 100644 --- a/frontend/src/redux/note-details/methods.ts +++ b/frontend/src/redux/note-details/methods.ts @@ -73,9 +73,19 @@ export const updateCursorPositions = (selection: CursorSelection): void => { * Updates the current note's metadata from the server. */ export const updateMetadata = async (): Promise => { - 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 + }) +} diff --git a/frontend/src/redux/note-details/reducer.ts b/frontend/src/redux/note-details/reducer.ts index de9393fd1..3463e808a 100644 --- a/frontend/src/redux/note-details/reducer.ts +++ b/frontend/src/redux/note-details/reducer.ts @@ -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 = ( - state: NoteDetails = initialState, +export const NoteDetailsReducer: Reducer = ( + 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 = ( 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 } diff --git a/frontend/src/redux/note-details/types.ts b/frontend/src/redux/note-details/types.ts index 96b16c204..e92b4f756 100644 --- a/frontend/src/redux/note-details/types.ts +++ b/frontend/src/redux/note-details/types.ts @@ -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 { type: NoteDetailsActionType.UPDATE_METADATA updatedMetadata: NoteMetadata } + +export interface UnloadNoteAction extends Action { + type: NoteDetailsActionType.UNLOAD_NOTE +} diff --git a/frontend/src/redux/note-details/types/note-details.ts b/frontend/src/redux/note-details/types/note-details.ts index e78f72628..9a4f19cad 100644 --- a/frontend/src/redux/note-details/types/note-details.ts +++ b/frontend/src/redux/note-details/types/note-details.ts @@ -26,3 +26,5 @@ export interface NoteDetails extends Omit