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:
commit
ca9759738f
@ -36,7 +36,8 @@ const PADDING_V = 10;
|
|||||||
type OnPressCallback=()=> void;
|
type OnPressCallback=()=> void;
|
||||||
|
|
||||||
export interface FolderPickerOptions {
|
export interface FolderPickerOptions {
|
||||||
enabled: boolean;
|
visible: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
selectedFolderId?: string;
|
selectedFolderId?: string;
|
||||||
onValueChange?: OnValueChangedListener;
|
onValueChange?: OnValueChangedListener;
|
||||||
mustSelect?: boolean;
|
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;
|
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 (
|
return (
|
||||||
<FolderPicker
|
<FolderPicker
|
||||||
themeId={themeId}
|
themeId={themeId}
|
||||||
@ -602,7 +605,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
|||||||
{betaIconComp}
|
{betaIconComp}
|
||||||
</>;
|
</>;
|
||||||
|
|
||||||
const titleComp = createTitleComponent(headerItemDisabled, hideableRightComponents);
|
const titleComp = createTitleComponent(hideableRightComponents);
|
||||||
|
|
||||||
const contextMenuStyle: ViewStyle = {
|
const contextMenuStyle: ViewStyle = {
|
||||||
paddingTop: PADDING_V,
|
paddingTop: PADDING_V,
|
||||||
|
@ -47,7 +47,7 @@ import { isSupportedLanguage } from '../../services/voiceTyping/vosk';
|
|||||||
import { ChangeEvent as EditorChangeEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
|
import { ChangeEvent as EditorChangeEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { RefObject, useContext } from 'react';
|
import { RefObject, useContext, useRef } from 'react';
|
||||||
import { SelectionRange } from '../NoteEditor/types';
|
import { SelectionRange } from '../NoteEditor/types';
|
||||||
import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||||
import { AppState } from '../../utils/types';
|
import { AppState } from '../../utils/types';
|
||||||
@ -1383,7 +1383,8 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
|||||||
|
|
||||||
public folderPickerOptions() {
|
public folderPickerOptions() {
|
||||||
const options = {
|
const options = {
|
||||||
enabled: !this.state.readOnly,
|
visible: !this.state.readOnly,
|
||||||
|
disabled: false,
|
||||||
selectedFolderId: this.state.folder ? this.state.folder.id : null,
|
selectedFolderId: this.state.folder ? this.state.folder.id : null,
|
||||||
onValueChange: this.folderPickerOptions_valueChanged,
|
onValueChange: this.folderPickerOptions_valueChanged,
|
||||||
};
|
};
|
||||||
@ -1391,7 +1392,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
|||||||
if (
|
if (
|
||||||
this.folderPickerOptions_
|
this.folderPickerOptions_
|
||||||
&& options.selectedFolderId === this.folderPickerOptions_.selectedFolderId
|
&& options.selectedFolderId === this.folderPickerOptions_.selectedFolderId
|
||||||
&& options.enabled === this.folderPickerOptions_.enabled
|
&& options.visible === this.folderPickerOptions_.visible
|
||||||
) {
|
) {
|
||||||
return this.folderPickerOptions_;
|
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
|
// which can cause some bugs where previously set state to another note would interfere
|
||||||
// how the new note should be rendered
|
// how the new note should be rendered
|
||||||
const NoteScreenWrapper = (props: Props) => {
|
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);
|
const dialogs = useContext(DialogContext);
|
||||||
return (
|
return (
|
||||||
<NoteScreenComponent key={props.noteId} dialogs={dialogs} {...props} />
|
<NoteScreenComponent key={noteId} dialogs={dialogs} {...props} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -214,11 +214,11 @@ class NotesScreenComponent extends BaseScreenComponent<ComponentProps, State> {
|
|||||||
|
|
||||||
public folderPickerOptions() {
|
public folderPickerOptions() {
|
||||||
const options = {
|
const options = {
|
||||||
enabled: this.props.noteSelectionEnabled,
|
visible: this.props.noteSelectionEnabled,
|
||||||
mustSelect: true,
|
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;
|
this.folderPickerOptions_ = options;
|
||||||
return this.folderPickerOptions_;
|
return this.folderPickerOptions_;
|
||||||
|
@ -86,7 +86,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
|
|||||||
<ScreenHeader
|
<ScreenHeader
|
||||||
title={_('Search')}
|
title={_('Search')}
|
||||||
folderPickerOptions={{
|
folderPickerOptions={{
|
||||||
enabled: props.noteSelectionEnabled,
|
visible: props.noteSelectionEnabled,
|
||||||
mustSelect: true,
|
mustSelect: true,
|
||||||
}}
|
}}
|
||||||
showSideMenuButton={false}
|
showSideMenuButton={false}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Day, msleep } from '@joplin/utils/time';
|
import { Day, msleep } from '@joplin/utils/time';
|
||||||
import Folder from '../../models/Folder';
|
import Folder from '../../models/Folder';
|
||||||
import Note from '../../models/Note';
|
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 permanentlyDeleteOldItems from './permanentlyDeleteOldItems';
|
||||||
import Setting from '../../models/Setting';
|
import Setting from '../../models/Setting';
|
||||||
|
|
||||||
@ -75,4 +75,42 @@ describe('permanentlyDeleteOldItems', () => {
|
|||||||
expect(await Folder.count()).toBe(1);
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -4,9 +4,44 @@ import Setting from '../../models/Setting';
|
|||||||
import Note from '../../models/Note';
|
import Note from '../../models/Note';
|
||||||
import { Day, Hour } from '@joplin/utils/time';
|
import { Day, Hour } from '@joplin/utils/time';
|
||||||
import shim from '../../shim';
|
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 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) => {
|
const permanentlyDeleteOldItems = async (ttl: number = null) => {
|
||||||
ttl = ttl === null ? Setting.value('trash.ttlDays') * Day : ttl;
|
ttl = ttl === null ? Setting.value('trash.ttlDays') * Day : ttl;
|
||||||
|
|
||||||
@ -17,13 +52,13 @@ const permanentlyDeleteOldItems = async (ttl: number = null) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await Folder.trashItemsOlderThan(ttl);
|
const toDelete = await itemsToDelete(ttl);
|
||||||
logger.info('Items to permanently delete:', result);
|
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.
|
// 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 });
|
const noteIds = await Folder.noteIds(folderId, { includeDeleted: true });
|
||||||
if (!noteIds.length) {
|
if (!noteIds.length) {
|
||||||
logger.info(`Deleting empty folder: ${folderId}`);
|
logger.info(`Deleting empty folder: ${folderId}`);
|
||||||
|
Loading…
Reference in New Issue
Block a user