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