mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-22 01:36:29 -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 { isDevMode, isTestMode } from '../../../utils/test-modes'
|
||||||
import { loadDarkMode } from './load-dark-mode'
|
import { loadDarkMode } from './load-dark-mode'
|
||||||
import { setUpI18n } from './setupI18n'
|
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'
|
import { fetchAndSetUser } from '../../login-page/utils/fetch-and-set-user'
|
||||||
|
|
||||||
const logger = new Logger('Application Loader')
|
const logger = new Logger('Application Loader')
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import * as AliasModule from '../../../../../../api/alias'
|
import * as AliasModule from '../../../../../../api/alias'
|
||||||
import * as NoteDetailsReduxModule from '../../../../../../redux/note-details/methods'
|
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 { mockI18n } from '../../../../../../test-utils/mock-i18n'
|
||||||
import { mockNotePermissions } from '../../../../../../test-utils/mock-note-permissions'
|
import { mockNotePermissions } from '../../../../../../test-utils/mock-note-permissions'
|
||||||
import { AliasesAddForm } from './aliases-add-form'
|
import { AliasesAddForm } from './aliases-add-form'
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
||||||
import type { Alias } from '../../../../../../api/alias/types'
|
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 { AliasesListEntry } from './aliases-list-entry'
|
||||||
import React, { Fragment, useMemo } from 'react'
|
import React, { Fragment, useMemo } from 'react'
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Form } from 'react-bootstrap'
|
import { Form } from 'react-bootstrap'
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
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'
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
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 { OnOffButtonGroup } from '../utils/on-off-button-group'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
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 { OnOffButtonGroup } from '../utils/on-off-button-group'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
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 { OnOffButtonGroup } from '../utils/on-off-button-group'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
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 { OnOffButtonGroup } from '../utils/on-off-button-group'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
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 { OnOffButtonGroup } from '../utils/on-off-button-group'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
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 { OnOffButtonGroup } from '../utils/on-off-button-group'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 equal from 'fast-deep-equal'
|
||||||
import { useSelector } from 'react-redux'
|
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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { store } from '..'
|
import { store } from '..'
|
||||||
import type { DarkModeConfigAction, DarkModePreference } from './types'
|
import type { DarkModePreference } from './types'
|
||||||
import { DarkModeConfigActionType } from './types'
|
import { darkModeActionsCreator } from './slice'
|
||||||
|
|
||||||
export const setDarkModePreference = (darkModePreference: DarkModePreference): void => {
|
export const setDarkModePreference = (darkModePreference: DarkModePreference): void => {
|
||||||
store.dispatch({
|
const action = darkModeActionsCreator.setDarkModePreference(darkModePreference)
|
||||||
type: DarkModeConfigActionType.SET_DARK_MODE,
|
store.dispatch(action)
|
||||||
darkModePreference
|
|
||||||
} as DarkModeConfigAction)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { Action } from 'redux'
|
|
||||||
|
|
||||||
export enum DarkModeConfigActionType {
|
|
||||||
SET_DARK_MODE = 'dark-mode/set'
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum DarkModePreference {
|
export enum DarkModePreference {
|
||||||
DARK,
|
DARK,
|
||||||
LIGHT,
|
LIGHT,
|
||||||
|
@ -18,5 +12,3 @@ export enum DarkModePreference {
|
||||||
export interface DarkModeConfig {
|
export interface DarkModeConfig {
|
||||||
darkModePreference: DarkModePreference
|
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 { HistoryEntryOrigin } from '../../api/history/types'
|
||||||
import { download } from '../../components/common/download/download'
|
import { download } from '../../components/common/download/download'
|
||||||
import { Logger } from '../../utils/logger'
|
import { Logger } from '../../utils/logger'
|
||||||
import { getGlobalState, store } from '../index'
|
import { store } from '../index'
|
||||||
import type { HistoryExportJson, RemoveEntryAction, SetEntriesAction, UpdateEntryAction, V1HistoryEntry } from './types'
|
import type { HistoryExportJson, V1HistoryEntry } from './types'
|
||||||
import { HistoryActionType } from './types'
|
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
|
import { historyActionsCreator } from './slice'
|
||||||
|
|
||||||
const log = new Logger('Redux > History')
|
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.
|
* @param entries The history entries to set into the redux state.
|
||||||
*/
|
*/
|
||||||
export const setHistoryEntries = (entries: HistoryEntryWithOrigin[]): void => {
|
export const setHistoryEntries = (entries: HistoryEntryWithOrigin[]): void => {
|
||||||
store.dispatch({
|
const action = historyActionsCreator.setEntries(entries)
|
||||||
type: HistoryActionType.SET_ENTRIES,
|
store.dispatch(action)
|
||||||
entries
|
|
||||||
} as SetEntriesAction)
|
|
||||||
storeLocalHistory()
|
storeLocalHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,10 +45,8 @@ export const importHistoryEntries = (entries: HistoryEntryWithOrigin[]): Promise
|
||||||
* Deletes all history entries in the redux, local-storage and on the server.
|
* Deletes all history entries in the redux, local-storage and on the server.
|
||||||
*/
|
*/
|
||||||
export const deleteAllHistoryEntries = (): Promise<unknown> => {
|
export const deleteAllHistoryEntries = (): Promise<unknown> => {
|
||||||
store.dispatch({
|
const action = historyActionsCreator.setEntries([])
|
||||||
type: HistoryActionType.SET_ENTRIES,
|
store.dispatch(action)
|
||||||
entries: []
|
|
||||||
} as SetEntriesAction)
|
|
||||||
storeLocalHistory()
|
storeLocalHistory()
|
||||||
return deleteRemoteHistory()
|
return deleteRemoteHistory()
|
||||||
}
|
}
|
||||||
|
@ -61,11 +57,11 @@ export const deleteAllHistoryEntries = (): Promise<unknown> => {
|
||||||
* @param newEntry The modified history entry.
|
* @param newEntry The modified history entry.
|
||||||
*/
|
*/
|
||||||
export const updateHistoryEntryRedux = (noteId: string, newEntry: HistoryEntry): void => {
|
export const updateHistoryEntryRedux = (noteId: string, newEntry: HistoryEntry): void => {
|
||||||
store.dispatch({
|
const action = historyActionsCreator.updateEntry({
|
||||||
type: HistoryActionType.UPDATE_ENTRY,
|
|
||||||
noteId,
|
noteId,
|
||||||
newEntry
|
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.
|
* @param noteId The note id of the history entry to delete.
|
||||||
*/
|
*/
|
||||||
export const removeHistoryEntry = async (noteId: string): Promise<void> => {
|
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) {
|
if (entryToDelete && entryToDelete.origin === HistoryEntryOrigin.REMOTE) {
|
||||||
await deleteRemoteHistoryEntry(noteId)
|
await deleteRemoteHistoryEntry(noteId)
|
||||||
}
|
}
|
||||||
store.dispatch({
|
const action = historyActionsCreator.removeEntry({ noteId })
|
||||||
type: HistoryActionType.REMOVE_ENTRY,
|
store.dispatch(action)
|
||||||
noteId
|
|
||||||
} as RemoveEntryAction)
|
|
||||||
storeLocalHistory()
|
storeLocalHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +93,7 @@ export const removeHistoryEntry = async (noteId: string): Promise<void> => {
|
||||||
* @param noteId The note id of the history entry to update.
|
* @param noteId The note id of the history entry to update.
|
||||||
*/
|
*/
|
||||||
export const toggleHistoryEntryPinning = async (noteId: string): Promise<void> => {
|
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)
|
const entryToUpdate = state.find((entry) => entry.identifier === noteId)
|
||||||
if (!entryToUpdate) {
|
if (!entryToUpdate) {
|
||||||
return Promise.reject(`History entry for note '${noteId}' not found`)
|
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.
|
* Exports the current history redux state into a JSON file that will be downloaded by the client.
|
||||||
*/
|
*/
|
||||||
export const downloadHistory = (): void => {
|
export const downloadHistory = (): void => {
|
||||||
const history = getGlobalState().history
|
const history = store.getState().history
|
||||||
history.forEach((entry: Partial<HistoryEntryWithOrigin>) => {
|
history.forEach((entry: Partial<HistoryEntryWithOrigin>) => {
|
||||||
delete entry.origin
|
delete entry.origin
|
||||||
})
|
})
|
||||||
|
@ -166,7 +160,7 @@ export const convertV1History = (oldHistory: V1HistoryEntry[]): HistoryEntryWith
|
||||||
*/
|
*/
|
||||||
export const refreshHistoryState = async (): Promise<void> => {
|
export const refreshHistoryState = async (): Promise<void> => {
|
||||||
const localEntries = loadLocalHistory()
|
const localEntries = loadLocalHistory()
|
||||||
if (!getGlobalState().user) {
|
if (!store.getState().user) {
|
||||||
setHistoryEntries(localEntries)
|
setHistoryEntries(localEntries)
|
||||||
return
|
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.
|
* Stores the history entries marked as local from the redux to the user's local-storage.
|
||||||
*/
|
*/
|
||||||
export const storeLocalHistory = (): void => {
|
export const storeLocalHistory = (): void => {
|
||||||
const history = getGlobalState().history
|
const history = store.getState().history
|
||||||
const localEntries = history.filter((entry) => entry.origin === HistoryEntryOrigin.LOCAL)
|
const localEntries = history.filter((entry) => entry.origin === HistoryEntryOrigin.LOCAL)
|
||||||
const entriesWithoutOrigin = localEntries.map((entry) => ({
|
const entriesWithoutOrigin = localEntries.map((entry) => ({
|
||||||
...entry,
|
...entry,
|
||||||
|
@ -196,10 +190,10 @@ export const storeLocalHistory = (): void => {
|
||||||
* Stores the history entries marked as remote from the redux to the server.
|
* Stores the history entries marked as remote from the redux to the server.
|
||||||
*/
|
*/
|
||||||
export const storeRemoteHistory = (): Promise<unknown> => {
|
export const storeRemoteHistory = (): Promise<unknown> => {
|
||||||
if (!getGlobalState().user) {
|
if (!store.getState().user) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
const history = getGlobalState().history
|
const history = store.getState().history
|
||||||
const remoteEntries = history.filter((entry) => entry.origin === HistoryEntryOrigin.REMOTE)
|
const remoteEntries = history.filter((entry) => entry.origin === HistoryEntryOrigin.REMOTE)
|
||||||
const remoteEntryDtos = remoteEntries.map(historyEntryToHistoryEntryPutDto)
|
const remoteEntryDtos = remoteEntries.map(historyEntryToHistoryEntryPutDto)
|
||||||
return setRemoteHistoryEntries(remoteEntryDtos)
|
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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { HistoryEntryWithOrigin } from '../../api/history/types'
|
import type { HistoryEntry, HistoryEntryWithOrigin } from '../../api/history/types'
|
||||||
import type { Action } from 'redux'
|
|
||||||
|
export type HistoryState = HistoryEntryWithOrigin[]
|
||||||
|
|
||||||
export interface V1HistoryEntry {
|
export interface V1HistoryEntry {
|
||||||
id: string
|
id: string
|
||||||
|
@ -19,32 +20,11 @@ export interface HistoryExportJson {
|
||||||
entries: HistoryEntryWithOrigin[]
|
entries: HistoryEntryWithOrigin[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum HistoryActionType {
|
export interface UpdateEntryPayload {
|
||||||
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
|
|
||||||
noteId: string
|
noteId: string
|
||||||
newEntry: HistoryEntryWithOrigin
|
newEntry: HistoryEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoveEntryAction extends Action<HistoryActionType> {
|
export interface RemoveEntryPayload {
|
||||||
type: HistoryActionType.REMOVE_ENTRY
|
|
||||||
noteId: string
|
noteId: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,28 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { isDevMode } from '../utils/test-modes'
|
import { isDevMode } from '../utils/test-modes'
|
||||||
import type { ApplicationState } from './application-state'
|
|
||||||
import { allReducers } from './reducers'
|
|
||||||
import { configureStore } from '@reduxjs/toolkit'
|
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({
|
export const store = configureStore({
|
||||||
reducer: allReducers,
|
reducer: {
|
||||||
|
darkMode: darkModeReducer,
|
||||||
|
editorConfig: editorConfigReducer,
|
||||||
|
user: userReducer,
|
||||||
|
rendererStatus: rendererStatusReducer,
|
||||||
|
realtimeStatus: realtimeStatusReducer,
|
||||||
|
history: historyReducer,
|
||||||
|
noteDetails: noteDetailsReducer
|
||||||
|
},
|
||||||
devTools: isDevMode
|
devTools: isDevMode
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export type ApplicationState = ReturnType<typeof store.getState>
|
||||||
|
|
||||||
export const getGlobalState = (): ApplicationState => store.getState()
|
export const getGlobalState = (): ApplicationState => store.getState()
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import { calculateLineStartIndexes } from './calculate-line-start-indexes'
|
import { calculateLineStartIndexes } from './calculate-line-start-indexes'
|
||||||
import { initialState } from './initial-state'
|
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 type { FrontmatterExtractionResult, NoteFrontmatter } from '@hedgedoc/commons'
|
||||||
import {
|
import {
|
||||||
convertRawFrontmatterToNoteFrontmatter,
|
convertRawFrontmatterToNoteFrontmatter,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { NoteDetails } from './types/note-details'
|
import type { NoteDetails } from './types'
|
||||||
import { defaultNoteFrontmatter } from '@hedgedoc/commons'
|
import { defaultNoteFrontmatter } from '@hedgedoc/commons'
|
||||||
|
|
||||||
export const initialState: NoteDetails = {
|
export const initialState: NoteDetails = {
|
||||||
|
|
|
@ -7,26 +7,16 @@ import { store } from '..'
|
||||||
import { getNoteMetadata } from '../../api/notes'
|
import { getNoteMetadata } from '../../api/notes'
|
||||||
import type { Note } from '../../api/notes/types'
|
import type { Note } from '../../api/notes/types'
|
||||||
import type { CursorSelection } from '../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
|
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 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.
|
* 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.
|
* @param content The note content as it is written inside the editor pane.
|
||||||
*/
|
*/
|
||||||
export const setNoteContent = (content: string): void => {
|
export const setNoteContent = (content: string): void => {
|
||||||
store.dispatch({
|
const action = noteDetailsActionsCreator.setNoteContent(content)
|
||||||
type: NoteDetailsActionType.SET_DOCUMENT_CONTENT,
|
store.dispatch(action)
|
||||||
content: content
|
|
||||||
} as SetNoteDocumentContentAction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,10 +24,8 @@ export const setNoteContent = (content: string): void => {
|
||||||
* @param apiResponse The NoteDTO received from the API to store into redux.
|
* @param apiResponse The NoteDTO received from the API to store into redux.
|
||||||
*/
|
*/
|
||||||
export const setNoteDataFromServer = (apiResponse: Note): void => {
|
export const setNoteDataFromServer = (apiResponse: Note): void => {
|
||||||
store.dispatch({
|
const action = noteDetailsActionsCreator.setNoteDataFromServer(apiResponse)
|
||||||
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER,
|
store.dispatch(action)
|
||||||
noteFromServer: apiResponse
|
|
||||||
} as SetNoteDetailsFromServerAction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,10 +33,8 @@ export const setNoteDataFromServer = (apiResponse: Note): void => {
|
||||||
* @param apiResponse The NotePermissionsDTO received from the API to store into redux.
|
* @param apiResponse The NotePermissionsDTO received from the API to store into redux.
|
||||||
*/
|
*/
|
||||||
export const setNotePermissionsFromServer = (apiResponse: NotePermissions): void => {
|
export const setNotePermissionsFromServer = (apiResponse: NotePermissions): void => {
|
||||||
store.dispatch({
|
const action = noteDetailsActionsCreator.setNotePermissionsFromServer(apiResponse)
|
||||||
type: NoteDetailsActionType.SET_NOTE_PERMISSIONS_FROM_SERVER,
|
store.dispatch(action)
|
||||||
notePermissionsFromServer: apiResponse
|
|
||||||
} as SetNotePermissionsFromServerAction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,17 +42,13 @@ export const setNotePermissionsFromServer = (apiResponse: NotePermissions): void
|
||||||
* @param firstHeading The content of the first heading found in the markdown content.
|
* @param firstHeading The content of the first heading found in the markdown content.
|
||||||
*/
|
*/
|
||||||
export const updateNoteTitleByFirstHeading = (firstHeading?: string): void => {
|
export const updateNoteTitleByFirstHeading = (firstHeading?: string): void => {
|
||||||
store.dispatch({
|
const action = noteDetailsActionsCreator.updateNoteTitleByFirstHeading(firstHeading)
|
||||||
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING,
|
store.dispatch(action)
|
||||||
firstHeading: firstHeading
|
|
||||||
} as UpdateNoteTitleByFirstHeadingAction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateCursorPositions = (selection: CursorSelection): void => {
|
export const updateCursorPositions = (selection: CursorSelection): void => {
|
||||||
store.dispatch({
|
const action = noteDetailsActionsCreator.updateCursorPosition(selection)
|
||||||
type: NoteDetailsActionType.UPDATE_CURSOR_POSITION,
|
store.dispatch(action)
|
||||||
selection
|
|
||||||
} as UpdateCursorPositionAction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -78,14 +60,11 @@ export const updateMetadata = async (): Promise<void> => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const updatedMetadata = await getNoteMetadata(noteDetails.id)
|
const updatedMetadata = await getNoteMetadata(noteDetails.id)
|
||||||
store.dispatch({
|
const action = noteDetailsActionsCreator.updateMetadata(updatedMetadata)
|
||||||
type: NoteDetailsActionType.UPDATE_METADATA,
|
store.dispatch(action)
|
||||||
updatedMetadata
|
|
||||||
} as UpdateMetadataAction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unloadNote = (): void => {
|
export const unloadNote = (): void => {
|
||||||
store.dispatch({
|
const action = noteDetailsActionsCreator.unloadNote()
|
||||||
type: NoteDetailsActionType.UNLOAD_NOTE
|
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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { NoteDetails } from '../types/note-details'
|
import type { NoteDetails } from '../types'
|
||||||
import { generateNoteTitle } from '@hedgedoc/commons'
|
import { generateNoteTitle } from '@hedgedoc/commons'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import type { NoteMetadata } from '../../../api/notes/types'
|
import type { NoteMetadata } from '../../../api/notes/types'
|
||||||
import { initialState } from '../initial-state'
|
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'
|
import { buildStateFromMetadataUpdate } from './build-state-from-metadata-update'
|
||||||
|
|
||||||
describe('build state from server permissions', () => {
|
describe('build state from server permissions', () => {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { NoteMetadata } from '../../../api/notes/types'
|
import type { NoteMetadata } from '../../../api/notes/types'
|
||||||
import type { NoteDetails } from '../types/note-details'
|
import type { NoteDetails } from '../types'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { initialState } from '../initial-state'
|
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 { buildStateFromServerPermissions } from './build-state-from-server-permissions'
|
||||||
import type { NotePermissions } from '@hedgedoc/commons'
|
import type { NotePermissions } from '@hedgedoc/commons'
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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'
|
import type { NotePermissions } from '@hedgedoc/commons'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import type { Note } from '../../../api/notes/types'
|
import type { Note } from '../../../api/notes/types'
|
||||||
import * as buildStateFromUpdatedMarkdownContentModule from '../build-state-from-updated-markdown-content'
|
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 { buildStateFromServerDto } from './build-state-from-set-note-data-from-server'
|
||||||
import { NoteTextDirection, NoteType } from '@hedgedoc/commons'
|
import { NoteTextDirection, NoteType } from '@hedgedoc/commons'
|
||||||
import { DateTime } from 'luxon'
|
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 { buildStateFromUpdatedMarkdownContent } from '../build-state-from-updated-markdown-content'
|
||||||
import { calculateLineStartIndexes } from '../calculate-line-start-indexes'
|
import { calculateLineStartIndexes } from '../calculate-line-start-indexes'
|
||||||
import { initialState } from '../initial-state'
|
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'
|
import { buildStateFromMetadataUpdate } from './build-state-from-metadata-update'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import * as buildStateFromUpdatedMarkdownContentLinesModule from '../build-state-from-updated-markdown-content'
|
import * as buildStateFromUpdatedMarkdownContentLinesModule from '../build-state-from-updated-markdown-content'
|
||||||
import { initialState } from '../initial-state'
|
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 { buildStateFromTaskListUpdate } from './build-state-from-task-list-update'
|
||||||
import { Mock } from 'ts-mockery'
|
import { Mock } from 'ts-mockery'
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { buildStateFromUpdatedMarkdownContentLines } from '../build-state-from-updated-markdown-content'
|
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'
|
import { Optional } from '@mrdrogdrog/optional'
|
||||||
|
|
||||||
const TASK_REGEX = /(\s*(?:[-*+]|\d+[.)]) )\[[ xX]?]( .*)/
|
const TASK_REGEX = /(\s*(?:[-*+]|\d+[.)]) )\[[ xX]?]( .*)/
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { CursorSelection } from '../../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
|
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 => {
|
export const buildStateFromUpdateCursorPosition = (state: NoteDetails, selection: CursorSelection): NoteDetails => {
|
||||||
const correctedSelection = isFromAfterTo(selection)
|
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
|
* 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 { CursorSelection } from '../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
|
||||||
import type { NotePermissions } from '@hedgedoc/commons'
|
import type { NoteFrontmatter } from '@hedgedoc/commons'
|
||||||
import type { Action } from 'redux'
|
|
||||||
|
|
||||||
export enum NoteDetailsActionType {
|
type UnnecessaryNoteAttributes = 'updatedAt' | 'createdAt' | 'tags' | 'description'
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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> {
|
export interface NoteDetails extends Omit<NoteMetadata, UnnecessaryNoteAttributes> {
|
||||||
type: NoteDetailsActionType.SET_DOCUMENT_CONTENT
|
updatedAt: number
|
||||||
content: string
|
createdAt: number
|
||||||
}
|
markdownContent: {
|
||||||
|
plain: string
|
||||||
/**
|
lines: string[]
|
||||||
* Action for overwriting the current state with the data received from the API.
|
lineStartIndexes: number[]
|
||||||
*/
|
}
|
||||||
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
|
|
||||||
selection: CursorSelection
|
selection: CursorSelection
|
||||||
}
|
firstHeading?: string
|
||||||
|
rawFrontmatter: string
|
||||||
/**
|
frontmatter: NoteFrontmatter
|
||||||
* Action for updating the metadata of the current note.
|
startOfContentLineOffset: number
|
||||||
*/
|
|
||||||
export interface UpdateMetadataAction extends Action<NoteDetailsActionType> {
|
|
||||||
type: NoteDetailsActionType.UPDATE_METADATA
|
|
||||||
updatedMetadata: NoteMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UnloadNoteAction extends Action<NoteDetailsActionType> {
|
|
||||||
type: NoteDetailsActionType.UNLOAD_NOTE
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { store } from '..'
|
import { store } from '..'
|
||||||
import type { SetRealtimeConnectionStatusAction, SetRealtimeSyncStatusAction, SetRealtimeUsersAction } from './types'
|
|
||||||
import { RealtimeStatusActionType } from './types'
|
|
||||||
import type { RealtimeUser } from '@hedgedoc/commons'
|
import type { RealtimeUser } from '@hedgedoc/commons'
|
||||||
|
import { realtimeStatusActionsCreator } from './slice'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches an event to add a user
|
* Dispatches an event to add a user
|
||||||
*/
|
*/
|
||||||
export const setRealtimeUsers = (users: RealtimeUser[], ownStyleIndex: number, ownDisplayName: string): void => {
|
export const setRealtimeUsers = (users: RealtimeUser[], ownStyleIndex: number, ownDisplayName: string): void => {
|
||||||
const action: SetRealtimeUsersAction = {
|
const action = realtimeStatusActionsCreator.setRealtimeUsers({
|
||||||
type: RealtimeStatusActionType.SET_REALTIME_USERS,
|
|
||||||
users,
|
users,
|
||||||
ownUser: {
|
ownUser: {
|
||||||
styleIndex: ownStyleIndex,
|
styleIndex: ownStyleIndex,
|
||||||
displayName: ownDisplayName
|
displayName: ownDisplayName
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
store.dispatch(action)
|
store.dispatch(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setRealtimeConnectionState = (status: boolean): void => {
|
export const setRealtimeConnectionState = (status: boolean): void => {
|
||||||
store.dispatch({
|
const action = realtimeStatusActionsCreator.setRealtimeConnectionStatus(status)
|
||||||
type: RealtimeStatusActionType.SET_REALTIME_CONNECTION_STATUS,
|
store.dispatch(action)
|
||||||
isConnected: status
|
|
||||||
} as SetRealtimeConnectionStatusAction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setRealtimeSyncedState = (status: boolean): void => {
|
export const setRealtimeSyncedState = (status: boolean): void => {
|
||||||
store.dispatch({
|
const action = realtimeStatusActionsCreator.setRealtimeSyncStatus(status)
|
||||||
type: RealtimeStatusActionType.SET_REALTIME_SYNCED_STATUS,
|
store.dispatch(action)
|
||||||
isSynced: status
|
|
||||||
} as SetRealtimeSyncStatusAction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resetRealtimeStatus = (): void => {
|
export const resetRealtimeStatus = (): void => {
|
||||||
store.dispatch({
|
const action = realtimeStatusActionsCreator.resetRealtimeStatus()
|
||||||
type: RealtimeStatusActionType.RESET_REALTIME_STATUS
|
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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { RealtimeUser } from '@hedgedoc/commons'
|
import type { RealtimeUser } from '@hedgedoc/commons'
|
||||||
import type { Action } from 'redux'
|
|
||||||
|
|
||||||
export enum RealtimeStatusActionType {
|
export interface SetRealtimeUsersPayload {
|
||||||
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
|
|
||||||
users: RealtimeUser[]
|
users: RealtimeUser[]
|
||||||
ownUser: {
|
ownUser: {
|
||||||
styleIndex: number
|
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 {
|
export interface RealtimeStatus {
|
||||||
onlineUsers: RealtimeUser[]
|
onlineUsers: RealtimeUser[]
|
||||||
isConnected: boolean
|
isConnected: boolean
|
||||||
|
@ -45,9 +22,3 @@ export interface RealtimeStatus {
|
||||||
styleIndex: number
|
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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { store } from '..'
|
import { store } from '..'
|
||||||
import type { SetRendererStatusAction } from './types'
|
import { rendererStatusActionsCreator } from './slice'
|
||||||
import { RendererStatusActionType } from './types'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches a global application state change for the "renderer ready" state.
|
* 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.
|
* @param rendererReady The new renderer ready state.
|
||||||
*/
|
*/
|
||||||
export const setRendererStatus = (rendererReady: boolean): void => {
|
export const setRendererStatus = (rendererReady: boolean): void => {
|
||||||
const action: SetRendererStatusAction = {
|
const action = rendererStatusActionsCreator.setRendererStatus(rendererReady)
|
||||||
type: RendererStatusActionType.SET_RENDERER_STATUS,
|
|
||||||
rendererReady
|
|
||||||
}
|
|
||||||
store.dispatch(action)
|
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
|
* 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 {
|
export interface RendererStatus {
|
||||||
rendererReady: boolean
|
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 { store } from '..'
|
||||||
import type { LoginUserInfo } from '../../api/me/types'
|
import type { LoginUserInfo } from '../../api/me/types'
|
||||||
import type { ClearUserAction, SetUserAction } from './types'
|
import { userActionsCreator } from './slice'
|
||||||
import { UserActionType } from './types'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the given user state into the redux.
|
* Sets the given user state into the redux.
|
||||||
* @param state The user state to set into the redux.
|
* @param state The user state to set into the redux.
|
||||||
*/
|
*/
|
||||||
export const setUser = (state: LoginUserInfo): void => {
|
export const setUser = (state: LoginUserInfo): void => {
|
||||||
const action: SetUserAction = {
|
const action = userActionsCreator.setUser(state)
|
||||||
type: UserActionType.SET_USER,
|
|
||||||
state
|
|
||||||
}
|
|
||||||
store.dispatch(action)
|
store.dispatch(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the user state from the redux.
|
* Clears the user state from the redux.
|
||||||
*/
|
*/
|
||||||
export const clearUser: () => void = () => {
|
export const clearUser = (): void => {
|
||||||
const action: ClearUserAction = {
|
const action = userActionsCreator.setUser(null)
|
||||||
type: UserActionType.CLEAR_USER
|
|
||||||
}
|
|
||||||
store.dispatch(action)
|
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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { LoginUserInfo } from '../../api/me/types'
|
import type { LoginUserInfo } from '../../api/me/types'
|
||||||
import type { Action } from 'redux'
|
|
||||||
|
|
||||||
export enum UserActionType {
|
export type UserState = LoginUserInfo | null
|
||||||
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
|
|
||||||
|
|
|
@ -4,17 +4,17 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import * as useApplicationStateModule from '../hooks/common/use-application-state'
|
import * as useApplicationStateModule from '../hooks/common/use-application-state'
|
||||||
import type { ApplicationState } from '../redux/application-state'
|
import type { ApplicationState } from '../redux'
|
||||||
import { initialState as initialStateDarkMode } from '../redux/dark-mode/reducers'
|
import { initialState as initialStateDarkMode } from '../redux/dark-mode/initial-state'
|
||||||
import { initialState as initialStateEditorConfig } from '../redux/editor/reducers'
|
import { initialState as initialStateEditorConfig } from '../redux/editor-config/initial-state'
|
||||||
import { initialState as initialStateNoteDetails } from '../redux/note-details/initial-state'
|
import { initialState as initialStateNoteDetails } from '../redux/note-details/initial-state'
|
||||||
import { initialState as initialStateRealtimeStatus } from '../redux/realtime/reducers'
|
import { initialState as initialStateRealtimeStatus } from '../redux/realtime/initial-state'
|
||||||
import { initialState as initialStateRendererStatus } from '../redux/renderer-status/reducers'
|
import { initialState as initialStateRendererStatus } from '../redux/renderer-status/initial-state'
|
||||||
import type { NoteDetails } from '../redux/note-details/types/note-details'
|
import type { NoteDetails } from '../redux/note-details/types'
|
||||||
import type { RealtimeStatus } from '../redux/realtime/types'
|
import type { RealtimeStatus } from '../redux/realtime/types'
|
||||||
import type { DeepPartial } from '@hedgedoc/commons'
|
import type { DeepPartial } from '@hedgedoc/commons'
|
||||||
|
|
||||||
jest.mock('../redux/editor/methods', () => ({
|
jest.mock('../redux/editor-config/methods', () => ({
|
||||||
loadFromLocalStorage: jest.fn().mockReturnValue(undefined)
|
loadFromLocalStorage: jest.fn().mockReturnValue(undefined)
|
||||||
}))
|
}))
|
||||||
jest.mock('../hooks/common/use-application-state')
|
jest.mock('../hooks/common/use-application-state')
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 { mockAppState } from './mock-app-state'
|
||||||
import type { DeepPartial, NotePermissions } from '@hedgedoc/commons'
|
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