1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-10 22:11:50 +02:00

Desktop, Mobile: Fixes #12097: Add ability to delete all history for individual notes (#12381)

This commit is contained in:
mrjo118
2025-06-07 12:52:55 +01:00
committed by GitHub
parent 73ed17e851
commit a47d7906af
6 changed files with 94 additions and 2 deletions

View File

@@ -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

1
.gitignore vendored
View File

@@ -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

View File

@@ -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<Props> = ({ themeId, noteId, onBack,
const [revisions, setRevisions] = useState<RevisionEntity[]>([]);
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<Props> = ({ 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<Props> = ({ themeId, noteId, onBack,
}
const restoreButtonTitle = _('Restore');
const deleteHistoryButtonTitle = _('Delete history');
const helpMessage = getHelpMessage(restoreButtonTitle);
const titleInput = (
@@ -183,6 +197,9 @@ const NoteRevisionViewerComponent: React.FC<Props> = ({ themeId, noteId, onBack,
<button disabled={!revisions.length || restoring} onClick={importButton_onClick} className='restore'style={{ ...theme.buttonStyle, marginLeft: 10, height: theme.inputStyle.height }}>
{restoreButtonTitle}
</button>
<button disabled={!revisions.length || deleting} onClick={deleteHistoryButton_onClick} className='deleteHistory'style={{ ...theme.buttonStyle, marginLeft: 10, height: theme.inputStyle.height }}>
{deleteHistoryButtonTitle}
</button>
<HelpButton tip={helpMessage} id="noteRevisionHelpButton" onClick={helpButton_onClick} />
</div>
);

View File

@@ -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> = props => {
const [currentRevisionId, setCurrentRevisionId] = useState<string>('');
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> = props => {
value: revision.id,
});
}
setHasRevisions(result.length > 0);
return result;
}, [revisions]);
@@ -142,6 +145,31 @@ const NoteRevisionViewer: React.FC<Props> = 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> = props => {
);
return <View style={styles.root}>
<ScreenHeader title={_('Note history')} />
<ScreenHeader menuOptions={menuOptions} title={_('Note history')} />
<View style={styles.controls}>
<Text variant='labelLarge'>{dropdownLabelText}</Text>
<Dropdown

View File

@@ -0,0 +1,36 @@
import { _ } from '../../../locale';
import Revision from '../../../models/Revision';
import shim, { MessageBoxType } from '../../../shim';
const { useCallback } = shim.react();
interface Props {
noteId?: string;
setDeleting(deleting: boolean): void;
resetScreenState(): void;
}
const useDeleteHistoryClick = ({
noteId, setDeleting, resetScreenState,
}: Props) => {
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;

View File

@@ -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;