Adapt react-client to use the real backend API (#1545)

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Co-authored-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Erik Michelson 2022-04-15 23:03:15 +02:00 committed by GitHub
parent 3399ed2023
commit 26f90505ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
227 changed files with 4726 additions and 2310 deletions

View file

@ -1,16 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { store } from '..'
import type { ApiUrlObject, SetApiUrlAction } from './types'
import { ApiUrlActionType } from './types'
export const setApiUrl = (state: ApiUrlObject): void => {
store.dispatch({
type: ApiUrlActionType.SET_API_URL,
state
} as SetApiUrlAction)
}

View file

@ -1,25 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Reducer } from 'redux'
import type { ApiUrlActions, ApiUrlObject } from './types'
import { ApiUrlActionType } from './types'
export const initialState: ApiUrlObject = {
apiUrl: ''
}
export const ApiUrlReducer: Reducer<ApiUrlObject, ApiUrlActions> = (
state: ApiUrlObject = initialState,
action: ApiUrlActions
) => {
switch (action.type) {
case ApiUrlActionType.SET_API_URL:
return action.state
default:
return state
}
}

View file

@ -1,22 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Action } from 'redux'
export enum ApiUrlActionType {
SET_API_URL = 'api-url/set'
}
export type ApiUrlActions = SetApiUrlAction
export interface SetApiUrlAction extends Action<ApiUrlActionType> {
type: ApiUrlActionType.SET_API_URL
state: ApiUrlObject
}
export interface ApiUrlObject {
apiUrl: string
}

View file

