mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 03:06:31 -05:00
refactor(redux): migrate to RTK2 store definition
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
8b501915f5
commit
d840a6f0b1
66 changed files with 526 additions and 846 deletions
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
/**
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
22
frontend/src/redux/application-state.d.ts
vendored
22
frontend/src/redux/application-state.d.ts
vendored
|
@ -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
|
||||
}
|
11
frontend/src/redux/dark-mode/initial-state.ts
Normal file
11
frontend/src/redux/dark-mode/initial-state.ts
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
22
frontend/src/redux/dark-mode/slice.ts
Normal file
22
frontend/src/redux/dark-mode/slice.ts
Normal 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
|
|
@ -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
|
||||
|
|
16
frontend/src/redux/editor-config/initial-state.ts
Normal file
16
frontend/src/redux/editor-config/initial-state.ts
Normal 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
|
||||
}
|
77
frontend/src/redux/editor-config/methods.ts
Normal file
77
frontend/src/redux/editor-config/methods.ts
Normal 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)
|
||||
}
|
||||
}
|
43
frontend/src/redux/editor-config/slice.ts
Normal file
43
frontend/src/redux/editor-config/slice.ts
Normal 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
|
14
frontend/src/redux/editor-config/types.ts
Normal file
14
frontend/src/redux/editor-config/types.ts
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
8
frontend/src/redux/history/initial-state.ts
Normal file
8
frontend/src/redux/history/initial-state.ts
Normal 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 = []
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
34
frontend/src/redux/history/slice.ts
Normal file
34
frontend/src/redux/history/slice.ts
Normal 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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
||||
/**
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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'
|
||||
|
||||
/**
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
/**
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
/**
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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]?]( .*)/
|
||||
|
|
|
@ -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)
|
||||
|
|
48
frontend/src/redux/note-details/slice.ts
Normal file
48
frontend/src/redux/note-details/slice.ts
Normal 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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
16
frontend/src/redux/realtime/initial-state.ts
Normal file
16
frontend/src/redux/realtime/initial-state.ts
Normal 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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
32
frontend/src/redux/realtime/slice.ts
Normal file
32
frontend/src/redux/realtime/slice.ts
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
10
frontend/src/redux/renderer-status/initial-state.ts
Normal file
10
frontend/src/redux/renderer-status/initial-state.ts
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
22
frontend/src/redux/renderer-status/slice.ts
Normal file
22
frontend/src/redux/renderer-status/slice.ts
Normal 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
|
|
@ -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
|
||||
|
|
8
frontend/src/redux/user/initial-state.ts
Normal file
8
frontend/src/redux/user/initial-state.ts
Normal 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
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
22
frontend/src/redux/user/slice.ts
Normal file
22
frontend/src/redux/user/slice.ts
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
17
frontend/src/utils/update-object.ts
Normal file
17
frontend/src/utils/update-object.ts
Normal 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]
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue