From ab9675544c8e195fe875b2401cea189cc89bdbbb Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Sun, 10 Jun 2018 19:15:40 +0100 Subject: [PATCH] All: Fixes #597: Also import sub-notebooks when importing JEX data --- CliClient/tests/services_InteropService.js | 37 +++++++++++++++++ ReactNativeClient/lib/models/Folder.js | 17 ++++++++ .../lib/services/InteropService.js | 10 ++++- .../services/InteropService_Importer_Raw.js | 41 ++++++++++++------- 4 files changed, 89 insertions(+), 16 deletions(-) diff --git a/CliClient/tests/services_InteropService.js b/CliClient/tests/services_InteropService.js index 90355048e..40b9f0959 100644 --- a/CliClient/tests/services_InteropService.js +++ b/CliClient/tests/services_InteropService.js @@ -13,6 +13,8 @@ const ArrayUtils = require('lib/ArrayUtils'); const ObjectUtils = require('lib/ObjectUtils'); const { shim } = require('lib/shim.js'); +jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); }); @@ -249,6 +251,41 @@ describe('services_InteropService', function() { expect(folder2.title).toBe('folder1'); })); + it('should export and import folder and its sub-folders', asyncTest(async () => { + + const service = new InteropService(); + const filePath = exportDir() + '/test.jex'; + let folder1 = await Folder.save({ title: "folder1" }); + let folder2 = await Folder.save({ title: "folder2", parent_id: folder1.id }); + let folder3 = await Folder.save({ title: "folder3", parent_id: folder2.id }); + let folder4 = await Folder.save({ title: "folder4", parent_id: folder2.id }); + let note1 = await Note.save({ title: 'ma note', parent_id: folder4.id }); + + await service.export({ path: filePath, sourceFolderIds: [folder1.id] }); + + await Note.delete(note1.id); + await Folder.delete(folder1.id); + await Folder.delete(folder2.id); + await Folder.delete(folder3.id); + await Folder.delete(folder4.id); + + await service.import({ path: filePath }); + + expect(await Note.count()).toBe(1); + expect(await Folder.count()).toBe(4); + + let folder1_2 = await Folder.loadByTitle('folder1'); + let folder2_2 = await Folder.loadByTitle('folder2'); + let folder3_2 = await Folder.loadByTitle('folder3'); + let folder4_2 = await Folder.loadByTitle('folder4'); + let note1_2 = await Note.loadByTitle('ma note'); + + expect(folder2_2.parent_id).toBe(folder1_2.id); + expect(folder3_2.parent_id).toBe(folder2_2.id); + expect(folder4_2.parent_id).toBe(folder2_2.id); + expect(note1_2.parent_id).toBe(folder4_2.id); + })); + it('should export and import links to notes', asyncTest(async () => { const service = new InteropService(); const filePath = exportDir() + '/test.jex'; diff --git a/ReactNativeClient/lib/models/Folder.js b/ReactNativeClient/lib/models/Folder.js index 4d24e9405..75cfee2c6 100644 --- a/ReactNativeClient/lib/models/Folder.js +++ b/ReactNativeClient/lib/models/Folder.js @@ -127,6 +127,23 @@ class Folder extends BaseItem { return output; } + static async childrenIds(folderId, recursive) { + if (recursive === false) throw new Error('Not implemented'); + + const folders = await this.db().selectAll('SELECT id FROM folders WHERE parent_id = ?', [folderId]); + + let output = []; + + for (let i = 0; i < folders.length; i++) { + const f = folders[i]; + output.push(f.id); + const subChildrenIds = await this.childrenIds(f.id, true); + output = output.concat(subChildrenIds); + } + + return output; + } + static async allAsTree(options = null) { const all = await this.all(options); diff --git a/ReactNativeClient/lib/services/InteropService.js b/ReactNativeClient/lib/services/InteropService.js index 241b8d6c6..28802fcfb 100644 --- a/ReactNativeClient/lib/services/InteropService.js +++ b/ReactNativeClient/lib/services/InteropService.js @@ -157,7 +157,7 @@ class InteropService { async export(options) { const exportPath = options.path ? options.path : null; - const sourceFolderIds = options.sourceFolderIds ? options.sourceFolderIds : []; + let sourceFolderIds = options.sourceFolderIds ? options.sourceFolderIds : []; const sourceNoteIds = options.sourceNoteIds ? options.sourceNoteIds : []; const exportFormat = options.format ? options.format : 'jex'; const result = { warnings: [] } @@ -174,6 +174,14 @@ class InteropService { let resourceIds = []; const folderIds = await Folder.allIds(); + let fullSourceFolderIds = sourceFolderIds.slice(); + for (let i = 0; i < sourceFolderIds.length; i++) { + const id = sourceFolderIds[i]; + const childrenIds = await Folder.childrenIds(id); + fullSourceFolderIds = fullSourceFolderIds.concat(childrenIds); + } + sourceFolderIds = fullSourceFolderIds; + for (let folderIndex = 0; folderIndex < folderIds.length; folderIndex++) { const folderId = folderIds[folderIndex]; if (sourceFolderIds.length && sourceFolderIds.indexOf(folderId) < 0) continue; diff --git a/ReactNativeClient/lib/services/InteropService_Importer_Raw.js b/ReactNativeClient/lib/services/InteropService_Importer_Raw.js index e02acb228..6884f73f4 100644 --- a/ReactNativeClient/lib/services/InteropService_Importer_Raw.js +++ b/ReactNativeClient/lib/services/InteropService_Importer_Raw.js @@ -56,6 +56,25 @@ class InteropService_Importer_Raw extends InteropService_Importer_Base { return defaultFolder_; } + const setFolderToImportTo = async (itemParentId) => { + // Logic is a bit complex here: + // - If a destination folder was specified, move the note to it. + // - Otherwise, if the associated folder exists, use this. + // - If it doesn't exist, use the default folder. This is the case for example when importing JEX archives that contain only one or more notes, but no folder. + const itemParentExists = folderExists(stats, itemParentId); + + if (!itemIdMap[itemParentId]) { + if (destinationFolderId) { + itemIdMap[itemParentId] = destinationFolderId; + } else if (!itemParentExists) { + const parentFolder = await defaultFolder(); + itemIdMap[itemParentId] = parentFolder.id; + } else { + itemIdMap[itemParentId] = uuid.create(); + } + } + } + for (let i = 0; i < stats.length; i++) { const stat = stats[i]; if (stat.isDirectory()) continue; @@ -70,23 +89,10 @@ class InteropService_Importer_Raw extends InteropService_Importer_Base { if (itemType === BaseModel.TYPE_NOTE) { - // Logic is a bit complex here: - // - If a destination folder was specified, move the note to it. - // - Otherwise, if the associated folder exists, use this. - // - If it doesn't exist, use the default folder. This is the case for example when importing JEX archives that contain only one or more notes, but no folder. - if (!itemIdMap[item.parent_id]) { - if (destinationFolderId) { - itemIdMap[item.parent_id] = destinationFolderId; - } else if (!folderExists(stats, item.parent_id)) { - const parentFolder = await defaultFolder(); - itemIdMap[item.parent_id] = parentFolder.id; - } else { - itemIdMap[item.parent_id] = uuid.create(); - } - } + await setFolderToImportTo(item.parent_id); if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create(); - item.id = itemIdMap[item.id]; //noteId; + item.id = itemIdMap[item.id]; item.parent_id = itemIdMap[item.parent_id]; item.body = await replaceLinkedItemIds(item.body); } else if (itemType === BaseModel.TYPE_FOLDER) { @@ -95,6 +101,11 @@ class InteropService_Importer_Raw extends InteropService_Importer_Base { if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create(); item.id = itemIdMap[item.id]; item.title = await Folder.findUniqueFolderTitle(item.title); + + if (item.parent_id) { + await setFolderToImportTo(item.parent_id); + item.parent_id = itemIdMap[item.parent_id]; + } } else if (itemType === BaseModel.TYPE_RESOURCE) { if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create(); item.id = itemIdMap[item.id];