Fix Communication between frontend and backend (#1201)

Co-authored-by: Tilman Vatteroth <git@tilmanvatteroth.de>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Philip Molares 2021-05-01 23:01:42 +02:00 committed by GitHub
parent 4a18e51c83
commit 9cf7980334
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 268 additions and 164 deletions

View file

@ -26,7 +26,7 @@ describe('History', () => {
describe('Pinning', () => { describe('Pinning', () => {
describe('working', () => { describe('working', () => {
beforeEach(() => { beforeEach(() => {
cy.intercept('PUT', '/api/private/history/features', (req) => { cy.intercept('PUT', '/api/private/me/history/features', (req) => {
req.reply(200, req.body) req.reply(200, req.body)
}) })
}) })
@ -60,7 +60,7 @@ describe('History', () => {
describe('failing', () => { describe('failing', () => {
beforeEach(() => { beforeEach(() => {
cy.intercept('PUT', '/api/private/history/features', { cy.intercept('PUT', '/api/private/me/history/features', {
statusCode: 401 statusCode: 401
}) })
}) })

View file

@ -45,7 +45,7 @@ export const config = {
saml: 'aufSAMLn.de' saml: 'aufSAMLn.de'
}, },
maxDocumentLength: 200, maxDocumentLength: 200,
specialLinks: { specialUrls: {
privacy: 'https://example.com/privacy', privacy: 'https://example.com/privacy',
termsOfUse: 'https://example.com/termsOfUse', termsOfUse: 'https://example.com/termsOfUse',
imprint: 'https://example.com/imprint' imprint: 'https://example.com/imprint'
@ -57,8 +57,8 @@ export const config = {
issueTrackerUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' issueTrackerUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
}, },
'iframeCommunication': { 'iframeCommunication': {
'editorOrigin': 'http://127.0.0.1:3001', 'editorOrigin': 'http://127.0.0.1:3001/',
'rendererOrigin': 'http://127.0.0.1:3001' 'rendererOrigin': 'http://127.0.0.1:3001/'
} }
} }

View file

@ -18,16 +18,21 @@ Cypress.Commands.add('visitTestEditor', (query?: string) => {
beforeEach(() => { beforeEach(() => {
cy.intercept(`/api/private/notes/${ testNoteId }-get`, { cy.intercept(`/api/private/notes/${ testNoteId }-get`, {
'id': 'ABC123', "content": "",
'alias': 'banner', "metadata": {
'lastChange': { "id": "ABC11",
'userId': 'test', "alias": "banner",
'timestamp': 1600033920 "version": 2,
}, "viewCount": 0,
'viewCount': 0, "updateTime": "2021-04-24T09:27:51.000Z",
'createTime': 1600033920, "updateUser": {
'content': '', "userName": "test",
'authorship': [], "displayName": "Testy",
'preVersionTwoNote': true "photo": "",
"email": ""
},
"createTime": "2021-04-24T09:27:51.000Z",
"editedBy": []
}
}) })
}) })

View file

@ -30,18 +30,19 @@
"maxDocumentLength": 100000, "maxDocumentLength": 100000,
"useImageProxy": false, "useImageProxy": false,
"plantumlServer": "https://www.plantuml.com/plantuml", "plantumlServer": "https://www.plantuml.com/plantuml",
"specialLinks": { "specialUrls": {
"privacy": "https://example.com/privacy", "privacy": "https://example.com/privacy",
"termsOfUse": "https://example.com/termsOfUse", "termsOfUse": "https://example.com/termsOfUse",
"imprint": "https://example.com/imprint" "imprint": "https://example.com/imprint"
}, },
"version": { "version": {
"version": "mock", "major": -1,
"sourceCodeUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "minor": -1,
"issueTrackerUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" "patch": -1,
"commit": "mock"
}, },
"iframeCommunication": { "iframeCommunication": {
"editorOrigin": "http://localhost:3001", "editorOrigin": "http://localhost:3001/",
"rendererOrigin": "http://localhost:3001" "rendererOrigin": "http://localhost:3001/"
} }
} }

View file

@ -1,13 +0,0 @@
{
"id": "ABC123",
"alias": "banner",
"lastChange": {
"userId": "test",
"timestamp": 1600033920
},
"viewcount": 0,
"createtime": 1600033920,
"content": "This is the test banner text",
"authorship": [],
"preVersionTwoNote": true
}

View file

@ -0,0 +1,18 @@
{
"content": "This is the test banner text",
"metadata": {
"id": "ABC11",
"alias": "banner",
"version": 2,
"viewCount": 0,
"updateTime": "2021-04-24T09:27:51.000Z",
"updateUser": {
"userName": "test",
"displayName": "Testy",
"photo": "",
"email": ""
},
"createTime": "2021-04-24T09:27:51.000Z",
"editedBy": []
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,13 +0,0 @@
{
"id": "ABC123",
"alias": "old",
"lastChange": {
"userId": "test",
"timestamp": 1600033920
},
"viewcount": 0,
"createtime": 1600033920,
"content": "test123",
"authorship": [],
"preVersionTwoNote": false
}

View file

@ -0,0 +1,18 @@
{
"content": "test123",
"metadata": {
"id": "ABC3",
"alias": "old",
"version": 1,
"viewCount": 0,
"updateTime": "2021-04-24T09:27:51.000Z",
"updateUser": {
"userName": "test",
"displayName": "Testy",
"photo": "",
"email": ""
},
"createTime": "2021-04-24T09:27:51.000Z",
"editedBy": []
}
}

View file

@ -12,7 +12,7 @@ export interface Config {
banner: BannerConfig, banner: BannerConfig,
customAuthNames: CustomAuthNames, customAuthNames: CustomAuthNames,
useImageProxy: boolean, useImageProxy: boolean,
specialLinks: SpecialLinks, specialUrls: SpecialUrls,
version: BackendVersion, version: BackendVersion,
plantumlServer: string | null, plantumlServer: string | null,
maxDocumentLength: number, maxDocumentLength: number,
@ -35,9 +35,11 @@ export interface BannerConfig {
} }
export interface BackendVersion { export interface BackendVersion {
version: string, major: number
sourceCodeUrl: string minor: number
issueTrackerUrl: string patch: number
preRelease?: string
commit?: string
} }
export interface AuthProvidersState { export interface AuthProvidersState {
@ -60,7 +62,7 @@ export interface CustomAuthNames {
saml: string; saml: string;
} }
export interface SpecialLinks { export interface SpecialUrls {
privacy: string, privacy: string,
termsOfUse: string, termsOfUse: string,
imprint: string, imprint: string,

11
src/api/group/types.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface GroupInfoDto {
name: string
displayName: string
special: boolean
}

View file

@ -8,13 +8,13 @@ import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
import { HistoryEntryDto, HistoryEntryPutDto, HistoryEntryUpdateDto } from './types' import { HistoryEntryDto, HistoryEntryPutDto, HistoryEntryUpdateDto } from './types'
export const getHistory = async (): Promise<HistoryEntryDto[]> => { export const getHistory = async (): Promise<HistoryEntryDto[]> => {
const response = await fetch(getApiUrl() + '/history') const response = await fetch(getApiUrl() + '/me/history')
expectResponseCode(response) expectResponseCode(response)
return await response.json() as Promise<HistoryEntryDto[]> return await response.json() as Promise<HistoryEntryDto[]>
} }
export const postHistory = async (entries: HistoryEntryPutDto[]): Promise<void> => { export const postHistory = async (entries: HistoryEntryPutDto[]): Promise<void> => {
const response = await fetch(getApiUrl() + '/history', { const response = await fetch(getApiUrl() + '/me/history', {
...defaultFetchConfig, ...defaultFetchConfig,
method: 'POST', method: 'POST',
body: JSON.stringify(entries) body: JSON.stringify(entries)
@ -23,7 +23,7 @@ export const postHistory = async (entries: HistoryEntryPutDto[]): Promise<void>
} }
export const updateHistoryEntryPinStatus = async (noteId: string, entry: HistoryEntryUpdateDto): Promise<void> => { export const updateHistoryEntryPinStatus = async (noteId: string, entry: HistoryEntryUpdateDto): Promise<void> => {
const response = await fetch(getApiUrl() + '/history/' + noteId, { const response = await fetch(getApiUrl() + '/me/history/' + noteId, {
...defaultFetchConfig, ...defaultFetchConfig,
method: 'PUT', method: 'PUT',
body: JSON.stringify(entry) body: JSON.stringify(entry)
@ -32,7 +32,7 @@ export const updateHistoryEntryPinStatus = async (noteId: string, entry: History
} }
export const deleteHistoryEntry = async (noteId: string): Promise<void> => { export const deleteHistoryEntry = async (noteId: string): Promise<void> => {
const response = await fetch(getApiUrl() + '/history/' + noteId, { const response = await fetch(getApiUrl() + '/me/history/' + noteId, {
...defaultFetchConfig, ...defaultFetchConfig,
method: 'DELETE' method: 'DELETE'
}) })
@ -40,7 +40,7 @@ export const deleteHistoryEntry = async (noteId: string): Promise<void> => {
} }
export const deleteHistory = async (): Promise<void> => { export const deleteHistory = async (): Promise<void> => {
const response = await fetch(getApiUrl() + '/history', { const response = await fetch(getApiUrl() + '/me/history', {
...defaultFetchConfig, ...defaultFetchConfig,
method: 'DELETE' method: 'DELETE'
}) })

View file

@ -6,9 +6,10 @@
import { UserResponse } from '../users/types' import { UserResponse } from '../users/types'
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils' import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
import { isMockMode } from '../../utils/test-modes'
export const getMe = async (): Promise<UserResponse> => { export const getMe = async (): Promise<UserResponse> => {
const response = await fetch(getApiUrl() + '/me', { const response = await fetch(getApiUrl() + `/me${ isMockMode() ? '-get' : '' }`, {
...defaultFetchConfig ...defaultFetchConfig
}) })
expectResponseCode(response) expectResponseCode(response)

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NoteDto } from './types'
import { NoteDetails } from '../../redux/note-details/types'
import { DateTime } from 'luxon'
import { initialState } from '../../redux/note-details/reducers'
export const noteDtoToNoteDetails = (note: NoteDto): NoteDetails => {
return {
markdownContent: note.content,
frontmatter: initialState.frontmatter,
id: note.metadata.id,
noteTitle: initialState.noteTitle,
createTime: DateTime.fromISO(note.metadata.createTime),
lastChange: {
userName: note.metadata.updateUser.userName,
timestamp: DateTime.fromISO(note.metadata.updateTime)
},
firstHeading: initialState.firstHeading,
viewCount: note.metadata.viewCount,
alias: note.metadata.alias,
authorship: note.metadata.editedBy
}
}

View file

@ -5,31 +5,17 @@
*/ */
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils' import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
import { NoteDto } from './types'
import { isMockMode } from '../../utils/test-modes'
interface LastChange { export const getNote = async (noteId: string): Promise<NoteDto> => {
userId: string
timestamp: number
}
export interface Note {
id: string
alias: string
lastChange: LastChange
viewCount: number
createTime: number
content: string
authorship: number[]
preVersionTwoNote: boolean
}
export const getNote = async (noteId: string): Promise<Note> => {
// The "-get" suffix is necessary, because in our mock api (filesystem) the note id might already be a folder. // The "-get" suffix is necessary, because in our mock api (filesystem) the note id might already be a folder.
// TODO: [mrdrogdrog] replace -get with actual api route as soon as api backend is ready. // TODO: [mrdrogdrog] replace -get with actual api route as soon as api backend is ready.
const response = await fetch(getApiUrl() + `/notes/${ noteId }-get`, { const response = await fetch(getApiUrl() + `/notes/${ noteId }${ isMockMode() ? '-get' : '' }`, {
...defaultFetchConfig ...defaultFetchConfig
}) })
expectResponseCode(response) expectResponseCode(response)
return await response.json() as Promise<Note> return await response.json() as Promise<NoteDto>
} }
export const deleteNote = async (noteId: string): Promise<void> => { export const deleteNote = async (noteId: string): Promise<void> => {

53
src/api/notes/types.d.ts vendored Normal file
View file

@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { UserInfoDto } from '../users/types'
import { GroupInfoDto } from '../group/types'
export interface NoteDto {
content: string
metadata: NoteMetadataDto
editedByAtPosition: NoteAuthorshipDto[]
}
export interface NoteMetadataDto {
id: string
alias: string
version: number
title: string
description: string
tags: string[]
updateTime: string
updateUser: UserInfoDto
viewCount: number
createTime: string
editedBy: string[]
permissions: NotePermissionsDto
}
export interface NoteAuthorshipDto {
userName: string
startPos: number
endPos: number
createdAt: string
updatedAt: string
}
export interface NotePermissionsDto {
owner: UserInfoDto
sharedToUsers: NoteUserPermissionEntryDto[]
sharedToGroups: NoteGroupPermissionEntryDto[]
}
export interface NoteUserPermissionEntryDto {
user: UserInfoDto
canEdit: boolean
}
export interface NoteGroupPermissionEntryDto {
group: GroupInfoDto
canEdit: boolean
}

View file

@ -12,3 +12,10 @@ export interface UserResponse {
photo: string photo: string
provider: LoginProvider provider: LoginProvider
} }
export interface UserInfoDto {
userName: string
displayName: string
photo: string
email: string
}

View file

@ -9,6 +9,7 @@ import { Redirect } from 'react-router'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { getNote } from '../../../api/notes' import { getNote } from '../../../api/notes'
import { NotFoundErrorScreen } from './not-found-error-screen' import { NotFoundErrorScreen } from './not-found-error-screen'
import { NoteDto } from '../../../api/notes/types'
interface RouteParameters { interface RouteParameters {
id: string id: string
@ -20,7 +21,7 @@ export const Redirector: React.FC = () => {
useEffect(() => { useEffect(() => {
getNote(id) getNote(id)
.then((noteFromAPI) => setError(!noteFromAPI.preVersionTwoNote)) .then((noteFromAPI: NoteDto) => setError(noteFromAPI.metadata.version !== 1))
.catch(() => setError(true)) .catch(() => setError(true))
}, [id]) }, [id])

View file

@ -48,7 +48,7 @@ export const DocumentReadOnlyPage: React.FC = () => {
</div> </div>
<ShowIf condition={ !error && !loading }> <ShowIf condition={ !error && !loading }>
<DocumentInfobar <DocumentInfobar
changedAuthor={ noteDetails.lastChange.userId ?? '' } changedAuthor={ noteDetails.lastChange.userName ?? '' }
changedTime={ noteDetails.lastChange.timestamp } changedTime={ noteDetails.lastChange.timestamp }
createdAuthor={ 'Test' } createdAuthor={ 'Test' }
createdTime={ noteDetails.createTime } createdTime={ noteDetails.createTime }

View file

@ -7,16 +7,13 @@
import React from 'react' import React from 'react'
import { Col, Row } from 'react-bootstrap' import { Col, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import links from '../../../../links.json' import links from '../../../../links.json'
import { ApplicationState } from '../../../../redux'
import { TranslatedExternalLink } from '../../../common/links/translated-external-link' import { TranslatedExternalLink } from '../../../common/links/translated-external-link'
import { TranslatedInternalLink } from '../../../common/links/translated-internal-link' import { TranslatedInternalLink } from '../../../common/links/translated-internal-link'
export const Links: React.FC = () => { export const Links: React.FC = () => {
useTranslation() useTranslation()
const backendIssueTracker = useSelector((state: ApplicationState) => state.config.version.issueTrackerUrl)
return ( return (
<Row className={ 'justify-content-center pt-4' }> <Row className={ 'justify-content-center pt-4' }>
<Col lg={ 4 }> <Col lg={ 4 }>
@ -43,7 +40,7 @@ export const Links: React.FC = () => {
<li> <li>
<TranslatedExternalLink <TranslatedExternalLink
i18nKey='editor.help.contacts.reportIssue' i18nKey='editor.help.contacts.reportIssue'
href={ backendIssueTracker } href={ links.backendIssues }
icon='tag' icon='tag'
className='text-primary' className='text-primary'
/> />

View file

@ -8,7 +8,7 @@ import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated' import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated'
import { ApplicationState } from '../../../redux' import { ApplicationState } from '../../../redux'
import { isTestMode } from '../../../utils/is-test-mode' import { isTestMode } from '../../../utils/test-modes'
import { RendererProps } from '../../render-page/markdown-document' import { RendererProps } from '../../render-page/markdown-document'
import { ImageDetails, RendererType } from '../../render-page/rendering-message' import { ImageDetails, RendererType } from '../../render-page/rendering-message'
import { useContextOrStandaloneIframeCommunicator } from '../render-context/iframe-communicator-context-provider' import { useContextOrStandaloneIframeCommunicator } from '../render-context/iframe-communicator-context-provider'
@ -44,7 +44,7 @@ export const RenderIframe: React.FC<RenderIframeProps> = (
const frameReference = useRef<HTMLIFrameElement>(null) const frameReference = useRef<HTMLIFrameElement>(null)
const rendererOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.rendererOrigin) const rendererOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.rendererOrigin)
const renderPageUrl = `${ rendererOrigin }/render` const renderPageUrl = `${ rendererOrigin }render`
const resetRendererReady = useCallback(() => setRendererReady(false), []) const resetRendererReady = useCallback(() => setRendererReady(false), [])
const iframeCommunicator = useContextOrStandaloneIframeCommunicator() const iframeCommunicator = useContextOrStandaloneIframeCommunicator()
const onIframeLoad = useOnIframeLoad(frameReference, iframeCommunicator, rendererOrigin, renderPageUrl, resetRendererReady) const onIframeLoad = useOnIframeLoad(frameReference, iframeCommunicator, rendererOrigin, renderPageUrl, resetRendererReady)

View file

@ -1,7 +1,7 @@
/* /*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { Fragment, useCallback, useState } from 'react' import React, { Fragment, useCallback, useState } from 'react'
@ -46,23 +46,19 @@ export const HistoryContent: React.FC<HistoryContentProps> = ({ viewState, entri
const [lastPageIndex, setLastPageIndex] = useState(0) const [lastPageIndex, setLastPageIndex] = useState(0)
const onPinClick = useCallback((noteId: string) => { const onPinClick = useCallback((noteId: string) => {
toggleHistoryEntryPinning(noteId).catch( toggleHistoryEntryPinning(noteId)
showErrorNotification(t('landing.history.error.updateEntry.text')) .catch(showErrorNotification(t('landing.history.error.updateEntry.text')))
)
}, [t]) }, [t])
const onDeleteClick = useCallback((noteId: string) => { const onDeleteClick = useCallback((noteId: string) => {
deleteNote(noteId).then(() => { deleteNote(noteId)
return removeHistoryEntry(noteId) .then(() => removeHistoryEntry(noteId))
}).catch( .catch(showErrorNotification(t('landing.history.error.deleteNote.text')))
showErrorNotification(t('landing.history.error.deleteNote.text'))
)
}, [t]) }, [t])
const onRemoveClick = useCallback((noteId: string) => { const onRemoveClick = useCallback((noteId: string) => {
removeHistoryEntry(noteId).catch( removeHistoryEntry(noteId)
showErrorNotification(t('landing.history.error.deleteEntry.text')) .catch(showErrorNotification(t('landing.history.error.deleteEntry.text')))
)
}, [t]) }, [t])
if (entries.length === 0) { if (entries.length === 0) {

View file

@ -18,7 +18,7 @@ import equal from 'fast-deep-equal'
export const PoweredByLinks: React.FC = () => { export const PoweredByLinks: React.FC = () => {
useTranslation() useTranslation()
const specialLinks = useSelector((state: ApplicationState) => Object.entries(state.config.specialLinks) as [string, string][], equal) const specialUrls: [string, string][] = useSelector((state: ApplicationState) => Object.entries(state.config.specialUrls) as [string, string][], equal)
return ( return (
<p> <p>
@ -28,7 +28,7 @@ export const PoweredByLinks: React.FC = () => {
&nbsp;|&nbsp; &nbsp;|&nbsp;
<TranslatedInternalLink href='/n/release-notes' i18nKey='landing.footer.releases'/> <TranslatedInternalLink href='/n/release-notes' i18nKey='landing.footer.releases'/>
{ {
specialLinks.map(([i18nKey, href]) => specialUrls.map(([i18nKey, href]) =>
<Fragment key={ i18nKey }> <Fragment key={ i18nKey }>
&nbsp;|&nbsp; &nbsp;|&nbsp;
<TranslatedExternalLink href={ href } i18nKey={ 'landing.footer.' + i18nKey }/> <TranslatedExternalLink href={ href } i18nKey={ 'landing.footer.' + i18nKey }/>

View file

@ -4,17 +4,32 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React from 'react' import React, { useMemo } from 'react'
import { CommonModal, CommonModalProps } from '../../../common/modals/common-modal' import { CommonModal, CommonModalProps } from '../../../common/modals/common-modal'
import { Modal, Row } from 'react-bootstrap' import { Modal, Row } from 'react-bootstrap'
import { VersionInfoModalColumn } from './version-info-modal-column' import { VersionInfoModalColumn } from './version-info-modal-column'
import frontendVersion from '../../../../version.json' import frontendVersion from '../../../../version.json'
import links from '../../../../links.json'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../redux' import { ApplicationState } from '../../../../redux'
import equal from 'fast-deep-equal' import equal from 'fast-deep-equal'
import { BackendVersion } from '../../../../api/config/types'
export const VersionInfoModal: React.FC<CommonModalProps> = ({ onHide, show }) => { export const VersionInfoModal: React.FC<CommonModalProps> = ({ onHide, show }) => {
const serverVersion = useSelector((state: ApplicationState) => state.config.version, equal) const serverVersion: BackendVersion = useSelector((state: ApplicationState) => state.config.version, equal)
const backendVersion = useMemo(() => {
const version = `${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`
if (serverVersion.preRelease) {
return `${version}-${serverVersion.preRelease}`
}
if (serverVersion.commit) {
return serverVersion.commit
}
return version
}, [serverVersion])
return ( return (
<CommonModal data-cy={ 'version-modal' } show={ show } onHide={ onHide } closeButton={ true } <CommonModal data-cy={ 'version-modal' } show={ show } onHide={ onHide } closeButton={ true }
@ -23,9 +38,9 @@ export const VersionInfoModal: React.FC<CommonModalProps> = ({ onHide, show }) =
<Row> <Row>
<VersionInfoModalColumn <VersionInfoModalColumn
titleI18nKey={ 'landing.versionInfo.serverVersion' } titleI18nKey={ 'landing.versionInfo.serverVersion' }
version={ serverVersion.version } version={ backendVersion }
issueTrackerLink={ serverVersion.issueTrackerUrl } issueTrackerLink={ links.backendIssues }
sourceCodeLink={ serverVersion.sourceCodeUrl }/> sourceCodeLink={ links.backendSourceCode }/>
<VersionInfoModalColumn <VersionInfoModalColumn
titleI18nKey={ 'landing.versionInfo.clientVersion' } titleI18nKey={ 'landing.versionInfo.clientVersion' }
version={ frontendVersion.version } version={ frontendVersion.version }

View file

@ -14,6 +14,7 @@ import { ApplicationState } from '../../redux'
import { TranslatedExternalLink } from '../common/links/translated-external-link' import { TranslatedExternalLink } from '../common/links/translated-external-link'
import { ShowIf } from '../common/show-if/show-if' import { ShowIf } from '../common/show-if/show-if'
import { getAndSetUser } from '../login-page/auth/utils' import { getAndSetUser } from '../login-page/auth/utils'
import { SpecialUrls } from '../../api/config/types'
export enum RegisterError { export enum RegisterError {
NONE = 'none', NONE = 'none',
@ -24,7 +25,7 @@ export enum RegisterError {
export const RegisterPage: React.FC = () => { export const RegisterPage: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const allowRegister = useSelector((state: ApplicationState) => state.config.allowRegister) const allowRegister = useSelector((state: ApplicationState) => state.config.allowRegister)
const specialLinks = useSelector((state: ApplicationState) => state.config.specialLinks) const specialUrls: SpecialUrls = useSelector((state: ApplicationState) => state.config.specialUrls)
const userExists = useSelector((state: ApplicationState) => !!state.user) const userExists = useSelector((state: ApplicationState) => !!state.user)
const [username, setUsername] = useState('') const [username, setUsername] = useState('')
@ -112,17 +113,17 @@ export const RegisterPage: React.FC = () => {
required required
/> />
</Form.Group> </Form.Group>
<ShowIf condition={ !!specialLinks?.termsOfUse || !!specialLinks?.privacy }> <ShowIf condition={ !!specialUrls?.termsOfUse || !!specialUrls?.privacy }>
<Trans i18nKey='login.register.infoTermsPrivacy'/> <Trans i18nKey='login.register.infoTermsPrivacy'/>
<ul> <ul>
<ShowIf condition={ !!specialLinks?.termsOfUse }> <ShowIf condition={ !!specialUrls?.termsOfUse }>
<li> <li>
<TranslatedExternalLink i18nKey='landing.footer.termsOfUse' href={ specialLinks.termsOfUse }/> <TranslatedExternalLink i18nKey='landing.footer.termsOfUse' href={ specialUrls.termsOfUse }/>
</li> </li>
</ShowIf> </ShowIf>
<ShowIf condition={ !!specialLinks?.privacy }> <ShowIf condition={ !!specialUrls?.privacy }>
<li> <li>
<TranslatedExternalLink i18nKey='landing.footer.privacy' href={ specialLinks.privacy }/> <TranslatedExternalLink i18nKey='landing.footer.privacy' href={ specialUrls.privacy }/>
</li> </li>
</ShowIf> </ShowIf>
</ul> </ul>

View file

@ -22,7 +22,7 @@ import { store } from './redux'
import * as serviceWorkerRegistration from './service-worker-registration' import * as serviceWorkerRegistration from './service-worker-registration'
import './style/dark.scss' import './style/dark.scss'
import './style/index.scss' import './style/index.scss'
import { isTestMode } from './utils/is-test-mode' import { isTestMode } from './utils/test-modes'
const EditorPage = React.lazy(() => import(/* webpackPrefetch: true *//* webpackChunkName: "editor" */ './components/editor-page/editor-page')) const EditorPage = React.lazy(() => import(/* webpackPrefetch: true *//* webpackChunkName: "editor" */ './components/editor-page/editor-page'))
const RenderPage = React.lazy(() => import (/* webpackPrefetch: true *//* webpackChunkName: "renderPage" */ './components/render-page/render-page')) const RenderPage = React.lazy(() => import (/* webpackPrefetch: true *//* webpackChunkName: "renderPage" */ './components/render-page/render-page'))

View file

@ -3,6 +3,8 @@
"community": "https://community.hedgedoc.org", "community": "https://community.hedgedoc.org",
"faq": "https://hedgedoc.org/faq/", "faq": "https://hedgedoc.org/faq/",
"githubOrg": "https://github.com/hedgedoc/", "githubOrg": "https://github.com/hedgedoc/",
"backendSourceCode": "https://github.com/hedgedoc/hedgedoc",
"backendIssues": "https://github.com/hedgedoc/hedgedoc/issues",
"mastodon": "https://social.hedgedoc.org", "mastodon": "https://social.hedgedoc.org",
"translate": "https://translate.hedgedoc.org", "translate": "https://translate.hedgedoc.org",
"webpage": "https://hedgedoc.org" "webpage": "https://hedgedoc.org"

View file

@ -8,7 +8,7 @@ import { Reducer } from 'redux'
import { BannerActions, BannerActionType, BannerState, SetBannerAction } from './types' import { BannerActions, BannerActionType, BannerState, SetBannerAction } from './types'
export const initialState: BannerState = { export const initialState: BannerState = {
show: true, show: false,
text: '', text: '',
timestamp: '' timestamp: ''
} }

View file

@ -40,15 +40,15 @@ export const initialState: Config = {
maxDocumentLength: 0, maxDocumentLength: 0,
useImageProxy: false, useImageProxy: false,
plantumlServer: null, plantumlServer: null,
specialLinks: { specialUrls: {
privacy: '', privacy: '',
termsOfUse: '', termsOfUse: '',
imprint: '' imprint: ''
}, },
version: { version: {
version: '', major: -1,
sourceCodeUrl: '', minor: -1,
issueTrackerUrl: '' patch: -1,
}, },
iframeCommunication: { iframeCommunication: {
editorOrigin: '', editorOrigin: '',

View file

@ -91,8 +91,8 @@ export const toggleHistoryEntryPinning = async (noteId: string): Promise<void> =
updateLocalHistoryEntry(noteId, entryToUpdate) updateLocalHistoryEntry(noteId, entryToUpdate)
} else { } else {
const historyUpdateDto = historyEntryToHistoryEntryUpdateDto(entryToUpdate) const historyUpdateDto = historyEntryToHistoryEntryUpdateDto(entryToUpdate)
updateHistoryEntryRedux(noteId, entryToUpdate)
await updateHistoryEntryPinStatus(noteId, historyUpdateDto) await updateHistoryEntryPinStatus(noteId, historyUpdateDto)
updateHistoryEntryRedux(noteId, entryToUpdate)
} }
} }

View file

@ -5,7 +5,7 @@
*/ */
import { store } from '..' import { store } from '..'
import { Note } from '../../api/notes' import { NoteDto } from '../../api/notes/types'
import { NoteFrontmatter } from '../../components/editor-page/note-frontmatter/note-frontmatter' import { NoteFrontmatter } from '../../components/editor-page/note-frontmatter/note-frontmatter'
import { initialState } from './reducers' import { initialState } from './reducers'
import { import {
@ -24,7 +24,7 @@ export const setNoteMarkdownContent = (content: string): void => {
} as SetNoteDetailsAction) } as SetNoteDetailsAction)
} }
export const setNoteDataFromServer = (apiResponse: Note): void => { export const setNoteDataFromServer = (apiResponse: NoteDto): void => {
store.dispatch({ store.dispatch({
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER, type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER,
note: apiResponse note: apiResponse

View file

@ -6,7 +6,6 @@
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import { Reducer } from 'redux' import { Reducer } from 'redux'
import { Note } from '../../api/notes'
import { import {
NoteFrontmatter, NoteFrontmatter,
NoteTextDirection, NoteTextDirection,
@ -22,6 +21,7 @@ import {
SetNoteFrontmatterFromRenderingAction, SetNoteFrontmatterFromRenderingAction,
UpdateNoteTitleByFirstHeadingAction UpdateNoteTitleByFirstHeadingAction
} from './types' } from './types'
import { noteDtoToNoteDetails } from '../../api/notes/dto-methods'
export const initialState: NoteDetails = { export const initialState: NoteDetails = {
markdownContent: '', markdownContent: '',
@ -29,10 +29,9 @@ export const initialState: NoteDetails = {
createTime: DateTime.fromSeconds(0), createTime: DateTime.fromSeconds(0),
lastChange: { lastChange: {
timestamp: DateTime.fromSeconds(0), timestamp: DateTime.fromSeconds(0),
userId: '' userName: ''
}, },
alias: '', alias: '',
preVersionTwoNote: false,
viewCount: 0, viewCount: 0,
authorship: [], authorship: [],
noteTitle: '', noteTitle: '',
@ -67,7 +66,7 @@ export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsAction> = (stat
noteTitle: generateNoteTitle(state.frontmatter, (action as UpdateNoteTitleByFirstHeadingAction).firstHeading) noteTitle: generateNoteTitle(state.frontmatter, (action as UpdateNoteTitleByFirstHeadingAction).firstHeading)
} }
case NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER: case NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER:
return convertNoteToNoteDetails((action as SetNoteDetailsFromServerAction).note) return noteDtoToNoteDetails((action as SetNoteDetailsFromServerAction).note)
case NoteDetailsActionType.SET_NOTE_FRONTMATTER: case NoteDetailsActionType.SET_NOTE_FRONTMATTER:
return { return {
...state, ...state,
@ -111,21 +110,4 @@ const generateNoteTitle = (frontmatter: NoteFrontmatter, firstHeading?: string)
} }
} }
const convertNoteToNoteDetails = (note: Note): NoteDetails => {
return {
markdownContent: note.content,
frontmatter: initialState.frontmatter,
id: note.id,
noteTitle: initialState.noteTitle,
createTime: DateTime.fromSeconds(note.createTime),
lastChange: {
userId: note.lastChange.userId,
timestamp: DateTime.fromSeconds(note.lastChange.timestamp)
},
firstHeading: initialState.firstHeading,
preVersionTwoNote: note.preVersionTwoNote,
viewCount: note.viewCount,
alias: note.alias,
authorship: note.authorship
}
}

View file

@ -6,8 +6,8 @@
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import { Action } from 'redux' import { Action } from 'redux'
import { Note } from '../../api/notes'
import { NoteFrontmatter } from '../../components/editor-page/note-frontmatter/note-frontmatter' import { NoteFrontmatter } from '../../components/editor-page/note-frontmatter/note-frontmatter'
import { NoteDto } from '../../api/notes/types'
export enum NoteDetailsActionType { export enum NoteDetailsActionType {
SET_DOCUMENT_CONTENT = 'note-details/set', SET_DOCUMENT_CONTENT = 'note-details/set',
@ -18,7 +18,7 @@ export enum NoteDetailsActionType {
} }
interface LastChange { interface LastChange {
userId: string userName: string
timestamp: DateTime timestamp: DateTime
} }
@ -27,10 +27,9 @@ export interface NoteDetails {
id: string id: string
createTime: DateTime createTime: DateTime
lastChange: LastChange lastChange: LastChange
preVersionTwoNote: boolean
viewCount: number viewCount: number
alias: string alias: string
authorship: number[] authorship: string[]
noteTitle: string noteTitle: string
firstHeading?: string firstHeading?: string
frontmatter: NoteFrontmatter frontmatter: NoteFrontmatter
@ -47,7 +46,7 @@ export interface SetNoteDetailsAction extends NoteDetailsAction {
export interface SetNoteDetailsFromServerAction extends NoteDetailsAction { export interface SetNoteDetailsFromServerAction extends NoteDetailsAction {
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER
note: Note note: NoteDto
} }
export interface UpdateNoteTitleByFirstHeadingAction extends NoteDetailsAction { export interface UpdateNoteTitleByFirstHeadingAction extends NoteDetailsAction {

View file

@ -39,9 +39,7 @@ export const dismissUiNotification = (notificationId: number): void => {
} as DismissUiNotificationAction) } as DismissUiNotificationAction)
} }
// Promises catch errors as any. export const showErrorNotification = (message: string) => (error: Error): void => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types
export const showErrorNotification = (message: string) => (error: any): void => {
console.error(message, error) console.error(message, error)
dispatchUiNotification(i18n.t('common.errorOccurred'), message, DEFAULT_DURATION_IN_SECONDS, 'exclamation-triangle') dispatchUiNotification(i18n.t('common.errorOccurred'), message, DEFAULT_DURATION_IN_SECONDS, 'exclamation-triangle')
} }

View file

@ -7,3 +7,7 @@
export const isTestMode = (): boolean => { export const isTestMode = (): boolean => {
return !!process.env.REACT_APP_TEST_MODE return !!process.env.REACT_APP_TEST_MODE
} }
export const isMockMode = (): boolean => {
return process.env.REACT_APP_BACKEND === undefined
}

View file

@ -1,5 +1,5 @@
{ {
"version": "0.0", "version": "0.0",
"sourceCodeUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "sourceCodeUrl": "https://github.com/hedgedoc/react-client",
"issueTrackerUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" "issueTrackerUrl": "https://github.com/hedgedoc/react-client/issues"
} }