diff --git a/cypress/integration/history.spec.ts b/cypress/integration/history.spec.ts index 7274c1010..60703a6f1 100644 --- a/cypress/integration/history.spec.ts +++ b/cypress/integration/history.spec.ts @@ -26,7 +26,7 @@ describe('History', () => { describe('Pinning', () => { describe('working', () => { beforeEach(() => { - cy.intercept('PUT', '/api/private/history/features', (req) => { + cy.intercept('PUT', '/api/private/me/history/features', (req) => { req.reply(200, req.body) }) }) @@ -60,7 +60,7 @@ describe('History', () => { describe('failing', () => { beforeEach(() => { - cy.intercept('PUT', '/api/private/history/features', { + cy.intercept('PUT', '/api/private/me/history/features', { statusCode: 401 }) }) diff --git a/cypress/support/config.ts b/cypress/support/config.ts index 4f6071dd8..9994c900e 100644 --- a/cypress/support/config.ts +++ b/cypress/support/config.ts @@ -45,7 +45,7 @@ export const config = { saml: 'aufSAMLn.de' }, maxDocumentLength: 200, - specialLinks: { + specialUrls: { privacy: 'https://example.com/privacy', termsOfUse: 'https://example.com/termsOfUse', imprint: 'https://example.com/imprint' @@ -57,8 +57,8 @@ export const config = { issueTrackerUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' }, 'iframeCommunication': { - 'editorOrigin': 'http://127.0.0.1:3001', - 'rendererOrigin': 'http://127.0.0.1:3001' + 'editorOrigin': 'http://127.0.0.1:3001/', + 'rendererOrigin': 'http://127.0.0.1:3001/' } } diff --git a/cypress/support/visit-test-editor.ts b/cypress/support/visit-test-editor.ts index 3d721c993..6ded85ffa 100644 --- a/cypress/support/visit-test-editor.ts +++ b/cypress/support/visit-test-editor.ts @@ -18,16 +18,21 @@ Cypress.Commands.add('visitTestEditor', (query?: string) => { beforeEach(() => { cy.intercept(`/api/private/notes/${ testNoteId }-get`, { - 'id': 'ABC123', - 'alias': 'banner', - 'lastChange': { - 'userId': 'test', - 'timestamp': 1600033920 - }, - 'viewCount': 0, - 'createTime': 1600033920, - 'content': '', - 'authorship': [], - 'preVersionTwoNote': true + "content": "", + "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": [] + } }) }) diff --git a/public/api/private/config b/public/api/private/config index da86543ab..d72f9942e 100644 --- a/public/api/private/config +++ b/public/api/private/config @@ -30,18 +30,19 @@ "maxDocumentLength": 100000, "useImageProxy": false, "plantumlServer": "https://www.plantuml.com/plantuml", - "specialLinks": { + "specialUrls": { "privacy": "https://example.com/privacy", "termsOfUse": "https://example.com/termsOfUse", "imprint": "https://example.com/imprint" }, "version": { - "version": "mock", - "sourceCodeUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - "issueTrackerUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + "major": -1, + "minor": -1, + "patch": -1, + "commit": "mock" }, "iframeCommunication": { - "editorOrigin": "http://localhost:3001", - "rendererOrigin": "http://localhost:3001" + "editorOrigin": "http://localhost:3001/", + "rendererOrigin": "http://localhost:3001/" } } diff --git a/public/api/private/me b/public/api/private/me-get similarity index 100% rename from public/api/private/me rename to public/api/private/me-get diff --git a/public/api/private/history b/public/api/private/me/history similarity index 100% rename from public/api/private/history rename to public/api/private/me/history diff --git a/public/api/private/notes/banner b/public/api/private/notes/banner deleted file mode 100644 index 3642978c1..000000000 --- a/public/api/private/notes/banner +++ /dev/null @@ -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 -} diff --git a/public/api/private/notes/banner-get b/public/api/private/notes/banner-get new file mode 100644 index 000000000..566b40220 --- /dev/null +++ b/public/api/private/notes/banner-get @@ -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": [] + } +} diff --git a/public/api/private/notes/features-get b/public/api/private/notes/features-get index 5e5550e24..e6b8c5d1e 100644 --- a/public/api/private/notes/features-get +++ b/public/api/private/notes/features-get @@ -1,13 +1,18 @@ { - "id": "ABC123", - "alias": "banner", - "lastChange": { - "userId": "test", - "timestamp": 1600033920 - }, - "viewCount": 0, - "createTime": 1600033920, "content": "---\ntitle: Features\ndescription: Many features, such wow!\nrobots: noindex\ntags:\n - hedgedoc\n - demo\n - react\nopengraph:\n title: Features\n---\n# Embedding demo\n[TOC]\n\n## markmap\n\n\n```markmap\n# MarkMap\n\n## Pro\n\n### written in typescript\n\n## Cons\n\n### must redeclare types\n```\n\n## Vega-Lite\n\n```vega-lite\n\n{\n \"$schema\": \"https://vega.github.io/schema/vega-lite/v5.json\",\n \"description\": \"Reproducing http://robslink.com/SAS/democd91/pyramid_pie.htm\",\n \"data\": {\n \"values\": [\n {\"category\": \"Sky\", \"value\": 75, \"order\": 3},\n {\"category\": \"Shady side of a pyramid\", \"value\": 10, \"order\": 1},\n {\"category\": \"Sunny side of a pyramid\", \"value\": 15, \"order\": 2}\n ]\n },\n \"mark\": {\"type\": \"arc\", \"outerRadius\": 80},\n \"encoding\": {\n \"theta\": {\n \"field\": \"value\", \"type\": \"quantitative\",\n \"scale\": {\"range\": [2.35619449, 8.639379797]},\n \"stack\": true\n },\n \"color\": {\n \"field\": \"category\", \"type\": \"nominal\",\n \"scale\": {\n \"domain\": [\"Sky\", \"Shady side of a pyramid\", \"Sunny side of a pyramid\"],\n \"range\": [\"#416D9D\", \"#674028\", \"#DEAC58\"]\n },\n \"legend\": {\n \"orient\": \"none\",\n \"title\": null,\n \"columns\": 1,\n \"legendX\": 200,\n \"legendY\": 80\n }\n },\n \"order\": {\n \"field\": \"order\"\n }\n },\n \"view\": {\"stroke\": null}\n}\n\n\n```\n\n## GraphViz\n\n```graphviz\ngraph {\n a -- b\n a -- b\n b -- a [color=blue]\n}\n```\n\n```graphviz\ndigraph structs {\n node [shape=record];\n struct1 [label=\" left| mid\ dle| right\"];\n struct2 [label=\" one| two\"];\n struct3 [label=\"hello\nworld |{ b |{c| d|e}| f}| g | h\"];\n struct1:f1 -> struct2:f0;\n struct1:f2 -> struct3:here;\n}\n```\n\n```graphviz\ndigraph G {\n main -> parse -> execute;\n main -> init;\n main -> cleanup;\n execute -> make_string;\n execute -> printf\n init -> make_string;\n main -> printf;\n execute -> compare;\n}\n```\n\n```graphviz\ndigraph D {\n node [fontname=\"Arial\"];\n node_A [shape=record label=\"shape=record|{above|middle|below}|right\"];\n node_B [shape=plaintext label=\"shape=plaintext|{curly|braces and|bars without}|effect\"];\n}\n```\n\n```graphviz\ndigraph D {\n A -> {B, C, D} -> {F}\n}\n```\n\n## High Res Image\n\n![Wheat Field with Cypresses](/img/highres.jpg)\n\n## Sequence Diagram (deprecated)\n\n```sequence\nTitle: Here is a title\nnote over A: asdd\nA->B: Normal line\nB-->C: Dashed line\nC->>D: Open arrow\nD-->>A: Dashed open arrow\nparticipant IOOO\n```\n\n## Mermaid\n\n```mermaid\ngantt\n title A Gantt Diagram\n\n section Section\n A task: a1, 2014-01-01, 30d\n Another task: after a1, 20d\n\n section Another\n Task in sec: 2014-01-12, 12d\n Another task: 24d\n```\n\n## Flowchart\n\n```flow\nst=>start: Start\ne=>end: End\nop=>operation: My Operation\nop2=>operation: lalala\ncond=>condition: Yes or No?\n\nst->op->op2->cond\ncond(yes)->e\ncond(no)->op2\n```\n\n## ABC\n\n```abc\nX:1\nT:Speed the Plough\nM:4/4\nC:Trad.\nK:G\n|:GABc dedB|dedB dedB|c2ec B2dB|c2A2 A2BA|\nGABc dedB|dedB dedB|c2ec B2dB|A2F2 G4:|\n|:g2gf gdBd|g2f2 e2d2|c2ec B2dB|c2A2 A2df|\ng2gf g2Bd|g2f2 e2d2|c2ec B2dB|A2F2 G4:|\n```\n\n## CSV\n\n```csv delimiter=; header\nUsername; Identifier;First name;Last name\n\"booker12; rbooker\";9012;Rachel;Booker\ngrey07;2070;Laura;Grey\njohnson81;4081;Craig;Johnson\njenkins46;9346;Mary;Jenkins\nsmith79;5079;Jamie;Smith\n```\n\n## some plain text\n\nLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.\n\n## KaTeX\nYou can render *LaTeX* mathematical expressions using **KaTeX**, as on [math.stackexchange.com](https://math.stackexchange.com/):\n\nThe *Gamma function* satisfying $\\Gamma(n) = (n-1)!\\quad\\forall n\\in\\mathbb N$ is via the Euler integral\n\n$$\nx = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}.\n$$\n\n$$\n\\Gamma(z) = \\int_0^\\infty t^{z-1}e^{-t}dt\\,.\n$$\n\n> More information about **LaTeX** mathematical expressions [here](https://meta.math.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference).\n\n## Blockquote\n> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n> [color=red] [name=John Doe] [time=2020-06-21 22:50]\n\n## Slideshare\n{%slideshare mazlan1/internet-of-things-the-tip-of-an-iceberg %}\n\n## Gist\nhttps://gist.github.com/schacon/1\n\n## YouTube\nhttps://www.youtube.com/watch?v=YE7VzlLtp-4\n\n## Vimeo\nhttps://vimeo.com/23237102\n\n## Asciinema\nhttps://asciinema.org/a/117928\n\n## PDF\n{%pdf https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf %}\n\n## Code highlighting\n```js=\nvar s = \"JavaScript syntax highlighting\";\nalert(s);\nfunction $initHighlight(block, cls) {\n try {\n if (cls.search(/\\bno\\-highlight\\b/) != -1)\n return process(block, true, 0x0F) +\n ' class=\"\"';\n } catch (e) {\n /* handle exception */\n }\n for (var i = 0 / 2; i < classes.length; i++) {\n if (checkCondition(classes[i]) === undefined)\n return /\\d+[\\s/]/g;\n }\n}\n```\n\n## PlantUML\n```plantuml\n@startuml\nparticipant Alice\nparticipant \"The **Famous** Bob\" as Bob\n\nAlice -> Bob : hello --there--\n... Some ~~long delay~~ ...\nBob -> Alice : ok\nnote left\n This is **bold**\n This is //italics//\n This is \"\"monospaced\"\"\n This is --stroked--\n This is __underlined__\n This is ~~waved~~\nend note\n\nAlice -> Bob : A //well formatted// message\nnote right of Alice\n This is displayed\n __left of__ Alice.\nend note\nnote left of Bob\n This is displayed\n **left of Alice Bob**.\nend note\nnote over Alice, Bob\n This is hosted by \nend note\n@enduml\n```\n\n## ToDo List\n\n- [ ] ToDos\n - [X] Buy some salad\n - [ ] Brush teeth\n - [x] Drink some water\n - [ ] **Click my box** and see the source code, if you're allowed to edit!\n\n", - "authorship": [], - "preVersionTwoNote": true + "metadata": { + "id": "ABC2", + "alias": "features", + "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": [] + } } diff --git a/public/api/private/notes/old b/public/api/private/notes/old deleted file mode 100644 index c4b194892..000000000 --- a/public/api/private/notes/old +++ /dev/null @@ -1,13 +0,0 @@ -{ - "id": "ABC123", - "alias": "old", - "lastChange": { - "userId": "test", - "timestamp": 1600033920 - }, - "viewcount": 0, - "createtime": 1600033920, - "content": "test123", - "authorship": [], - "preVersionTwoNote": false -} diff --git a/public/api/private/notes/old-get b/public/api/private/notes/old-get new file mode 100644 index 000000000..028529b58 --- /dev/null +++ b/public/api/private/notes/old-get @@ -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": [] + } +} diff --git a/src/api/config/types.d.ts b/src/api/config/types.d.ts index f8c367202..71ddf3f5b 100644 --- a/src/api/config/types.d.ts +++ b/src/api/config/types.d.ts @@ -12,7 +12,7 @@ export interface Config { banner: BannerConfig, customAuthNames: CustomAuthNames, useImageProxy: boolean, - specialLinks: SpecialLinks, + specialUrls: SpecialUrls, version: BackendVersion, plantumlServer: string | null, maxDocumentLength: number, @@ -35,9 +35,11 @@ export interface BannerConfig { } export interface BackendVersion { - version: string, - sourceCodeUrl: string - issueTrackerUrl: string + major: number + minor: number + patch: number + preRelease?: string + commit?: string } export interface AuthProvidersState { @@ -60,7 +62,7 @@ export interface CustomAuthNames { saml: string; } -export interface SpecialLinks { +export interface SpecialUrls { privacy: string, termsOfUse: string, imprint: string, diff --git a/src/api/group/types.d.ts b/src/api/group/types.d.ts new file mode 100644 index 000000000..f6e2c8c1f --- /dev/null +++ b/src/api/group/types.d.ts @@ -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 +} diff --git a/src/api/history/index.ts b/src/api/history/index.ts index 2786ec14a..77d521dc9 100644 --- a/src/api/history/index.ts +++ b/src/api/history/index.ts @@ -8,13 +8,13 @@ import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils' import { HistoryEntryDto, HistoryEntryPutDto, HistoryEntryUpdateDto } from './types' export const getHistory = async (): Promise => { - const response = await fetch(getApiUrl() + '/history') + const response = await fetch(getApiUrl() + '/me/history') expectResponseCode(response) return await response.json() as Promise } export const postHistory = async (entries: HistoryEntryPutDto[]): Promise => { - const response = await fetch(getApiUrl() + '/history', { + const response = await fetch(getApiUrl() + '/me/history', { ...defaultFetchConfig, method: 'POST', body: JSON.stringify(entries) @@ -23,7 +23,7 @@ export const postHistory = async (entries: HistoryEntryPutDto[]): Promise } export const updateHistoryEntryPinStatus = async (noteId: string, entry: HistoryEntryUpdateDto): Promise => { - const response = await fetch(getApiUrl() + '/history/' + noteId, { + const response = await fetch(getApiUrl() + '/me/history/' + noteId, { ...defaultFetchConfig, method: 'PUT', body: JSON.stringify(entry) @@ -32,7 +32,7 @@ export const updateHistoryEntryPinStatus = async (noteId: string, entry: History } export const deleteHistoryEntry = async (noteId: string): Promise => { - const response = await fetch(getApiUrl() + '/history/' + noteId, { + const response = await fetch(getApiUrl() + '/me/history/' + noteId, { ...defaultFetchConfig, method: 'DELETE' }) @@ -40,7 +40,7 @@ export const deleteHistoryEntry = async (noteId: string): Promise => { } export const deleteHistory = async (): Promise => { - const response = await fetch(getApiUrl() + '/history', { + const response = await fetch(getApiUrl() + '/me/history', { ...defaultFetchConfig, method: 'DELETE' }) diff --git a/src/api/me/index.ts b/src/api/me/index.ts index 92626a0fd..1488677e6 100644 --- a/src/api/me/index.ts +++ b/src/api/me/index.ts @@ -6,9 +6,10 @@ import { UserResponse } from '../users/types' import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils' +import { isMockMode } from '../../utils/test-modes' export const getMe = async (): Promise => { - const response = await fetch(getApiUrl() + '/me', { + const response = await fetch(getApiUrl() + `/me${ isMockMode() ? '-get' : '' }`, { ...defaultFetchConfig }) expectResponseCode(response) diff --git a/src/api/notes/dto-methods.ts b/src/api/notes/dto-methods.ts new file mode 100644 index 000000000..4602e4e7e --- /dev/null +++ b/src/api/notes/dto-methods.ts @@ -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 + } +} diff --git a/src/api/notes/index.ts b/src/api/notes/index.ts index 794ca6eec..09acad7ee 100644 --- a/src/api/notes/index.ts +++ b/src/api/notes/index.ts @@ -5,31 +5,17 @@ */ import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils' +import { NoteDto } from './types' +import { isMockMode } from '../../utils/test-modes' -interface LastChange { - 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 => { +export const getNote = async (noteId: string): Promise => { // 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. - const response = await fetch(getApiUrl() + `/notes/${ noteId }-get`, { + const response = await fetch(getApiUrl() + `/notes/${ noteId }${ isMockMode() ? '-get' : '' }`, { ...defaultFetchConfig }) expectResponseCode(response) - return await response.json() as Promise + return await response.json() as Promise } export const deleteNote = async (noteId: string): Promise => { diff --git a/src/api/notes/types.d.ts b/src/api/notes/types.d.ts new file mode 100644 index 000000000..cb22ba96f --- /dev/null +++ b/src/api/notes/types.d.ts @@ -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 +} diff --git a/src/api/users/types.d.ts b/src/api/users/types.d.ts index ef42d329b..2d5affa6b 100644 --- a/src/api/users/types.d.ts +++ b/src/api/users/types.d.ts @@ -12,3 +12,10 @@ export interface UserResponse { photo: string provider: LoginProvider } + +export interface UserInfoDto { + userName: string + displayName: string + photo: string + email: string +} diff --git a/src/components/common/routing/redirector.tsx b/src/components/common/routing/redirector.tsx index 9cf5582c8..8977fd77d 100644 --- a/src/components/common/routing/redirector.tsx +++ b/src/components/common/routing/redirector.tsx @@ -9,6 +9,7 @@ import { Redirect } from 'react-router' import { useParams } from 'react-router-dom' import { getNote } from '../../../api/notes' import { NotFoundErrorScreen } from './not-found-error-screen' +import { NoteDto } from '../../../api/notes/types' interface RouteParameters { id: string @@ -20,7 +21,7 @@ export const Redirector: React.FC = () => { useEffect(() => { getNote(id) - .then((noteFromAPI) => setError(!noteFromAPI.preVersionTwoNote)) + .then((noteFromAPI: NoteDto) => setError(noteFromAPI.metadata.version !== 1)) .catch(() => setError(true)) }, [id]) diff --git a/src/components/document-read-only-page/document-read-only-page.tsx b/src/components/document-read-only-page/document-read-only-page.tsx index 59df0961d..2aa813ccf 100644 --- a/src/components/document-read-only-page/document-read-only-page.tsx +++ b/src/components/document-read-only-page/document-read-only-page.tsx @@ -48,7 +48,7 @@ export const DocumentReadOnlyPage: React.FC = () => { { useTranslation() - const backendIssueTracker = useSelector((state: ApplicationState) => state.config.version.issueTrackerUrl) return ( @@ -43,7 +40,7 @@ export const Links: React.FC = () => {
  • diff --git a/src/components/editor-page/renderer-pane/render-iframe.tsx b/src/components/editor-page/renderer-pane/render-iframe.tsx index 68c493c57..40b5a0782 100644 --- a/src/components/editor-page/renderer-pane/render-iframe.tsx +++ b/src/components/editor-page/renderer-pane/render-iframe.tsx @@ -8,7 +8,7 @@ import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react import { useSelector } from 'react-redux' import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated' 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 { ImageDetails, RendererType } from '../../render-page/rendering-message' import { useContextOrStandaloneIframeCommunicator } from '../render-context/iframe-communicator-context-provider' @@ -44,7 +44,7 @@ export const RenderIframe: React.FC = ( const frameReference = useRef(null) const rendererOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.rendererOrigin) - const renderPageUrl = `${ rendererOrigin }/render` + const renderPageUrl = `${ rendererOrigin }render` const resetRendererReady = useCallback(() => setRendererReady(false), []) const iframeCommunicator = useContextOrStandaloneIframeCommunicator() const onIframeLoad = useOnIframeLoad(frameReference, iframeCommunicator, rendererOrigin, renderPageUrl, resetRendererReady) diff --git a/src/components/history-page/history-content/history-content.tsx b/src/components/history-page/history-content/history-content.tsx index 845a79aa3..ad3e428f0 100644 --- a/src/components/history-page/history-content/history-content.tsx +++ b/src/components/history-page/history-content/history-content.tsx @@ -1,7 +1,7 @@ /* - SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - - SPDX-License-Identifier: AGPL-3.0-only + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only */ import React, { Fragment, useCallback, useState } from 'react' @@ -46,23 +46,19 @@ export const HistoryContent: React.FC = ({ viewState, entri const [lastPageIndex, setLastPageIndex] = useState(0) const onPinClick = useCallback((noteId: string) => { - toggleHistoryEntryPinning(noteId).catch( - showErrorNotification(t('landing.history.error.updateEntry.text')) - ) + toggleHistoryEntryPinning(noteId) + .catch(showErrorNotification(t('landing.history.error.updateEntry.text'))) }, [t]) const onDeleteClick = useCallback((noteId: string) => { - deleteNote(noteId).then(() => { - return removeHistoryEntry(noteId) - }).catch( - showErrorNotification(t('landing.history.error.deleteNote.text')) - ) + deleteNote(noteId) + .then(() => removeHistoryEntry(noteId)) + .catch(showErrorNotification(t('landing.history.error.deleteNote.text'))) }, [t]) const onRemoveClick = useCallback((noteId: string) => { - removeHistoryEntry(noteId).catch( - showErrorNotification(t('landing.history.error.deleteEntry.text')) - ) + removeHistoryEntry(noteId) + .catch(showErrorNotification(t('landing.history.error.deleteEntry.text'))) }, [t]) if (entries.length === 0) { diff --git a/src/components/landing-layout/footer/powered-by-links.tsx b/src/components/landing-layout/footer/powered-by-links.tsx index e5ce4d7df..7a5252a77 100644 --- a/src/components/landing-layout/footer/powered-by-links.tsx +++ b/src/components/landing-layout/footer/powered-by-links.tsx @@ -18,7 +18,7 @@ import equal from 'fast-deep-equal' export const PoweredByLinks: React.FC = () => { 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 (

    @@ -28,7 +28,7 @@ export const PoweredByLinks: React.FC = () => {  |  { - specialLinks.map(([i18nKey, href]) => + specialUrls.map(([i18nKey, href]) =>  |  diff --git a/src/components/landing-layout/footer/version-info/version-info-modal.tsx b/src/components/landing-layout/footer/version-info/version-info-modal.tsx index f7fbe336a..1cbbd8c46 100644 --- a/src/components/landing-layout/footer/version-info/version-info-modal.tsx +++ b/src/components/landing-layout/footer/version-info/version-info-modal.tsx @@ -4,17 +4,32 @@ * 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 { Modal, Row } from 'react-bootstrap' import { VersionInfoModalColumn } from './version-info-modal-column' import frontendVersion from '../../../../version.json' +import links from '../../../../links.json' import { useSelector } from 'react-redux' import { ApplicationState } from '../../../../redux' import equal from 'fast-deep-equal' +import { BackendVersion } from '../../../../api/config/types' export const VersionInfoModal: React.FC = ({ 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 ( = ({ onHide, show }) = + version={ backendVersion } + issueTrackerLink={ links.backendIssues } + sourceCodeLink={ links.backendSourceCode }/> { const { t } = useTranslation() 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 [username, setUsername] = useState('') @@ -112,17 +113,17 @@ export const RegisterPage: React.FC = () => { required /> - +

      - +
    • - +
    • - +
    • - +
    diff --git a/src/index.tsx b/src/index.tsx index 697ac584f..91812b9ab 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -22,7 +22,7 @@ import { store } from './redux' import * as serviceWorkerRegistration from './service-worker-registration' import './style/dark.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 RenderPage = React.lazy(() => import (/* webpackPrefetch: true *//* webpackChunkName: "renderPage" */ './components/render-page/render-page')) diff --git a/src/links.json b/src/links.json index 669cec985..16b715613 100644 --- a/src/links.json +++ b/src/links.json @@ -3,6 +3,8 @@ "community": "https://community.hedgedoc.org", "faq": "https://hedgedoc.org/faq/", "githubOrg": "https://github.com/hedgedoc/", + "backendSourceCode": "https://github.com/hedgedoc/hedgedoc", + "backendIssues": "https://github.com/hedgedoc/hedgedoc/issues", "mastodon": "https://social.hedgedoc.org", "translate": "https://translate.hedgedoc.org", "webpage": "https://hedgedoc.org" diff --git a/src/redux/banner/reducers.ts b/src/redux/banner/reducers.ts index 87ad5d24c..b305c5734 100644 --- a/src/redux/banner/reducers.ts +++ b/src/redux/banner/reducers.ts @@ -8,7 +8,7 @@ import { Reducer } from 'redux' import { BannerActions, BannerActionType, BannerState, SetBannerAction } from './types' export const initialState: BannerState = { - show: true, + show: false, text: '', timestamp: '' } diff --git a/src/redux/config/reducers.ts b/src/redux/config/reducers.ts index e0501b0b5..11d3dcb13 100644 --- a/src/redux/config/reducers.ts +++ b/src/redux/config/reducers.ts @@ -40,15 +40,15 @@ export const initialState: Config = { maxDocumentLength: 0, useImageProxy: false, plantumlServer: null, - specialLinks: { + specialUrls: { privacy: '', termsOfUse: '', imprint: '' }, version: { - version: '', - sourceCodeUrl: '', - issueTrackerUrl: '' + major: -1, + minor: -1, + patch: -1, }, iframeCommunication: { editorOrigin: '', diff --git a/src/redux/history/methods.ts b/src/redux/history/methods.ts index 4d51f2160..17fa232d9 100644 --- a/src/redux/history/methods.ts +++ b/src/redux/history/methods.ts @@ -91,8 +91,8 @@ export const toggleHistoryEntryPinning = async (noteId: string): Promise = updateLocalHistoryEntry(noteId, entryToUpdate) } else { const historyUpdateDto = historyEntryToHistoryEntryUpdateDto(entryToUpdate) - updateHistoryEntryRedux(noteId, entryToUpdate) await updateHistoryEntryPinStatus(noteId, historyUpdateDto) + updateHistoryEntryRedux(noteId, entryToUpdate) } } diff --git a/src/redux/note-details/methods.ts b/src/redux/note-details/methods.ts index ec42f8b61..eae5470cf 100644 --- a/src/redux/note-details/methods.ts +++ b/src/redux/note-details/methods.ts @@ -5,7 +5,7 @@ */ 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 { initialState } from './reducers' import { @@ -24,7 +24,7 @@ export const setNoteMarkdownContent = (content: string): void => { } as SetNoteDetailsAction) } -export const setNoteDataFromServer = (apiResponse: Note): void => { +export const setNoteDataFromServer = (apiResponse: NoteDto): void => { store.dispatch({ type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER, note: apiResponse diff --git a/src/redux/note-details/reducers.ts b/src/redux/note-details/reducers.ts index 6b75fa21d..93954cd42 100644 --- a/src/redux/note-details/reducers.ts +++ b/src/redux/note-details/reducers.ts @@ -6,7 +6,6 @@ import { DateTime } from 'luxon' import { Reducer } from 'redux' -import { Note } from '../../api/notes' import { NoteFrontmatter, NoteTextDirection, @@ -22,6 +21,7 @@ import { SetNoteFrontmatterFromRenderingAction, UpdateNoteTitleByFirstHeadingAction } from './types' +import { noteDtoToNoteDetails } from '../../api/notes/dto-methods' export const initialState: NoteDetails = { markdownContent: '', @@ -29,10 +29,9 @@ export const initialState: NoteDetails = { createTime: DateTime.fromSeconds(0), lastChange: { timestamp: DateTime.fromSeconds(0), - userId: '' + userName: '' }, alias: '', - preVersionTwoNote: false, viewCount: 0, authorship: [], noteTitle: '', @@ -67,7 +66,7 @@ export const NoteDetailsReducer: Reducer = (stat noteTitle: generateNoteTitle(state.frontmatter, (action as UpdateNoteTitleByFirstHeadingAction).firstHeading) } case NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER: - return convertNoteToNoteDetails((action as SetNoteDetailsFromServerAction).note) + return noteDtoToNoteDetails((action as SetNoteDetailsFromServerAction).note) case NoteDetailsActionType.SET_NOTE_FRONTMATTER: return { ...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 - } -} + diff --git a/src/redux/note-details/types.ts b/src/redux/note-details/types.ts index 313c50202..a8e5769da 100644 --- a/src/redux/note-details/types.ts +++ b/src/redux/note-details/types.ts @@ -6,8 +6,8 @@ import { DateTime } from 'luxon' import { Action } from 'redux' -import { Note } from '../../api/notes' import { NoteFrontmatter } from '../../components/editor-page/note-frontmatter/note-frontmatter' +import { NoteDto } from '../../api/notes/types' export enum NoteDetailsActionType { SET_DOCUMENT_CONTENT = 'note-details/set', @@ -18,7 +18,7 @@ export enum NoteDetailsActionType { } interface LastChange { - userId: string + userName: string timestamp: DateTime } @@ -27,10 +27,9 @@ export interface NoteDetails { id: string createTime: DateTime lastChange: LastChange - preVersionTwoNote: boolean viewCount: number alias: string - authorship: number[] + authorship: string[] noteTitle: string firstHeading?: string frontmatter: NoteFrontmatter @@ -47,7 +46,7 @@ export interface SetNoteDetailsAction extends NoteDetailsAction { export interface SetNoteDetailsFromServerAction extends NoteDetailsAction { type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER - note: Note + note: NoteDto } export interface UpdateNoteTitleByFirstHeadingAction extends NoteDetailsAction { diff --git a/src/redux/ui-notifications/methods.ts b/src/redux/ui-notifications/methods.ts index 8979df41b..183fbb282 100644 --- a/src/redux/ui-notifications/methods.ts +++ b/src/redux/ui-notifications/methods.ts @@ -39,9 +39,7 @@ export const dismissUiNotification = (notificationId: number): void => { } as DismissUiNotificationAction) } -// Promises catch errors as any. -// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types -export const showErrorNotification = (message: string) => (error: any): void => { +export const showErrorNotification = (message: string) => (error: Error): void => { console.error(message, error) dispatchUiNotification(i18n.t('common.errorOccurred'), message, DEFAULT_DURATION_IN_SECONDS, 'exclamation-triangle') } diff --git a/src/utils/is-test-mode.ts b/src/utils/test-modes.ts similarity index 68% rename from src/utils/is-test-mode.ts rename to src/utils/test-modes.ts index e8f99ed1f..5acf215f8 100644 --- a/src/utils/is-test-mode.ts +++ b/src/utils/test-modes.ts @@ -7,3 +7,7 @@ export const isTestMode = (): boolean => { return !!process.env.REACT_APP_TEST_MODE } + +export const isMockMode = (): boolean => { + return process.env.REACT_APP_BACKEND === undefined +} diff --git a/src/version.json b/src/version.json index 557c4184a..3a4216eb2 100644 --- a/src/version.json +++ b/src/version.json @@ -1,5 +1,5 @@ { "version": "0.0", - "sourceCodeUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - "issueTrackerUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + "sourceCodeUrl": "https://github.com/hedgedoc/react-client", + "issueTrackerUrl": "https://github.com/hedgedoc/react-client/issues" }