refactor(redux): migrate to RTK2 store definition

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2024-02-11 02:53:23 +01:00
parent 8b501915f5
commit d840a6f0b1
66 changed files with 526 additions and 846 deletions

View file

@ -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')

View file

@ -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'

View file

@ -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'

View file

@ -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'
/**

View file

@ -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'

View file

@ -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'

View file

@ -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'

View file

@ -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'

View file

@ -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'

View file

@ -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'

View file

@ -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'

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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<DarkModeConfig, DarkModeConfigAction> = (
state: DarkModeConfig = initialState,
action: DarkModeConfigAction
) => {
switch (action.type) {
case DarkModeConfigActionType.SET_DARK_MODE:
return {
darkModePreference: action.darkModePreference
}
default:
return state
}
}

View file

@ -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<DarkModeConfig['darkModePreference']>) => {
state.darkModePreference = action.payload
}
}
})
export const darkModeActionsCreator = darkModeSlice.actions
export const darkModeReducer = darkModeSlice.reducer

View file

@ -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<DarkModeConfigActionType.SET_DARK_MODE> & DarkModeConfig

View file

@ -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
}

View file

@ -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<EditorConfig>) : 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)
}
}

View file

@ -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<EditorConfig['syncScroll']>) => {
state.syncScroll = action.payload
},
setLigatures: (state, action: PayloadAction<EditorConfig['ligatures']>) => {
state.ligatures = action.payload
},
setSmartPaste: (state, action: PayloadAction<EditorConfig['smartPaste']>) => {
state.smartPaste = action.payload
},
setSpellCheck: (state, action: PayloadAction<EditorConfig['spellCheck']>) => {
state.spellCheck = action.payload
},
setLineWrapping: (state, action: PayloadAction<EditorConfig['lineWrapping']>) => {
state.lineWrapping = action.payload
},
setIndentWithTabs: (state, action: PayloadAction<EditorConfig['indentWithTabs']>) => {
state.indentWithTabs = action.payload
},
setIndentSpaces: (state, action: PayloadAction<EditorConfig['indentSpaces']>) => {
state.indentSpaces = action.payload
},
setEditorConfig: (state, action: PayloadAction<EditorConfig>) => {
return action.payload
}
}
})
export const editorConfigActionsCreator = editorConfigSlice.actions
export const editorConfigReducer = editorConfigSlice.reducer

View file

@ -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
}

View file

@ -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)
}

View file

@ -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<EditorConfig, EditorConfigActions> = (
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<EditorConfig>
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)
}
}

View file

@ -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<EditorConfigActionType> {
type: EditorConfigActionType.LOAD_FROM_LOCAL_STORAGE
}
export interface SetEditorLineWrappingAction extends Action<EditorConfigActionType> {
type: EditorConfigActionType.SET_LINE_WRAPPING
lineWrapping: boolean
}
export interface SetEditorSyncScrollAction extends Action<EditorConfigActionType> {
type: EditorConfigActionType.SET_SYNC_SCROLL
syncScroll: boolean
}
export interface SetEditorLigaturesAction extends Action<EditorConfigActionType> {
type: EditorConfigActionType.SET_LIGATURES
ligatures: boolean
}
export interface SetEditorSmartPasteAction extends Action<EditorConfigActionType> {
type: EditorConfigActionType.SET_SMART_PASTE
smartPaste: boolean
}
export interface SetEditorSpellCheckAction extends Action<EditorConfigActionType> {
type: EditorConfigActionType.SET_SPELL_CHECK
spellCheck: boolean
}
export interface SetEditorIndentWithTabsAction extends Action<EditorConfigActionType> {
type: EditorConfigActionType.SET_INDENT_WITH_TABS
indentWithTabs: boolean
}
export interface SetEditorIndentSpacesAction extends Action<EditorConfigActionType> {
type: EditorConfigActionType.SET_INDENT_SPACES
indentSpaces: number
}

View file

@ -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 = []

View file

@ -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<unknown> => {
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<unknown> => {
* @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<void> => {
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<void> => {
* @param noteId The note id of the history entry to update.
*/
export const toggleHistoryEntryPinning = async (noteId: string): Promise<void> => {
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<void> =
* 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<HistoryEntryWithOrigin>) => {
delete entry.origin
})
@ -166,7 +160,7 @@ export const convertV1History = (oldHistory: V1HistoryEntry[]): HistoryEntryWith
*/
export const refreshHistoryState = async (): Promise<void> => {
const localEntries = loadLocalHistory()
if (!getGlobalState().user) {
if (!store.getState().user) {
setHistoryEntries(localEntries)
return
}
@ -179,7 +173,7 @@ export const refreshHistoryState = async (): Promise<void> => {
* 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<unknown> => {
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)

View file

@ -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<HistoryEntryWithOrigin[], HistoryActions> = (
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
}
}

View file

@ -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<HistoryState>) => {
return action.payload
},
updateEntry: (state, action: PayloadAction<UpdateEntryPayload>) => {
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<RemoveEntryPayload>) => {
return state.filter((entry) => entry.identifier !== action.payload.noteId)
}
}
})
export const historyActionsCreator = historySlice.actions
export const historyReducer = historySlice.reducer

