1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-18 09:35:20 +02:00

Merge remote-tracking branch 'origin/release-3.1' into dev

This commit is contained in:
Henry Heino 2024-12-09 08:29:56 -08:00
commit ca9759738f
6 changed files with 103 additions and 16 deletions

View File

@ -36,7 +36,8 @@ const PADDING_V = 10;
type OnPressCallback=()=> void;
export interface FolderPickerOptions {
enabled: boolean;
visible: boolean;
disabled?: boolean;
selectedFolderId?: string;
onValueChange?: OnValueChangedListener;
mustSelect?: boolean;
@ -515,10 +516,12 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
});
}
const createTitleComponent = (disabled: boolean, hideableAfterTitleComponents: ReactElement) => {
const createTitleComponent = (hideableAfterTitleComponents: ReactElement) => {
const folderPickerOptions = this.props.folderPickerOptions;
if (folderPickerOptions && folderPickerOptions.enabled) {
if (folderPickerOptions && folderPickerOptions.visible) {
const hasSelectedNotes = this.props.selectedNoteIds.length > 0;
const disabled = this.props.folderPickerOptions.disabled ?? !hasSelectedNotes;
return (
<FolderPicker
themeId={themeId}
@ -602,7 +605,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
{betaIconComp}
</>;
const titleComp = createTitleComponent(headerItemDisabled, hideableRightComponents);
const titleComp = createTitleComponent(hideableRightComponents);
const contextMenuStyle: ViewStyle = {
paddingTop: PADDING_V,

View File

@ -47,7 +47,7 @@ import { isSupportedLanguage } from '../../services/voiceTyping/vosk';
import { ChangeEvent as EditorChangeEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
import { join } from 'path';
import { Dispatch } from 'redux';
import { RefObject, useContext } from 'react';
import { RefObject, useContext, useRef } from 'react';
import { SelectionRange } from '../NoteEditor/types';
import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import { AppState } from '../../utils/types';
@ -1383,7 +1383,8 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
public folderPickerOptions() {
const options = {
enabled: !this.state.readOnly,
visible: !this.state.readOnly,
disabled: false,
selectedFolderId: this.state.folder ? this.state.folder.id : null,
onValueChange: this.folderPickerOptions_valueChanged,
};
@ -1391,7 +1392,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
if (
this.folderPickerOptions_
&& options.selectedFolderId === this.folderPickerOptions_.selectedFolderId
&& options.enabled === this.folderPickerOptions_.enabled
&& options.visible === this.folderPickerOptions_.visible
) {
return this.folderPickerOptions_;
}
@ -1649,9 +1650,19 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
// which can cause some bugs where previously set state to another note would interfere
// how the new note should be rendered
const NoteScreenWrapper = (props: Props) => {
const lastNonNullNoteIdRef = useRef(props.noteId);
if (props.noteId) {
lastNonNullNoteIdRef.current = props.noteId;
}
// This keeps the current note open even if it's no longer present in selectedNoteIds.
// This might happen, for example, if the selected note is moved to an unselected
// folder.
const noteId = lastNonNullNoteIdRef.current;
const dialogs = useContext(DialogContext);
return (
<NoteScreenComponent key={props.noteId} dialogs={dialogs} {...props} />
<NoteScreenComponent key={noteId} dialogs={dialogs} {...props} />
);
};

View File

@ -214,11 +214,11 @@ class NotesScreenComponent extends BaseScreenComponent<ComponentProps, State> {
public folderPickerOptions() {
const options = {
enabled: this.props.noteSelectionEnabled,
visible: this.props.noteSelectionEnabled,
mustSelect: true,
};
if (this.folderPickerOptions_ && options.enabled === this.folderPickerOptions_.enabled) return this.folderPickerOptions_;
if (this.folderPickerOptions_ && options.visible === this.folderPickerOptions_.visible) return this.folderPickerOptions_;
this.folderPickerOptions_ = options;
return this.folderPickerOptions_;

View File

@ -86,7 +86,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
<ScreenHeader
title={_('Search')}
folderPickerOptions={{
enabled: props.noteSelectionEnabled,
visible: props.noteSelectionEnabled,
mustSelect: true,
}}
showSideMenuButton={false}

View File

@ -1,7 +1,7 @@
import { Day, msleep } from '@joplin/utils/time';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import { setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
import { setupDatabaseAndSynchronizer, simulateReadOnlyShareEnv, switchClient } from '../../testing/test-utils';
import permanentlyDeleteOldItems from './permanentlyDeleteOldItems';
import Setting from '../../models/Setting';
@ -75,4 +75,42 @@ describe('permanentlyDeleteOldItems', () => {
expect(await Folder.count()).toBe(1);
});
it('should not auto-delete read-only items', async () => {
const shareId = 'testShare';
// Simulates a folder having been deleted a long time ago
const longTimeAgo = 1000;
const readOnlyFolder = await Folder.save({
title: 'Read-only folder',
share_id: shareId,
deleted_time: longTimeAgo,
});
const readOnlyNote1 = await Note.save({
title: 'Read-only note',
parent_id: readOnlyFolder.id,
share_id: shareId,
deleted_time: longTimeAgo,
});
const readOnlyNote2 = await Note.save({
title: 'Read-only note 2',
share_id: shareId,
deleted_time: longTimeAgo,
});
const writableNote = await Note.save({
title: 'Editable note',
deleted_time: longTimeAgo,
});
const cleanup = simulateReadOnlyShareEnv(shareId);
await permanentlyDeleteOldItems(Day);
// Should preserve only the read-only items.
expect(await Folder.load(readOnlyFolder.id)).toBeTruthy();
expect(await Note.load(readOnlyNote1.id)).toBeTruthy();
expect(await Note.load(readOnlyNote2.id)).toBeTruthy();
expect(await Note.load(writableNote.id)).toBeFalsy();
cleanup();
});
});

View File

@ -4,9 +4,44 @@ import Setting from '../../models/Setting';
import Note from '../../models/Note';
import { Day, Hour } from '@joplin/utils/time';
import shim from '../../shim';
import { itemIsReadOnlySync } from '../../models/utils/readOnly';
import BaseItem from '../../models/BaseItem';
import { ModelType } from '../../BaseModel';
import ItemChange from '../../models/ItemChange';
const logger = Logger.create('permanentlyDeleteOldData');
const readOnlyItemsRemoved = async (itemIds: string[], itemType: ModelType) => {
const result = [];
for (const id of itemIds) {
const item = await BaseItem.loadItem(itemType, id);
// Only do the share-related read-only checks. If other checks are done,
// readOnly will always be true because the item is in the trash.
const shareChecksOnly = true;
const readOnly = itemIsReadOnlySync(
itemType,
ItemChange.SOURCE_UNSPECIFIED,
item,
Setting.value('sync.userId'),
BaseItem.syncShareCache,
shareChecksOnly,
);
if (!readOnly) {
result.push(id);
}
}
return result;
};
const itemsToDelete = async (ttl: number|null = null) => {
const result = await Folder.trashItemsOlderThan(ttl);
const folderIds = await readOnlyItemsRemoved(result.folderIds, ModelType.Folder);
const noteIds = await readOnlyItemsRemoved(result.noteIds, ModelType.Note);
return { folderIds, noteIds };
};
const permanentlyDeleteOldItems = async (ttl: number = null) => {
ttl = ttl === null ? Setting.value('trash.ttlDays') * Day : ttl;
@ -17,13 +52,13 @@ const permanentlyDeleteOldItems = async (ttl: number = null) => {
return;
}
const result = await Folder.trashItemsOlderThan(ttl);
logger.info('Items to permanently delete:', result);
const toDelete = await itemsToDelete(ttl);
logger.info('Items to permanently delete:', toDelete);
await Note.batchDelete(result.noteIds, { sourceDescription: 'permanentlyDeleteOldItems' });
await Note.batchDelete(toDelete.noteIds, { sourceDescription: 'permanentlyDeleteOldItems' });
// We only auto-delete folders if they are empty.
for (const folderId of result.folderIds) {
for (const folderId of toDelete.folderIds) {
const noteIds = await Folder.noteIds(folderId, { includeDeleted: true });
if (!noteIds.length) {
logger.info(`Deleting empty folder: ${folderId}`);