From 43e40bcf5ad5aa4a6495364898f4bb4454a55df4 Mon Sep 17 00:00:00 2001 From: Andrej Lifinzew Date: Sun, 26 Feb 2023 13:13:45 +0100 Subject: [PATCH] CLI: Resolves #1728: Create subnotebooks (#6722) --- .eslintignore | 2 + .gitignore | 2 + packages/app-cli/app/base-command.js | 97 --------------------- packages/app-cli/app/command-mkbook.js | 21 ----- packages/app-cli/app/command-mkbook.test.ts | 50 +++++++++++ packages/app-cli/app/command-mkbook.ts | 65 ++++++++++++++ 6 files changed, 119 insertions(+), 118 deletions(-) delete mode 100644 packages/app-cli/app/base-command.js delete mode 100644 packages/app-cli/app/command-mkbook.js create mode 100644 packages/app-cli/app/command-mkbook.test.ts create mode 100644 packages/app-cli/app/command-mkbook.ts diff --git a/.eslintignore b/.eslintignore index 1578d9434..ff8f54135 100644 --- a/.eslintignore +++ b/.eslintignore @@ -79,6 +79,8 @@ packages/app-cli/app/LinkSelector.js packages/app-cli/app/base-command.js packages/app-cli/app/command-done.test.js packages/app-cli/app/command-e2ee.js +packages/app-cli/app/command-mkbook.js +packages/app-cli/app/command-mkbook.test.js packages/app-cli/app/command-settingschema.js packages/app-cli/app/command-sync.js packages/app-cli/app/command-testing.js diff --git a/.gitignore b/.gitignore index 1bd166dd5..3b71c84b4 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,8 @@ packages/app-cli/app/LinkSelector.js packages/app-cli/app/base-command.js packages/app-cli/app/command-done.test.js packages/app-cli/app/command-e2ee.js +packages/app-cli/app/command-mkbook.js +packages/app-cli/app/command-mkbook.test.js packages/app-cli/app/command-settingschema.js packages/app-cli/app/command-sync.js packages/app-cli/app/command-testing.js diff --git a/packages/app-cli/app/base-command.js b/packages/app-cli/app/base-command.js deleted file mode 100644 index 682ae33a9..000000000 --- a/packages/app-cli/app/base-command.js +++ /dev/null @@ -1,97 +0,0 @@ -"use strict"; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const locale_1 = require("@joplin/lib/locale"); -const registry_js_1 = require("@joplin/lib/registry.js"); -class BaseCommand { - constructor() { - this.stdout_ = null; - this.prompt_ = null; - } - usage() { - throw new Error('Usage not defined'); - } - encryptionCheck(item) { - if (item && item.encryption_applied) - throw new Error((0, locale_1._)('Cannot change encrypted item')); - } - description() { - throw new Error('Description not defined'); - } - action(_args) { - return __awaiter(this, void 0, void 0, function* () { - throw new Error('Action not defined'); - }); - } - compatibleUis() { - return ['cli', 'gui']; - } - supportsUi(ui) { - return this.compatibleUis().indexOf(ui) >= 0; - } - options() { - return []; - } - hidden() { - return false; - } - enabled() { - return true; - } - cancellable() { - return false; - } - cancel() { - return __awaiter(this, void 0, void 0, function* () { }); - } - name() { - const r = this.usage().split(' '); - return r[0]; - } - setDispatcher(fn) { - this.dispatcher_ = fn; - } - dispatch(action) { - if (!this.dispatcher_) - throw new Error('Dispatcher not defined'); - return this.dispatcher_(action); - } - setStdout(fn) { - this.stdout_ = fn; - } - stdout(text) { - if (this.stdout_) - this.stdout_(text); - } - setPrompt(fn) { - this.prompt_ = fn; - } - prompt(message, options = null) { - return __awaiter(this, void 0, void 0, function* () { - if (!this.prompt_) - throw new Error('Prompt is undefined'); - return yield this.prompt_(message, options); - }); - } - metadata() { - return { - name: this.name(), - usage: this.usage(), - options: this.options(), - hidden: this.hidden(), - }; - } - logger() { - return registry_js_1.reg.logger(); - } -} -exports.default = BaseCommand; -//# sourceMappingURL=base-command.js.map \ No newline at end of file diff --git a/packages/app-cli/app/command-mkbook.js b/packages/app-cli/app/command-mkbook.js deleted file mode 100644 index 9dbdaff01..000000000 --- a/packages/app-cli/app/command-mkbook.js +++ /dev/null @@ -1,21 +0,0 @@ -const BaseCommand = require('./base-command').default; -const { app } = require('./app.js'); -const { _ } = require('@joplin/lib/locale'); -const Folder = require('@joplin/lib/models/Folder').default; - -class Command extends BaseCommand { - usage() { - return 'mkbook '; - } - - description() { - return _('Creates a new notebook.'); - } - - async action(args) { - const folder = await Folder.save({ title: args['new-notebook'] }, { userSideValidation: true }); - app().switchCurrentFolder(folder); - } -} - -module.exports = Command; diff --git a/packages/app-cli/app/command-mkbook.test.ts b/packages/app-cli/app/command-mkbook.test.ts new file mode 100644 index 000000000..6e7e1de19 --- /dev/null +++ b/packages/app-cli/app/command-mkbook.test.ts @@ -0,0 +1,50 @@ +import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils'; +import { setupCommandForTesting, setupApplication } from './utils/testUtils'; +import Folder from '@joplin/lib/models/Folder'; +const Command = require('./command-mkbook'); + + +describe('command-mkbook', () => { + + beforeEach(async () => { + await setupDatabaseAndSynchronizer(1); + await switchClient(1); + await setupApplication(); + }); + + + it('should create a subfolder in first folder', async () => { + const command = setupCommandForTesting(Command); + await command.action({ 'new-notebook': 'folder1', options: {} }); + await command.action({ 'new-notebook': 'folder1_1', options: { parent: 'folder1' } }); + + const folder1 = await Folder.loadByTitle('folder1'); + const folder1_1 = await Folder.loadByTitle('folder1_1'); + + expect(folder1.title).toBe('folder1'); + expect(folder1_1.parent_id).toBe(folder1.id); + }); + + it('should not be possible to create a subfolder without an argument.', async () => { + const command = setupCommandForTesting(Command); + await command.action({ 'new-notebook': 'folder2', options: {} }); + await expect(command.action({ 'new-notebook': 'folder2_1', options: { parent: true } })).rejects.toThrowError(); + }); + + it('should not be possible to create subfolder in ambiguous destination folder', async () => { + const command = setupCommandForTesting(Command); + await command.action({ 'new-notebook': 'folder3', options: {} }); + await command.action({ 'new-notebook': 'folder3', options: {} }); // ambiguous folder + await expect(command.action({ 'new-notebook': 'folder3_1', options: { parent: 'folder3' } })).rejects.toThrowError(); + + // check if duplicate entries have been created. + const folderAll = await Folder.all(); + const folders3 = folderAll.filter(x => x.title === 'folder3'); + expect(folders3.length).toBe(2); + + // check if something has been created in one of the duplicate entries. + expect(await Folder.childrenIds(folders3[0].id)).toEqual([]); + expect(await Folder.childrenIds(folders3[1].id)).toEqual([]); + }); +}); + diff --git a/packages/app-cli/app/command-mkbook.ts b/packages/app-cli/app/command-mkbook.ts new file mode 100644 index 000000000..9677f446d --- /dev/null +++ b/packages/app-cli/app/command-mkbook.ts @@ -0,0 +1,65 @@ +const BaseCommand = require('./base-command').default; +const { app } = require('./app.js'); +import { _ } from '@joplin/lib/locale'; +import BaseModel from '@joplin/lib/BaseModel'; +import Folder from '@joplin/lib/models/Folder'; +import { FolderEntity } from '@joplin/lib/services/database/types'; + +class Command extends BaseCommand { + usage() { + return 'mkbook '; + } + + description() { + return _('Creates a new notebook.'); + } + + options() { + return [ + ['-p, --parent ', _('Create a new notebook under a parent notebook.')], + ]; + } + + // validDestinationFolder check for presents and ambiguous folders + async validDestinationFolder(targetFolder: string) { + + const destinationFolder = await app().loadItem(BaseModel.TYPE_FOLDER, targetFolder); + if (!destinationFolder) { + throw new Error(_('Cannot find: "%s"', targetFolder)); + } + + const destinationDups = await Folder.search({ titlePattern: targetFolder, limit: 2 }); + if (destinationDups.length > 1) { + throw new Error(_('Ambiguous notebook "%s". Please use short notebook id instead - press "ti" to see the short notebook id', targetFolder)); + } + + return destinationFolder; + } + + async saveAndSwitchFolder(newFolder: FolderEntity) { + + const folder = await Folder.save(newFolder, { userSideValidation: true }); + app().switchCurrentFolder(folder); + + } + + async action(args: any) { + const targetFolder = args.options.parent; + + const newFolder: FolderEntity = { + title: args['new-notebook'], + }; + + if (targetFolder) { + + const destinationFolder = await this.validDestinationFolder(targetFolder); + newFolder.parent_id = destinationFolder.id; + await this.saveAndSwitchFolder(newFolder); + + } else { + await this.saveAndSwitchFolder(newFolder); + } + } +} + +module.exports = Command;