@ -7,20 +7,18 @@
import type { OptionalUserState } from './user/types'
import type { Config } from '../api/config/types'
import type { OptionalMotdState } from './motd/types'
import type { HistoryEntry } from './history/types'
import type { ApiUrlObject } from './api-url/types'
import type { EditorConfig } from './editor/types'
import type { DarkModeConfig } from './dark-mode/types'
import type { NoteDetails } from './note-details/types/note-details'
import type { UiNotificationState } from './ui-notifications/types'
import type { RendererStatus } from './renderer-status/types'
import type { HistoryEntryWithOrigin } from '../api/history/types'
export interface ApplicationState {
user: OptionalUserState
config: Config
motd: OptionalMotdState
history: HistoryEntry[]
apiUrl: ApiUrlObject
history: HistoryEntryWithOrigin[]
editorConfig: EditorConfig
darkMode: DarkModeConfig
noteDetails: NoteDetails

View file

@ -12,40 +12,24 @@ import { ConfigActionType } from './types'
export const initialState: Config = {
allowAnonymous: true,
allowRegister: true,
authProviders: {
facebook: false,
github: false,
twitter: false,
gitlab: false,
dropbox: false,
ldap: false,
google: false,
saml: false,
oauth2: false,
local: false
},
authProviders: [],
branding: {
name: '',
logo: ''
},
customAuthNames: {
ldap: '',
oauth2: '',
saml: ''
},
maxDocumentLength: 0,
useImageProxy: false,
plantumlServer: null,
specialUrls: {
privacy: '',
termsOfUse: '',
imprint: ''
privacy: undefined,
termsOfUse: undefined,
imprint: undefined
},
version: {
major: -1,
minor: -1,
patch: -1
major: 0,
minor: 0,
patch: 0
},
plantumlServer: undefined,
maxDocumentLength: 0,
iframeCommunication: {
editorOrigin: '',
rendererOrigin: ''

View file

@ -1,39 +1,34 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { getGlobalState, store } from '../index'
import type {
HistoryEntry,
HistoryExportJson,
RemoveEntryAction,
SetEntriesAction,
UpdateEntryAction,
V1HistoryEntry
} from './types'
import { HistoryActionType, HistoryEntryOrigin } from './types'
import type { HistoryExportJson, RemoveEntryAction, SetEntriesAction, UpdateEntryAction, V1HistoryEntry } from './types'
import { HistoryActionType } from './types'
import { download } from '../../components/common/download/download'
import { DateTime } from 'luxon'
import {
deleteHistory,
deleteHistoryEntry,
getHistory,
postHistory,
updateHistoryEntryPinStatus
deleteRemoteHistory,
deleteRemoteHistoryEntry,
getRemoteHistory,
setRemoteHistoryEntries,
updateRemoteHistoryEntryPinStatus
} from '../../api/history'
import {
historyEntryDtoToHistoryEntry,
historyEntryToHistoryEntryPutDto,
historyEntryToHistoryEntryUpdateDto
} from '../../api/history/dto-methods'
import { addRemoteOriginToHistoryEntry, historyEntryToHistoryEntryPutDto } from '../../api/history/dto-methods'
import { Logger } from '../../utils/logger'
import type { HistoryEntry, HistoryEntryWithOrigin } from '../../api/history/types'
import { HistoryEntryOrigin } from '../../api/history/types'
import { showErrorNotification } from '../ui-notifications/methods'
const log = new Logger('Redux > History')
export const setHistoryEntries = (entries: HistoryEntry[]): void => {
/**
* Sets the given history entries into the current redux state and updates the local-storage.
* @param entries The history entries to set into the redux state.
*/
export const setHistoryEntries = (entries: HistoryEntryWithOrigin[]): void => {
store.dispatch({
type: HistoryActionType.SET_ENTRIES,
entries
@ -41,20 +36,32 @@ export const setHistoryEntries = (entries: HistoryEntry[]): void => {
storeLocalHistory()
}
export const importHistoryEntries = (entries: HistoryEntry[]): Promise<void> => {
/**
* Imports the given history entries into redux state and local-storage and remote based on their associated origin label.
* @param entries The history entries to import.
*/
export const importHistoryEntries = (entries: HistoryEntryWithOrigin[]): Promise<unknown> => {
setHistoryEntries(entries)
return storeRemoteHistory()
}
export const deleteAllHistoryEntries = (): Promise<void> => {
/**
* Deletes all history entries in the redux, local-storage and on the server.
*/
export const deleteAllHistoryEntries = (): Promise<unknown> => {
store.dispatch({
type: HistoryActionType.SET_ENTRIES,
entries: []
} as SetEntriesAction)
storeLocalHistory()
return deleteHistory()
return deleteRemoteHistory()
}
/**
* Updates a single history entry in the redux.
* @param noteId The note id of the history entry to update.
* @param newEntry The modified history entry.
*/
export const updateHistoryEntryRedux = (noteId: string, newEntry: HistoryEntry): void => {
store.dispatch({
type: HistoryActionType.UPDATE_ENTRY,
@ -63,15 +70,24 @@ export const updateHistoryEntryRedux = (noteId: string, newEntry: HistoryEntry):
} as UpdateEntryAction)
}
/**
* Updates a single history entry in the local-storage.
* @param noteId The note id of the history entry to update.
* @param newEntry The modified history entry.
*/
export const updateLocalHistoryEntry = (noteId: string, newEntry: HistoryEntry): void => {
updateHistoryEntryRedux(noteId, newEntry)
storeLocalHistory()
}
/**
* Removes a single history entry for a given note id.
* @param noteId The note id of the history entry to delete.
*/
export const removeHistoryEntry = async (noteId: string): Promise<void> => {
const entryToDelete = getGlobalState().history.find((entry) => entry.identifier === noteId)
if (entryToDelete && entryToDelete.origin === HistoryEntryOrigin.REMOTE) {
await deleteHistoryEntry(noteId)
await deleteRemoteHistoryEntry(noteId)
}
store.dispatch({
type: HistoryActionType.REMOVE_ENTRY,
@ -80,6 +96,10 @@ export const removeHistoryEntry = async (noteId: string): Promise<void> => {
storeLocalHistory()
}
/**
* Toggles the pinning state of a single history entry.
* @param noteId The note id of the history entry to update.
*/
export const toggleHistoryEntryPinning = async (noteId: string): Promise<void> => {
const state = getGlobalState().history
const entryToUpdate = state.find((entry) => entry.identifier === noteId)
@ -93,15 +113,17 @@ export const toggleHistoryEntryPinning = async (noteId: string): Promise<void> =
if (entryToUpdate.origin === HistoryEntryOrigin.LOCAL) {
updateLocalHistoryEntry(noteId, entryToUpdate)
} else {
const historyUpdateDto = historyEntryToHistoryEntryUpdateDto(entryToUpdate)
await updateHistoryEntryPinStatus(noteId, historyUpdateDto)
await updateRemoteHistoryEntryPinStatus(noteId, entryToUpdate.pinStatus)
updateHistoryEntryRedux(noteId, entryToUpdate)
}
}
/**
* Exports the current history redux state into a JSON file that will be downloaded by the client.
*/
export const downloadHistory = (): void => {
const history = getGlobalState().history
history.forEach((entry: Partial<HistoryEntry>) => {
history.forEach((entry: Partial<HistoryEntryWithOrigin>) => {
delete entry.origin
})
const json = JSON.stringify({
@ -111,22 +133,39 @@ export const downloadHistory = (): void => {
download(json, `history_${Date.now()}.json`, 'application/json')
}
export const mergeHistoryEntries = (a: HistoryEntry[], b: HistoryEntry[]): HistoryEntry[] => {
/**
* Merges two arrays of history entries while removing duplicates.
* @param a The first input array of history entries.
* @param b The second input array of history entries. This array takes precedence when duplicates were found.
* @return The merged array of history entries without duplicates.
*/
export const mergeHistoryEntries = (
a: HistoryEntryWithOrigin[],
b: HistoryEntryWithOrigin[]
): HistoryEntryWithOrigin[] => {
const noDuplicates = a.filter((entryA) => !b.some((entryB) => entryA.identifier === entryB.identifier))
return noDuplicates.concat(b)
}
export const convertV1History = (oldHistory: V1HistoryEntry[]): HistoryEntry[] => {
/**
* Converts an array of local HedgeDoc v1 history entries to HedgeDoc v2 history entries.
* @param oldHistory An array of HedgeDoc v1 history entries.
* @return An array of HedgeDoc v2 history entries associated with the local origin label.
*/
export const convertV1History = (oldHistory: V1HistoryEntry[]): HistoryEntryWithOrigin[] => {
return oldHistory.map((entry) => ({
identifier: entry.id,
title: entry.text,
tags: entry.tags,
lastVisited: DateTime.fromMillis(entry.time).toISO(),
lastVisitedAt: DateTime.fromMillis(entry.time).toISO(),
pinStatus: entry.pinned,
origin: HistoryEntryOrigin.LOCAL
}))
}
/**
* Refreshes the history redux state by reloading the local history and fetching the remote history if the user is logged-in.
*/
export const refreshHistoryState = async (): Promise<void> => {
const localEntries = loadLocalHistory()
if (!getGlobalState().user) {
@ -138,10 +177,16 @@ export const refreshHistoryState = async (): Promise<void> => {
setHistoryEntries(allEntries)
}
/**
* Refreshes the history state and shows an error in case of failure.
*/
export const safeRefreshHistoryState = (): void => {
refreshHistoryState().catch(showErrorNotification('landing.history.error.getHistory.text'))
}
/**
* Stores the history entries marked as local from the redux to the user's local-storage.
*/
export const storeLocalHistory = (): void => {
const history = getGlobalState().history
const localEntries = history.filter((entry) => entry.origin === HistoryEntryOrigin.LOCAL)
@ -152,17 +197,24 @@ export const storeLocalHistory = (): void => {
window.localStorage.setItem('history', JSON.stringify(entriesWithoutOrigin))
}
export const storeRemoteHistory = (): Promise<void> => {
/**
* Stores the history entries marked as remote from the redux to the server.
*/
export const storeRemoteHistory = (): Promise<unknown> => {
if (!getGlobalState().user) {
return Promise.resolve()
}
const history = getGlobalState().history
const remoteEntries = history.filter((entry) => entry.origin === HistoryEntryOrigin.REMOTE)
const remoteEntryDtos = remoteEntries.map(historyEntryToHistoryEntryPutDto)
return postHistory(remoteEntryDtos)
return setRemoteHistoryEntries(remoteEntryDtos)
}
const loadLocalHistory = (): HistoryEntry[] => {
/**
* Loads the local history from local-storage, converts from V1 format if necessary and returns the history entries with a local origin label.
* @return The local history entries with the origin set to local.
*/
const loadLocalHistory = (): HistoryEntryWithOrigin[] => {
const localV1Json = window.localStorage.getItem('notehistory')
if (localV1Json) {
try {
@ -181,7 +233,7 @@ const loadLocalHistory = (): HistoryEntry[] => {
}
try {
const localHistory = JSON.parse(localJson) as HistoryEntry[]
const localHistory = JSON.parse(localJson) as HistoryEntryWithOrigin[]
localHistory.forEach((entry) => {
entry.origin = HistoryEntryOrigin.LOCAL
})
@ -192,10 +244,14 @@ const loadLocalHistory = (): HistoryEntry[] => {
}
}
const loadRemoteHistory = async (): Promise<HistoryEntry[]> => {
/**
* Loads the remote history and maps each entry with a remote origin label.
* @return The remote history entries with the origin set to remote.
*/
const loadRemoteHistory = async (): Promise<HistoryEntryWithOrigin[]> => {
try {
const remoteHistory = await getHistory()
return remoteHistory.map(historyEntryDtoToHistoryEntry)
const remoteHistory = await getRemoteHistory()
return remoteHistory.map(addRemoteOriginToHistoryEntry)
} catch (error) {
log.error('Error while fetching history entries from server', error)
return []

View file

@ -5,15 +5,16 @@
*/
import type { Reducer } from 'redux'
import type { HistoryActions, HistoryEntry } from './types'
import type { HistoryActions } from './types'
import { HistoryActionType } from './types'
import type { HistoryEntryWithOrigin } from '../../api/history/types'
// 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<HistoryEntry[], HistoryActions> = (
state: HistoryEntry[] = [],
export const HistoryReducer: Reducer<HistoryEntryWithOrigin[], HistoryActions> = (
state: HistoryEntryWithOrigin[] = [],
action: HistoryActions
) => {
switch (action.type) {

View file

@ -5,20 +5,7 @@
*/
import type { Action } from 'redux'
export enum HistoryEntryOrigin {
LOCAL,
REMOTE
}
export interface HistoryEntry {
identifier: string
title: string
lastVisited: string
tags: string[]
pinStatus: boolean
origin: HistoryEntryOrigin
}
import type { HistoryEntryWithOrigin } from '../../api/history/types'
export interface V1HistoryEntry {
id: string
@ -30,7 +17,7 @@ export interface V1HistoryEntry {
export interface HistoryExportJson {
version: number
entries: HistoryEntry[]
entries: HistoryEntryWithOrigin[]
}
export enum HistoryActionType {
@ -44,21 +31,21 @@ export type HistoryActions = SetEntriesAction | AddEntryAction | UpdateEntryActi
export interface SetEntriesAction extends Action<HistoryActionType> {
type: HistoryActionType.SET_ENTRIES
entries: HistoryEntry[]
entries: HistoryEntryWithOrigin[]
}
export interface AddEntryAction extends Action<HistoryActionType> {
type: HistoryActionType.ADD_ENTRY
newEntry: HistoryEntry
newEntry: HistoryEntryWithOrigin
}
export interface UpdateEntryAction extends Action<HistoryActionType> {
type: HistoryActionType.UPDATE_ENTRY
noteId: string
newEntry: HistoryEntry
newEntry: HistoryEntryWithOrigin
}
export interface RemoveEntryAction extends HistoryEntry {
export interface RemoveEntryAction extends Action<HistoryActionType> {
type: HistoryActionType.REMOVE_ENTRY
noteId: string
}

View file

@ -63,7 +63,7 @@ const buildStateFromMarkdownContentAndLines = (
lineStartIndexes
},
rawFrontmatter: '',
noteTitle: generateNoteTitle(initialState.frontmatter, state.firstHeading),
title: generateNoteTitle(initialState.frontmatter, state.firstHeading),
frontmatter: initialState.frontmatter,
frontmatterRendererInfo: initialState.frontmatterRendererInfo
}
@ -89,7 +89,7 @@ const buildStateFromFrontmatterUpdate = (
...state,
rawFrontmatter: frontmatterExtraction.rawText,
frontmatter: frontmatter,
noteTitle: generateNoteTitle(frontmatter, state.firstHeading),
title: generateNoteTitle(frontmatter, state.firstHeading),
frontmatterRendererInfo: {
lineOffset: frontmatterExtraction.lineOffset,
deprecatedSyntax: frontmatter.deprecatedTagsSyntax,
@ -100,7 +100,7 @@ const buildStateFromFrontmatterUpdate = (
} catch (e) {
return {
...state,
noteTitle: generateNoteTitle(initialState.frontmatter, state.firstHeading),
title: generateNoteTitle(initialState.frontmatter, state.firstHeading),
rawFrontmatter: frontmatterExtraction.rawText,
frontmatter: initialState.frontmatter,
frontmatterRendererInfo: {

View file

@ -18,6 +18,9 @@ export const initialSlideOptions: SlideOptions = {
}
export const initialState: NoteDetails = {
updatedAt: DateTime.fromSeconds(0),
updateUsername: null,
version: 0,
markdownContent: {
plain: '',
lines: [],
@ -32,15 +35,17 @@ export const initialState: NoteDetails = {
slideOptions: initialSlideOptions
},
id: '',
createTime: DateTime.fromSeconds(0),
lastChange: {
timestamp: DateTime.fromSeconds(0),
username: ''
createdAt: DateTime.fromSeconds(0),
aliases: [],
primaryAddress: '',
permissions: {
owner: null,
sharedToGroups: [],
sharedToUsers: []
},
alias: '',
viewCount: 0,
authorship: [],
noteTitle: '',
editedBy: [],
title: '',
firstHeading: '',
frontmatter: {
title: '',

View file

@ -1,11 +1,11 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { store } from '..'
import type { NoteDto } from '../../api/notes/types'
import type { Note, NotePermissions } from '../../api/notes/types'
import type {
AddTableAtCursorAction,
FormatSelectionAction,
@ -14,6 +14,7 @@ import type {
ReplaceInMarkdownContentAction,
SetNoteDetailsFromServerAction,
SetNoteDocumentContentAction,
SetNotePermissionsFromServerAction,
UpdateCursorPositionAction,
UpdateNoteTitleByFirstHeadingAction,
UpdateTaskListCheckboxAction
@ -36,13 +37,24 @@ export const setNoteContent = (content: string): void => {
* Sets the note metadata for the current note from an API response DTO to the redux.
* @param apiResponse The NoteDTO received from the API to store into redux.
*/
export const setNoteDataFromServer = (apiResponse: NoteDto): void => {
export const setNoteDataFromServer = (apiResponse: Note): void => {
store.dispatch({
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER,
dto: apiResponse
noteFromServer: apiResponse
} as SetNoteDetailsFromServerAction)
}
/**
* Sets the note permissions for the current note from an API response DTO to the redux.
* @param apiResponse The NotePermissionsDTO received from the API to store into redux.
*/
export const setNotePermissionsFromServer = (apiResponse: NotePermissions): void => {
store.dispatch({
type: NoteDetailsActionType.SET_NOTE_PERMISSIONS_FROM_SERVER,
notePermissionsFromServer: apiResponse
} as SetNotePermissionsFromServerAction)
}
/**
* Updates the note title in the redux by the first heading found in the markdown content.
* @param firstHeading The content of the first heading found in the markdown content.

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -18,6 +18,7 @@ import { buildStateFromReplaceSelection } from './reducers/build-state-from-repl
import { buildStateFromTaskListUpdate } from './reducers/build-state-from-task-list-update'
import { buildStateFromSelectionFormat } from './reducers/build-state-from-selection-format'
import { buildStateFromReplaceInMarkdownContent } from './reducers/build-state-from-replace-in-markdown-content'
import { buildStateFromServerPermissions } from './reducers/build-state-from-server-permissions'
export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
state: NoteDetails = initialState,
@ -28,10 +29,12 @@ export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
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.SET_NOTE_DATA_FROM_SERVER:
return buildStateFromServerDto(action.dto)
return buildStateFromServerDto(action.noteFromServer)
case NoteDetailsActionType.UPDATE_TASK_LIST_CHECKBOX:
return buildStateFromTaskListUpdate(state, action.changedLine, action.checkboxChecked)
case NoteDetailsActionType.REPLACE_IN_MARKDOWN_CONTENT:

View file

@ -19,8 +19,8 @@ describe('build state from first heading update', () => {
})
it('generates a new state with the given first heading', () => {
const startState = { ...initialState, firstHeading: 'heading', noteTitle: 'noteTitle' }
const startState = { ...initialState, firstHeading: 'heading', title: 'noteTitle' }
const actual = buildStateFromFirstHeadingUpdate(startState, 'new first heading')
expect(actual).toStrictEqual({ ...initialState, firstHeading: 'new first heading', noteTitle: 'generated title' })
expect(actual).toStrictEqual({ ...initialState, firstHeading: 'new first heading', title: 'generated title' })
})
})

View file

@ -17,6 +17,6 @@ export const buildStateFromFirstHeadingUpdate = (state: NoteDetails, firstHeadin
return {
...state,
firstHeading: firstHeading,
noteTitle: generateNoteTitle(state.frontmatter, firstHeading)
title: generateNoteTitle(state.frontmatter, firstHeading)
}
}

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { initialState } from '../initial-state'
import type { NotePermissions } from '../../../api/notes/types'
import { buildStateFromServerPermissions } from './build-state-from-server-permissions'
import type { NoteDetails } from '../types/note-details'
describe('build state from server permissions', () => {
it('creates a new state with the given permissions', () => {
const state: NoteDetails = { ...initialState }
const permissions: NotePermissions = {
owner: 'test-owner',
sharedToUsers: [
{
username: 'test-user',
canEdit: true
}
],
sharedToGroups: [
{
groupName: 'test-group',
canEdit: false
}
]
}
expect(buildStateFromServerPermissions(state, permissions)).toStrictEqual({ ...state, permissions: permissions })
})
})

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NoteDetails } from '../types/note-details'
import type { NotePermissions } from '../../../api/notes/types'
/**
* Builds the updated state from a given previous state and updated NotePermissions data.
* @param state The previous note details state.
* @param serverPermissions The updated NotePermissions data.
*/
export const buildStateFromServerPermissions = (
state: NoteDetails,
serverPermissions: NotePermissions
): NoteDetails => {
return {
...state,
permissions: serverPermissions
}
}

View file

@ -3,8 +3,6 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NoteDto } from '../../../api/notes/types'
import { buildStateFromServerDto } from './build-state-from-set-note-data-from-server'
import * as buildStateFromUpdatedMarkdownContentModule from '../build-state-from-updated-markdown-content'
import { Mock } from 'ts-mockery'
@ -12,6 +10,7 @@ import type { NoteDetails } from '../types/note-details'
import { NoteTextDirection, NoteType } from '../types/note-details'
import { DateTime } from 'luxon'
import { initialSlideOptions } from '../initial-state'
import type { Note } from '../../../api/notes/types'
describe('build state from set note data from server', () => {
const buildStateFromUpdatedMarkdownContentMock = jest.spyOn(
@ -29,54 +28,42 @@ describe('build state from set note data from server', () => {
})
it('builds a new state from the given note dto', () => {
const noteDto: NoteDto = {
const noteDto: Note = {
content: 'line1\nline2',
metadata: {
primaryAddress: 'alias',
version: 5678,
alias: 'alias',
aliases: [
{
noteId: 'id',
primaryAlias: true,
name: 'alias'
}
],
id: 'id',
createTime: '2012-05-25T09:08:34.123',
createdAt: '2012-05-25T09:08:34.123',
description: 'description',
editedBy: ['editedBy'],
permissions: {
owner: {
username: 'username',
photo: 'photo',
email: 'email',
displayName: 'displayName'
},
owner: 'username',
sharedToGroups: [
{
canEdit: true,
group: {
displayName: 'groupdisplayname',
name: 'groupname',
special: true
}
groupName: 'groupName'
}
],
sharedToUsers: [
{
canEdit: true,
user: {
username: 'shareusername',
email: 'shareemail',
photo: 'sharephoto',
displayName: 'sharedisplayname'
}
username: 'shareusername'
}
]
},
viewCount: 987,
tags: ['tag'],
title: 'title',
updateTime: '2020-05-25T09:08:34.123',
updateUser: {
username: 'updateusername',
photo: 'updatephoto',
email: 'updateemail',
displayName: 'updatedisplayname'
}
updatedAt: '2020-05-25T09:08:34.123',
updateUsername: 'updateusername'
},
editedByAtPosition: [
{
@ -84,7 +71,7 @@ describe('build state from set note data from server', () => {
createdAt: 'createdAt',
startPos: 9,
updatedAt: 'updatedAt',
userName: 'userName'
username: 'userName'
}
]
}
@ -117,7 +104,7 @@ describe('build state from set note data from server', () => {
lineOffset: 0,
slideOptions: initialSlideOptions
},
noteTitle: '',
title: 'title',
selection: { from: 0 },
markdownContent: {
plain: 'line1\nline2',
@ -127,14 +114,35 @@ describe('build state from set note data from server', () => {
firstHeading: '',
rawFrontmatter: '',
id: 'id',
createTime: DateTime.fromISO('2012-05-25T09:08:34.123'),
lastChange: {
username: 'updateusername',
timestamp: DateTime.fromISO('2020-05-25T09:08:34.123')
},
createdAt: DateTime.fromISO('2012-05-25T09:08:34.123'),
updatedAt: DateTime.fromISO('2020-05-25T09:08:34.123'),
updateUsername: 'updateusername',
viewCount: 987,
alias: 'alias',
authorship: ['editedBy']
aliases: [
{
name: 'alias',
noteId: 'id',
primaryAlias: true
}
],
primaryAddress: 'alias',
version: 5678,
editedBy: ['editedBy'],
permissions: {
owner: 'username',
sharedToGroups: [
{
canEdit: true,
groupName: 'groupName'
}
],
sharedToUsers: [
{
canEdit: true,
username: 'shareusername'
}
]
}
}
const result = buildStateFromServerDto(noteDto)

View file

@ -4,19 +4,19 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NoteDto } from '../../../api/notes/types'
import type { NoteDetails } from '../types/note-details'
import { buildStateFromUpdatedMarkdownContent } from '../build-state-from-updated-markdown-content'
import { initialState } from '../initial-state'
import { DateTime } from 'luxon'
import { calculateLineStartIndexes } from '../calculate-line-start-indexes'
import type { Note } from '../../../api/notes/types'
/**
* Builds a {@link NoteDetails} redux state from a DTO received as an API response.
* @param dto The first DTO received from the API containing the relevant information about the note.
* @return An updated {@link NoteDetails} redux state.
*/
export const buildStateFromServerDto = (dto: NoteDto): NoteDetails => {
export const buildStateFromServerDto = (dto: Note): NoteDetails => {
const newState = convertNoteDtoToNoteDetails(dto)
return buildStateFromUpdatedMarkdownContent(newState, newState.markdownContent.plain)
}
@ -27,24 +27,26 @@ export const buildStateFromServerDto = (dto: NoteDto): NoteDetails => {
* @param note The NoteDTO as defined in the backend.
* @return The NoteDetails object corresponding to the DTO.
*/
const convertNoteDtoToNoteDetails = (note: NoteDto): NoteDetails => {
const convertNoteDtoToNoteDetails = (note: Note): NoteDetails => {
const newLines = note.content.split('\n')
return {
...initialState,
updateUsername: note.metadata.updateUsername,
permissions: note.metadata.permissions,
editedBy: note.metadata.editedBy,
primaryAddress: note.metadata.primaryAddress,
id: note.metadata.id,
aliases: note.metadata.aliases,
title: note.metadata.title,
version: note.metadata.version,
viewCount: note.metadata.viewCount,
markdownContent: {
plain: note.content,
lines: newLines,
lineStartIndexes: calculateLineStartIndexes(newLines)
},
rawFrontmatter: '',
id: note.metadata.id,
createTime: DateTime.fromISO(note.metadata.createTime),
lastChange: {
username: note.metadata.updateUser.username,
timestamp: DateTime.fromISO(note.metadata.updateTime)
},
viewCount: note.metadata.viewCount,
alias: note.metadata.alias,
authorship: note.metadata.editedBy
createdAt: DateTime.fromISO(note.metadata.createdAt),
updatedAt: DateTime.fromISO(note.metadata.updatedAt)
}
}

View file

@ -1,16 +1,17 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Action } from 'redux'
import type { NoteDto } from '../../api/notes/types'
import type { Note, NotePermissions } from '../../api/notes/types'
import type { CursorSelection } from '../editor/types'
export enum NoteDetailsActionType {
SET_DOCUMENT_CONTENT = 'note-details/content/set',
SET_NOTE_DATA_FROM_SERVER = 'note-details/data/server/set',
SET_NOTE_PERMISSIONS_FROM_SERVER = 'note-details/data/permissions/set',
UPDATE_NOTE_TITLE_BY_FIRST_HEADING = 'note-details/update-note-title-by-first-heading',
UPDATE_TASK_LIST_CHECKBOX = 'note-details/update-task-list-checkbox',
UPDATE_CURSOR_POSITION = 'note-details/updateCursorPosition',
@ -44,6 +45,7 @@ export enum FormatType {
export type NoteDetailsActions =
| SetNoteDocumentContentAction
| SetNoteDetailsFromServerAction
| SetNotePermissionsFromServerAction
| UpdateNoteTitleByFirstHeadingAction
| UpdateTaskListCheckboxAction
| UpdateCursorPositionAction
@ -65,7 +67,15 @@ export interface SetNoteDocumentContentAction extends Action<NoteDetailsActionTy
*/
export interface SetNoteDetailsFromServerAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER
dto: NoteDto
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
}
/**

View file

@ -8,31 +8,26 @@ import type { DateTime } from 'luxon'
import type { SlideOptions } from './slide-show-options'
import type { ISO6391 } from './iso6391'
import type { CursorSelection } from '../../editor/types'
import type { NoteMetadata } from '../../../api/notes/types'
type UnnecessaryNoteAttributes = 'updatedAt' | 'createdAt' | 'tags' | 'description'
/**
* Redux state containing the currently loaded note with its content and metadata.
*/
export interface NoteDetails {
export interface NoteDetails extends Omit<NoteMetadata, UnnecessaryNoteAttributes> {
updatedAt: DateTime
createdAt: DateTime
markdownContent: {
plain: string
lines: string[]
lineStartIndexes: number[]
}
selection: CursorSelection
firstHeading?: string
rawFrontmatter: string
frontmatter: NoteFrontmatter
frontmatterRendererInfo: RendererFrontmatterInfo
id: string
createTime: DateTime
lastChange: {
username: string
timestamp: DateTime
}
viewCount: number
alias: string
authorship: string[]
noteTitle: string
firstHeading?: string
}
export type Iso6391Language = typeof ISO6391[number]

View file

@ -9,7 +9,6 @@ import { combineReducers } from 'redux'
import { UserReducer } from './user/reducers'
import { ConfigReducer } from './config/reducers'
import { MotdReducer } from './motd/reducers'
import { ApiUrlReducer } from './api-url/reducers'
import { HistoryReducer } from './history/reducers'
import { EditorConfigReducer } from './editor/reducers'
import { DarkModeConfigReducer } from './dark-mode/reducers'
@ -22,7 +21,6 @@ export const allReducers: Reducer<ApplicationState> = combineReducers<Applicatio
user: UserReducer,
config: ConfigReducer,
motd: MotdReducer,
apiUrl: ApiUrlReducer,
history: HistoryReducer,
editorConfig: EditorConfigReducer,
darkMode: DarkModeConfigReducer,

View file

@ -5,10 +5,15 @@
*/
import { store } from '..'
import type { ClearUserAction, SetUserAction, UserState } from './types'
import type { ClearUserAction, SetUserAction } from './types'
import { UserActionType } from './types'
import type { LoginUserInfo } from '../../api/me/types'
export const setUser: (state: UserState) => void = (state: UserState) => {
/**
* Sets the given user state into the redux.
* @param state The user state to set into the redux.
*/
export const setUser = (state: LoginUserInfo): void => {
const action: SetUserAction = {
type: UserActionType.SET_USER,
state
@ -16,6 +21,9 @@ export const setUser: (state: UserState) => void = (state: UserState) => {
store.dispatch(action)
}
/**
* Clears the user state from the redux.
*/
export const clearUser: () => void = () => {
const action: ClearUserAction = {
type: UserActionType.CLEAR_USER

View file

@ -5,6 +5,7 @@
*/
import type { Action } from 'redux'
import type { LoginUserInfo } from '../../api/me/types'
export enum UserActionType {
SET_USER = 'user/set',
@ -15,32 +16,11 @@ export type UserActions = SetUserAction | ClearUserAction
export interface SetUserAction extends Action<UserActionType> {
type: UserActionType.SET_USER
state: UserState
state: LoginUserInfo
}
export interface ClearUserAction extends Action<UserActionType> {
type: UserActionType.CLEAR_USER
}
export interface UserState {
username: string
displayName: string
email: string
photo: string
provider: LoginProvider
}
export enum LoginProvider {
FACEBOOK = 'facebook',
GITHUB = 'github',
TWITTER = 'twitter',
GITLAB = 'gitlab',
DROPBOX = 'dropbox',
GOOGLE = 'google',
SAML = 'saml',
OAUTH2 = 'oauth2',
LOCAL = 'local',
LDAP = 'ldap'
}
export type OptionalUserState = UserState | null
export type OptionalUserState = LoginUserInfo | null