View file

@ -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<HistoryActionType> {
type: HistoryActionType.SET_ENTRIES
entries: HistoryEntryWithOrigin[]
}
export interface AddEntryAction extends Action<HistoryActionType> {
type: HistoryActionType.ADD_ENTRY
newEntry: HistoryEntryWithOrigin
}
export interface UpdateEntryAction extends Action<HistoryActionType> {
type: HistoryActionType.UPDATE_ENTRY
export interface UpdateEntryPayload {
noteId: string
newEntry: HistoryEntryWithOrigin
newEntry: HistoryEntry
}
export interface RemoveEntryAction extends Action<HistoryActionType> {
type: HistoryActionType.REMOVE_ENTRY
export interface RemoveEntryPayload {
noteId: string
}

View file

@ -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<typeof store.getState>
export const getGlobalState = (): ApplicationState => store.getState()

View file

@ -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,

View file

@ -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 = {

View file

@ -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<void> => {
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)
}

View file

@ -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<OptionalNoteDetails, NoteDetailsActions> = (
state: OptionalNoteDetails = null,
action: NoteDetailsActions
) => {
if (action.type === NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER) {
return buildStateFromServerDto(action.noteFromServer)
}
if (state === null) {
return null
}
switch (action.type) {
case NoteDetailsActionType.UPDATE_CURSOR_POSITION:
return buildStateFromUpdateCursorPosition(state, action.selection)
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
}
}

View file

@ -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'
/**

View file

@ -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', () => {

View file

@ -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'
/**

View file

@ -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'

View file

@ -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'
/**

View file

@ -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'

View file

@ -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'
/**

View file

@ -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'

View file

@ -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]?]( .*)/

View file

@ -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)

View file

@ -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<Note>) {
return buildStateFromServerDto(action.payload)
},
setNoteContent(state, action: PayloadAction<string>) {
return buildStateFromUpdatedMarkdownContent(state, action.payload)
},
setNotePermissionsFromServer(state, action: PayloadAction<NotePermissions>) {
return buildStateFromServerPermissions(state, action.payload)
},
updateNoteTitleByFirstHeading(state, action: PayloadAction<string | undefined>) {
return buildStateFromFirstHeadingUpdate(state, action.payload)
},
updateMetadata(state, action: PayloadAction<NoteMetadata>) {
return buildStateFromMetadataUpdate(state, action.payload)
},
updateCursorPosition(state, action: PayloadAction<CursorSelection>) {
return buildStateFromUpdateCursorPosition(state, action.payload)
},
unloadNote() {
return initialState
}
}
})
export const noteDetailsActionsCreator = noteDetailsSlice.actions
export const noteDetailsReducer = noteDetailsSlice.reducer

View file

@ -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<NoteDetailsActionType> {
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<NoteDetailsActionType> {
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<NoteDetailsActionType> {
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<NoteDetailsActionType> {
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING
firstHeading?: string
}
export interface UpdateCursorPositionAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType.UPDATE_CURSOR_POSITION
export interface NoteDetails extends Omit<NoteMetadata, UnnecessaryNoteAttributes> {
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<NoteDetailsActionType> {
type: NoteDetailsActionType.UPDATE_METADATA
updatedMetadata: NoteMetadata
}
export interface UnloadNoteAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType.UNLOAD_NOTE
firstHeading?: string
rawFrontmatter: string
frontmatter: NoteFrontmatter
startOfContentLineOffset: number
}

View file

@ -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<NoteMetadata, UnnecessaryNoteAttributes> {
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

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -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<RealtimeStatus, RealtimeStatusActions> = (
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
}
}

View file

@ -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<SetRealtimeUsersPayload>) {
state.onlineUsers = action.payload.users
state.ownUser = action.payload.ownUser
},
setRealtimeConnectionStatus(state, action: PayloadAction<RealtimeStatus['isConnected']>) {
state.isConnected = action.payload
},
setRealtimeSyncStatus(state, action: PayloadAction<RealtimeStatus['isSynced']>) {
state.isSynced = action.payload
},
resetRealtimeStatus() {
return initialState
}
}
})
export const realtimeStatusActionsCreator = realtimeStatusSlice.actions
export const realtimeStatusReducer = realtimeStatusSlice.reducer

View file

@ -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<RealtimeStatusActionType> {
type: RealtimeStatusActionType.SET_REALTIME_USERS
export interface SetRealtimeUsersPayload {
users: RealtimeUser[]
ownUser: {
styleIndex: number
@ -22,20 +13,6 @@ export interface SetRealtimeUsersAction extends Action<RealtimeStatusActionType>
}
}
export interface SetRealtimeConnectionStatusAction extends Action<RealtimeStatusActionType> {
type: RealtimeStatusActionType.SET_REALTIME_CONNECTION_STATUS
isConnected: boolean
}
export interface SetRealtimeSyncStatusAction extends Action<RealtimeStatusActionType> {
type: RealtimeStatusActionType.SET_REALTIME_SYNCED_STATUS
isSynced: boolean
}
export interface ResetRealtimeStatusAction extends Action<RealtimeStatusActionType> {
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

View file

@ -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<ApplicationState> = combineReducers<ApplicationState>({
user: UserReducer,
history: HistoryReducer,
editorConfig: EditorConfigReducer,
darkMode: DarkModeConfigReducer,
noteDetails: NoteDetailsReducer,
rendererStatus: RendererStatusReducer,
realtimeStatus: RealtimeStatusReducer
})

View file

@ -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
}

View file

@ -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)
}

View file

@ -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<RendererStatus, RendererStatusActions> = (
state: RendererStatus = initialState,
action: RendererStatusActions
) => {
switch (action.type) {
case RendererStatusActionType.SET_RENDERER_STATUS:
return {
...state,
rendererReady: action.rendererReady
}
default:
return state
}
}

View file

@ -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<RendererStatus['rendererReady']>) => {
state.rendererReady = action.payload
}
}
})
export const rendererStatusActionsCreator = rendererStatusSlice.actions
export const rendererStatusReducer = rendererStatusSlice.reducer

View file

@ -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<RendererStatusActionType> {
type: RendererStatusActionType.SET_RENDERER_STATUS
rendererReady: boolean
}
export type RendererStatusActions = SetRendererStatusAction

View file

@ -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

View file

@ -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)
}

View file

@ -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<OptionalUserState, UserActions> = (
state: OptionalUserState = null,
action: UserActions
) => {
switch (action.type) {
case UserActionType.SET_USER:
return action.state
case UserActionType.CLEAR_USER:
return null
default:
return state
}
}

View file

@ -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<UserState>) => {
return action.payload
}
}
})
export const userActionsCreator = userSlice.actions
export const userReducer = userSlice.reducer

View file

@ -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<UserActionType> {
type: UserActionType.SET_USER
state: LoginUserInfo
}
export interface ClearUserAction extends Action<UserActionType> {
type: UserActionType.CLEAR_USER
}
export type OptionalUserState = LoginUserInfo | null
export type UserState = LoginUserInfo | null

View file

@ -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')

View file

@ -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'

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const updateObject = <T extends Record<string, unknown>>(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]
}
})
}