From d840a6f0b10176c7c5b382d8250b8ac4e15c29b7 Mon Sep 17 00:00:00 2001 From: Erik Michelson Date: Sun, 11 Feb 2024 02:53:23 +0100 Subject: [PATCH] refactor(redux): migrate to RTK2 store definition Signed-off-by: Erik Michelson --- .../application-loader/initializers/index.ts | 2 +- .../aliases-modal/aliases-add-form.spec.tsx | 2 +- .../aliases-modal/aliases-list.tsx | 2 +- .../editor/indent-spaces-setting-input.tsx | 2 +- .../indent-with-tabs-setting-button-group.tsx | 2 +- .../editor/ligature-setting-button-group.tsx | 2 +- .../line-wrapping-setting-button-group.tsx | 2 +- .../smart-paste-setting-button-group.tsx | 2 +- .../spellcheck-setting-button-group.tsx | 2 +- .../sync-scroll-setting-button-group.tsx | 2 +- .../src/hooks/common/use-application-state.ts | 2 +- frontend/src/redux/application-state.d.ts | 22 ---- frontend/src/redux/dark-mode/initial-state.ts | 11 ++ frontend/src/redux/dark-mode/methods.ts | 10 +- frontend/src/redux/dark-mode/reducers.ts | 26 ---- frontend/src/redux/dark-mode/slice.ts | 22 ++++ frontend/src/redux/dark-mode/types.ts | 8 -- .../src/redux/editor-config/initial-state.ts | 16 +++ frontend/src/redux/editor-config/methods.ts | 77 ++++++++++++ frontend/src/redux/editor-config/slice.ts | 43 +++++++ frontend/src/redux/editor-config/types.ts | 14 +++ frontend/src/redux/editor/methods.ts | 80 ------------- frontend/src/redux/editor/reducers.ts | 113 ------------------ frontend/src/redux/editor/types.ts | 76 ------------ frontend/src/redux/history/initial-state.ts | 8 ++ frontend/src/redux/history/methods.ts | 44 +++---- frontend/src/redux/history/reducers.ts | 29 ----- frontend/src/redux/history/slice.ts | 34 ++++++ frontend/src/redux/history/types.ts | 32 +---- frontend/src/redux/index.ts | 21 +++- ...ild-state-from-updated-markdown-content.ts | 2 +- .../src/redux/note-details/initial-state.ts | 2 +- frontend/src/redux/note-details/methods.ts | 51 +++----- frontend/src/redux/note-details/reducer.ts | 43 ------- .../build-state-from-first-heading-update.ts | 2 +- .../build-state-from-metadata-update.spec.ts | 2 +- .../build-state-from-metadata-update.ts | 2 +- ...uild-state-from-server-permissions.spec.ts | 2 +- .../build-state-from-server-permissions.ts | 2 +- ...ate-from-set-note-data-from-server.spec.ts | 2 +- ...ld-state-from-set-note-data-from-server.ts | 2 +- .../build-state-from-task-list-update.spec.ts | 2 +- .../build-state-from-task-list-update.ts | 2 +- ...build-state-from-update-cursor-position.ts | 2 +- frontend/src/redux/note-details/slice.ts | 48 ++++++++ frontend/src/redux/note-details/types.ts | 81 +++---------- .../redux/note-details/types/note-details.ts | 30 ----- frontend/src/redux/realtime/initial-state.ts | 16 +++ frontend/src/redux/realtime/methods.ts | 25 ++-- frontend/src/redux/realtime/reducers.ts | 53 -------- frontend/src/redux/realtime/slice.ts | 32 +++++ frontend/src/redux/realtime/types.ts | 31 +---- frontend/src/redux/reducers.ts | 25 ---- .../redux/renderer-status/initial-state.ts | 10 ++ frontend/src/redux/renderer-status/methods.ts | 8 +- .../src/redux/renderer-status/reducers.ts | 34 ------ frontend/src/redux/renderer-status/slice.ts | 22 ++++ frontend/src/redux/renderer-status/types.ts | 13 -- frontend/src/redux/user/initial-state.ts | 8 ++ frontend/src/redux/user/methods.ts | 14 +-- frontend/src/redux/user/reducers.ts | 22 ---- frontend/src/redux/user/slice.ts | 22 ++++ frontend/src/redux/user/types.ts | 19 +-- frontend/src/test-utils/mock-app-state.ts | 14 +-- .../src/test-utils/mock-note-permissions.ts | 2 +- frontend/src/utils/update-object.ts | 17 +++ 66 files changed, 526 insertions(+), 846 deletions(-) delete mode 100644 frontend/src/redux/application-state.d.ts create mode 100644 frontend/src/redux/dark-mode/initial-state.ts delete mode 100644 frontend/src/redux/dark-mode/reducers.ts create mode 100644 frontend/src/redux/dark-mode/slice.ts create mode 100644 frontend/src/redux/editor-config/initial-state.ts create mode 100644 frontend/src/redux/editor-config/methods.ts create mode 100644 frontend/src/redux/editor-config/slice.ts create mode 100644 frontend/src/redux/editor-config/types.ts delete mode 100644 frontend/src/redux/editor/methods.ts delete mode 100644 frontend/src/redux/editor/reducers.ts delete mode 100644 frontend/src/redux/editor/types.ts create mode 100644 frontend/src/redux/history/initial-state.ts delete mode 100644 frontend/src/redux/history/reducers.ts create mode 100644 frontend/src/redux/history/slice.ts delete mode 100644 frontend/src/redux/note-details/reducer.ts create mode 100644 frontend/src/redux/note-details/slice.ts delete mode 100644 frontend/src/redux/note-details/types/note-details.ts create mode 100644 frontend/src/redux/realtime/initial-state.ts delete mode 100644 frontend/src/redux/realtime/reducers.ts create mode 100644 frontend/src/redux/realtime/slice.ts delete mode 100644 frontend/src/redux/reducers.ts create mode 100644 frontend/src/redux/renderer-status/initial-state.ts delete mode 100644 frontend/src/redux/renderer-status/reducers.ts create mode 100644 frontend/src/redux/renderer-status/slice.ts create mode 100644 frontend/src/redux/user/initial-state.ts delete mode 100644 frontend/src/redux/user/reducers.ts create mode 100644 frontend/src/redux/user/slice.ts create mode 100644 frontend/src/utils/update-object.ts diff --git a/frontend/src/components/application-loader/initializers/index.ts b/frontend/src/components/application-loader/initializers/index.ts index 6e4fe946a..be60c708c 100644 --- a/frontend/src/components/application-loader/initializers/index.ts +++ b/frontend/src/components/application-loader/initializers/index.ts @@ -8,7 +8,7 @@ import { Logger } from '../../../utils/logger' import { isDevMode, isTestMode } from '../../../utils/test-modes' import { loadDarkMode } from './load-dark-mode' import { setUpI18n } from './setupI18n' -import { loadFromLocalStorage } from '../../../redux/editor/methods' +import { loadFromLocalStorage } from '../../../redux/editor-config/methods' import { fetchAndSetUser } from '../../login-page/utils/fetch-and-set-user' const logger = new Logger('Application Loader') diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.spec.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.spec.tsx index 37b755d54..29bdc7008 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.spec.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.spec.tsx @@ -5,7 +5,7 @@ */ import * as AliasModule from '../../../../../../api/alias' import * as NoteDetailsReduxModule from '../../../../../../redux/note-details/methods' -import type { NoteDetails } from '../../../../../../redux/note-details/types/note-details' +import type { NoteDetails } from '../../../../../../redux/note-details/types' import { mockI18n } from '../../../../../../test-utils/mock-i18n' import { mockNotePermissions } from '../../../../../../test-utils/mock-note-permissions' import { AliasesAddForm } from './aliases-add-form' 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 aab779897..e2a845b06 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 @@ -5,7 +5,7 @@ */ import { useApplicationState } from '../../../../../../hooks/common/use-application-state' import type { Alias } from '../../../../../../api/alias/types' -import type { ApplicationState } from '../../../../../../redux/application-state' +import type { ApplicationState } from '../../../../../../redux' import { AliasesListEntry } from './aliases-list-entry' import React, { Fragment, useMemo } from 'react' diff --git a/frontend/src/components/global-dialogs/settings-dialog/editor/indent-spaces-setting-input.tsx b/frontend/src/components/global-dialogs/settings-dialog/editor/indent-spaces-setting-input.tsx index 250365f7e..792bb96d0 100644 --- a/frontend/src/components/global-dialogs/settings-dialog/editor/indent-spaces-setting-input.tsx +++ b/frontend/src/components/global-dialogs/settings-dialog/editor/indent-spaces-setting-input.tsx @@ -6,7 +6,7 @@ import React from 'react' import { Form } from 'react-bootstrap' import { useApplicationState } from '../../../../hooks/common/use-application-state' -import { setEditorIndentSpaces } from '../../../../redux/editor/methods' +import { setEditorIndentSpaces } from '../../../../redux/editor-config/methods' import { useCallback } from 'react' /** diff --git a/frontend/src/components/global-dialogs/settings-dialog/editor/indent-with-tabs-setting-button-group.tsx b/frontend/src/components/global-dialogs/settings-dialog/editor/indent-with-tabs-setting-button-group.tsx index 94770efca..9888aebfa 100644 --- a/frontend/src/components/global-dialogs/settings-dialog/editor/indent-with-tabs-setting-button-group.tsx +++ b/frontend/src/components/global-dialogs/settings-dialog/editor/indent-with-tabs-setting-button-group.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useApplicationState } from '../../../../hooks/common/use-application-state' -import { setEditorIndentWithTabs } from '../../../../redux/editor/methods' +import { setEditorIndentWithTabs } from '../../../../redux/editor-config/methods' import { OnOffButtonGroup } from '../utils/on-off-button-group' import React from 'react' diff --git a/frontend/src/components/global-dialogs/settings-dialog/editor/ligature-setting-button-group.tsx b/frontend/src/components/global-dialogs/settings-dialog/editor/ligature-setting-button-group.tsx index 4a3efe55b..20846ab88 100644 --- a/frontend/src/components/global-dialogs/settings-dialog/editor/ligature-setting-button-group.tsx +++ b/frontend/src/components/global-dialogs/settings-dialog/editor/ligature-setting-button-group.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useApplicationState } from '../../../../hooks/common/use-application-state' -import { setEditorLigatures } from '../../../../redux/editor/methods' +import { setEditorLigatures } from '../../../../redux/editor-config/methods' import { OnOffButtonGroup } from '../utils/on-off-button-group' import React from 'react' diff --git a/frontend/src/components/global-dialogs/settings-dialog/editor/line-wrapping-setting-button-group.tsx b/frontend/src/components/global-dialogs/settings-dialog/editor/line-wrapping-setting-button-group.tsx index 866476ca5..dbdd21704 100644 --- a/frontend/src/components/global-dialogs/settings-dialog/editor/line-wrapping-setting-button-group.tsx +++ b/frontend/src/components/global-dialogs/settings-dialog/editor/line-wrapping-setting-button-group.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useApplicationState } from '../../../../hooks/common/use-application-state' -import { setEditorLineWrapping } from '../../../../redux/editor/methods' +import { setEditorLineWrapping } from '../../../../redux/editor-config/methods' import { OnOffButtonGroup } from '../utils/on-off-button-group' import React from 'react' diff --git a/frontend/src/components/global-dialogs/settings-dialog/editor/smart-paste-setting-button-group.tsx b/frontend/src/components/global-dialogs/settings-dialog/editor/smart-paste-setting-button-group.tsx index 1b6dd43bd..0533ae84f 100644 --- a/frontend/src/components/global-dialogs/settings-dialog/editor/smart-paste-setting-button-group.tsx +++ b/frontend/src/components/global-dialogs/settings-dialog/editor/smart-paste-setting-button-group.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useApplicationState } from '../../../../hooks/common/use-application-state' -import { setEditorSmartPaste } from '../../../../redux/editor/methods' +import { setEditorSmartPaste } from '../../../../redux/editor-config/methods' import { OnOffButtonGroup } from '../utils/on-off-button-group' import React from 'react' diff --git a/frontend/src/components/global-dialogs/settings-dialog/editor/spellcheck-setting-button-group.tsx b/frontend/src/components/global-dialogs/settings-dialog/editor/spellcheck-setting-button-group.tsx index 6e6903f15..738342bc5 100644 --- a/frontend/src/components/global-dialogs/settings-dialog/editor/spellcheck-setting-button-group.tsx +++ b/frontend/src/components/global-dialogs/settings-dialog/editor/spellcheck-setting-button-group.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useApplicationState } from '../../../../hooks/common/use-application-state' -import { setEditorSpellCheck } from '../../../../redux/editor/methods' +import { setEditorSpellCheck } from '../../../../redux/editor-config/methods' import { OnOffButtonGroup } from '../utils/on-off-button-group' import React from 'react' diff --git a/frontend/src/components/global-dialogs/settings-dialog/editor/sync-scroll-setting-button-group.tsx b/frontend/src/components/global-dialogs/settings-dialog/editor/sync-scroll-setting-button-group.tsx index ef50a1b90..aff350a0c 100644 --- a/frontend/src/components/global-dialogs/settings-dialog/editor/sync-scroll-setting-button-group.tsx +++ b/frontend/src/components/global-dialogs/settings-dialog/editor/sync-scroll-setting-button-group.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useApplicationState } from '../../../../hooks/common/use-application-state' -import { setEditorSyncScroll } from '../../../../redux/editor/methods' +import { setEditorSyncScroll } from '../../../../redux/editor-config/methods' import { OnOffButtonGroup } from '../utils/on-off-button-group' import React from 'react' diff --git a/frontend/src/hooks/common/use-application-state.ts b/frontend/src/hooks/common/use-application-state.ts index 65aaff8f8..5682c25c1 100644 --- a/frontend/src/hooks/common/use-application-state.ts +++ b/frontend/src/hooks/common/use-application-state.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import type { ApplicationState } from '../../redux/application-state' +import type { ApplicationState } from '../../redux' import equal from 'fast-deep-equal' import { useSelector } from 'react-redux' diff --git a/frontend/src/redux/application-state.d.ts b/frontend/src/redux/application-state.d.ts deleted file mode 100644 index 3b71843ec..000000000 --- a/frontend/src/redux/application-state.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { HistoryEntryWithOrigin } from '../api/history/types' -import type { DarkModeConfig } from './dark-mode/types' -import type { EditorConfig } from './editor/types' -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: OptionalNoteDetails - rendererStatus: RendererStatus - realtimeStatus: RealtimeStatus -} diff --git a/frontend/src/redux/dark-mode/initial-state.ts b/frontend/src/redux/dark-mode/initial-state.ts new file mode 100644 index 000000000..259d13254 --- /dev/null +++ b/frontend/src/redux/dark-mode/initial-state.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { DarkModeConfig } from './types' +import { DarkModePreference } from './types' + +export const initialState: DarkModeConfig = { + darkModePreference: DarkModePreference.AUTO +} diff --git a/frontend/src/redux/dark-mode/methods.ts b/frontend/src/redux/dark-mode/methods.ts index 3abf2ed83..4ad5e4d01 100644 --- a/frontend/src/redux/dark-mode/methods.ts +++ b/frontend/src/redux/dark-mode/methods.ts @@ -4,12 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { store } from '..' -import type { DarkModeConfigAction, DarkModePreference } from './types' -import { DarkModeConfigActionType } from './types' +import type { DarkModePreference } from './types' +import { darkModeActionsCreator } from './slice' export const setDarkModePreference = (darkModePreference: DarkModePreference): void => { - store.dispatch({ - type: DarkModeConfigActionType.SET_DARK_MODE, - darkModePreference - } as DarkModeConfigAction) + const action = darkModeActionsCreator.setDarkModePreference(darkModePreference) + store.dispatch(action) } diff --git a/frontend/src/redux/dark-mode/reducers.ts b/frontend/src/redux/dark-mode/reducers.ts deleted file mode 100644 index 47afd8aed..000000000 --- a/frontend/src/redux/dark-mode/reducers.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { DarkModeConfig, DarkModeConfigAction } from './types' -import { DarkModeConfigActionType, DarkModePreference } from './types' -import type { Reducer } from 'redux' - -export const initialState: DarkModeConfig = { - darkModePreference: DarkModePreference.AUTO -} - -export const DarkModeConfigReducer: Reducer = ( - state: DarkModeConfig = initialState, - action: DarkModeConfigAction -) => { - switch (action.type) { - case DarkModeConfigActionType.SET_DARK_MODE: - return { - darkModePreference: action.darkModePreference - } - default: - return state - } -} diff --git a/frontend/src/redux/dark-mode/slice.ts b/frontend/src/redux/dark-mode/slice.ts new file mode 100644 index 000000000..1c565aeb3 --- /dev/null +++ b/frontend/src/redux/dark-mode/slice.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' +import { initialState } from './initial-state' +import type { DarkModeConfig } from './types' + +const darkModeSlice = createSlice({ + name: 'darkMode', + initialState, + reducers: { + setDarkModePreference: (state, action: PayloadAction) => { + state.darkModePreference = action.payload + } + } +}) + +export const darkModeActionsCreator = darkModeSlice.actions +export const darkModeReducer = darkModeSlice.reducer diff --git a/frontend/src/redux/dark-mode/types.ts b/frontend/src/redux/dark-mode/types.ts index 6825ca28f..5c09a8207 100644 --- a/frontend/src/redux/dark-mode/types.ts +++ b/frontend/src/redux/dark-mode/types.ts @@ -3,12 +3,6 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Action } from 'redux' - -export enum DarkModeConfigActionType { - SET_DARK_MODE = 'dark-mode/set' -} - export enum DarkModePreference { DARK, LIGHT, @@ -18,5 +12,3 @@ export enum DarkModePreference { export interface DarkModeConfig { darkModePreference: DarkModePreference } - -export type DarkModeConfigAction = Action & DarkModeConfig diff --git a/frontend/src/redux/editor-config/initial-state.ts b/frontend/src/redux/editor-config/initial-state.ts new file mode 100644 index 000000000..f8531ef66 --- /dev/null +++ b/frontend/src/redux/editor-config/initial-state.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { EditorConfig } from './types' + +export const initialState: EditorConfig = { + ligatures: true, + syncScroll: true, + smartPaste: true, + spellCheck: true, + lineWrapping: true, + indentWithTabs: false, + indentSpaces: 2 +} diff --git a/frontend/src/redux/editor-config/methods.ts b/frontend/src/redux/editor-config/methods.ts new file mode 100644 index 000000000..2fc89b51e --- /dev/null +++ b/frontend/src/redux/editor-config/methods.ts @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { store } from '..' +import { editorConfigActionsCreator } from './slice' +import type { EditorConfig } from './types' +import { initialState } from './initial-state' +import { updateObject } from '../../utils/update-object' +import { Logger } from '../../utils/logger' + +const log = new Logger('Redux > EditorConfig') + +export const setEditorSyncScroll = (syncScroll: boolean): void => { + const action = editorConfigActionsCreator.setSyncScroll(syncScroll) + store.dispatch(action) + saveToLocalStorage() +} + +export const setEditorLineWrapping = (lineWrapping: boolean): void => { + const action = editorConfigActionsCreator.setLineWrapping(lineWrapping) + store.dispatch(action) + saveToLocalStorage() +} + +export const setEditorLigatures = (ligatures: boolean): void => { + const action = editorConfigActionsCreator.setLigatures(ligatures) + store.dispatch(action) + saveToLocalStorage() +} + +export const setEditorSmartPaste = (smartPaste: boolean): void => { + const action = editorConfigActionsCreator.setSmartPaste(smartPaste) + store.dispatch(action) + saveToLocalStorage() +} + +export const setEditorSpellCheck = (spellCheck: boolean): void => { + const action = editorConfigActionsCreator.setSpellCheck(spellCheck) + store.dispatch(action) + saveToLocalStorage() +} + +export const setEditorIndentWithTabs = (indentWithTabs: boolean): void => { + const action = editorConfigActionsCreator.setIndentWithTabs(indentWithTabs) + store.dispatch(action) + saveToLocalStorage() +} + +export const setEditorIndentSpaces = (indentSpaces: number): void => { + const action = editorConfigActionsCreator.setIndentSpaces(indentSpaces) + store.dispatch(action) + saveToLocalStorage() +} + +export const loadFromLocalStorage = (): void => { + try { + const config = { ...initialState } + const stored = window.localStorage.getItem('editorConfig') + const parsed = stored ? (JSON.parse(stored) as Partial) : null + updateObject(config, parsed) + const action = editorConfigActionsCreator.setEditorConfig(config) + store.dispatch(action) + } catch (error) { + log.error('Failed to load editor config from local storage', error) + } +} + +const saveToLocalStorage = (): void => { + try { + const state = store.getState() + window.localStorage.setItem('editorConfig', JSON.stringify(state.editorConfig)) + } catch (error) { + log.error('Failed to save editor config to local storage', error) + } +} diff --git a/frontend/src/redux/editor-config/slice.ts b/frontend/src/redux/editor-config/slice.ts new file mode 100644 index 000000000..7cbf67c19 --- /dev/null +++ b/frontend/src/redux/editor-config/slice.ts @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' +import { initialState } from './initial-state' +import type { EditorConfig } from './types' + +const editorConfigSlice = createSlice({ + name: 'editorConfig', + initialState, + reducers: { + setSyncScroll: (state, action: PayloadAction) => { + state.syncScroll = action.payload + }, + setLigatures: (state, action: PayloadAction) => { + state.ligatures = action.payload + }, + setSmartPaste: (state, action: PayloadAction) => { + state.smartPaste = action.payload + }, + setSpellCheck: (state, action: PayloadAction) => { + state.spellCheck = action.payload + }, + setLineWrapping: (state, action: PayloadAction) => { + state.lineWrapping = action.payload + }, + setIndentWithTabs: (state, action: PayloadAction) => { + state.indentWithTabs = action.payload + }, + setIndentSpaces: (state, action: PayloadAction) => { + state.indentSpaces = action.payload + }, + setEditorConfig: (state, action: PayloadAction) => { + return action.payload + } + } +}) + +export const editorConfigActionsCreator = editorConfigSlice.actions +export const editorConfigReducer = editorConfigSlice.reducer diff --git a/frontend/src/redux/editor-config/types.ts b/frontend/src/redux/editor-config/types.ts new file mode 100644 index 000000000..80ef77b2c --- /dev/null +++ b/frontend/src/redux/editor-config/types.ts @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +export interface EditorConfig { + syncScroll: boolean + ligatures: boolean + smartPaste: boolean + spellCheck: boolean + lineWrapping: boolean + indentWithTabs: boolean + indentSpaces: number +} diff --git a/frontend/src/redux/editor/methods.ts b/frontend/src/redux/editor/methods.ts deleted file mode 100644 index 6ed0c8e8e..000000000 --- a/frontend/src/redux/editor/methods.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { store } from '..' -import type { - LoadFromLocalStorageAction, - SetEditorLigaturesAction, - SetEditorLineWrappingAction, - SetEditorSmartPasteAction, - SetEditorSyncScrollAction, - SetEditorSpellCheckAction, - SetEditorIndentWithTabsAction, - SetEditorIndentSpacesAction -} from './types' -import { EditorConfigActionType } from './types' - -export const setEditorSyncScroll = (syncScroll: boolean): void => { - const action: SetEditorSyncScrollAction = { - type: EditorConfigActionType.SET_SYNC_SCROLL, - syncScroll - } - store.dispatch(action) -} - -export const setEditorLineWrapping = (lineWrapping: boolean): void => { - const action: SetEditorLineWrappingAction = { - type: EditorConfigActionType.SET_LINE_WRAPPING, - lineWrapping - } - store.dispatch(action) -} - -export const setEditorLigatures = (ligatures: boolean): void => { - const action: SetEditorLigaturesAction = { - type: EditorConfigActionType.SET_LIGATURES, - ligatures - } - store.dispatch(action) -} - -export const setEditorSmartPaste = (smartPaste: boolean): void => { - const action: SetEditorSmartPasteAction = { - type: EditorConfigActionType.SET_SMART_PASTE, - smartPaste - } - store.dispatch(action) -} - -export const setEditorSpellCheck = (spellCheck: boolean): void => { - const action: SetEditorSpellCheckAction = { - type: EditorConfigActionType.SET_SPELL_CHECK, - spellCheck - } - store.dispatch(action) -} - -export const setEditorIndentWithTabs = (indentWithTabs: boolean): void => { - const action: SetEditorIndentWithTabsAction = { - type: EditorConfigActionType.SET_INDENT_WITH_TABS, - indentWithTabs - } - store.dispatch(action) -} - -export const setEditorIndentSpaces = (indentSpaces: number): void => { - const action: SetEditorIndentSpacesAction = { - type: EditorConfigActionType.SET_INDENT_SPACES, - indentSpaces - } - store.dispatch(action) -} - -export const loadFromLocalStorage = (): void => { - const action: LoadFromLocalStorageAction = { - type: EditorConfigActionType.LOAD_FROM_LOCAL_STORAGE - } - store.dispatch(action) -} diff --git a/frontend/src/redux/editor/reducers.ts b/frontend/src/redux/editor/reducers.ts deleted file mode 100644 index f24db2bf6..000000000 --- a/frontend/src/redux/editor/reducers.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { EditorConfig, EditorConfigActions } from './types' -import { EditorConfigActionType } from './types' -import type { Reducer } from 'redux' -import { Logger } from '../../utils/logger' - -const logger = new Logger('EditorConfig Local Storage') - -export const initialState: EditorConfig = { - ligatures: true, - syncScroll: true, - smartPaste: true, - spellCheck: true, - lineWrapping: true, - indentWithTabs: false, - indentSpaces: 2 -} - -export const EditorConfigReducer: Reducer = ( - state: EditorConfig = initialState, - action: EditorConfigActions -) => { - let newState: EditorConfig - switch (action.type) { - case EditorConfigActionType.LOAD_FROM_LOCAL_STORAGE: - return loadFromLocalStorage() ?? initialState - case EditorConfigActionType.SET_SYNC_SCROLL: - newState = { - ...state, - syncScroll: action.syncScroll - } - saveToLocalStorage(newState) - return newState - case EditorConfigActionType.SET_LIGATURES: - newState = { - ...state, - ligatures: action.ligatures - } - saveToLocalStorage(newState) - return newState - case EditorConfigActionType.SET_SMART_PASTE: - newState = { - ...state, - smartPaste: action.smartPaste - } - saveToLocalStorage(newState) - return newState - case EditorConfigActionType.SET_SPELL_CHECK: - newState = { - ...state, - spellCheck: action.spellCheck - } - saveToLocalStorage(newState) - return newState - case EditorConfigActionType.SET_LINE_WRAPPING: - newState = { - ...state, - lineWrapping: action.lineWrapping - } - saveToLocalStorage(newState) - return newState - case EditorConfigActionType.SET_INDENT_WITH_TABS: - newState = { - ...state, - indentWithTabs: action.indentWithTabs - } - saveToLocalStorage(newState) - return newState - case EditorConfigActionType.SET_INDENT_SPACES: - newState = { - ...state, - indentSpaces: action.indentSpaces - } - saveToLocalStorage(newState) - return newState - default: - return state - } -} - -export const loadFromLocalStorage = (): EditorConfig | undefined => { - try { - const stored = window.localStorage.getItem('editorConfig') - if (!stored) { - return undefined - } - const storedConfiguration = JSON.parse(stored) as Partial - return { - ligatures: storedConfiguration?.ligatures === true ?? true, - syncScroll: storedConfiguration?.syncScroll === true ?? true, - smartPaste: storedConfiguration?.smartPaste === true ?? true, - spellCheck: storedConfiguration?.spellCheck === true ?? true, - lineWrapping: storedConfiguration?.lineWrapping === true ?? true, - indentWithTabs: storedConfiguration?.indentWithTabs === true ?? false, - indentSpaces: storedConfiguration?.indentSpaces ?? 2 - } - } catch (_) { - return undefined - } -} - -export const saveToLocalStorage = (editorConfig: EditorConfig): void => { - try { - const json = JSON.stringify(editorConfig) - localStorage.setItem('editorConfig', json) - } catch (error) { - logger.error('Error while saving editor config in local storage', error) - } -} diff --git a/frontend/src/redux/editor/types.ts b/frontend/src/redux/editor/types.ts deleted file mode 100644 index e88d194ed..000000000 --- a/frontend/src/redux/editor/types.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { Action } from 'redux' - -export enum EditorConfigActionType { - SET_SYNC_SCROLL = 'editor/syncScroll/set', - LOAD_FROM_LOCAL_STORAGE = 'editor/preferences/load', - SET_LIGATURES = 'editor/preferences/setLigatures', - SET_LINE_WRAPPING = 'editor/preferences/setLineWrapping', - SET_SMART_PASTE = 'editor/preferences/setSmartPaste', - SET_SPELL_CHECK = 'editor/preferences/setSpellCheck', - SET_INDENT_WITH_TABS = 'editor/preferences/setIndentWithTabs', - SET_INDENT_SPACES = 'editor/preferences/setIndentSpaces' -} - -export interface EditorConfig { - syncScroll: boolean - ligatures: boolean - smartPaste: boolean - spellCheck: boolean - lineWrapping: boolean - indentWithTabs: boolean - indentSpaces: number -} - -export type EditorConfigActions = - | SetEditorSyncScrollAction - | SetEditorLigaturesAction - | SetEditorSmartPasteAction - | SetEditorLineWrappingAction - | SetEditorSpellCheckAction - | SetEditorIndentWithTabsAction - | SetEditorIndentSpacesAction - | LoadFromLocalStorageAction - -export interface LoadFromLocalStorageAction extends Action { - type: EditorConfigActionType.LOAD_FROM_LOCAL_STORAGE -} - -export interface SetEditorLineWrappingAction extends Action { - type: EditorConfigActionType.SET_LINE_WRAPPING - lineWrapping: boolean -} - -export interface SetEditorSyncScrollAction extends Action { - type: EditorConfigActionType.SET_SYNC_SCROLL - syncScroll: boolean -} - -export interface SetEditorLigaturesAction extends Action { - type: EditorConfigActionType.SET_LIGATURES - ligatures: boolean -} - -export interface SetEditorSmartPasteAction extends Action { - type: EditorConfigActionType.SET_SMART_PASTE - smartPaste: boolean -} - -export interface SetEditorSpellCheckAction extends Action { - type: EditorConfigActionType.SET_SPELL_CHECK - spellCheck: boolean -} - -export interface SetEditorIndentWithTabsAction extends Action { - type: EditorConfigActionType.SET_INDENT_WITH_TABS - indentWithTabs: boolean -} - -export interface SetEditorIndentSpacesAction extends Action { - type: EditorConfigActionType.SET_INDENT_SPACES - indentSpaces: number -} diff --git a/frontend/src/redux/history/initial-state.ts b/frontend/src/redux/history/initial-state.ts new file mode 100644 index 000000000..33f78b3ec --- /dev/null +++ b/frontend/src/redux/history/initial-state.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { HistoryState } from './types' + +export const initialState: HistoryState = [] diff --git a/frontend/src/redux/history/methods.ts b/frontend/src/redux/history/methods.ts index d1bf5282d..013b4fa73 100644 --- a/frontend/src/redux/history/methods.ts +++ b/frontend/src/redux/history/methods.ts @@ -15,10 +15,10 @@ import type { HistoryEntry, HistoryEntryWithOrigin } from '../../api/history/typ import { HistoryEntryOrigin } from '../../api/history/types' import { download } from '../../components/common/download/download' import { Logger } from '../../utils/logger' -import { getGlobalState, store } from '../index' -import type { HistoryExportJson, RemoveEntryAction, SetEntriesAction, UpdateEntryAction, V1HistoryEntry } from './types' -import { HistoryActionType } from './types' +import { store } from '../index' +import type { HistoryExportJson, V1HistoryEntry } from './types' import { DateTime } from 'luxon' +import { historyActionsCreator } from './slice' const log = new Logger('Redux > History') @@ -27,10 +27,8 @@ const log = new Logger('Redux > History') * @param entries The history entries to set into the redux state. */ export const setHistoryEntries = (entries: HistoryEntryWithOrigin[]): void => { - store.dispatch({ - type: HistoryActionType.SET_ENTRIES, - entries - } as SetEntriesAction) + const action = historyActionsCreator.setEntries(entries) + store.dispatch(action) storeLocalHistory() } @@ -47,10 +45,8 @@ export const importHistoryEntries = (entries: HistoryEntryWithOrigin[]): Promise * Deletes all history entries in the redux, local-storage and on the server. */ export const deleteAllHistoryEntries = (): Promise => { - store.dispatch({ - type: HistoryActionType.SET_ENTRIES, - entries: [] - } as SetEntriesAction) + const action = historyActionsCreator.setEntries([]) + store.dispatch(action) storeLocalHistory() return deleteRemoteHistory() } @@ -61,11 +57,11 @@ export const deleteAllHistoryEntries = (): Promise => { * @param newEntry The modified history entry. */ export const updateHistoryEntryRedux = (noteId: string, newEntry: HistoryEntry): void => { - store.dispatch({ - type: HistoryActionType.UPDATE_ENTRY, + const action = historyActionsCreator.updateEntry({ noteId, newEntry - } as UpdateEntryAction) + }) + store.dispatch(action) } /** @@ -83,14 +79,12 @@ export const updateLocalHistoryEntry = (noteId: string, newEntry: HistoryEntry): * @param noteId The note id of the history entry to delete. */ export const removeHistoryEntry = async (noteId: string): Promise => { - const entryToDelete = getGlobalState().history.find((entry) => entry.identifier === noteId) + const entryToDelete = store.getState().history.find((entry) => entry.identifier === noteId) if (entryToDelete && entryToDelete.origin === HistoryEntryOrigin.REMOTE) { await deleteRemoteHistoryEntry(noteId) } - store.dispatch({ - type: HistoryActionType.REMOVE_ENTRY, - noteId - } as RemoveEntryAction) + const action = historyActionsCreator.removeEntry({ noteId }) + store.dispatch(action) storeLocalHistory() } @@ -99,7 +93,7 @@ export const removeHistoryEntry = async (noteId: string): Promise => { * @param noteId The note id of the history entry to update. */ export const toggleHistoryEntryPinning = async (noteId: string): Promise => { - const state = getGlobalState().history + const state = store.getState().history const entryToUpdate = state.find((entry) => entry.identifier === noteId) if (!entryToUpdate) { return Promise.reject(`History entry for note '${noteId}' not found`) @@ -120,7 +114,7 @@ export const toggleHistoryEntryPinning = async (noteId: string): Promise = * Exports the current history redux state into a JSON file that will be downloaded by the client. */ export const downloadHistory = (): void => { - const history = getGlobalState().history + const history = store.getState().history history.forEach((entry: Partial) => { delete entry.origin }) @@ -166,7 +160,7 @@ export const convertV1History = (oldHistory: V1HistoryEntry[]): HistoryEntryWith */ export const refreshHistoryState = async (): Promise => { const localEntries = loadLocalHistory() - if (!getGlobalState().user) { + if (!store.getState().user) { setHistoryEntries(localEntries) return } @@ -179,7 +173,7 @@ export const refreshHistoryState = async (): Promise => { * Stores the history entries marked as local from the redux to the user's local-storage. */ export const storeLocalHistory = (): void => { - const history = getGlobalState().history + const history = store.getState().history const localEntries = history.filter((entry) => entry.origin === HistoryEntryOrigin.LOCAL) const entriesWithoutOrigin = localEntries.map((entry) => ({ ...entry, @@ -196,10 +190,10 @@ export const storeLocalHistory = (): void => { * Stores the history entries marked as remote from the redux to the server. */ export const storeRemoteHistory = (): Promise => { - if (!getGlobalState().user) { + if (!store.getState().user) { return Promise.resolve() } - const history = getGlobalState().history + const history = store.getState().history const remoteEntries = history.filter((entry) => entry.origin === HistoryEntryOrigin.REMOTE) const remoteEntryDtos = remoteEntries.map(historyEntryToHistoryEntryPutDto) return setRemoteHistoryEntries(remoteEntryDtos) diff --git a/frontend/src/redux/history/reducers.ts b/frontend/src/redux/history/reducers.ts deleted file mode 100644 index 4a13b764d..000000000 --- a/frontend/src/redux/history/reducers.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { HistoryEntryWithOrigin } from '../../api/history/types' -import type { HistoryActions } from './types' -import { HistoryActionType } from './types' -import type { Reducer } from 'redux' - -// Q: Why is the reducer initialized with an empty array instead of the actual history entries like in the config reducer? -// A: The history reducer will be created without entries because of async entry retrieval. -// Entries will be added after reducer initialization. - -export const HistoryReducer: Reducer = ( - state: HistoryEntryWithOrigin[] = [], - action: HistoryActions -) => { - switch (action.type) { - case HistoryActionType.SET_ENTRIES: - return action.entries - case HistoryActionType.UPDATE_ENTRY: - return [...state.filter((entry) => entry.identifier !== action.noteId), action.newEntry] - case HistoryActionType.REMOVE_ENTRY: - return state.filter((entry) => entry.identifier !== action.noteId) - default: - return state - } -} diff --git a/frontend/src/redux/history/slice.ts b/frontend/src/redux/history/slice.ts new file mode 100644 index 000000000..b96c2d4d8 --- /dev/null +++ b/frontend/src/redux/history/slice.ts @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' +import { initialState } from './initial-state' +import type { HistoryState, RemoveEntryPayload, UpdateEntryPayload } from './types' +import type { HistoryEntryWithOrigin } from '../../api/history/types' + +const historySlice = createSlice({ + name: 'history', + initialState, + reducers: { + setEntries: (state, action: PayloadAction) => { + return action.payload + }, + updateEntry: (state, action: PayloadAction) => { + const entryToUpdateIndex = state.findIndex((entry) => entry.identifier === action.payload.noteId) + if (entryToUpdateIndex < 0) { + return state + } + const updatedEntry: HistoryEntryWithOrigin = { ...state[entryToUpdateIndex], ...action.payload.newEntry } + return state.toSpliced(entryToUpdateIndex, 1, updatedEntry) + }, + removeEntry: (state, action: PayloadAction) => { + return state.filter((entry) => entry.identifier !== action.payload.noteId) + } + } +}) + +export const historyActionsCreator = historySlice.actions +export const historyReducer = historySlice.reducer diff --git a/frontend/src/redux/history/types.ts b/frontend/src/redux/history/types.ts index b70ae193b..bada8afb0 100644 --- a/frontend/src/redux/history/types.ts +++ b/frontend/src/redux/history/types.ts @@ -3,8 +3,9 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import type { HistoryEntryWithOrigin } from '../../api/history/types' -import type { Action } from 'redux' +import type { HistoryEntry, HistoryEntryWithOrigin } from '../../api/history/types' + +export type HistoryState = HistoryEntryWithOrigin[] export interface V1HistoryEntry { id: string @@ -19,32 +20,11 @@ export interface HistoryExportJson { entries: HistoryEntryWithOrigin[] } -export enum HistoryActionType { - SET_ENTRIES = 'SET_ENTRIES', - ADD_ENTRY = 'ADD_ENTRY', - UPDATE_ENTRY = 'UPDATE_ENTRY', - REMOVE_ENTRY = 'REMOVE_ENTRY' -} - -export type HistoryActions = SetEntriesAction | AddEntryAction | UpdateEntryAction | RemoveEntryAction - -export interface SetEntriesAction extends Action { - type: HistoryActionType.SET_ENTRIES - entries: HistoryEntryWithOrigin[] -} - -export interface AddEntryAction extends Action { - type: HistoryActionType.ADD_ENTRY - newEntry: HistoryEntryWithOrigin -} - -export interface UpdateEntryAction extends Action { - type: HistoryActionType.UPDATE_ENTRY +export interface UpdateEntryPayload { noteId: string - newEntry: HistoryEntryWithOrigin + newEntry: HistoryEntry } -export interface RemoveEntryAction extends Action { - type: HistoryActionType.REMOVE_ENTRY +export interface RemoveEntryPayload { noteId: string } diff --git a/frontend/src/redux/index.ts b/frontend/src/redux/index.ts index 34632841e..ad9bed194 100644 --- a/frontend/src/redux/index.ts +++ b/frontend/src/redux/index.ts @@ -4,13 +4,28 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { isDevMode } from '../utils/test-modes' -import type { ApplicationState } from './application-state' -import { allReducers } from './reducers' import { configureStore } from '@reduxjs/toolkit' +import { darkModeReducer } from './dark-mode/slice' +import { editorConfigReducer } from './editor-config/slice' +import { userReducer } from './user/slice' +import { rendererStatusReducer } from './renderer-status/slice' +import { realtimeStatusReducer } from './realtime/slice' +import { historyReducer } from './history/slice' +import { noteDetailsReducer } from './note-details/slice' export const store = configureStore({ - reducer: allReducers, + reducer: { + darkMode: darkModeReducer, + editorConfig: editorConfigReducer, + user: userReducer, + rendererStatus: rendererStatusReducer, + realtimeStatus: realtimeStatusReducer, + history: historyReducer, + noteDetails: noteDetailsReducer + }, devTools: isDevMode }) +export type ApplicationState = ReturnType + export const getGlobalState = (): ApplicationState => store.getState() diff --git a/frontend/src/redux/note-details/build-state-from-updated-markdown-content.ts b/frontend/src/redux/note-details/build-state-from-updated-markdown-content.ts index 1ce7acb0a..56fb0acde 100644 --- a/frontend/src/redux/note-details/build-state-from-updated-markdown-content.ts +++ b/frontend/src/redux/note-details/build-state-from-updated-markdown-content.ts @@ -5,7 +5,7 @@ */ import { calculateLineStartIndexes } from './calculate-line-start-indexes' import { initialState } from './initial-state' -import type { NoteDetails } from './types/note-details' +import type { NoteDetails } from './types' import type { FrontmatterExtractionResult, NoteFrontmatter } from '@hedgedoc/commons' import { convertRawFrontmatterToNoteFrontmatter, diff --git a/frontend/src/redux/note-details/initial-state.ts b/frontend/src/redux/note-details/initial-state.ts index e65bb3c4c..e4aa751fc 100644 --- a/frontend/src/redux/note-details/initial-state.ts +++ b/frontend/src/redux/note-details/initial-state.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import type { NoteDetails } from './types/note-details' +import type { NoteDetails } from './types' import { defaultNoteFrontmatter } from '@hedgedoc/commons' export const initialState: NoteDetails = { diff --git a/frontend/src/redux/note-details/methods.ts b/frontend/src/redux/note-details/methods.ts index 52113656a..0a3bde0c3 100644 --- a/frontend/src/redux/note-details/methods.ts +++ b/frontend/src/redux/note-details/methods.ts @@ -7,26 +7,16 @@ import { store } from '..' import { getNoteMetadata } from '../../api/notes' import type { Note } from '../../api/notes/types' import type { CursorSelection } from '../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection' -import type { - SetNoteDetailsFromServerAction, - SetNoteDocumentContentAction, - SetNotePermissionsFromServerAction, - UpdateCursorPositionAction, - UpdateMetadataAction, - UpdateNoteTitleByFirstHeadingAction -} from './types' -import { NoteDetailsActionType } from './types' import type { NotePermissions } from '@hedgedoc/commons' +import { noteDetailsActionsCreator } from './slice' /** * Sets the content of the current note, extracts and parses the frontmatter and extracts the markdown content part. * @param content The note content as it is written inside the editor pane. */ export const setNoteContent = (content: string): void => { - store.dispatch({ - type: NoteDetailsActionType.SET_DOCUMENT_CONTENT, - content: content - } as SetNoteDocumentContentAction) + const action = noteDetailsActionsCreator.setNoteContent(content) + store.dispatch(action) } /** @@ -34,10 +24,8 @@ export const setNoteContent = (content: string): void => { * @param apiResponse The NoteDTO received from the API to store into redux. */ export const setNoteDataFromServer = (apiResponse: Note): void => { - store.dispatch({ - type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER, - noteFromServer: apiResponse - } as SetNoteDetailsFromServerAction) + const action = noteDetailsActionsCreator.setNoteDataFromServer(apiResponse) + store.dispatch(action) } /** @@ -45,10 +33,8 @@ export const setNoteDataFromServer = (apiResponse: Note): void => { * @param apiResponse The NotePermissionsDTO received from the API to store into redux. */ export const setNotePermissionsFromServer = (apiResponse: NotePermissions): void => { - store.dispatch({ - type: NoteDetailsActionType.SET_NOTE_PERMISSIONS_FROM_SERVER, - notePermissionsFromServer: apiResponse - } as SetNotePermissionsFromServerAction) + const action = noteDetailsActionsCreator.setNotePermissionsFromServer(apiResponse) + store.dispatch(action) } /** @@ -56,17 +42,13 @@ export const setNotePermissionsFromServer = (apiResponse: NotePermissions): void * @param firstHeading The content of the first heading found in the markdown content. */ export const updateNoteTitleByFirstHeading = (firstHeading?: string): void => { - store.dispatch({ - type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING, - firstHeading: firstHeading - } as UpdateNoteTitleByFirstHeadingAction) + const action = noteDetailsActionsCreator.updateNoteTitleByFirstHeading(firstHeading) + store.dispatch(action) } export const updateCursorPositions = (selection: CursorSelection): void => { - store.dispatch({ - type: NoteDetailsActionType.UPDATE_CURSOR_POSITION, - selection - } as UpdateCursorPositionAction) + const action = noteDetailsActionsCreator.updateCursorPosition(selection) + store.dispatch(action) } /** @@ -78,14 +60,11 @@ export const updateMetadata = async (): Promise => { return } const updatedMetadata = await getNoteMetadata(noteDetails.id) - store.dispatch({ - type: NoteDetailsActionType.UPDATE_METADATA, - updatedMetadata - } as UpdateMetadataAction) + const action = noteDetailsActionsCreator.updateMetadata(updatedMetadata) + store.dispatch(action) } export const unloadNote = (): void => { - store.dispatch({ - type: NoteDetailsActionType.UNLOAD_NOTE - }) + const action = noteDetailsActionsCreator.unloadNote() + store.dispatch(action) } diff --git a/frontend/src/redux/note-details/reducer.ts b/frontend/src/redux/note-details/reducer.ts deleted file mode 100644 index 3463e808a..000000000 --- a/frontend/src/redux/note-details/reducer.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { buildStateFromUpdatedMarkdownContent } from './build-state-from-updated-markdown-content' -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' -import { buildStateFromServerDto } from './reducers/build-state-from-set-note-data-from-server' -import { buildStateFromUpdateCursorPosition } from './reducers/build-state-from-update-cursor-position' -import type { NoteDetailsActions } from './types' -import { NoteDetailsActionType } from './types' -import type { OptionalNoteDetails } from './types/note-details' -import type { Reducer } from 'redux' - -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) - case NoteDetailsActionType.SET_DOCUMENT_CONTENT: - return buildStateFromUpdatedMarkdownContent(state, action.content) - case NoteDetailsActionType.SET_NOTE_PERMISSIONS_FROM_SERVER: - return buildStateFromServerPermissions(state, action.notePermissionsFromServer) - case NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING: - return buildStateFromFirstHeadingUpdate(state, action.firstHeading) - 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/reducers/build-state-from-first-heading-update.ts b/frontend/src/redux/note-details/reducers/build-state-from-first-heading-update.ts index 95aa97ab2..b7b17311e 100644 --- a/frontend/src/redux/note-details/reducers/build-state-from-first-heading-update.ts +++ b/frontend/src/redux/note-details/reducers/build-state-from-first-heading-update.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import type { NoteDetails } from '../types/note-details' +import type { NoteDetails } from '../types' import { generateNoteTitle } from '@hedgedoc/commons' /** diff --git a/frontend/src/redux/note-details/reducers/build-state-from-metadata-update.spec.ts b/frontend/src/redux/note-details/reducers/build-state-from-metadata-update.spec.ts index 98fea9e5d..f92f74424 100644 --- a/frontend/src/redux/note-details/reducers/build-state-from-metadata-update.spec.ts +++ b/frontend/src/redux/note-details/reducers/build-state-from-metadata-update.spec.ts @@ -5,7 +5,7 @@ */ import type { NoteMetadata } from '../../../api/notes/types' import { initialState } from '../initial-state' -import type { NoteDetails } from '../types/note-details' +import type { NoteDetails } from '../types' import { buildStateFromMetadataUpdate } from './build-state-from-metadata-update' describe('build state from server permissions', () => { diff --git a/frontend/src/redux/note-details/reducers/build-state-from-metadata-update.ts b/frontend/src/redux/note-details/reducers/build-state-from-metadata-update.ts index 6378e258d..a571e0759 100644 --- a/frontend/src/redux/note-details/reducers/build-state-from-metadata-update.ts +++ b/frontend/src/redux/note-details/reducers/build-state-from-metadata-update.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import type { NoteMetadata } from '../../../api/notes/types' -import type { NoteDetails } from '../types/note-details' +import type { NoteDetails } from '../types' import { DateTime } from 'luxon' /** diff --git a/frontend/src/redux/note-details/reducers/build-state-from-server-permissions.spec.ts b/frontend/src/redux/note-details/reducers/build-state-from-server-permissions.spec.ts index 88c4ec7e8..fc510ae59 100644 --- a/frontend/src/redux/note-details/reducers/build-state-from-server-permissions.spec.ts +++ b/frontend/src/redux/note-details/reducers/build-state-from-server-permissions.spec.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { initialState } from '../initial-state' -import type { NoteDetails } from '../types/note-details' +import type { NoteDetails } from '../types' import { buildStateFromServerPermissions } from './build-state-from-server-permissions' import type { NotePermissions } from '@hedgedoc/commons' diff --git a/frontend/src/redux/note-details/reducers/build-state-from-server-permissions.ts b/frontend/src/redux/note-details/reducers/build-state-from-server-permissions.ts index 7281f0f2f..bbf98895b 100644 --- a/frontend/src/redux/note-details/reducers/build-state-from-server-permissions.ts +++ b/frontend/src/redux/note-details/reducers/build-state-from-server-permissions.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import type { NoteDetails } from '../types/note-details' +import type { NoteDetails } from '../types' import type { NotePermissions } from '@hedgedoc/commons' /** diff --git a/frontend/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.spec.ts b/frontend/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.spec.ts index 70ff50c4b..847802ceb 100644 --- a/frontend/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.spec.ts +++ b/frontend/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.spec.ts @@ -5,7 +5,7 @@ */ import type { Note } from '../../../api/notes/types' import * as buildStateFromUpdatedMarkdownContentModule from '../build-state-from-updated-markdown-content' -import type { NoteDetails } from '../types/note-details' +import type { NoteDetails } from '../types' import { buildStateFromServerDto } from './build-state-from-set-note-data-from-server' import { NoteTextDirection, NoteType } from '@hedgedoc/commons' import { DateTime } from 'luxon' diff --git a/frontend/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.ts b/frontend/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.ts index 620ff9de3..543637d09 100644 --- a/frontend/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.ts +++ b/frontend/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.ts @@ -7,7 +7,7 @@ import type { Note } from '../../../api/notes/types' import { buildStateFromUpdatedMarkdownContent } from '../build-state-from-updated-markdown-content' import { calculateLineStartIndexes } from '../calculate-line-start-indexes' import { initialState } from '../initial-state' -import type { NoteDetails } from '../types/note-details' +import type { NoteDetails } from '../types' import { buildStateFromMetadataUpdate } from './build-state-from-metadata-update' /** diff --git a/frontend/src/redux/note-details/reducers/build-state-from-task-list-update.spec.ts b/frontend/src/redux/note-details/reducers/build-state-from-task-list-update.spec.ts index d07ec1db3..88311c020 100644 --- a/frontend/src/redux/note-details/reducers/build-state-from-task-list-update.spec.ts +++ b/frontend/src/redux/note-details/reducers/build-state-from-task-list-update.spec.ts @@ -5,7 +5,7 @@ */ import * as buildStateFromUpdatedMarkdownContentLinesModule from '../build-state-from-updated-markdown-content' import { initialState } from '../initial-state' -import type { NoteDetails } from '../types/note-details' +import type { NoteDetails } from '../types' import { buildStateFromTaskListUpdate } from './build-state-from-task-list-update' import { Mock } from 'ts-mockery' diff --git a/frontend/src/redux/note-details/reducers/build-state-from-task-list-update.ts b/frontend/src/redux/note-details/reducers/build-state-from-task-list-update.ts index 02467dd86..1968a46ac 100644 --- a/frontend/src/redux/note-details/reducers/build-state-from-task-list-update.ts +++ b/frontend/src/redux/note-details/reducers/build-state-from-task-list-update.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { buildStateFromUpdatedMarkdownContentLines } from '../build-state-from-updated-markdown-content' -import type { NoteDetails } from '../types/note-details' +import type { NoteDetails } from '../types' import { Optional } from '@mrdrogdrog/optional' const TASK_REGEX = /(\s*(?:[-*+]|\d+[.)]) )\[[ xX]?]( .*)/ diff --git a/frontend/src/redux/note-details/reducers/build-state-from-update-cursor-position.ts b/frontend/src/redux/note-details/reducers/build-state-from-update-cursor-position.ts index ec7aad164..7ae460668 100644 --- a/frontend/src/redux/note-details/reducers/build-state-from-update-cursor-position.ts +++ b/frontend/src/redux/note-details/reducers/build-state-from-update-cursor-position.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import type { CursorSelection } from '../../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection' -import type { NoteDetails } from '../types/note-details' +import type { NoteDetails } from '../types' export const buildStateFromUpdateCursorPosition = (state: NoteDetails, selection: CursorSelection): NoteDetails => { const correctedSelection = isFromAfterTo(selection) diff --git a/frontend/src/redux/note-details/slice.ts b/frontend/src/redux/note-details/slice.ts new file mode 100644 index 000000000..3d9857f04 --- /dev/null +++ b/frontend/src/redux/note-details/slice.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' +import { initialState } from './initial-state' +import { buildStateFromServerDto } from './reducers/build-state-from-set-note-data-from-server' +import type { Note, NoteMetadata } from '../../api/notes/types' +import { buildStateFromUpdatedMarkdownContent } from './build-state-from-updated-markdown-content' +import type { NotePermissions } from '@hedgedoc/commons/dist/esm' +import { buildStateFromServerPermissions } from './reducers/build-state-from-server-permissions' +import { buildStateFromFirstHeadingUpdate } from './reducers/build-state-from-first-heading-update' +import { buildStateFromMetadataUpdate } from './reducers/build-state-from-metadata-update' +import type { CursorSelection } from '../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection' +import { buildStateFromUpdateCursorPosition } from './reducers/build-state-from-update-cursor-position' + +const noteDetailsSlice = createSlice({ + name: 'noteDetails', + initialState, + reducers: { + setNoteDataFromServer(_, action: PayloadAction) { + return buildStateFromServerDto(action.payload) + }, + setNoteContent(state, action: PayloadAction) { + return buildStateFromUpdatedMarkdownContent(state, action.payload) + }, + setNotePermissionsFromServer(state, action: PayloadAction) { + return buildStateFromServerPermissions(state, action.payload) + }, + updateNoteTitleByFirstHeading(state, action: PayloadAction) { + return buildStateFromFirstHeadingUpdate(state, action.payload) + }, + updateMetadata(state, action: PayloadAction) { + return buildStateFromMetadataUpdate(state, action.payload) + }, + updateCursorPosition(state, action: PayloadAction) { + return buildStateFromUpdateCursorPosition(state, action.payload) + }, + unloadNote() { + return initialState + } + } +}) + +export const noteDetailsActionsCreator = noteDetailsSlice.actions +export const noteDetailsReducer = noteDetailsSlice.reducer diff --git a/frontend/src/redux/note-details/types.ts b/frontend/src/redux/note-details/types.ts index e92b4f756..08f8f0f9d 100644 --- a/frontend/src/redux/note-details/types.ts +++ b/frontend/src/redux/note-details/types.ts @@ -3,75 +3,26 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Note, NoteMetadata } from '../../api/notes/types' +import type { NoteMetadata } from '../../api/notes/types' import type { CursorSelection } from '../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection' -import type { NotePermissions } from '@hedgedoc/commons' -import type { Action } from 'redux' +import type { NoteFrontmatter } from '@hedgedoc/commons' -export enum NoteDetailsActionType { - SET_DOCUMENT_CONTENT = 'note-details/content/set', - SET_NOTE_DATA_FROM_SERVER = 'note-details/data/server/set', - SET_NOTE_PERMISSIONS_FROM_SERVER = 'note-details/data/permissions/set', - UPDATE_NOTE_TITLE_BY_FIRST_HEADING = 'note-details/update-note-title-by-first-heading', - UPDATE_CURSOR_POSITION = 'note-details/updateCursorPosition', - UPDATE_METADATA = 'note-details/update-metadata', - UNLOAD_NOTE = 'note-details/unload-note' -} - -export type NoteDetailsActions = - | SetNoteDocumentContentAction - | SetNoteDetailsFromServerAction - | SetNotePermissionsFromServerAction - | UpdateNoteTitleByFirstHeadingAction - | UpdateCursorPositionAction - | UpdateMetadataAction - | UnloadNoteAction +type UnnecessaryNoteAttributes = 'updatedAt' | 'createdAt' | 'tags' | 'description' /** - * Action for updating the document content of the currently loaded note. + * Redux state containing the currently loaded note with its content and metadata. */ -export interface SetNoteDocumentContentAction extends Action { - type: NoteDetailsActionType.SET_DOCUMENT_CONTENT - content: string -} - -/** - * Action for overwriting the current state with the data received from the API. - */ -export interface SetNoteDetailsFromServerAction extends Action { - type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER - noteFromServer: Note -} - -/** - * Action for overwriting the current permission state with the data received from the API. - */ -export interface SetNotePermissionsFromServerAction extends Action { - type: NoteDetailsActionType.SET_NOTE_PERMISSIONS_FROM_SERVER - notePermissionsFromServer: NotePermissions -} - -/** - * Action for updating the note title of the currently loaded note by using frontmatter data or the first heading. - */ -export interface UpdateNoteTitleByFirstHeadingAction extends Action { - type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING - firstHeading?: string -} - -export interface UpdateCursorPositionAction extends Action { - type: NoteDetailsActionType.UPDATE_CURSOR_POSITION +export interface NoteDetails extends Omit { + updatedAt: number + createdAt: number + markdownContent: { + plain: string + lines: string[] + lineStartIndexes: number[] + } selection: CursorSelection -} - -/** - * Action for updating the metadata of the current note. - */ -export interface UpdateMetadataAction extends Action { - type: NoteDetailsActionType.UPDATE_METADATA - updatedMetadata: NoteMetadata -} - -export interface UnloadNoteAction extends Action { - type: NoteDetailsActionType.UNLOAD_NOTE + firstHeading?: string + rawFrontmatter: string + frontmatter: NoteFrontmatter + startOfContentLineOffset: number } diff --git a/frontend/src/redux/note-details/types/note-details.ts b/frontend/src/redux/note-details/types/note-details.ts deleted file mode 100644 index 9a4f19cad..000000000 --- a/frontend/src/redux/note-details/types/note-details.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { NoteMetadata } from '../../../api/notes/types' -import type { CursorSelection } from '../../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection' -import type { NoteFrontmatter } from '@hedgedoc/commons' - -type UnnecessaryNoteAttributes = 'updatedAt' | 'createdAt' | 'tags' | 'description' - -/** - * Redux state containing the currently loaded note with its content and metadata. - */ -export interface NoteDetails extends Omit { - updatedAt: number - createdAt: number - markdownContent: { - plain: string - lines: string[] - lineStartIndexes: number[] - } - selection: CursorSelection - firstHeading?: string - rawFrontmatter: string - frontmatter: NoteFrontmatter - startOfContentLineOffset: number -} - -export type OptionalNoteDetails = NoteDetails | null diff --git a/frontend/src/redux/realtime/initial-state.ts b/frontend/src/redux/realtime/initial-state.ts new file mode 100644 index 000000000..c0f38ac8f --- /dev/null +++ b/frontend/src/redux/realtime/initial-state.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { RealtimeStatus } from './types' + +export const initialState: RealtimeStatus = { + isSynced: false, + isConnected: false, + onlineUsers: [], + ownUser: { + displayName: '', + styleIndex: 0 + } +} diff --git a/frontend/src/redux/realtime/methods.ts b/frontend/src/redux/realtime/methods.ts index c35110153..91312b742 100644 --- a/frontend/src/redux/realtime/methods.ts +++ b/frontend/src/redux/realtime/methods.ts @@ -4,41 +4,34 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { store } from '..' -import type { SetRealtimeConnectionStatusAction, SetRealtimeSyncStatusAction, SetRealtimeUsersAction } from './types' -import { RealtimeStatusActionType } from './types' import type { RealtimeUser } from '@hedgedoc/commons' +import { realtimeStatusActionsCreator } from './slice' /** * Dispatches an event to add a user */ export const setRealtimeUsers = (users: RealtimeUser[], ownStyleIndex: number, ownDisplayName: string): void => { - const action: SetRealtimeUsersAction = { - type: RealtimeStatusActionType.SET_REALTIME_USERS, + const action = realtimeStatusActionsCreator.setRealtimeUsers({ users, ownUser: { styleIndex: ownStyleIndex, displayName: ownDisplayName } - } + }) store.dispatch(action) } export const setRealtimeConnectionState = (status: boolean): void => { - store.dispatch({ - type: RealtimeStatusActionType.SET_REALTIME_CONNECTION_STATUS, - isConnected: status - } as SetRealtimeConnectionStatusAction) + const action = realtimeStatusActionsCreator.setRealtimeConnectionStatus(status) + store.dispatch(action) } export const setRealtimeSyncedState = (status: boolean): void => { - store.dispatch({ - type: RealtimeStatusActionType.SET_REALTIME_SYNCED_STATUS, - isSynced: status - } as SetRealtimeSyncStatusAction) + const action = realtimeStatusActionsCreator.setRealtimeSyncStatus(status) + store.dispatch(action) } export const resetRealtimeStatus = (): void => { - store.dispatch({ - type: RealtimeStatusActionType.RESET_REALTIME_STATUS - }) + const action = realtimeStatusActionsCreator.resetRealtimeStatus() + store.dispatch(action) } diff --git a/frontend/src/redux/realtime/reducers.ts b/frontend/src/redux/realtime/reducers.ts deleted file mode 100644 index 90eedc7e1..000000000 --- a/frontend/src/redux/realtime/reducers.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { RealtimeStatus, RealtimeStatusActions } from './types' -import { RealtimeStatusActionType } from './types' -import type { Reducer } from 'redux' - -export const initialState: RealtimeStatus = { - isSynced: false, - isConnected: false, - onlineUsers: [], - ownUser: { - displayName: '', - styleIndex: 0 - } -} - -/** - * Applies {@link RealtimeStatusReducer realtime actions} to the global application state. - * - * @param state the current state - * @param action the action that should get applied - * @return The new changed state - */ -export const RealtimeStatusReducer: Reducer = ( - state = initialState, - action: RealtimeStatusActions -) => { - switch (action.type) { - case RealtimeStatusActionType.SET_REALTIME_USERS: - return { - ...state, - onlineUsers: action.users, - ownUser: action.ownUser - } - case RealtimeStatusActionType.SET_REALTIME_CONNECTION_STATUS: - return { - ...state, - isConnected: action.isConnected - } - case RealtimeStatusActionType.SET_REALTIME_SYNCED_STATUS: - return { - ...state, - isSynced: action.isSynced - } - case RealtimeStatusActionType.RESET_REALTIME_STATUS: - return initialState - default: - return state - } -} diff --git a/frontend/src/redux/realtime/slice.ts b/frontend/src/redux/realtime/slice.ts new file mode 100644 index 000000000..3ba2cad00 --- /dev/null +++ b/frontend/src/redux/realtime/slice.ts @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' +import { initialState } from './initial-state' +import type { RealtimeStatus, SetRealtimeUsersPayload } from './types' + +const realtimeStatusSlice = createSlice({ + name: 'realtimeStatus', + initialState, + reducers: { + setRealtimeUsers(state, action: PayloadAction) { + state.onlineUsers = action.payload.users + state.ownUser = action.payload.ownUser + }, + setRealtimeConnectionStatus(state, action: PayloadAction) { + state.isConnected = action.payload + }, + setRealtimeSyncStatus(state, action: PayloadAction) { + state.isSynced = action.payload + }, + resetRealtimeStatus() { + return initialState + } + } +}) + +export const realtimeStatusActionsCreator = realtimeStatusSlice.actions +export const realtimeStatusReducer = realtimeStatusSlice.reducer diff --git a/frontend/src/redux/realtime/types.ts b/frontend/src/redux/realtime/types.ts index 931decebf..987a951e0 100644 --- a/frontend/src/redux/realtime/types.ts +++ b/frontend/src/redux/realtime/types.ts @@ -4,17 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import type { RealtimeUser } from '@hedgedoc/commons' -import type { Action } from 'redux' -export enum RealtimeStatusActionType { - SET_REALTIME_USERS = 'realtime/set-users', - SET_REALTIME_CONNECTION_STATUS = 'realtime/set-connection-status', - SET_REALTIME_SYNCED_STATUS = 'realtime/set-synced-status', - RESET_REALTIME_STATUS = 'realtime/reset-realtime-status' -} - -export interface SetRealtimeUsersAction extends Action { - type: RealtimeStatusActionType.SET_REALTIME_USERS +export interface SetRealtimeUsersPayload { users: RealtimeUser[] ownUser: { styleIndex: number @@ -22,20 +13,6 @@ export interface SetRealtimeUsersAction extends Action } } -export interface SetRealtimeConnectionStatusAction extends Action { - type: RealtimeStatusActionType.SET_REALTIME_CONNECTION_STATUS - isConnected: boolean -} - -export interface SetRealtimeSyncStatusAction extends Action { - type: RealtimeStatusActionType.SET_REALTIME_SYNCED_STATUS - isSynced: boolean -} - -export interface ResetRealtimeStatusAction extends Action { - type: RealtimeStatusActionType.RESET_REALTIME_STATUS -} - export interface RealtimeStatus { onlineUsers: RealtimeUser[] isConnected: boolean @@ -45,9 +22,3 @@ export interface RealtimeStatus { styleIndex: number } } - -export type RealtimeStatusActions = - | SetRealtimeUsersAction - | SetRealtimeConnectionStatusAction - | SetRealtimeSyncStatusAction - | ResetRealtimeStatusAction diff --git a/frontend/src/redux/reducers.ts b/frontend/src/redux/reducers.ts deleted file mode 100644 index 38a00e5c0..000000000 --- a/frontend/src/redux/reducers.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { ApplicationState } from './application-state' -import { DarkModeConfigReducer } from './dark-mode/reducers' -import { EditorConfigReducer } from './editor/reducers' -import { HistoryReducer } from './history/reducers' -import { NoteDetailsReducer } from './note-details/reducer' -import { RealtimeStatusReducer } from './realtime/reducers' -import { RendererStatusReducer } from './renderer-status/reducers' -import { UserReducer } from './user/reducers' -import type { Reducer } from 'redux' -import { combineReducers } from 'redux' - -export const allReducers: Reducer = combineReducers({ - user: UserReducer, - history: HistoryReducer, - editorConfig: EditorConfigReducer, - darkMode: DarkModeConfigReducer, - noteDetails: NoteDetailsReducer, - rendererStatus: RendererStatusReducer, - realtimeStatus: RealtimeStatusReducer -}) diff --git a/frontend/src/redux/renderer-status/initial-state.ts b/frontend/src/redux/renderer-status/initial-state.ts new file mode 100644 index 000000000..3db6fcfdb --- /dev/null +++ b/frontend/src/redux/renderer-status/initial-state.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { RendererStatus } from './types' + +export const initialState: RendererStatus = { + rendererReady: false +} diff --git a/frontend/src/redux/renderer-status/methods.ts b/frontend/src/redux/renderer-status/methods.ts index a287631cd..b7b1472f7 100644 --- a/frontend/src/redux/renderer-status/methods.ts +++ b/frontend/src/redux/renderer-status/methods.ts @@ -4,8 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { store } from '..' -import type { SetRendererStatusAction } from './types' -import { RendererStatusActionType } from './types' +import { rendererStatusActionsCreator } from './slice' /** * Dispatches a global application state change for the "renderer ready" state. @@ -13,9 +12,6 @@ import { RendererStatusActionType } from './types' * @param rendererReady The new renderer ready state. */ export const setRendererStatus = (rendererReady: boolean): void => { - const action: SetRendererStatusAction = { - type: RendererStatusActionType.SET_RENDERER_STATUS, - rendererReady - } + const action = rendererStatusActionsCreator.setRendererStatus(rendererReady) store.dispatch(action) } diff --git a/frontend/src/redux/renderer-status/reducers.ts b/frontend/src/redux/renderer-status/reducers.ts deleted file mode 100644 index 8086d8746..000000000 --- a/frontend/src/redux/renderer-status/reducers.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { RendererStatus, RendererStatusActions } from './types' -import { RendererStatusActionType } from './types' -import type { Reducer } from 'redux' - -export const initialState: RendererStatus = { - rendererReady: false -} - -/** - * Applies {@link RendererStatusActions renderer status actions} to the global application state. - * - * @param state the current state - * @param action the action that should get applied - * @return The new changed state - */ -export const RendererStatusReducer: Reducer = ( - state: RendererStatus = initialState, - action: RendererStatusActions -) => { - switch (action.type) { - case RendererStatusActionType.SET_RENDERER_STATUS: - return { - ...state, - rendererReady: action.rendererReady - } - default: - return state - } -} diff --git a/frontend/src/redux/renderer-status/slice.ts b/frontend/src/redux/renderer-status/slice.ts new file mode 100644 index 000000000..9ef2b98c5 --- /dev/null +++ b/frontend/src/redux/renderer-status/slice.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { initialState } from './initial-state' +import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' +import type { RendererStatus } from './types' + +const rendererStatusSlice = createSlice({ + name: 'rendererStatus', + initialState, + reducers: { + setRendererStatus: (state, action: PayloadAction) => { + state.rendererReady = action.payload + } + } +}) + +export const rendererStatusActionsCreator = rendererStatusSlice.actions +export const rendererStatusReducer = rendererStatusSlice.reducer diff --git a/frontend/src/redux/renderer-status/types.ts b/frontend/src/redux/renderer-status/types.ts index a3b5825b5..842a07f1d 100644 --- a/frontend/src/redux/renderer-status/types.ts +++ b/frontend/src/redux/renderer-status/types.ts @@ -3,19 +3,6 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Action } from 'redux' - -export enum RendererStatusActionType { - SET_RENDERER_STATUS = 'renderer-status/set-ready' -} - export interface RendererStatus { rendererReady: boolean } - -export interface SetRendererStatusAction extends Action { - type: RendererStatusActionType.SET_RENDERER_STATUS - rendererReady: boolean -} - -export type RendererStatusActions = SetRendererStatusAction diff --git a/frontend/src/redux/user/initial-state.ts b/frontend/src/redux/user/initial-state.ts new file mode 100644 index 000000000..c61e3ff7d --- /dev/null +++ b/frontend/src/redux/user/initial-state.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { UserState } from './types' + +export const initialState: UserState = null diff --git a/frontend/src/redux/user/methods.ts b/frontend/src/redux/user/methods.ts index 5ff29cefc..d8521ee8b 100644 --- a/frontend/src/redux/user/methods.ts +++ b/frontend/src/redux/user/methods.ts @@ -5,27 +5,21 @@ */ import { store } from '..' import type { LoginUserInfo } from '../../api/me/types' -import type { ClearUserAction, SetUserAction } from './types' -import { UserActionType } from './types' +import { userActionsCreator } from './slice' /** * Sets the given user state into the redux. * @param state The user state to set into the redux. */ export const setUser = (state: LoginUserInfo): void => { - const action: SetUserAction = { - type: UserActionType.SET_USER, - state - } + const action = userActionsCreator.setUser(state) store.dispatch(action) } /** * Clears the user state from the redux. */ -export const clearUser: () => void = () => { - const action: ClearUserAction = { - type: UserActionType.CLEAR_USER - } +export const clearUser = (): void => { + const action = userActionsCreator.setUser(null) store.dispatch(action) } diff --git a/frontend/src/redux/user/reducers.ts b/frontend/src/redux/user/reducers.ts deleted file mode 100644 index 27e91b89d..000000000 --- a/frontend/src/redux/user/reducers.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { OptionalUserState, UserActions } from './types' -import { UserActionType } from './types' -import type { Reducer } from 'redux' - -export const UserReducer: Reducer = ( - state: OptionalUserState = null, - action: UserActions -) => { - switch (action.type) { - case UserActionType.SET_USER: - return action.state - case UserActionType.CLEAR_USER: - return null - default: - return state - } -} diff --git a/frontend/src/redux/user/slice.ts b/frontend/src/redux/user/slice.ts new file mode 100644 index 000000000..73ade59d3 --- /dev/null +++ b/frontend/src/redux/user/slice.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' +import { initialState } from './initial-state' +import type { UserState } from './types' + +const userSlice = createSlice({ + name: 'user', + initialState, + reducers: { + setUser: (state, action: PayloadAction) => { + return action.payload + } + } +}) + +export const userActionsCreator = userSlice.actions +export const userReducer = userSlice.reducer diff --git a/frontend/src/redux/user/types.ts b/frontend/src/redux/user/types.ts index 129f17a3b..011e8d6f1 100644 --- a/frontend/src/redux/user/types.ts +++ b/frontend/src/redux/user/types.ts @@ -4,22 +4,5 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import type { LoginUserInfo } from '../../api/me/types' -import type { Action } from 'redux' -export enum UserActionType { - SET_USER = 'user/set', - CLEAR_USER = 'user/clear' -} - -export type UserActions = SetUserAction | ClearUserAction - -export interface SetUserAction extends Action { - type: UserActionType.SET_USER - state: LoginUserInfo -} - -export interface ClearUserAction extends Action { - type: UserActionType.CLEAR_USER -} - -export type OptionalUserState = LoginUserInfo | null +export type UserState = LoginUserInfo | null diff --git a/frontend/src/test-utils/mock-app-state.ts b/frontend/src/test-utils/mock-app-state.ts index 729c7afb8..70fb1a1ce 100644 --- a/frontend/src/test-utils/mock-app-state.ts +++ b/frontend/src/test-utils/mock-app-state.ts @@ -4,17 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import * as useApplicationStateModule from '../hooks/common/use-application-state' -import type { ApplicationState } from '../redux/application-state' -import { initialState as initialStateDarkMode } from '../redux/dark-mode/reducers' -import { initialState as initialStateEditorConfig } from '../redux/editor/reducers' +import type { ApplicationState } from '../redux' +import { initialState as initialStateDarkMode } from '../redux/dark-mode/initial-state' +import { initialState as initialStateEditorConfig } from '../redux/editor-config/initial-state' import { initialState as initialStateNoteDetails } from '../redux/note-details/initial-state' -import { initialState as initialStateRealtimeStatus } from '../redux/realtime/reducers' -import { initialState as initialStateRendererStatus } from '../redux/renderer-status/reducers' -import type { NoteDetails } from '../redux/note-details/types/note-details' +import { initialState as initialStateRealtimeStatus } from '../redux/realtime/initial-state' +import { initialState as initialStateRendererStatus } from '../redux/renderer-status/initial-state' +import type { NoteDetails } from '../redux/note-details/types' import type { RealtimeStatus } from '../redux/realtime/types' import type { DeepPartial } from '@hedgedoc/commons' -jest.mock('../redux/editor/methods', () => ({ +jest.mock('../redux/editor-config/methods', () => ({ loadFromLocalStorage: jest.fn().mockReturnValue(undefined) })) jest.mock('../hooks/common/use-application-state') diff --git a/frontend/src/test-utils/mock-note-permissions.ts b/frontend/src/test-utils/mock-note-permissions.ts index 4ca09453b..0aa8758a3 100644 --- a/frontend/src/test-utils/mock-note-permissions.ts +++ b/frontend/src/test-utils/mock-note-permissions.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import type { ApplicationState } from '../redux/application-state' +import type { ApplicationState } from '../redux' import { mockAppState } from './mock-app-state' import type { DeepPartial, NotePermissions } from '@hedgedoc/commons' diff --git a/frontend/src/utils/update-object.ts b/frontend/src/utils/update-object.ts new file mode 100644 index 000000000..5ea80f435 --- /dev/null +++ b/frontend/src/utils/update-object.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +export const updateObject = >(oldObject: T, newValues: T | null): void => { + if (typeof newValues !== 'object' || newValues === null) { + return + } + Object.keys(oldObject).forEach((key) => { + if (Object.prototype.hasOwnProperty.call(newValues, key) && typeof oldObject[key] === typeof newValues[key]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + oldObject[key] = newValues[key] + } + }) +}