From d67e44f540677fdb6a2781d19f69f26fe9d2d1b6 Mon Sep 17 00:00:00 2001 From: Erik Michelson Date: Sat, 17 May 2025 23:27:15 +0200 Subject: [PATCH] refactor: remove history page This needs to be done since the backend does not include code for the history page anymore. This will be replaced with the explore page in the near future anyway. Co-authored-by: Philip Molares Signed-off-by: Philip Molares Signed-off-by: Erik Michelson --- frontend/cypress/e2e/history.spec.ts | 206 -------------- frontend/cypress/e2e/revision.spec.ts | 6 +- frontend/cypress/support/visit-test-editor.ts | 9 +- frontend/cypress/support/visit.ts | 4 - frontend/locales/en.json | 81 ------ frontend/src/api/history/dto-methods.ts | 34 --- frontend/src/api/history/index.ts | 69 ----- frontend/src/api/history/types.ts | 32 --- frontend/src/app/(editor)/history/page.tsx | 41 --- .../application-loader/initializers/index.ts | 5 - .../editor-page/editor-page-content.tsx | 2 - .../hooks/use-update-local-history-entry.ts | 56 ---- .../aliases-modal/aliases-add-form.tsx | 8 +- .../aliases-modal/aliases-list-entry.tsx | 15 +- .../aliases-modal/aliases-list.spec.tsx | 22 +- .../aliases-modal/aliases-list.tsx | 5 +- .../delete-note-sidebar-entry.tsx | 8 +- .../permission-entry-special-group.tsx | 20 +- .../permission-entry-user.tsx | 20 +- .../permission-section-owner.tsx | 8 +- .../permission-section-users.tsx | 8 +- .../pin-note-sidebar-entry.tsx | 22 +- .../revisions-modal/delete-revision-modal.tsx | 8 +- .../revisions-modal/revision-list-entry.tsx | 2 +- .../entry-menu/delete-note-item.tsx | 43 --- .../entry-menu/entry-menu.module.scss | 13 - .../history-page/entry-menu/entry-menu.tsx | 92 ------- .../entry-menu/remove-note-entry-item.tsx | 51 ---- .../history-card/history-card-list.tsx | 49 ---- .../history-card/history-card.module.scss | 13 - .../history-card/history-card.tsx | 106 -------- .../history-content/history-content.tsx | 130 --------- .../history-table/history-table-row.tsx | 77 ------ .../history-table/history-table.module.scss | 26 -- .../history-table/history-table.tsx | 82 ------ .../history-toolbar/clear-history-button.tsx | 58 ---- .../history-toolbar/export-history-button.tsx | 24 -- .../history-refresh-button.tsx | 25 -- .../history-toolbar-state.d.ts | 15 - .../history-toolbar/history-toolbar.tsx | 103 ------- .../history-view-mode-toggle-button.tsx | 54 ---- .../hooks/use-safe-refresh-history-state.tsx | 18 -- .../history-toolbar/import-history-button.tsx | 142 ---------- .../history-toolbar/keyword-search-input.tsx | 35 --- .../sort-by-last-visited-button.tsx | 34 --- .../history-toolbar/sort-by-title-button.tsx | 34 --- .../history-toolbar/tag-selection-input.tsx | 50 ---- ...history-toolbar-state-context-provider.tsx | 37 --- .../toolbar-context/toolbar-context.d.ts | 9 - .../use-history-toolbar-state.tsx | 20 -- .../use-sync-toolbar-state-to-url-effect.ts | 48 ---- .../pin-button/pin-button.module.scss | 23 -- .../history-page/pin-button/pin-button.tsx | 39 --- .../history-page/sort-button/sort-button.tsx | 69 ----- .../history-page/use-history-entry-title.ts | 21 -- frontend/src/components/history-page/utils.ts | 103 ------- .../help-dropdown/history-button.tsx | 25 -- .../login-page/guest/guest-card.tsx | 2 - frontend/src/hooks/common/use-is-owner.ts | 2 +- frontend/src/hooks/common/use-may-edit.ts | 2 +- frontend/src/pages/api/private/me/history.ts | 47 ---- .../pages/api/private/notes/features/index.ts | 9 +- .../api/private/notes/features/revisions/0.ts | 2 +- .../api/private/notes/features/revisions/1.ts | 2 +- .../private/notes/features/revisions/index.ts | 4 +- frontend/src/pages/api/private/notes/index.ts | 9 +- .../api/private/notes/slide-example/index.ts | 9 +- frontend/src/redux/history/initial-state.ts | 8 - frontend/src/redux/history/methods.ts | 257 ------------------ frontend/src/redux/history/slice.ts | 34 --- frontend/src/redux/history/types.ts | 30 -- frontend/src/redux/index.ts | 2 - .../build-state-from-metadata-update.spec.ts | 4 - ...ate-from-set-note-data-from-server.spec.ts | 20 +- frontend/src/test-utils/mock-app-state.ts | 1 - 75 files changed, 76 insertions(+), 2727 deletions(-) delete mode 100644 frontend/cypress/e2e/history.spec.ts delete mode 100644 frontend/src/api/history/dto-methods.ts delete mode 100644 frontend/src/api/history/index.ts delete mode 100644 frontend/src/api/history/types.ts delete mode 100644 frontend/src/app/(editor)/history/page.tsx delete mode 100644 frontend/src/components/editor-page/hooks/use-update-local-history-entry.ts delete mode 100644 frontend/src/components/history-page/entry-menu/delete-note-item.tsx delete mode 100644 frontend/src/components/history-page/entry-menu/entry-menu.module.scss delete mode 100644 frontend/src/components/history-page/entry-menu/entry-menu.tsx delete mode 100644 frontend/src/components/history-page/entry-menu/remove-note-entry-item.tsx delete mode 100644 frontend/src/components/history-page/history-card/history-card-list.tsx delete mode 100644 frontend/src/components/history-page/history-card/history-card.module.scss delete mode 100644 frontend/src/components/history-page/history-card/history-card.tsx delete mode 100644 frontend/src/components/history-page/history-content/history-content.tsx delete mode 100644 frontend/src/components/history-page/history-table/history-table-row.tsx delete mode 100644 frontend/src/components/history-page/history-table/history-table.module.scss delete mode 100644 frontend/src/components/history-page/history-table/history-table.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/clear-history-button.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/export-history-button.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/history-refresh-button.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/history-toolbar-state.d.ts delete mode 100644 frontend/src/components/history-page/history-toolbar/history-toolbar.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/history-view-mode-toggle-button.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/hooks/use-safe-refresh-history-state.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/import-history-button.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/keyword-search-input.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/sort-by-last-visited-button.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/sort-by-title-button.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/tag-selection-input.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/toolbar-context/history-toolbar-state-context-provider.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/toolbar-context/toolbar-context.d.ts delete mode 100644 frontend/src/components/history-page/history-toolbar/toolbar-context/use-history-toolbar-state.tsx delete mode 100644 frontend/src/components/history-page/history-toolbar/toolbar-context/use-sync-toolbar-state-to-url-effect.ts delete mode 100644 frontend/src/components/history-page/pin-button/pin-button.module.scss delete mode 100644 frontend/src/components/history-page/pin-button/pin-button.tsx delete mode 100644 frontend/src/components/history-page/sort-button/sort-button.tsx delete mode 100644 frontend/src/components/history-page/use-history-entry-title.ts delete mode 100644 frontend/src/components/history-page/utils.ts delete mode 100644 frontend/src/components/layout/app-bar/app-bar-elements/help-dropdown/history-button.tsx delete mode 100644 frontend/src/pages/api/private/me/history.ts delete mode 100644 frontend/src/redux/history/initial-state.ts delete mode 100644 frontend/src/redux/history/methods.ts delete mode 100644 frontend/src/redux/history/slice.ts delete mode 100644 frontend/src/redux/history/types.ts diff --git a/frontend/cypress/e2e/history.spec.ts b/frontend/cypress/e2e/history.spec.ts deleted file mode 100644 index 0c29ad6ec..000000000 --- a/frontend/cypress/e2e/history.spec.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { HistoryEntry } from '../../src/api/history/types' - -describe('History', () => { - describe('History Mode', () => { - beforeEach(() => { - cy.visitHistory() - }) - - it('Cards', () => { - cy.getByCypressId('history-card').should('be.visible') - }) - - it('Table', () => { - cy.getByCypressId('history-mode-table').click() - cy.getByCypressId('history-table').should('be.visible') - }) - }) - - describe('entry title', () => { - describe('is as given when not empty', () => { - beforeEach(() => { - cy.clearLocalStorage('history') - cy.intercept('GET', '/api/private/me/history', { - body: [ - { - identifier: 'cypress', - title: 'Features', - lastVisitedAt: '2020-05-16T22:26:56.547Z', - pinStatus: false, - tags: [] - } as HistoryEntry - ] - }) - cy.visitHistory() - }) - - it('in table view', () => { - cy.getByCypressId('history-mode-table').click() - cy.getByCypressId('history-table').should('be.visible') - cy.getByCypressId('history-entry-title').contains('Features') - }) - - it('in cards view', () => { - cy.getByCypressId('history-entry-title').contains('Features') - }) - }) - describe('is untitled when not empty', () => { - beforeEach(() => { - cy.clearLocalStorage('history') - cy.intercept('GET', '/api/private/me/history', { - body: [ - { - identifier: 'cypress-no-title', - title: '', - lastVisitedAt: '2020-05-16T22:26:56.547Z', - pinStatus: false, - tags: [] - } as HistoryEntry - ] - }) - cy.visitHistory() - }) - - it('in table view', () => { - cy.getByCypressId('history-mode-table').click() - cy.getByCypressId('history-table').should('be.visible') - cy.getByCypressId('history-entry-title').contains('Untitled') - }) - - it('in cards view', () => { - cy.getByCypressId('history-entry-title').contains('Untitled') - }) - }) - }) - - describe('Pinning', () => { - beforeEach(() => { - cy.visitHistory() - }) - - describe('working', () => { - beforeEach(() => { - cy.intercept('PUT', '/api/private/me/history/features', (req) => { - req.reply(200, req.body) - }) - }) - - it('Cards', () => { - cy.getByCypressId('history-card').should('be.visible') - cy.get('[data-cypress-card-title=Features]') - .findByCypressId('history-entry-pin-button') - .should('have.attr', 'data-cypress-pinned', 'true') - .click() - cy.get('[data-cypress-card-title=Features]') - .findByCypressId('history-entry-pin-button') - .should('have.attr', 'data-cypress-pinned', 'false') - }) - - it('Table', () => { - cy.getByCypressId('history-mode-table').click() - cy.get('[data-cypress-entry-title=Features]') - .findByCypressId('history-entry-pin-button') - .should('have.attr', 'data-cypress-pinned', 'true') - .click() - cy.get('[data-cypress-entry-title=Features]') - .findByCypressId('history-entry-pin-button') - .should('have.attr', 'data-cypress-pinned', 'false') - }) - }) - - describe('failing', () => { - beforeEach(() => { - cy.intercept('PUT', '/api/private/me/history/features', { - statusCode: 401 - }) - }) - - it('Cards', () => { - cy.getByCypressId('history-card').should('be.visible') - cy.get('[data-cypress-card-title=Features]') - .findByCypressId('history-entry-pin-button') - .should('have.attr', 'data-cypress-pinned', 'true') - .click() - cy.getByCypressId('notification-toast').should('be.visible') - }) - - it('Table', () => { - cy.getByCypressId('history-mode-table').click() - cy.get('[data-cypress-entry-title=Features]') - .findByCypressId('history-entry-pin-button') - .should('have.attr', 'data-cypress-pinned', 'true') - .click() - cy.getByCypressId('notification-toast').should('be.visible') - }) - }) - }) - - describe('Import', () => { - beforeEach(() => { - cy.clearLocalStorage('history') - cy.intercept('GET', '/api/private/me/history', { - body: [] - }) - cy.visitHistory() - cy.logOut() - - cy.fixture('history.json').as('history') - cy.fixture('history-2.json').as('history-2') - cy.fixture('invalid-history.txt').as('invalid-history') - }) - - it('works with valid file', () => { - cy.getByCypressId('import-history-file-button').should('be.visible') - cy.getByCypressId('import-history-file-input').selectFile( - { - contents: '@history', - fileName: 'history.json', - mimeType: 'application/json' - }, - { force: true } - ) - cy.getByCypressId('history-entry-title').should('have.length', 1).contains('cy-Test') - }) - - it('fails on invalid file', () => { - cy.getByCypressId('import-history-file-button').should('be.visible') - cy.getByCypressId('import-history-file-input').selectFile( - { - contents: '@invalid-history', - fileName: 'invalid-history.txt', - mimeType: 'text/plain' - }, - { force: true } - ) - cy.getByCypressId('notification-toast').should('be.visible') - }) - - it('works when selecting two files with the same name', () => { - cy.getByCypressId('import-history-file-button').should('be.visible') - cy.getByCypressId('import-history-file-input').selectFile( - { - contents: '@history', - fileName: 'history.json', - mimeType: 'application/json' - }, - { force: true } - ) - cy.getByCypressId('history-entry-title').should('have.length', 1).contains('cy-Test') - cy.getByCypressId('import-history-file-button').should('be.visible') - cy.getByCypressId('import-history-file-input').selectFile( - { - contents: '@history-2', - fileName: 'history.json', - mimeType: 'application/json' - }, - { force: true } - ) - cy.getByCypressId('history-entry-title').should('have.length', 2).contains('cy-Test2') - }) - }) -}) diff --git a/frontend/cypress/e2e/revision.spec.ts b/frontend/cypress/e2e/revision.spec.ts index 329b0d55d..c99d5b05f 100644 --- a/frontend/cypress/e2e/revision.spec.ts +++ b/frontend/cypress/e2e/revision.spec.ts @@ -19,7 +19,7 @@ describe('Revision modal', () => { createdAt: defaultCreatedAt, length: 2788, authorUsernames: [], - anonymousAuthorCount: 4, + guestAuthorUuids: ['1', '2', '3', '4'], title: 'Features', description: 'Many features, such wow!', tags: ['hedgedoc', 'demo', 'react'] @@ -29,7 +29,7 @@ describe('Revision modal', () => { createdAt: defaultCreatedAt, length: 2782, authorUsernames: [], - anonymousAuthorCount: 2, + guestAuthorUuids: ['1', '2'], title: 'Features', description: 'Many more features, such wow!', tags: ['hedgedoc', 'demo', 'react'] @@ -81,7 +81,7 @@ describe('Revision modal', () => { edits: [], length: 2788, authorUsernames: [], - anonymousAuthorCount: 4, + authorGuestUuids: ['1', '2', '3'], content: testContent }) diff --git a/frontend/cypress/support/visit-test-editor.ts b/frontend/cypress/support/visit-test-editor.ts index 420239e89..804242634 100644 --- a/frontend/cypress/support/visit-test-editor.ts +++ b/frontend/cypress/support/visit-test-editor.ts @@ -8,20 +8,13 @@ import type { NoteDto } from '@hedgedoc/commons' export const testNoteId = 'test' const mockMetadata = { id: testNoteId, - aliases: [ - { - name: 'mock-note', - primaryAlias: true, - noteId: testNoteId - } - ], + aliases: ['mock-note'], primaryAlias: 'mock-note', title: 'Mock Note', description: 'Mocked note for testing', tags: ['test', 'mock', 'cypress'], updatedAt: '2021-04-24T09:27:51.000Z', lastUpdatedBy: null, - viewCount: 0, version: 2, createdAt: '2021-04-24T09:27:51.000Z', editedBy: [], diff --git a/frontend/cypress/support/visit.ts b/frontend/cypress/support/visit.ts index 201bb15fa..83ae20709 100644 --- a/frontend/cypress/support/visit.ts +++ b/frontend/cypress/support/visit.ts @@ -19,10 +19,6 @@ Cypress.Commands.add('visitHome', () => { return cy.visit('/', { retryOnNetworkFailure: true, retryOnStatusCodeFailure: true }) }) -Cypress.Commands.add('visitHistory', () => { - return cy.visit(`/history`, { retryOnNetworkFailure: true, retryOnStatusCodeFailure: true }) -}) - export enum PAGE_MODE { EDITOR = 'n', PRESENTATION = 'p', diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 1b40a936f..016ff979f 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -59,88 +59,8 @@ "markdownWhileLoading": "Loading...", "markdownLoadingError": "Error while fetching intro content" }, - "history": { - "error": { - "getHistory": { - "title": "Load History Error", - "text": "While trying to load the history form the server an error occurred" - }, - "deleteHistory": { - "title": "Delete History Error", - "text": "While trying to delete the history on the server an error occurred" - }, - "setHistory": { - "title": "Upload History Error", - "text": "While trying to upload the history to the server an error occurred" - }, - "deleteNote": { - "title": "Delete Note Error", - "text": "While trying to delete a note on the server an error occurred" - }, - "updateEntry": { - "title": "Update History Entry Error", - "text": "While trying to update a history entry on the server an error occurred" - }, - "deleteEntry": { - "title": "Delete History Entry Error", - "text": "While trying to delete a history entry on the server an error occurred" - }, - "notFoundEntry": { - "title": "History Entry not found", - "text": "We can't find the history entry you requested." - } - }, - "noHistory": "No history", - "localHistory": "Below is history from this browser", - "toolbar": { - "cards": "Cards", - "table": "Table", - "selectTags": "Select tags…", - "searchKeywords": "Search keyword…", - "sortByTitle": "Sort by title", - "sortByLastVisited": "Sort by time", - "export": "Export history", - "import": "Import history", - "clear": "Clear history", - "refresh": "Refresh history", - "uploadAll": "Sync the complete history to the server" - }, - "modal": { - "clearHistory": { - "title": "Delete history", - "question": "Do you want to clear the history?", - "disclaimer": "This won't delete any notes." - }, - "importHistoryError": { - "title": "An error occurred", - "textWithFile": "While trying to import history from '{{fileName}}' an error occurred.", - "textWithoutFile": "You did not provide any files to upload the history from.", - "tooNewVersion": "The file '{{fileName}}' comes from a newer client and can't be imported." - }, - "removeNote": { - "title": "Remove note from history", - "question": "Do you really want to remove this note from your history?", - "warning": "This just removes the history entry and won't delete the note itself.", - "button": "Remove note from history" - } - }, - "tableHeader": { - "title": "Title", - "actions": "Actions", - "tags": "Tags", - "lastVisit": "Last Visit" - }, - "menu": { - "recentNotes": "Recent notes", - "entryLocal": "Saved in your browser history", - "entryRemote": "Saved in your user history", - "removeEntry": "Remove from history", - "deleteNote": "Delete note" - } - }, "navigation": { "intro": "Intro", - "history": "History", "newGuestNote": "New guest note", "newNote": "New note" }, @@ -509,7 +429,6 @@ "views": { "presentation": {}, "readOnly": { - "viewCount": "views", "editNote": "Edit this note" } }, diff --git a/frontend/src/api/history/dto-methods.ts b/frontend/src/api/history/dto-methods.ts deleted file mode 100644 index ab3c25d62..000000000 --- a/frontend/src/api/history/dto-methods.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { HistoryEntry, HistoryEntryPutDto, HistoryEntryWithOrigin } from './types' -import { HistoryEntryOrigin } from './types' - -/** - * Transform a {@link HistoryEntry} into a {@link HistoryEntryWithOrigin}. - * - * @param entry the entry to build from - * @return the history entry with an origin - */ -export const addRemoteOriginToHistoryEntry = (entry: HistoryEntry): HistoryEntryWithOrigin => { - return { - ...entry, - origin: HistoryEntryOrigin.REMOTE - } -} - -/** - * Create a {@link HistoryEntryPutDto} from a {@link HistoryEntry}. - * - * @param entry the entry to build the dto from - * @return the dto for the api - */ -export const historyEntryToHistoryEntryPutDto = (entry: HistoryEntry): HistoryEntryPutDto => { - return { - pinStatus: entry.pinStatus, - lastVisitedAt: entry.lastVisitedAt, - note: entry.identifier - } -} diff --git a/frontend/src/api/history/index.ts b/frontend/src/api/history/index.ts deleted file mode 100644 index 3ef125d11..000000000 --- a/frontend/src/api/history/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder' -import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder' -import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder' -import { PutApiRequestBuilder } from '../common/api-request-builder/put-api-request-builder' -import type { ChangePinStatusDto, HistoryEntry, HistoryEntryPutDto } from './types' - -/** - * Fetches the remote history for the user from the server. - * - * @return The remote history entries of the user. - * @throws {Error} when the api request wasn't successful. - */ -export const getRemoteHistory = async (): Promise => { - const response = await new GetApiRequestBuilder('me/history').sendRequest() - return response.asParsedJsonObject() -} - -/** - * Replaces the remote history of the user with the given history entries. - * - * @param entries The history entries to store remotely. - * @throws {Error} when the api request wasn't successful. - */ -export const setRemoteHistoryEntries = async (entries: HistoryEntryPutDto[]): Promise => { - await new PostApiRequestBuilder('me/history').withJsonBody(entries).sendRequest() -} - -/** - * Updates a remote history entry's pin state. - * - * @param noteIdOrAlias The note id for which to update the pinning state. - * @param pinStatus True when the note should be pinned, false otherwise. - * @throws {Error} when the api request wasn't successful. - */ -export const updateRemoteHistoryEntryPinStatus = async ( - noteIdOrAlias: string, - pinStatus: boolean -): Promise => { - const response = await new PutApiRequestBuilder('me/history/' + noteIdOrAlias) - .withJsonBody({ - pinStatus - }) - .sendRequest() - return response.asParsedJsonObject() -} - -/** - * Deletes a remote history entry. - * - * @param noteIdOrAlias The note id or alias of the history entry to remove. - * @throws {Error} when the api request wasn't successful. - */ -export const deleteRemoteHistoryEntry = async (noteIdOrAlias: string): Promise => { - await new DeleteApiRequestBuilder('me/history/' + noteIdOrAlias).sendRequest() -} - -/** - * Deletes the complete remote history. - * - * @throws {Error} when the api request wasn't successful. - */ -export const deleteRemoteHistory = async (): Promise => { - await new DeleteApiRequestBuilder('me/history').sendRequest() -} diff --git a/frontend/src/api/history/types.ts b/frontend/src/api/history/types.ts deleted file mode 100644 index 9b1d972e3..000000000 --- a/frontend/src/api/history/types.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -export enum HistoryEntryOrigin { - LOCAL = 'local', - REMOTE = 'remote' -} - -export interface HistoryEntryPutDto { - note: string - pinStatus: boolean - lastVisitedAt: string -} - -export interface HistoryEntry { - identifier: string - title: string - owner: string | null - lastVisitedAt: string - tags: string[] - pinStatus: boolean -} - -export interface HistoryEntryWithOrigin extends HistoryEntry { - origin: HistoryEntryOrigin -} - -export interface ChangePinStatusDto { - pinStatus: boolean -} diff --git a/frontend/src/app/(editor)/history/page.tsx b/frontend/src/app/(editor)/history/page.tsx deleted file mode 100644 index 4c55aedf8..000000000 --- a/frontend/src/app/(editor)/history/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client' - -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { HistoryContent } from '../../../components/history-page/history-content/history-content' -import { HistoryToolbar } from '../../../components/history-page/history-toolbar/history-toolbar' -import { useSafeRefreshHistoryStateCallback } from '../../../components/history-page/history-toolbar/hooks/use-safe-refresh-history-state' -import { HistoryToolbarStateContextProvider } from '../../../components/history-page/history-toolbar/toolbar-context/history-toolbar-state-context-provider' -import { LandingLayout } from '../../../components/landing-layout/landing-layout' -import type { NextPage } from 'next' -import React, { useEffect } from 'react' -import { Row } from 'react-bootstrap' -import { useTranslation } from 'react-i18next' - -/** - * The page that shows the local and remote note history. - */ -const HistoryPage: NextPage = () => { - useTranslation() - - const safeRefreshHistoryStateCallback = useSafeRefreshHistoryStateCallback() - useEffect(() => { - safeRefreshHistoryStateCallback() - }, [safeRefreshHistoryStateCallback]) - - return ( - - - - - - - - - ) -} - -export default HistoryPage diff --git a/frontend/src/components/application-loader/initializers/index.ts b/frontend/src/components/application-loader/initializers/index.ts index 390e5ca4c..5d3a96184 100644 --- a/frontend/src/components/application-loader/initializers/index.ts +++ b/frontend/src/components/application-loader/initializers/index.ts @@ -3,7 +3,6 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { refreshHistoryState } from '../../../redux/history/methods' import { Logger } from '../../../utils/logger' import { isDevMode, isTestMode } from '../../../utils/test-modes' import { loadDarkMode } from './load-dark-mode' @@ -66,10 +65,6 @@ export const createSetUpTaskList = (): InitTask[] => { name: 'Fetch user information', task: fetchUserInformation }, - { - name: 'Load history state', - task: refreshHistoryState - }, { name: 'Load preferences', task: loadFromLocalStorageAsync diff --git a/frontend/src/components/editor-page/editor-page-content.tsx b/frontend/src/components/editor-page/editor-page-content.tsx index 6346827b6..30560ae0b 100644 --- a/frontend/src/components/editor-page/editor-page-content.tsx +++ b/frontend/src/components/editor-page/editor-page-content.tsx @@ -11,7 +11,6 @@ import { useComponentsFromAppExtensions } from './editor-pane/hooks/use-componen import { useNoteAndAppTitle } from './head-meta-properties/use-note-and-app-title' import { useScrollState } from './hooks/use-scroll-state' import { useSetScrollSource } from './hooks/use-set-scroll-source' -import { useUpdateLocalHistoryEntry } from './hooks/use-update-local-history-entry' import { RendererPane } from './renderer-pane/renderer-pane' import { Sidebar } from './sidebar/sidebar' import { Splitter } from './splitter/splitter' @@ -32,7 +31,6 @@ export enum ScrollSource { export const EditorPageContent: React.FC = () => { useTranslation() usePrintKeyboardShortcut() - useUpdateLocalHistoryEntry() const scrollSource = useRef(ScrollSource.EDITOR) const [editorScrollState, onMarkdownRendererScroll] = useScrollState(scrollSource, ScrollSource.EDITOR) diff --git a/frontend/src/components/editor-page/hooks/use-update-local-history-entry.ts b/frontend/src/components/editor-page/hooks/use-update-local-history-entry.ts deleted file mode 100644 index d4b2e7270..000000000 --- a/frontend/src/components/editor-page/hooks/use-update-local-history-entry.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { HistoryEntryWithOrigin } from '../../../api/history/types' -import { HistoryEntryOrigin } from '../../../api/history/types' -import { useApplicationState } from '../../../hooks/common/use-application-state' -import { getGlobalState } from '../../../redux' -import { updateLocalHistoryEntry } from '../../../redux/history/methods' -import equal from 'fast-deep-equal' -import { useEffect, useRef } from 'react' - -/** - * An effect that uses information of the current note state to update a local {@link HistoryEntryWithOrigin history entry}. - * The entry is updated when the title or tags of the note change. - */ -export const useUpdateLocalHistoryEntry = (): void => { - const id = useApplicationState((state) => state.noteDetails?.id) - const userExists = useApplicationState((state) => !!state.user) - const currentNoteTitle = useApplicationState((state) => state.noteDetails?.title ?? '') - const currentNoteTags = useApplicationState((state) => state.noteDetails?.frontmatter.tags ?? []) - const currentNoteOwner = useApplicationState((state) => state.noteDetails?.permissions.owner) - const lastNoteTitle = useRef('') - const lastNoteTags = useRef([]) - - useEffect(() => { - if (userExists || id === undefined) { - return - } - if (currentNoteTitle === lastNoteTitle.current && equal(currentNoteTags, lastNoteTags.current)) { - return - } - const history = getGlobalState().history - const entry: HistoryEntryWithOrigin = history.find((entry) => entry.identifier === id) ?? { - identifier: id, - title: '', - pinStatus: false, - lastVisitedAt: '', - tags: [], - origin: HistoryEntryOrigin.LOCAL, - owner: null - } - if (entry.origin === HistoryEntryOrigin.REMOTE) { - return - } - const updatedEntry = { ...entry } - updatedEntry.title = currentNoteTitle - updatedEntry.tags = currentNoteTags - updatedEntry.owner = currentNoteOwner - updatedEntry.lastVisitedAt = new Date().toISOString() - updateLocalHistoryEntry(id, updatedEntry) - lastNoteTitle.current = currentNoteTitle - lastNoteTags.current = currentNoteTags - }, [id, userExists, currentNoteTitle, currentNoteTags, currentNoteOwner]) -} diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.tsx index c43761a03..87ff88382 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-add-form.tsx @@ -24,24 +24,24 @@ const validAliasRegex = /^[a-z0-9_-]*$/ */ export const AliasesAddForm: React.FC = () => { const { showErrorNotification } = useUiNotifications() - const noteId = useApplicationState((state) => state.noteDetails?.id) + const noteAlias = useApplicationState((state) => state.noteDetails?.primaryAlias) const isOwner = useIsOwner() const [newAlias, setNewAlias] = useState('') const onAddAlias = useCallback( (event: FormEvent) => { event.preventDefault() - if (noteId === undefined) { + if (noteAlias === undefined) { return } - addAlias(noteId, newAlias) + addAlias(noteAlias, newAlias) .then(updateMetadata) .catch(showErrorNotification('editor.modal.aliases.errorAddingAlias')) .finally(() => { setNewAlias('') }) }, - [noteId, newAlias, setNewAlias, showErrorNotification] + [noteAlias, newAlias, setNewAlias, showErrorNotification] ) const onNewAliasInputChange = useOnInputChange(setNewAlias) diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list-entry.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list-entry.tsx index 702b6d5b3..d5ac9b92d 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list-entry.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/aliases-sidebar-entry/aliases-modal/aliases-list-entry.tsx @@ -15,10 +15,10 @@ import { Badge } from 'react-bootstrap' import { Button } from 'react-bootstrap' import { Star as IconStar, X as IconX } from 'react-bootstrap-icons' import { useTranslation, Trans } from 'react-i18next' -import type { AliasDto } from '@hedgedoc/commons' +import { useApplicationState } from '../../../../../../hooks/common/use-application-state' export interface AliasesListEntryProps { - alias: AliasDto + alias: string } /** @@ -29,16 +29,17 @@ export interface AliasesListEntryProps { export const AliasesListEntry: React.FC = ({ alias }) => { const { t } = useTranslation() const { showErrorNotification } = useUiNotifications() + const primaryAlias = useApplicationState((state) => state.noteDetails?.primaryAlias) const isOwner = useIsOwner() const onRemoveClick = useCallback(() => { - deleteAlias(alias.name) + deleteAlias(alias) .then(updateMetadata) .catch(showErrorNotification(t('editor.modal.aliases.errorRemovingAlias'))) }, [alias, t, showErrorNotification]) const onMakePrimaryClick = useCallback(() => { - markAliasAsPrimary(alias.name) + markAliasAsPrimary(alias) .then(updateMetadata) .catch(showErrorNotification(t('editor.modal.aliases.errorMakingPrimary'))) }, [alias, t, showErrorNotification]) @@ -50,15 +51,15 @@ export const AliasesListEntry: React.FC = ({ alias }) => return (
  • - {alias.name} - {alias.primaryAlias && ( + {alias} + {alias === primaryAlias && ( )}
    - {!alias.primaryAlias && ( + {alias !== primaryAlias && ( - -
    - -
    -
    - -
    -
    - - ) -} diff --git a/frontend/src/components/history-page/history-toolbar/export-history-button.tsx b/frontend/src/components/history-page/history-toolbar/export-history-button.tsx deleted file mode 100644 index 6bfd44ebc..000000000 --- a/frontend/src/components/history-page/history-toolbar/export-history-button.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { useTranslatedText } from '../../../hooks/common/use-translated-text' -import { downloadHistory } from '../../../redux/history/methods' -import { UiIcon } from '../../common/icons/ui-icon' -import React from 'react' -import { Button } from 'react-bootstrap' -import { Download as IconDownload } from 'react-bootstrap-icons' - -/** - * Renders a button to export the history. - */ -export const ExportHistoryButton: React.FC = () => { - const buttonTitle = useTranslatedText('landing.history.toolbar.export') - - return ( - - ) -} diff --git a/frontend/src/components/history-page/history-toolbar/history-refresh-button.tsx b/frontend/src/components/history-page/history-toolbar/history-refresh-button.tsx deleted file mode 100644 index aa70f4bb6..000000000 --- a/frontend/src/components/history-page/history-toolbar/history-refresh-button.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { useTranslatedText } from '../../../hooks/common/use-translated-text' -import { UiIcon } from '../../common/icons/ui-icon' -import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-history-state' -import React from 'react' -import { Button } from 'react-bootstrap' -import { ArrowRepeat as IconArrowRepeat } from 'react-bootstrap-icons' - -/** - * Fetches the current history from the server. - */ -export const HistoryRefreshButton: React.FC = () => { - const refreshHistory = useSafeRefreshHistoryStateCallback() - const buttonTitle = useTranslatedText('landing.history.toolbar.refresh') - - return ( - - ) -} diff --git a/frontend/src/components/history-page/history-toolbar/history-toolbar-state.d.ts b/frontend/src/components/history-page/history-toolbar/history-toolbar-state.d.ts deleted file mode 100644 index 0ac949e78..000000000 --- a/frontend/src/components/history-page/history-toolbar/history-toolbar-state.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { SortModeEnum } from '../sort-button/sort-button' -import type { ViewStateEnum } from './history-toolbar' - -export type HistoryToolbarState = { - viewState: ViewStateEnum - search: string - selectedTags: string[] - titleSortDirection: SortModeEnum - lastVisitedSortDirection: SortModeEnum -} diff --git a/frontend/src/components/history-page/history-toolbar/history-toolbar.tsx b/frontend/src/components/history-page/history-toolbar/history-toolbar.tsx deleted file mode 100644 index aea54fa37..000000000 --- a/frontend/src/components/history-page/history-toolbar/history-toolbar.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { HistoryEntryOrigin } from '../../../api/history/types' -import { useApplicationState } from '../../../hooks/common/use-application-state' -import { useTranslatedText } from '../../../hooks/common/use-translated-text' -import { importHistoryEntries, setHistoryEntries } from '../../../redux/history/methods' -import { UiIcon } from '../../common/icons/ui-icon' -import { useUiNotifications } from '../../notifications/ui-notification-boundary' -import { ClearHistoryButton } from './clear-history-button' -import { ExportHistoryButton } from './export-history-button' -import { HistoryRefreshButton } from './history-refresh-button' -import { HistoryViewModeToggleButton } from './history-view-mode-toggle-button' -import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-history-state' -import { ImportHistoryButton } from './import-history-button' -import { KeywordSearchInput } from './keyword-search-input' -import { SortByLastVisitedButton } from './sort-by-last-visited-button' -import { SortByTitleButton } from './sort-by-title-button' -import { TagSelectionInput } from './tag-selection-input' -import { useSyncToolbarStateToUrlEffect } from './toolbar-context/use-sync-toolbar-state-to-url-effect' -import React, { useCallback } from 'react' -import { Button, Col } from 'react-bootstrap' -import { CloudUpload as IconCloudUpload } from 'react-bootstrap-icons' -import { useIsLoggedIn } from '../../../hooks/common/use-is-logged-in' - -export enum ViewStateEnum { - CARD, - TABLE -} - -/** - * Renders the toolbar for the history page that contains controls for filtering and sorting. - */ -export const HistoryToolbar: React.FC = () => { - const historyEntries = useApplicationState((state) => state.history) - const userExists = useIsLoggedIn() - const { showErrorNotification } = useUiNotifications() - const safeRefreshHistoryState = useSafeRefreshHistoryStateCallback() - useSyncToolbarStateToUrlEffect() - - const onUploadAllToRemote = useCallback(() => { - if (!userExists) { - return - } - const localEntries = historyEntries - .filter((entry) => entry.origin === HistoryEntryOrigin.LOCAL) - .map((entry) => entry.identifier) - historyEntries.forEach((entry) => (entry.origin = HistoryEntryOrigin.REMOTE)) - importHistoryEntries(historyEntries).catch((error: Error) => { - showErrorNotification('landing.history.error.setHistory.text')(error) - historyEntries.forEach((entry) => { - if (localEntries.includes(entry.identifier)) { - entry.origin = HistoryEntryOrigin.LOCAL - } - }) - setHistoryEntries(historyEntries) - safeRefreshHistoryState() - }) - }, [userExists, historyEntries, showErrorNotification, safeRefreshHistoryState]) - - const uploadAllButtonTitle = useTranslatedText('landing.history.toolbar.uploadAll') - - return ( - -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    - {userExists && ( -
    - -
    - )} -
    - -
    - - ) -} diff --git a/frontend/src/components/history-page/history-toolbar/history-view-mode-toggle-button.tsx b/frontend/src/components/history-page/history-toolbar/history-view-mode-toggle-button.tsx deleted file mode 100644 index 717dd6a63..000000000 --- a/frontend/src/components/history-page/history-toolbar/history-view-mode-toggle-button.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { useTranslatedText } from '../../../hooks/common/use-translated-text' -import { cypressId } from '../../../utils/cypress-attribute' -import { UiIcon } from '../../common/icons/ui-icon' -import { ViewStateEnum } from './history-toolbar' -import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state' -import React, { useCallback } from 'react' -import { Button, ToggleButtonGroup } from 'react-bootstrap' -import { StickyFill as IconStickyFill, Table as IconTable } from 'react-bootstrap-icons' - -/** - * Toggles the view mode of the history entries between list and card view. - */ -export const HistoryViewModeToggleButton: React.FC = () => { - const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState() - - const onViewStateChange = useCallback( - (newViewState: ViewStateEnum) => { - setHistoryToolbarState((state) => ({ - ...state, - viewState: newViewState - })) - }, - [setHistoryToolbarState] - ) - - const cardsButtonTitle = useTranslatedText('landing.history.toolbar.cards') - const tableButtonTitle = useTranslatedText('landing.history.toolbar.table') - - const onCardsButtonClick = useCallback(() => onViewStateChange(ViewStateEnum.CARD), [onViewStateChange]) - const onTableButtonClick = useCallback(() => onViewStateChange(ViewStateEnum.TABLE), [onViewStateChange]) - - return ( - - - - - ) -} diff --git a/frontend/src/components/history-page/history-toolbar/hooks/use-safe-refresh-history-state.tsx b/frontend/src/components/history-page/history-toolbar/hooks/use-safe-refresh-history-state.tsx deleted file mode 100644 index 129d59dd6..000000000 --- a/frontend/src/components/history-page/history-toolbar/hooks/use-safe-refresh-history-state.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { refreshHistoryState } from '../../../../redux/history/methods' -import { useUiNotifications } from '../../../notifications/ui-notification-boundary' -import { useCallback } from 'react' - -/** - * Tries to refresh the history from the backend and shows notification if that request fails. - */ -export const useSafeRefreshHistoryStateCallback = () => { - const { showErrorNotification } = useUiNotifications() - return useCallback(() => { - refreshHistoryState().catch(showErrorNotification('landing.history.error.getHistory.text')) - }, [showErrorNotification]) -} diff --git a/frontend/src/components/history-page/history-toolbar/import-history-button.tsx b/frontend/src/components/history-page/history-toolbar/import-history-button.tsx deleted file mode 100644 index e7fe8bacb..000000000 --- a/frontend/src/components/history-page/history-toolbar/import-history-button.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { HistoryEntryWithOrigin } from '../../../api/history/types' -import { HistoryEntryOrigin } from '../../../api/history/types' -import { useApplicationState } from '../../../hooks/common/use-application-state' -import { useTranslatedText } from '../../../hooks/common/use-translated-text' -import { convertV1History, importHistoryEntries, mergeHistoryEntries } from '../../../redux/history/methods' -import type { HistoryExportJson, V1HistoryEntry } from '../../../redux/history/types' -import { cypressId } from '../../../utils/cypress-attribute' -import { UiIcon } from '../../common/icons/ui-icon' -import { useUiNotifications } from '../../notifications/ui-notification-boundary' -import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-history-state' -import React, { useCallback, useRef, useState } from 'react' -import { Button } from 'react-bootstrap' -import { Upload as IconUpload } from 'react-bootstrap-icons' -import { useIsLoggedIn } from '../../../hooks/common/use-is-logged-in' - -/** - * Button that lets the user select a history JSON file and uploads imports that into the history. - */ -export const ImportHistoryButton: React.FC = () => { - const userExists = useIsLoggedIn() - const historyState = useApplicationState((state) => state.history) - const uploadInput = useRef(null) - const [fileName, setFilename] = useState('') - const { showErrorNotification, dispatchUiNotification } = useUiNotifications() - const safeRefreshHistoryState = useSafeRefreshHistoryStateCallback() - - const onImportHistory = useCallback( - (entries: HistoryEntryWithOrigin[]): void => { - entries.forEach((entry) => (entry.origin = userExists ? HistoryEntryOrigin.REMOTE : HistoryEntryOrigin.LOCAL)) - importHistoryEntries(mergeHistoryEntries(historyState, entries)).catch((error: Error) => { - showErrorNotification('landing.history.error.setHistory.text')(error) - safeRefreshHistoryState() - }) - }, - [historyState, safeRefreshHistoryState, showErrorNotification, userExists] - ) - - const resetInputField = useCallback(() => { - if (!uploadInput.current) { - return - } - uploadInput.current.value = '' - }, [uploadInput]) - - const onUploadButtonClick = useCallback(() => uploadInput.current?.click(), [uploadInput]) - - const handleUpload = (event: React.ChangeEvent): void => { - const { validity, files } = event.target - if (files && files[0] && validity.valid) { - const file = files[0] - setFilename(file.name) - if (file.type !== 'application/json' && file.type !== '') { - void dispatchUiNotification('common.errorOccurred', 'landing.history.modal.importHistoryError.textWithFile', { - contentI18nOptions: { - fileName - } - }) - resetInputField() - return - } - //TODO: [mrdrogdrog] The following whole block can be shortened using our `readFile` util. - // But I won't do it right now because the whole components needs a make over and that's definitely out of scope for my current PR. - // https://github.com/hedgedoc/hedgedoc/issues/5042 - const fileReader = new FileReader() - fileReader.onload = (event) => { - if (event.target && event.target.result) { - try { - const result = event.target.result as string - const data = JSON.parse(result) as HistoryExportJson - if (data) { - if (data.version) { - if (data.version === 2) { - onImportHistory(data.entries) - } else { - // probably a newer version we can't support - void dispatchUiNotification( - 'common.errorOccurred', - 'landing.history.modal.importHistoryError.tooNewVersion', - { - contentI18nOptions: { - fileName - } - } - ) - } - } else { - const oldEntries = JSON.parse(result) as V1HistoryEntry[] - onImportHistory(convertV1History(oldEntries)) - } - } - resetInputField() - } catch { - void dispatchUiNotification( - 'common.errorOccurred', - 'landing.history.modal.importHistoryError.textWithFile', - { - contentI18nOptions: { - fileName - } - } - ) - } - } - } - fileReader.readAsText(file) - } else { - void dispatchUiNotification( - 'common.errorOccurred', - 'landing.history.modal.importHistoryError.textWithOutFile', - {} - ) - resetInputField() - } - } - - const buttonTitle = useTranslatedText('landing.history.toolbar.import') - - return ( -
    - - -
    - ) -} diff --git a/frontend/src/components/history-page/history-toolbar/keyword-search-input.tsx b/frontend/src/components/history-page/history-toolbar/keyword-search-input.tsx deleted file mode 100644 index 621fc5a35..000000000 --- a/frontend/src/components/history-page/history-toolbar/keyword-search-input.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { useOnInputChange } from '../../../hooks/common/use-on-input-change' -import { useTranslatedText } from '../../../hooks/common/use-translated-text' -import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state' -import React from 'react' -import { FormControl } from 'react-bootstrap' - -/** - * A text input that is used to filter history entries for specific keywords. - */ -export const KeywordSearchInput: React.FC = () => { - const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState() - - const onChange = useOnInputChange((search) => { - setHistoryToolbarState((state) => ({ - ...state, - search - })) - }) - - const searchKeywordsText = useTranslatedText('landing.history.toolbar.searchKeywords') - - return ( - - ) -} diff --git a/frontend/src/components/history-page/history-toolbar/sort-by-last-visited-button.tsx b/frontend/src/components/history-page/history-toolbar/sort-by-last-visited-button.tsx deleted file mode 100644 index 98be5a083..000000000 --- a/frontend/src/components/history-page/history-toolbar/sort-by-last-visited-button.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { SortButton, SortModeEnum } from '../sort-button/sort-button' -import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state' -import React, { useCallback } from 'react' -import { Trans, useTranslation } from 'react-i18next' - -/** - * Controls if history entries should be sorted by the last visited date. - */ -export const SortByLastVisitedButton: React.FC = () => { - useTranslation() - const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState() - - const lastVisitedSortChanged = useCallback( - (direction: SortModeEnum) => { - setHistoryToolbarState((state) => ({ - ...state, - lastVisitedSortDirection: direction, - titleSortDirection: SortModeEnum.no - })) - }, - [setHistoryToolbarState] - ) - - return ( - - - - ) -} diff --git a/frontend/src/components/history-page/history-toolbar/sort-by-title-button.tsx b/frontend/src/components/history-page/history-toolbar/sort-by-title-button.tsx deleted file mode 100644 index 3c939882d..000000000 --- a/frontend/src/components/history-page/history-toolbar/sort-by-title-button.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { SortButton, SortModeEnum } from '../sort-button/sort-button' -import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state' -import React, { useCallback } from 'react' -import { Trans, useTranslation } from 'react-i18next' - -/** - * Controls if history entries should be sorted by title. - */ -export const SortByTitleButton: React.FC = () => { - useTranslation() - const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState() - - const titleSortChanged = useCallback( - (direction: SortModeEnum) => { - setHistoryToolbarState((state) => ({ - ...state, - lastVisitedSortDirection: SortModeEnum.no, - titleSortDirection: direction - })) - }, - [setHistoryToolbarState] - ) - - return ( - - - - ) -} diff --git a/frontend/src/components/history-page/history-toolbar/tag-selection-input.tsx b/frontend/src/components/history-page/history-toolbar/tag-selection-input.tsx deleted file mode 100644 index 44b48618b..000000000 --- a/frontend/src/components/history-page/history-toolbar/tag-selection-input.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { useApplicationState } from '../../../hooks/common/use-application-state' -import { useTranslatedText } from '../../../hooks/common/use-translated-text' -import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state' -import React, { useCallback, useMemo } from 'react' -import { Typeahead } from 'react-bootstrap-typeahead' -import type { Option } from 'react-bootstrap-typeahead/types/types' - -/** - * Renders an input field that filters history entries by selected tags. - */ -export const TagSelectionInput: React.FC = () => { - const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState() - - const historyEntries = useApplicationState((state) => state.history) - - const tags = useMemo(() => { - const allTags = historyEntries - .map((entry) => entry.tags) - .flat() - .sort((first, second) => first.toLowerCase().localeCompare(second.toLowerCase())) - return Array.from(new Set(allTags)) - }, [historyEntries]) - - const onChange = useCallback( - (selectedTags: Option[]) => { - setHistoryToolbarState((state) => ({ - ...state, - selectedTags: selectedTags as string[] - })) - }, - [setHistoryToolbarState] - ) - - const placeholderText = useTranslatedText('landing.history.toolbar.selectTags') - return ( - - ) -} diff --git a/frontend/src/components/history-page/history-toolbar/toolbar-context/history-toolbar-state-context-provider.tsx b/frontend/src/components/history-page/history-toolbar/toolbar-context/history-toolbar-state-context-provider.tsx deleted file mode 100644 index df30f0cbb..000000000 --- a/frontend/src/components/history-page/history-toolbar/toolbar-context/history-toolbar-state-context-provider.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { useArrayStringUrlParameter } from '../../../../hooks/common/use-array-string-url-parameter' -import { useSingleStringUrlParameter } from '../../../../hooks/common/use-single-string-url-parameter' -import { SortModeEnum } from '../../sort-button/sort-button' -import { ViewStateEnum } from '../history-toolbar' -import type { HistoryToolbarState } from '../history-toolbar-state' -import type { HistoryToolbarStateWithDispatcher } from './toolbar-context' -import type { PropsWithChildren } from 'react' -import React, { createContext, useState } from 'react' - -export const historyToolbarStateContext = createContext(undefined) - -/** - * Provides a {@link React.Context react context} for the current state of the toolbar. - * - * @param children The children that should receive the toolbar state via context. - */ -export const HistoryToolbarStateContextProvider: React.FC> = ({ children }) => { - const search = useSingleStringUrlParameter('search', '') - const selectedTags = useArrayStringUrlParameter('selectedTags') - - const stateWithDispatcher = useState(() => ({ - viewState: ViewStateEnum.CARD, - search: search, - selectedTags: selectedTags, - titleSortDirection: SortModeEnum.no, - lastVisitedSortDirection: SortModeEnum.down - })) - - return ( - {children} - ) -} diff --git a/frontend/src/components/history-page/history-toolbar/toolbar-context/toolbar-context.d.ts b/frontend/src/components/history-page/history-toolbar/toolbar-context/toolbar-context.d.ts deleted file mode 100644 index e63c0e27f..000000000 --- a/frontend/src/components/history-page/history-toolbar/toolbar-context/toolbar-context.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { HistoryToolbarState } from '../history-toolbar-state' -import type { Dispatch, SetStateAction } from 'react' - -export type HistoryToolbarStateWithDispatcher = [HistoryToolbarState, Dispatch>] diff --git a/frontend/src/components/history-page/history-toolbar/toolbar-context/use-history-toolbar-state.tsx b/frontend/src/components/history-page/history-toolbar/toolbar-context/use-history-toolbar-state.tsx deleted file mode 100644 index 8764a8ac6..000000000 --- a/frontend/src/components/history-page/history-toolbar/toolbar-context/use-history-toolbar-state.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { historyToolbarStateContext } from './history-toolbar-state-context-provider' -import type { HistoryToolbarStateWithDispatcher } from './toolbar-context' -import { Optional } from '@mrdrogdrog/optional' -import { useContext } from 'react' - -/** - * Receives a {@link React.Context react context} for the history toolbar state. - * - * @throws Error if no context was set - */ -export const useHistoryToolbarState: () => HistoryToolbarStateWithDispatcher = () => { - return Optional.ofNullable(useContext(historyToolbarStateContext)).orElseThrow( - () => new Error('No toolbar context found. Did you forget to use the provider component?') - ) -} diff --git a/frontend/src/components/history-page/history-toolbar/toolbar-context/use-sync-toolbar-state-to-url-effect.ts b/frontend/src/components/history-page/history-toolbar/toolbar-context/use-sync-toolbar-state-to-url-effect.ts deleted file mode 100644 index ba20f6410..000000000 --- a/frontend/src/components/history-page/history-toolbar/toolbar-context/use-sync-toolbar-state-to-url-effect.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { useHistoryToolbarState } from './use-history-toolbar-state' -import equal from 'fast-deep-equal' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' -import { useEffect } from 'react' - -/** - * Pushes the current search and tag selection into the navigation history stack of the browser. - */ -export const useSyncToolbarStateToUrlEffect = (): void => { - const router = useRouter() - const searchParams = useSearchParams() - const [state] = useHistoryToolbarState() - const pathname = usePathname() - - useEffect(() => { - if (!searchParams || !pathname) { - return - } - - const urlParameterSearch = searchParams.get('search') ?? '' - const urlParameterSelectedTags = searchParams.getAll('selectedTags') - const params = new URLSearchParams(searchParams.toString()) - let shouldUpdate = false - - if (!equal(state.search, urlParameterSearch)) { - if (!state.search) { - params.delete('search') - } else { - params.set('search', state.search) - } - shouldUpdate = true - } - if (!equal(state.selectedTags, urlParameterSelectedTags)) { - params.delete('selectedTags') - state.selectedTags.forEach((tag) => params.append('selectedTags', tag)) - shouldUpdate = true - } - - if (shouldUpdate) { - router.push(`${pathname}?${params.toString()}`) - } - }, [state, router, searchParams, pathname]) -} diff --git a/frontend/src/components/history-page/pin-button/pin-button.module.scss b/frontend/src/components/history-page/pin-button/pin-button.module.scss deleted file mode 100644 index fb6be1253..000000000 --- a/frontend/src/components/history-page/pin-button/pin-button.module.scss +++ /dev/null @@ -1,23 +0,0 @@ -/*! - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -.history-pin { - height: 2.5rem; - width: 2.5rem; - svg { - opacity: 0.5; - transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out; - } - - &:hover svg { - opacity: 1; - } - - &.pinned svg { - color: #d43f3a; - opacity: 1; - } -} diff --git a/frontend/src/components/history-page/pin-button/pin-button.tsx b/frontend/src/components/history-page/pin-button/pin-button.tsx deleted file mode 100644 index 175a4c6f4..000000000 --- a/frontend/src/components/history-page/pin-button/pin-button.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute' -import { UiIcon } from '../../common/icons/ui-icon' -import styles from './pin-button.module.scss' -import React from 'react' -import { Button } from 'react-bootstrap' -import { PinFill as IconPinFill } from 'react-bootstrap-icons' - -export interface PinButtonProps { - isPinned: boolean - onPinClick: () => void - isDark: boolean - className?: string -} - -/** - * Renders a button with a pin icon. - * - * @param isPinned The initial state of this button. - * @param onPinClick The callback, that is fired when the button is clicked. - * @param isDark If the button should be rendered in dark or not. - * @param className Additional classes directly given to the button - */ -export const PinButton: React.FC = ({ isPinned, onPinClick, isDark, className }) => { - return ( - - ) -} diff --git a/frontend/src/components/history-page/sort-button/sort-button.tsx b/frontend/src/components/history-page/sort-button/sort-button.tsx deleted file mode 100644 index 1069552e2..000000000 --- a/frontend/src/components/history-page/sort-button/sort-button.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { IconButton } from '../../common/icon-button/icon-button' -import React, { useCallback, useMemo } from 'react' -import type { ButtonProps } from 'react-bootstrap' -import { SortAlphaDown as IconSortAlphaDown, SortAlphaUp as IconSortAlphaUp, X as IconX } from 'react-bootstrap-icons' - -export enum SortModeEnum { - up = 1, - down = -1, - no = 0 -} - -export interface SortButtonProps extends ButtonProps { - onDirectionChange: (direction: SortModeEnum) => void - direction: SortModeEnum -} - -/** - * Switches the sorting direction based on the previous direction. - * - * @param direction The previous sorting direction - * @return The new sorting direction - */ -const toggleDirection = (direction: SortModeEnum) => { - switch (direction) { - case SortModeEnum.no: - return SortModeEnum.up - case SortModeEnum.up: - return SortModeEnum.down - case SortModeEnum.down: - default: - return SortModeEnum.no - } -} - -/** - * Renders a button to change the sorting order of a list. - * - * @param children The children elements that should be rendered inside the button - * @param variant The variant of the button - * @param onDirectionChange Callback that is fired when the sorting direction is changed - * @param direction The sorting direction that is used - */ -export const SortButton: React.FC = ({ children, onDirectionChange, direction }) => { - const toggleSort = useCallback(() => { - onDirectionChange(toggleDirection(direction)) - }, [direction, onDirectionChange]) - - const icon = useMemo(() => { - switch (direction) { - case SortModeEnum.down: - return IconSortAlphaDown - case SortModeEnum.up: - return IconSortAlphaUp - case SortModeEnum.no: - return IconX - } - }, [direction]) - - return ( - - {children} - - ) -} diff --git a/frontend/src/components/history-page/use-history-entry-title.ts b/frontend/src/components/history-page/use-history-entry-title.ts deleted file mode 100644 index 1a0a51652..000000000 --- a/frontend/src/components/history-page/use-history-entry-title.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { HistoryEntryWithOrigin } from '../../api/history/types' -import { useMemo } from 'react' -import { useTranslation } from 'react-i18next' - -/** - * Hook that returns the title of a note in the history if present or the translation for "untitled" otherwise. - * - * @param entry The history entry containing a title property, that might be an empty string. - * @return A memoized string containing either the title of the entry or the translated version of "untitled". - */ -export const useHistoryEntryTitle = (entry: HistoryEntryWithOrigin): string => { - const { t } = useTranslation() - return useMemo(() => { - return entry.title !== '' ? entry.title : t('editor.untitledNote') - }, [t, entry]) -} diff --git a/frontend/src/components/history-page/utils.ts b/frontend/src/components/history-page/utils.ts deleted file mode 100644 index 4995b984f..000000000 --- a/frontend/src/components/history-page/utils.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { HistoryEntryWithOrigin } from '../../api/history/types' -import type { HistoryToolbarState } from './history-toolbar/history-toolbar-state' -import { SortModeEnum } from './sort-button/sort-button' -import { DateTime } from 'luxon' - -/** - * Parses a given ISO formatted date string and outputs it as a date and time string. - * - * @param date The date in ISO format. - * @return The date formatted as date and time string. - */ -export const formatHistoryDate = (date: string): string => DateTime.fromISO(date).toFormat('DDDD T') - -/** - * Applies sorting and filter rules that match a given toolbar state to a list of history entries. - * - * @param entries The history entries to sort and filter. - * @param toolbarState The state of the history toolbar (sorting rules, keyword and tag input). - * @return The list of filtered and sorted history entries. - */ -export const sortAndFilterEntries = ( - entries: HistoryEntryWithOrigin[], - toolbarState: HistoryToolbarState -): HistoryEntryWithOrigin[] => { - const filteredBySelectedTagsEntries = filterBySelectedTags(entries, toolbarState.selectedTags) - const filteredByKeywordSearchEntries = filterByKeywordSearch(filteredBySelectedTagsEntries, toolbarState.search) - return sortEntries(filteredByKeywordSearchEntries, toolbarState) -} - -/** - * Filters the given history entries by the given tags. - * - * @param entries The history entries to filter. - * @param selectedTags The tags that were selected as filter criteria. - * @return The list of filtered history entries. - */ -const filterBySelectedTags = (entries: HistoryEntryWithOrigin[], selectedTags: string[]): HistoryEntryWithOrigin[] => { - return entries.filter((entry) => { - return selectedTags.length === 0 || arrayCommonCheck(entry.tags, selectedTags) - }) -} - -/** - * Checks whether the entries of array 1 are contained in array 2. - * - * @param array1 The first input array. - * @param array2 The second input array. - * @return true if all entries from array 1 are contained in array 2, false otherwise. - */ -const arrayCommonCheck = (array1: T[], array2: T[]): boolean => { - const foundElement = array1.find((element1) => array2.find((element2) => element2 === element1)) - return !!foundElement -} - -/** - * Filters the given history entries by the given search term. Works case-insensitive. - * - * @param entries The history entries to filter. - * @param keywords The search term. - * @return The history entries that contain the search term in their title. - */ -const filterByKeywordSearch = (entries: HistoryEntryWithOrigin[], keywords: string): HistoryEntryWithOrigin[] => { - const searchTerm = keywords.toLowerCase() - return entries.filter((entry) => entry.title.toLowerCase().includes(searchTerm)) -} - -/** - * Sorts the given history entries by the sorting rules of the provided toolbar state. - * - * @param entries The history entries to sort. - * @param viewState The toolbar state containing the sorting options. - * @return The sorted history entries. - */ -const sortEntries = (entries: HistoryEntryWithOrigin[], viewState: HistoryToolbarState): HistoryEntryWithOrigin[] => { - return entries.sort((firstEntry, secondEntry) => { - if (firstEntry.pinStatus && !secondEntry.pinStatus) { - return -1 - } - if (!firstEntry.pinStatus && secondEntry.pinStatus) { - return 1 - } - - if (viewState.titleSortDirection !== SortModeEnum.no) { - return firstEntry.title.localeCompare(secondEntry.title) * viewState.titleSortDirection - } - - if (viewState.lastVisitedSortDirection !== SortModeEnum.no) { - if (firstEntry.lastVisitedAt > secondEntry.lastVisitedAt) { - return 1 * viewState.lastVisitedSortDirection - } - if (firstEntry.lastVisitedAt < secondEntry.lastVisitedAt) { - return -1 * viewState.lastVisitedSortDirection - } - } - - return 0 - }) -} diff --git a/frontend/src/components/layout/app-bar/app-bar-elements/help-dropdown/history-button.tsx b/frontend/src/components/layout/app-bar/app-bar-elements/help-dropdown/history-button.tsx deleted file mode 100644 index 4e27af3c0..000000000 --- a/frontend/src/components/layout/app-bar/app-bar-elements/help-dropdown/history-button.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import React from 'react' -import { Button } from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' -import Link from 'next/link' - -/** - * A button that links to the history page. - */ -export const HistoryButton: React.FC = () => { - useTranslation() - - return ( - - - - ) -} diff --git a/frontend/src/components/login-page/guest/guest-card.tsx b/frontend/src/components/login-page/guest/guest-card.tsx index 5c9b25e3d..185229a88 100644 --- a/frontend/src/components/login-page/guest/guest-card.tsx +++ b/frontend/src/components/login-page/guest/guest-card.tsx @@ -7,7 +7,6 @@ import React from 'react' import { Card } from 'react-bootstrap' import { NewNoteButton } from '../../common/new-note-button/new-note-button' -import { HistoryButton } from '../../layout/app-bar/app-bar-elements/help-dropdown/history-button' import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config' import { Trans, useTranslation } from 'react-i18next' import { PermissionLevel } from '@hedgedoc/commons' @@ -32,7 +31,6 @@ export const GuestCard: React.FC = () => {
    -
    {guestAccessLevel !== PermissionLevel.CREATE && (
    diff --git a/frontend/src/hooks/common/use-is-owner.ts b/frontend/src/hooks/common/use-is-owner.ts index 7cfbb76df..e7a06291d 100644 --- a/frontend/src/hooks/common/use-is-owner.ts +++ b/frontend/src/hooks/common/use-is-owner.ts @@ -13,7 +13,7 @@ import { useMemo } from 'react' * @return True, if the current user is owner. */ export const useIsOwner = (): boolean => { - const me: string | undefined = useApplicationState((state) => state.user?.username) + const me: string | null | undefined = useApplicationState((state) => state.user?.username) const permissions = useApplicationState((state) => state.noteDetails?.permissions) return useMemo(() => (permissions === undefined ? false : userIsOwner(permissions, me)), [permissions, me]) diff --git a/frontend/src/hooks/common/use-may-edit.ts b/frontend/src/hooks/common/use-may-edit.ts index f45fe63f6..df4f9ccae 100644 --- a/frontend/src/hooks/common/use-may-edit.ts +++ b/frontend/src/hooks/common/use-may-edit.ts @@ -13,7 +13,7 @@ import { useMemo } from 'react' * @return True, if the current user is allowed to write. */ export const useMayEdit = (): boolean => { - const me: string | undefined = useApplicationState((state) => state.user?.username) + const me: string | undefined | null = useApplicationState((state) => state.user?.username) const permissions = useApplicationState((state) => state.noteDetails?.permissions) return useMemo(() => (!permissions ? false : userCanEdit(permissions, me)), [permissions, me]) diff --git a/frontend/src/pages/api/private/me/history.ts b/frontend/src/pages/api/private/me/history.ts deleted file mode 100644 index f9b83b76c..000000000 --- a/frontend/src/pages/api/private/me/history.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { HistoryEntry } from '../../../../api/history/types' -import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' -import type { NextApiRequest, NextApiResponse } from 'next' - -const handler = (req: NextApiRequest, res: NextApiResponse) => { - respondToMatchingRequest(HttpMethod.GET, req, res, [ - { - identifier: 'slide-example', - title: 'Slide example', - lastVisitedAt: '2020-05-30T15:20:36.088Z', - pinStatus: true, - tags: ['features', 'cool', 'updated'], - owner: null - }, - { - identifier: 'features', - title: 'Features', - lastVisitedAt: '2020-05-31T15:20:36.088Z', - pinStatus: true, - tags: ['features', 'cool', 'updated'], - owner: null - }, - { - identifier: 'ODakLc2MQkyyFc_Xmb53sg', - title: 'Non existent', - lastVisitedAt: '2020-05-25T19:48:14.025Z', - pinStatus: false, - tags: [], - owner: null - }, - { - identifier: 'l8JuWxApTR6Fqa0LCrpnLg', - title: 'Non existent', - lastVisitedAt: '2020-05-24T16:04:36.433Z', - pinStatus: false, - tags: ['agenda', 'HedgeDoc community', 'community call'], - owner: 'test' - } - ]) -} - -export default handler diff --git a/frontend/src/pages/api/private/notes/features/index.ts b/frontend/src/pages/api/private/notes/features/index.ts index f5ea3841c..897016be7 100644 --- a/frontend/src/pages/api/private/notes/features/index.ts +++ b/frontend/src/pages/api/private/notes/features/index.ts @@ -14,7 +14,6 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => { metadata: { id: 'exampleId', version: 2, - viewCount: 0, updatedAt: '2021-04-24T09:27:51.000Z', createdAt: '2021-04-24T09:27:51.000Z', lastUpdatedBy: null, @@ -23,13 +22,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => { title: 'Features', tags: ['hedgedoc', 'demo', 'react'], description: 'Many features, such wow!', - aliases: [ - { - name: 'features', - primaryAlias: true, - noteId: 'exampleId' - } - ], + aliases: ['features'], permissions: { owner: 'tilman', sharedToUsers: [ diff --git a/frontend/src/pages/api/private/notes/features/revisions/0.ts b/frontend/src/pages/api/private/notes/features/revisions/0.ts index 1a7e7955e..ed76cfb9e 100644 --- a/frontend/src/pages/api/private/notes/features/revisions/0.ts +++ b/frontend/src/pages/api/private/notes/features/revisions/0.ts @@ -115,7 +115,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => { edits: [], length: 2782, authorUsernames: [], - anonymousAuthorCount: 2, + authorGuestUuids: ['1', '2', '3'], content: `--- title: Features description: Many features, such wow! diff --git a/frontend/src/pages/api/private/notes/features/revisions/1.ts b/frontend/src/pages/api/private/notes/features/revisions/1.ts index 53be7be80..de8da9e3b 100644 --- a/frontend/src/pages/api/private/notes/features/revisions/1.ts +++ b/frontend/src/pages/api/private/notes/features/revisions/1.ts @@ -63,7 +63,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => { edits: [], length: 2788, authorUsernames: [], - anonymousAuthorCount: 4, + authorGuestUuids: ['1', '2', '3'], content: `--- title: Features description: Many more features, such wow! diff --git a/frontend/src/pages/api/private/notes/features/revisions/index.ts b/frontend/src/pages/api/private/notes/features/revisions/index.ts index 021ec252f..88625224e 100644 --- a/frontend/src/pages/api/private/notes/features/revisions/index.ts +++ b/frontend/src/pages/api/private/notes/features/revisions/index.ts @@ -14,7 +14,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => { createdAt: '2021-12-29T17:54:11.000Z', length: 2788, authorUsernames: [], - anonymousAuthorCount: 4, + authorGuestUuids: ['1', '2', '3'], title: 'Features', description: 'Many features, such wow!', tags: ['hedgedoc', 'demo', 'react'] @@ -24,7 +24,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => { createdAt: '2021-12-21T16:59:42.000Z', length: 2782, authorUsernames: [], - anonymousAuthorCount: 2, + authorGuestUuids: ['1', '2', '3'], title: 'Features', description: 'Many more features, such wow!', tags: ['hedgedoc', 'demo', 'react'] diff --git a/frontend/src/pages/api/private/notes/index.ts b/frontend/src/pages/api/private/notes/index.ts index bb62b63c9..5e3274193 100644 --- a/frontend/src/pages/api/private/notes/index.ts +++ b/frontend/src/pages/api/private/notes/index.ts @@ -17,7 +17,6 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => { metadata: { id: 'featuresId', version: 2, - viewCount: 0, updatedAt: '2021-04-24T09:27:51.000Z', createdAt: '2021-04-24T09:27:51.000Z', lastUpdatedBy: null, @@ -26,13 +25,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => { title: 'New note', tags: ['hedgedoc', 'demo', 'react'], description: 'Many features, such wow!', - aliases: [ - { - name: 'features', - primaryAlias: true, - noteId: 'featuresId' - } - ], + aliases: ['features'], permissions: { owner: 'tilman', sharedToUsers: [ diff --git a/frontend/src/pages/api/private/notes/slide-example/index.ts b/frontend/src/pages/api/private/notes/slide-example/index.ts index beb9abc78..77552dcd3 100644 --- a/frontend/src/pages/api/private/notes/slide-example/index.ts +++ b/frontend/src/pages/api/private/notes/slide-example/index.ts @@ -15,7 +15,6 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => { id: 'slideId', primaryAlias: 'slide-example', version: 2, - viewCount: 8, updatedAt: '2021-04-30T18:38:23.000Z', lastUpdatedBy: null, createdAt: '2021-04-30T18:38:14.000Z', @@ -23,13 +22,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => { title: 'Slide example', tags: [], description: '', - aliases: [ - { - noteId: 'slideId', - primaryAlias: true, - name: 'slide-example' - } - ], + aliases: ['slideId'], permissions: { owner: 'erik', sharedToUsers: [ diff --git a/frontend/src/redux/history/initial-state.ts b/frontend/src/redux/history/initial-state.ts deleted file mode 100644 index 33f78b3ec..000000000 --- a/frontend/src/redux/history/initial-state.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { HistoryState } from './types' - -export const initialState: HistoryState = [] diff --git a/frontend/src/redux/history/methods.ts b/frontend/src/redux/history/methods.ts deleted file mode 100644 index 27d3a8341..000000000 --- a/frontend/src/redux/history/methods.ts +++ /dev/null @@ -1,257 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { - deleteRemoteHistory, - deleteRemoteHistoryEntry, - getRemoteHistory, - setRemoteHistoryEntries, - updateRemoteHistoryEntryPinStatus -} from '../../api/history' -import { addRemoteOriginToHistoryEntry, historyEntryToHistoryEntryPutDto } from '../../api/history/dto-methods' -import type { HistoryEntry, HistoryEntryWithOrigin } from '../../api/history/types' -import { HistoryEntryOrigin } from '../../api/history/types' -import { download } from '../../components/common/download/download' -import { Logger } from '../../utils/logger' -import { store } from '../index' -import type { HistoryExportJson, V1HistoryEntry } from './types' -import { DateTime } from 'luxon' -import { historyActionsCreator } from './slice' - -const log = new Logger('Redux > History') - -/** - * 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 => { - const action = historyActionsCreator.setEntries(entries) - store.dispatch(action) - storeLocalHistory() -} - -/** - * 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 => { - setHistoryEntries(entries) - return storeRemoteHistory() -} - -/** - * Deletes all history entries in the redux, local-storage and on the server. - */ -export const deleteAllHistoryEntries = (): Promise => { - const action = historyActionsCreator.setEntries([]) - store.dispatch(action) - storeLocalHistory() - 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 => { - const action = historyActionsCreator.updateEntry({ - noteId, - newEntry - }) - store.dispatch(action) -} - -/** - * 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 => { - const entryToDelete = store.getState().history.find((entry) => entry.identifier === noteId) - if (entryToDelete && entryToDelete.origin === HistoryEntryOrigin.REMOTE) { - await deleteRemoteHistoryEntry(noteId) - } - const action = historyActionsCreator.removeEntry({ noteId }) - store.dispatch(action) - 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 => { - const state = store.getState().history - const entryToUpdate = state.find((entry) => entry.identifier === noteId) - if (!entryToUpdate) { - return Promise.reject(new Error(`History entry for note '${noteId}' not found`)) - } - const updatedEntry = { - ...entryToUpdate, - pinStatus: !entryToUpdate.pinStatus - } - if (entryToUpdate.origin === HistoryEntryOrigin.LOCAL) { - updateLocalHistoryEntry(noteId, updatedEntry) - } else { - await updateRemoteHistoryEntryPinStatus(noteId, updatedEntry.pinStatus) - updateHistoryEntryRedux(noteId, updatedEntry) - } -} - -/** - * Exports the current history redux state into a JSON file that will be downloaded by the client. - */ -export const downloadHistory = (): void => { - const history = store.getState().history - history.forEach((entry: Partial) => { - delete entry.origin - }) - const json = JSON.stringify({ - version: 2, - entries: history - } as HistoryExportJson) - download(json, `history_${Date.now()}.json`, 'application/json') -} - -/** - * 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) -} - -/** - * 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, - lastVisitedAt: DateTime.fromMillis(entry.time).toISO(), - pinStatus: entry.pinned, - origin: HistoryEntryOrigin.LOCAL, - owner: null - })) -} - -/** - * 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 => { - const localEntries = loadLocalHistory() - if (!store.getState().user) { - setHistoryEntries(localEntries) - return - } - const remoteEntries = await loadRemoteHistory() - const allEntries = mergeHistoryEntries(localEntries, remoteEntries) - setHistoryEntries(allEntries) -} - -/** - * Stores the history entries marked as local from the redux to the user's local-storage. - */ -export const storeLocalHistory = (): void => { - const history = store.getState().history - const localEntries = history.filter((entry) => entry.origin === HistoryEntryOrigin.LOCAL) - const entriesWithoutOrigin = localEntries.map((entry) => ({ - ...entry, - origin: undefined - })) - try { - window.localStorage.setItem('history', JSON.stringify(entriesWithoutOrigin)) - } catch (error) { - log.error("Can't save history", error) - } -} - -/** - * Stores the history entries marked as remote from the redux to the server. - */ -export const storeRemoteHistory = (): Promise => { - if (!store.getState().user) { - return Promise.resolve() - } - const history = store.getState().history - const remoteEntries = history.filter((entry) => entry.origin === HistoryEntryOrigin.REMOTE) - const remoteEntryDtos = remoteEntries.map(historyEntryToHistoryEntryPutDto) - return setRemoteHistoryEntries(remoteEntryDtos) -} - -/** - * 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 = readV1HistoryEntriesFromLocalStorage() - if (localV1Json) { - try { - const localV1History = JSON.parse(JSON.parse(localV1Json) as string) as V1HistoryEntry[] - window.localStorage.removeItem('notehistory') - return convertV1History(localV1History) - } catch (error) { - log.error('Error while converting old history entries', error) - return [] - } - } - - const localJson = window.localStorage.getItem('history') - if (!localJson) { - return [] - } - - try { - const localHistory = JSON.parse(localJson) as HistoryEntryWithOrigin[] - localHistory.forEach((entry) => { - entry.origin = HistoryEntryOrigin.LOCAL - }) - return localHistory - } catch (error) { - log.error('Error while parsing locally stored history entries', error) - return [] - } -} - -const readV1HistoryEntriesFromLocalStorage = () => { - try { - return window.localStorage.getItem('notehistory') - } catch { - return null - } -} - -/** - * 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 => { - try { - const remoteHistory = await getRemoteHistory() - return remoteHistory.map(addRemoteOriginToHistoryEntry) - } catch (error) { - log.error('Error while fetching history entries from server', error) - return [] - } -} diff --git a/frontend/src/redux/history/slice.ts b/frontend/src/redux/history/slice.ts deleted file mode 100644 index b96c2d4d8..000000000 --- a/frontend/src/redux/history/slice.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { PayloadAction } from '@reduxjs/toolkit' -import { createSlice } from '@reduxjs/toolkit' -import { initialState } from './initial-state' -import type { HistoryState, RemoveEntryPayload, UpdateEntryPayload } from './types' -import type { HistoryEntryWithOrigin } from '../../api/history/types' - -const historySlice = createSlice({ - name: 'history', - initialState, - reducers: { - setEntries: (state, action: PayloadAction) => { - return action.payload - }, - updateEntry: (state, action: PayloadAction) => { - const entryToUpdateIndex = state.findIndex((entry) => entry.identifier === action.payload.noteId) - if (entryToUpdateIndex < 0) { - return state - } - const updatedEntry: HistoryEntryWithOrigin = { ...state[entryToUpdateIndex], ...action.payload.newEntry } - return state.toSpliced(entryToUpdateIndex, 1, updatedEntry) - }, - removeEntry: (state, action: PayloadAction) => { - return state.filter((entry) => entry.identifier !== action.payload.noteId) - } - } -}) - -export const historyActionsCreator = historySlice.actions -export const historyReducer = historySlice.reducer diff --git a/frontend/src/redux/history/types.ts b/frontend/src/redux/history/types.ts deleted file mode 100644 index bada8afb0..000000000 --- a/frontend/src/redux/history/types.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { HistoryEntry, HistoryEntryWithOrigin } from '../../api/history/types' - -export type HistoryState = HistoryEntryWithOrigin[] - -export interface V1HistoryEntry { - id: string - text: string - time: number - tags: string[] - pinned: boolean -} - -export interface HistoryExportJson { - version: number - entries: HistoryEntryWithOrigin[] -} - -export interface UpdateEntryPayload { - noteId: string - newEntry: HistoryEntry -} - -export interface RemoveEntryPayload { - noteId: string -} diff --git a/frontend/src/redux/index.ts b/frontend/src/redux/index.ts index 1a7f332e3..22fc08fe3 100644 --- a/frontend/src/redux/index.ts +++ b/frontend/src/redux/index.ts @@ -10,7 +10,6 @@ import { editorConfigReducer } from './editor-config/slice' import { userReducer } from './user/slice' import { rendererStatusReducer } from './renderer-status/slice' import { realtimeStatusReducer } from './realtime/slice' -import { historyReducer } from './history/slice' import { noteDetailsReducer } from './note-details/slice' import { printModeReducer } from './print-mode/slice' @@ -21,7 +20,6 @@ export const store = configureStore({ user: userReducer, rendererStatus: rendererStatusReducer, realtimeStatus: realtimeStatusReducer, - history: historyReducer, noteDetails: noteDetailsReducer, printMode: printModeReducer }, diff --git a/frontend/src/redux/note-details/reducers/build-state-from-metadata-update.spec.ts b/frontend/src/redux/note-details/reducers/build-state-from-metadata-update.spec.ts index f039d33d0..a6c81b91f 100644 --- a/frontend/src/redux/note-details/reducers/build-state-from-metadata-update.spec.ts +++ b/frontend/src/redux/note-details/reducers/build-state-from-metadata-update.spec.ts @@ -22,11 +22,9 @@ describe('build state from server permissions', () => { primaryAlias: 'test-id', tags: ['test'], description: 'test', - id: 'test-id', aliases: [], title: 'test', version: 2, - viewCount: 42, createdAt: '2022-09-18T18:51:00.000+02:00', updatedAt: '2022-09-18T18:52:00.000+02:00' } @@ -40,11 +38,9 @@ describe('build state from server permissions', () => { }, editedBy: [], primaryAlias: 'test-id', - id: 'test-id', aliases: [], title: 'test', version: 2, - viewCount: 42, createdAt: 1663519860, updatedAt: 1663519920 }) diff --git a/frontend/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.spec.ts b/frontend/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.spec.ts index 3d2e5c183..07b23dfdd 100644 --- a/frontend/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.spec.ts +++ b/frontend/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.spec.ts @@ -34,14 +34,7 @@ describe('build state from set note data from server', () => { metadata: { primaryAlias: 'alias', version: 5678, - aliases: [ - { - noteId: 'id', - primaryAlias: true, - name: 'alias' - } - ], - id: 'id', + aliases: ['alias'], createdAt: '2012-05-25T09:08:34.123', description: 'description', editedBy: ['editedBy'], @@ -60,7 +53,6 @@ describe('build state from set note data from server', () => { } ] }, - viewCount: 987, tags: ['tag'], title: 'title', updatedAt: '2020-05-25T09:08:34.123', @@ -107,18 +99,10 @@ describe('build state from set note data from server', () => { }, firstHeading: '', rawFrontmatter: '', - id: 'id', createdAt: DateTime.fromISO('2012-05-25T09:08:34.123').toSeconds(), updatedAt: DateTime.fromISO('2020-05-25T09:08:34.123').toSeconds(), lastUpdatedBy: 'updateusername', - viewCount: 987, - aliases: [ - { - name: 'alias', - noteId: 'id', - primaryAlias: true - } - ], + aliases: ['alias'], primaryAlias: 'alias', version: 5678, editedBy: ['editedBy'], diff --git a/frontend/src/test-utils/mock-app-state.ts b/frontend/src/test-utils/mock-app-state.ts index 989ea26b8..264b168b5 100644 --- a/frontend/src/test-utils/mock-app-state.ts +++ b/frontend/src/test-utils/mock-app-state.ts @@ -34,7 +34,6 @@ export const mockAppState = (state?: DeepPartial) => { ...initialStateEditorConfig, ...state?.editorConfig }, - history: [], // Yes this allows no mocking and is therefore technically not correct, but the type is difficult to fix and we will remove it soon anyway. noteDetails: { ...initialStateNoteDetails, ...state?.noteDetails