mirror of
https://github.com/laurent22/joplin.git
synced 2025-03-11 14:09:55 +02:00
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
This commit is contained in:
parent
49cd17e520
commit
78b8839ae3
@ -110,7 +110,9 @@ packages/app-cli/app/command-mkbook.js
|
|||||||
packages/app-cli/app/command-mv.js
|
packages/app-cli/app/command-mv.js
|
||||||
packages/app-cli/app/command-ren.js
|
packages/app-cli/app/command-ren.js
|
||||||
packages/app-cli/app/command-restore.js
|
packages/app-cli/app/command-restore.js
|
||||||
|
packages/app-cli/app/command-rmbook.test.js
|
||||||
packages/app-cli/app/command-rmbook.js
|
packages/app-cli/app/command-rmbook.js
|
||||||
|
packages/app-cli/app/command-rmnote.test.js
|
||||||
packages/app-cli/app/command-rmnote.js
|
packages/app-cli/app/command-rmnote.js
|
||||||
packages/app-cli/app/command-set.js
|
packages/app-cli/app/command-set.js
|
||||||
packages/app-cli/app/command-settingschema.js
|
packages/app-cli/app/command-settingschema.js
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -90,7 +90,9 @@ packages/app-cli/app/command-mkbook.js
|
|||||||
packages/app-cli/app/command-mv.js
|
packages/app-cli/app/command-mv.js
|
||||||
packages/app-cli/app/command-ren.js
|
packages/app-cli/app/command-ren.js
|
||||||
packages/app-cli/app/command-restore.js
|
packages/app-cli/app/command-restore.js
|
||||||
|
packages/app-cli/app/command-rmbook.test.js
|
||||||
packages/app-cli/app/command-rmbook.js
|
packages/app-cli/app/command-rmbook.js
|
||||||
|
packages/app-cli/app/command-rmnote.test.js
|
||||||
packages/app-cli/app/command-rmnote.js
|
packages/app-cli/app/command-rmnote.js
|
||||||
packages/app-cli/app/command-set.js
|
packages/app-cli/app/command-set.js
|
||||||
packages/app-cli/app/command-settingschema.js
|
packages/app-cli/app/command-settingschema.js
|
||||||
|
81
packages/app-cli/app/command-rmbook.test.ts
Normal file
81
packages/app-cli/app/command-rmbook.test.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||||
|
import { setupCommandForTesting, setupApplication } from './utils/testUtils';
|
||||||
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
|
import Note from '@joplin/lib/models/Note';
|
||||||
|
const Command = require('./command-rmbook');
|
||||||
|
|
||||||
|
const setUpCommand = () => {
|
||||||
|
const command = setupCommandForTesting(Command);
|
||||||
|
const promptMock = jest.fn(() => true);
|
||||||
|
command.setPrompt(promptMock);
|
||||||
|
|
||||||
|
return { command, promptMock };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
describe('command-rmbook', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await setupDatabaseAndSynchronizer(1);
|
||||||
|
await switchClient(1);
|
||||||
|
await setupApplication();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ask before moving to the trash', async () => {
|
||||||
|
await Folder.save({ title: 'folder1' });
|
||||||
|
|
||||||
|
const { command, promptMock } = setUpCommand();
|
||||||
|
|
||||||
|
await command.action({ 'notebook': 'folder1', options: {} });
|
||||||
|
|
||||||
|
expect(promptMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const folder1 = await Folder.loadByTitle('folder1');
|
||||||
|
expect(folder1.deleted_time).not.toBeFalsy();
|
||||||
|
expect((await Note.allItemsInTrash()).folderIds).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancelling a prompt should prevent deletion', async () => {
|
||||||
|
await Folder.save({ title: 'folder1' });
|
||||||
|
|
||||||
|
const { command, promptMock } = setUpCommand();
|
||||||
|
promptMock.mockImplementation(() => false);
|
||||||
|
await command.action({ 'notebook': 'folder1', options: {} });
|
||||||
|
|
||||||
|
expect((await Note.allItemsInTrash()).folderIds).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not prompt when the force flag is given', async () => {
|
||||||
|
const { id: folder1Id } = await Folder.save({ title: 'folder1' });
|
||||||
|
const { id: folder2Id } = await Folder.save({ title: 'folder2', parent_id: folder1Id });
|
||||||
|
|
||||||
|
const { command, promptMock } = setUpCommand();
|
||||||
|
await command.action({ 'notebook': 'folder1', options: { force: true } });
|
||||||
|
|
||||||
|
expect(promptMock).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
expect((await Note.allItemsInTrash()).folderIds.includes(folder1Id)).toBe(true);
|
||||||
|
expect((await Note.allItemsInTrash()).folderIds.includes(folder2Id)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support permanent deletion', async () => {
|
||||||
|
const { id: folder1Id } = await Folder.save({ title: 'folder1' });
|
||||||
|
const { id: folder2Id } = await Folder.save({ title: 'folder2' });
|
||||||
|
|
||||||
|
const { command, promptMock } = setUpCommand();
|
||||||
|
await command.action({ 'notebook': 'folder1', options: { permanent: true, force: true } });
|
||||||
|
expect(promptMock).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Should be permanently deleted.
|
||||||
|
expect((await Note.allItemsInTrash()).folderIds.includes(folder1Id)).toBe(false);
|
||||||
|
expect(await Folder.load(folder1Id, { includeDeleted: true })).toBe(undefined);
|
||||||
|
|
||||||
|
// folder2 should not be deleted
|
||||||
|
expect(await Folder.load(folder2Id, { includeDeleted: false })).toBeTruthy();
|
||||||
|
|
||||||
|
// Should prompt before deleting
|
||||||
|
await command.action({ 'notebook': 'folder2', options: { permanent: true } });
|
||||||
|
expect(promptMock).toHaveBeenCalled();
|
||||||
|
expect(await Folder.load(folder2Id, { includeDeleted: false })).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -3,7 +3,7 @@ import app from './app';
|
|||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import Folder from '@joplin/lib/models/Folder';
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
import BaseModel from '@joplin/lib/BaseModel';
|
import BaseModel from '@joplin/lib/BaseModel';
|
||||||
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
|
import { substrWithEllipsis } from '@joplin/lib/string-utils';
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
public override usage() {
|
public override usage() {
|
||||||
@ -15,7 +15,10 @@ class Command extends BaseCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override options() {
|
public override options() {
|
||||||
return [['-f, --force', _('Deletes the notebook without asking for confirmation.')]];
|
return [
|
||||||
|
['-f, --force', _('Deletes the notebook without asking for confirmation.')],
|
||||||
|
['-p, --permanent', _('Permanently deletes the notebook, skipping the trash.')],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async action(args: any) {
|
public override async action(args: any) {
|
||||||
@ -24,11 +27,19 @@ class Command extends BaseCommand {
|
|||||||
|
|
||||||
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
|
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
|
||||||
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
|
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
|
||||||
const msg = _('Move notebook "%s" to the trash?\n\nAll notes and sub-notebooks within this notebook will also be moved to the trash.', substrWithEllipsis(folder.title, 0, 32));
|
|
||||||
|
const permanent = args.options?.permanent === true || !!folder.deleted_time;
|
||||||
|
const ellipsizedFolderTitle = substrWithEllipsis(folder.title, 0, 32);
|
||||||
|
let msg;
|
||||||
|
if (permanent) {
|
||||||
|
msg = _('Permanently delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will be permanently deleted.', ellipsizedFolderTitle);
|
||||||
|
} else {
|
||||||
|
msg = _('Move notebook "%s" to the trash?\n\nAll notes and sub-notebooks within this notebook will also be moved to the trash.', ellipsizedFolderTitle);
|
||||||
|
}
|
||||||
const ok = force ? true : await this.prompt(msg, { booleanAnswerDefault: 'n' });
|
const ok = force ? true : await this.prompt(msg, { booleanAnswerDefault: 'n' });
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
|
|
||||||
await Folder.delete(folder.id, { toTrash: true, sourceDescription: 'rmbook command' });
|
await Folder.delete(folder.id, { toTrash: !permanent, sourceDescription: 'rmbook command' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
57
packages/app-cli/app/command-rmnote.test.ts
Normal file
57
packages/app-cli/app/command-rmnote.test.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||||
|
import { setupCommandForTesting, setupApplication } from './utils/testUtils';
|
||||||
|
import Note from '@joplin/lib/models/Note';
|
||||||
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
|
import app from './app';
|
||||||
|
import { getTrashFolderId } from '@joplin/lib/services/trash';
|
||||||
|
const Command = require('./command-rmnote');
|
||||||
|
|
||||||
|
const setUpCommand = () => {
|
||||||
|
const command = setupCommandForTesting(Command);
|
||||||
|
const promptMock = jest.fn(() => true);
|
||||||
|
command.setPrompt(promptMock);
|
||||||
|
|
||||||
|
return { command, promptMock };
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTestNote = async () => {
|
||||||
|
const folder = await Folder.save({ title: 'folder' });
|
||||||
|
app().switchCurrentFolder(folder);
|
||||||
|
return await Note.save({ title: 'note1', parent_id: folder.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
describe('command-rmnote', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await setupDatabaseAndSynchronizer(1);
|
||||||
|
await switchClient(1);
|
||||||
|
await setupApplication();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should move to the trash by default, without prompting', async () => {
|
||||||
|
const { id: noteId } = await createTestNote();
|
||||||
|
|
||||||
|
const { command, promptMock } = setUpCommand();
|
||||||
|
await command.action({ 'note-pattern': 'note1', options: {} });
|
||||||
|
expect(promptMock).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect((await Note.allItemsInTrash()).noteIds.includes(noteId)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should permanently delete trashed items by default, with prompting', async () => {
|
||||||
|
const { id: noteId } = await createTestNote();
|
||||||
|
const { command, promptMock } = setUpCommand();
|
||||||
|
|
||||||
|
// Should not prompt when deleting from a folder
|
||||||
|
await command.action({ 'note-pattern': 'note1', options: {} });
|
||||||
|
expect(promptMock).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
// Should prompt when deleting from trash
|
||||||
|
app().switchCurrentFolder(await Folder.load(getTrashFolderId()));
|
||||||
|
await command.action({ 'note-pattern': 'note1', options: {} });
|
||||||
|
expect(promptMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(await Note.load(noteId, { includeDeleted: true })).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -2,7 +2,7 @@ import BaseCommand from './base-command';
|
|||||||
import app from './app';
|
import app from './app';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import Note from '@joplin/lib/models/Note';
|
import Note from '@joplin/lib/models/Note';
|
||||||
import BaseModel from '@joplin/lib/BaseModel';
|
import BaseModel, { DeleteOptions } from '@joplin/lib/BaseModel';
|
||||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
@ -15,7 +15,10 @@ class Command extends BaseCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override options() {
|
public override options() {
|
||||||
return [['-f, --force', _('Deletes the notes without asking for confirmation.')]];
|
return [
|
||||||
|
['-f, --force', _('Deletes the notes without asking for confirmation.')],
|
||||||
|
['-p, --permanent', _('Deletes notes permanently, skipping the trash.')],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async action(args: any) {
|
public override async action(args: any) {
|
||||||
@ -30,10 +33,22 @@ class Command extends BaseCommand {
|
|||||||
ok = await this.prompt(_('%d notes match this pattern. Delete them?', notes.length), { booleanAnswerDefault: 'n' });
|
ok = await this.prompt(_('%d notes match this pattern. Delete them?', notes.length), { booleanAnswerDefault: 'n' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const permanent = (args.options?.permanent === true) || notes.every(n => !!n.deleted_time);
|
||||||
|
if (!force && permanent) {
|
||||||
|
const message = (
|
||||||
|
notes.length === 1 ? _('This will permanently delete the note "%s". Continue?', notes[0].title) : _('%d notes will be permanently deleted. Continue?', notes.length)
|
||||||
|
);
|
||||||
|
ok = await this.prompt(message, { booleanAnswerDefault: 'n' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
|
|
||||||
const ids = notes.map(n => n.id);
|
const ids = notes.map(n => n.id);
|
||||||
await Note.batchDelete(ids, { toTrash: true, sourceDescription: 'rmnote command' });
|
const options: DeleteOptions = {
|
||||||
|
toTrash: !permanent,
|
||||||
|
sourceDescription: 'rmnote',
|
||||||
|
};
|
||||||
|
await Note.batchDelete(ids, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,4 +95,5 @@ Notyf
|
|||||||
unresponded
|
unresponded
|
||||||
activeline
|
activeline
|
||||||
Prec
|
Prec
|
||||||
Trashable
|
ellipsized
|
||||||
|
Trashable
|
Loading…
x
Reference in New Issue
Block a user