From a47d7906af124c506a36e95b459a5211bac80c6e Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Sat, 7 Jun 2025 12:52:55 +0100 Subject: [PATCH] Desktop, Mobile: Fixes #12097: Add ability to delete all history for individual notes (#12381) --- .eslintignore | 1 + .gitignore | 1 + .../app-desktop/gui/NoteRevisionViewer.tsx | 17 +++++++++ .../components/screens/NoteRevisionViewer.tsx | 32 +++++++++++++++-- .../useDeleteHistoryClick.ts | 36 +++++++++++++++++++ packages/lib/models/Revision.ts | 9 +++++ 6 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 packages/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick.ts diff --git a/.eslintignore b/.eslintignore index c5ef6defde..6f87bcba7a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1086,6 +1086,7 @@ packages/lib/components/EncryptionConfigScreen/utils.test.js packages/lib/components/EncryptionConfigScreen/utils.js packages/lib/components/shared/NoteList/getEmptyFolderMessage.js packages/lib/components/shared/NoteRevisionViewer/getHelpMessage.js +packages/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick.js packages/lib/components/shared/SamlShared.js packages/lib/components/shared/ShareNoteDialog/onUnshareNoteClick.js packages/lib/components/shared/ShareNoteDialog/types.js diff --git a/.gitignore b/.gitignore index 59f57c73a1..30bee03158 100644 --- a/.gitignore +++ b/.gitignore @@ -1061,6 +1061,7 @@ packages/lib/components/EncryptionConfigScreen/utils.test.js packages/lib/components/EncryptionConfigScreen/utils.js packages/lib/components/shared/NoteList/getEmptyFolderMessage.js packages/lib/components/shared/NoteRevisionViewer/getHelpMessage.js +packages/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick.js packages/lib/components/shared/SamlShared.js packages/lib/components/shared/ShareNoteDialog/onUnshareNoteClick.js packages/lib/components/shared/ShareNoteDialog/types.js diff --git a/packages/app-desktop/gui/NoteRevisionViewer.tsx b/packages/app-desktop/gui/NoteRevisionViewer.tsx index 1aa5f33bd9..0350087a08 100644 --- a/packages/app-desktop/gui/NoteRevisionViewer.tsx +++ b/packages/app-desktop/gui/NoteRevisionViewer.tsx @@ -24,6 +24,7 @@ import useMarkupToHtml from './hooks/useMarkupToHtml'; import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata'; import { focus } from '@joplin/lib/utils/focusHandler'; +import useDeleteHistoryClick from '@joplin/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick'; interface Props { themeId: number; @@ -89,6 +90,7 @@ const NoteRevisionViewerComponent: React.FC = ({ themeId, noteId, onBack, const [revisions, setRevisions] = useState([]); const [currentRevId, setCurrentRevId] = useState(''); const [restoring, setRestoring] = useState(false); + const [deleting, setDeleting] = useState(false); const note = useNoteContent( viewerRef, currentRevId, revisions, themeId, customCss, scrollbarSize, fontFamily, @@ -111,6 +113,17 @@ const NoteRevisionViewerComponent: React.FC = ({ themeId, noteId, onBack, await shim.showMessageBox(RevisionService.instance().restoreSuccessMessage(note), { type: MessageBoxType.Info }); }, [note]); + const resetScreenState = useCallback(() => { + setRevisions([]); + setCurrentRevId(null); + }, []); + + const deleteHistoryButton_onClick = useDeleteHistoryClick({ + noteId: note?.id, + setDeleting, + resetScreenState, + }); + const backButton_click = useCallback(() => { if (onBack) onBack(); }, [onBack]); @@ -169,6 +182,7 @@ const NoteRevisionViewerComponent: React.FC = ({ themeId, noteId, onBack, } const restoreButtonTitle = _('Restore'); + const deleteHistoryButtonTitle = _('Delete history'); const helpMessage = getHelpMessage(restoreButtonTitle); const titleInput = ( @@ -183,6 +197,9 @@ const NoteRevisionViewerComponent: React.FC = ({ themeId, noteId, onBack, + ); diff --git a/packages/app-mobile/components/screens/NoteRevisionViewer.tsx b/packages/app-mobile/components/screens/NoteRevisionViewer.tsx index 71ff0e3965..8032e066c2 100644 --- a/packages/app-mobile/components/screens/NoteRevisionViewer.tsx +++ b/packages/app-mobile/components/screens/NoteRevisionViewer.tsx @@ -7,7 +7,7 @@ import Revision from '@joplin/lib/models/Revision'; import BaseModel, { ModelType } from '@joplin/lib/BaseModel'; import { IconButton, Text } from 'react-native-paper'; import Dropdown from '../Dropdown'; -import ScreenHeader from '../ScreenHeader'; +import ScreenHeader, { MenuOptionType } from '../ScreenHeader'; import { formatMsToLocal } from '@joplin/utils/time'; import { useCallback, useContext, useMemo, useState } from 'react'; import { PrimaryButton } from '../buttons'; @@ -21,6 +21,7 @@ import shim, { MessageBoxType } from '@joplin/lib/shim'; import { themeStyle } from '../global-style'; import getHelpMessage from '@joplin/lib/components/shared/NoteRevisionViewer/getHelpMessage'; import { DialogContext } from '../DialogManager'; +import useDeleteHistoryClick from '@joplin/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick'; interface Props { themeId: number; @@ -113,6 +114,7 @@ const NoteRevisionViewer: React.FC = props => { const [currentRevisionId, setCurrentRevisionId] = useState(''); const { note, resources } = useRevisionNote(revisions, currentRevisionId); const [initialScroll, setInitialScroll] = useState(0); + const [hasRevisions, setHasRevisions] = useState(false); const options = useMemo(() => { const result = []; @@ -123,6 +125,7 @@ const NoteRevisionViewer: React.FC = props => { value: revision.id, }); } + setHasRevisions(result.length > 0); return result; }, [revisions]); @@ -142,6 +145,31 @@ const NoteRevisionViewer: React.FC = props => { } }, [note]); + const resetScreenState = useCallback(() => { + setCurrentRevisionId(null); + setHasRevisions(false); + revisions.length = 0; + options.length = 0; + }, [revisions, options]); + + const [deleting, setDeleting] = useState(false); + const deleteHistory_onPress = useDeleteHistoryClick({ + noteId, + setDeleting, + resetScreenState, + }); + + const disableDeleteHistory = deleting || !hasRevisions; + const menuOptions = useMemo(() => { + const output: MenuOptionType[] = [{ + title: _('Delete history'), + onPress: deleteHistory_onPress, + disabled: disableDeleteHistory, + }]; + + return output; + }, [deleteHistory_onPress, disableDeleteHistory]); + const restoreButtonTitle = _('Restore'); const helpMessageText = getHelpMessage(restoreButtonTitle); const dialogs = useContext(DialogContext); @@ -160,7 +188,7 @@ const NoteRevisionViewer: React.FC = props => { ); return - + {dropdownLabelText} { + return useCallback(async () => { + if (!noteId) return; + const response = await shim.showMessageBox(_('Are you sure you want to delete all history for this note? This cannot be undone.'), { + title: _('Warning'), + buttons: [_('Yes'), _('No')], + type: MessageBoxType.Confirm, + }); + + if (response === 0) { + setDeleting(true); + try { + await Revision.deleteHistoryForNote(noteId); + await shim.showMessageBox(_('Note history has been deleted.'), { type: MessageBoxType.Info }); + } finally { + setDeleting(false); + } + resetScreenState(); + } + }, [noteId, setDeleting, resetScreenState]); +}; + +export default useDeleteHistoryClick; diff --git a/packages/lib/models/Revision.ts b/packages/lib/models/Revision.ts index 135e8f6828..5041f17b0d 100644 --- a/packages/lib/models/Revision.ts +++ b/packages/lib/models/Revision.ts @@ -373,6 +373,15 @@ export default class Revision extends BaseItem { } } + public static async deleteHistoryForNote(noteId: string) { + const revisions: RevisionEntity[] = await this.modelSelectAll( + 'SELECT id FROM revisions WHERE item_type = ? AND item_id = ? ORDER BY item_updated_time DESC', + [ModelType.Note, noteId], + ); + + await this.batchDelete(revisions.map(item => item.id), { sourceDescription: 'Revision.deleteHistoryForNote' }); + } + public static async revisionExists(itemType: ModelType, itemId: string, updatedTime: number) { const existingRev = await Revision.latestRevision(itemType, itemId); return existingRev && existingRev.item_updated_time === updatedTime;