mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-27 08:21:03 +02:00
Electron: Export/Import links to notes
This commit is contained in:
parent
3aeb49b469
commit
a6a351e68d
39
CliClient/tests/models_Note.js
Normal file
39
CliClient/tests/models_Note.js
Normal file
@ -0,0 +1,39 @@
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { shim } = require('lib/shim');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
describe('models_Note', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should find resource and note IDs', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'ma deuxième note', body: 'Lien vers première note : ' + Note.markdownTag(note1), parent_id: folder1.id });
|
||||
|
||||
let items = await Note.linkedItems(note2.body);
|
||||
expect(items.length).toBe(1);
|
||||
expect(items[0].id).toBe(note1.id);
|
||||
|
||||
await shim.attachFileToNote(note2, __dirname + '/../tests/support/photo.jpg');
|
||||
note2 = await Note.load(note2.id);
|
||||
items = await Note.linkedItems(note2.body);
|
||||
expect(items.length).toBe(2);
|
||||
expect(items[0].type_).toBe(BaseModel.TYPE_NOTE);
|
||||
expect(items[1].type_).toBe(BaseModel.TYPE_RESOURCE);
|
||||
}));
|
||||
|
||||
});
|
@ -180,7 +180,7 @@ describe('services_InteropService', function() {
|
||||
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
|
||||
note1 = await Note.load(note1.id);
|
||||
let resourceIds = Note.linkedResourceIds(note1.body);
|
||||
let resourceIds = await Note.linkedResourceIds(note1.body);
|
||||
let resource1 = await Resource.load(resourceIds[0]);
|
||||
|
||||
await service.export({ path: filePath });
|
||||
@ -193,7 +193,7 @@ describe('services_InteropService', function() {
|
||||
|
||||
let note2 = (await Note.all())[0];
|
||||
expect(note2.body).not.toBe(note1.body);
|
||||
resourceIds = Note.linkedResourceIds(note2.body);
|
||||
resourceIds = await Note.linkedResourceIds(note2.body);
|
||||
expect(resourceIds.length).toBe(1);
|
||||
let resource2 = await Resource.load(resourceIds[0]);
|
||||
expect(resource2.id).not.toBe(resource1.id);
|
||||
@ -249,4 +249,28 @@ describe('services_InteropService', function() {
|
||||
expect(folder2.title).toBe('folder1');
|
||||
}));
|
||||
|
||||
it('should export and import links to notes', asyncTest(async () => {
|
||||
const service = new InteropService();
|
||||
const filePath = exportDir() + '/test.jex';
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'ma deuxième note', body: 'Lien vers première note : ' + Note.markdownTag(note1), parent_id: folder1.id });
|
||||
|
||||
await service.export({ path: filePath, sourceFolderIds: [folder1.id] });
|
||||
|
||||
await Note.delete(note1.id);
|
||||
await Note.delete(note2.id);
|
||||
await Folder.delete(folder1.id);
|
||||
|
||||
await service.import({ path: filePath });
|
||||
|
||||
expect(await Note.count()).toBe(2);
|
||||
expect(await Folder.count()).toBe(1);
|
||||
|
||||
let note1_2 = await Note.loadByTitle('ma note');
|
||||
let note2_2 = await Note.loadByTitle('ma deuxième note');
|
||||
|
||||
expect(note2_2.body.indexOf(note1_2.id) >= 0).toBe(true);
|
||||
}));
|
||||
|
||||
});
|
@ -107,7 +107,7 @@ class Note extends BaseItem {
|
||||
return BaseModel.TYPE_NOTE;
|
||||
}
|
||||
|
||||
static linkedResourceIds(body) {
|
||||
static linkedItemIds(body) {
|
||||
// For example: ![](:/fcca2938a96a22570e8eae2565bc6b0b)
|
||||
if (!body || body.length <= 32) return [];
|
||||
const matches = body.match(/\(:\/.{32}\)/g);
|
||||
@ -115,6 +115,35 @@ class Note extends BaseItem {
|
||||
return matches.map((m) => m.substr(3, 32));
|
||||
}
|
||||
|
||||
static async linkedItems(body) {
|
||||
const itemIds = this.linkedItemIds(body);
|
||||
const output = [];
|
||||
|
||||
for (let i = 0; i < itemIds.length; i++) {
|
||||
const item = await BaseItem.loadItemById(itemIds[i]);
|
||||
if (!item) continue;
|
||||
output.push(item);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static async linkedItemIdsByType(type, body) {
|
||||
const items = await this.linkedItems(body);
|
||||
const output = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.type_ === type) output.push(item.id);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static async linkedResourceIds(body) {
|
||||
return await this.linkedItemIdsByType(BaseModel.TYPE_RESOURCE, body);
|
||||
}
|
||||
|
||||
static new(parentId = '') {
|
||||
let output = super.new();
|
||||
output.parent_id = parentId;
|
||||
|
@ -189,7 +189,7 @@ class InteropService {
|
||||
await queueExportItem(BaseModel.TYPE_NOTE, note);
|
||||
exportedNoteIds.push(noteId);
|
||||
|
||||
const rids = Note.linkedResourceIds(note.body);
|
||||
const rids = await Note.linkedResourceIds(note.body);
|
||||
resourceIds = resourceIds.concat(rids);
|
||||
}
|
||||
}
|
||||
|
@ -18,22 +18,19 @@ const { uuid } = require('lib/uuid.js');
|
||||
class InteropService_Importer_Raw extends InteropService_Importer_Base {
|
||||
|
||||
async exec(result) {
|
||||
const noteIdMap = {};
|
||||
const folderIdMap = {};
|
||||
const resourceIdMap = {};
|
||||
const tagIdMap = {};
|
||||
const itemIdMap = {};
|
||||
const createdResources = {};
|
||||
const noteTagsToCreate = [];
|
||||
const destinationFolderId = this.options_.destinationFolderId;
|
||||
|
||||
const replaceResourceNoteIds = (noteBody) => {
|
||||
const replaceLinkedItemIds = async (noteBody) => {
|
||||
let output = noteBody;
|
||||
const resourceIds = Note.linkedResourceIds(noteBody);
|
||||
const itemIds = Note.linkedItemIds(noteBody);
|
||||
|
||||
for (let i = 0; i < resourceIds.length; i++) {
|
||||
const id = resourceIds[i];
|
||||
if (!resourceIdMap[id]) resourceIdMap[id] = uuid.create();
|
||||
output = output.replace(new RegExp(id, 'gi'), resourceIdMap[id]);
|
||||
for (let i = 0; i < itemIds.length; i++) {
|
||||
const id = itemIds[i];
|
||||
if (!itemIdMap[id]) itemIdMap[id] = uuid.create();
|
||||
output = output.replace(new RegExp(id, 'gi'), itemIdMap[id]);
|
||||
}
|
||||
|
||||
return output;
|
||||
@ -77,41 +74,40 @@ class InteropService_Importer_Raw extends InteropService_Importer_Base {
|
||||
// - 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 (!folderIdMap[item.parent_id]) {
|
||||
if (!itemIdMap[item.parent_id]) {
|
||||
if (destinationFolderId) {
|
||||
folderIdMap[item.parent_id] = destinationFolderId;
|
||||
itemIdMap[item.parent_id] = destinationFolderId;
|
||||
} else if (!folderExists(stats, item.parent_id)) {
|
||||
const parentFolder = await defaultFolder();
|
||||
folderIdMap[item.parent_id] = parentFolder.id;
|
||||
itemIdMap[item.parent_id] = parentFolder.id;
|
||||
} else {
|
||||
folderIdMap[item.parent_id] = uuid.create();
|
||||
itemIdMap[item.parent_id] = uuid.create();
|
||||
}
|
||||
}
|
||||
|
||||
const noteId = uuid.create();
|
||||
noteIdMap[item.id] = noteId;
|
||||
item.id = noteId;
|
||||
item.parent_id = folderIdMap[item.parent_id];
|
||||
item.body = replaceResourceNoteIds(item.body);
|
||||
if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
|
||||
item.id = itemIdMap[item.id]; //noteId;
|
||||
item.parent_id = itemIdMap[item.parent_id];
|
||||
item.body = await replaceLinkedItemIds(item.body);
|
||||
} else if (itemType === BaseModel.TYPE_FOLDER) {
|
||||
if (destinationFolderId) continue;
|
||||
|
||||
if (!folderIdMap[item.id]) folderIdMap[item.id] = uuid.create();
|
||||
item.id = folderIdMap[item.id];
|
||||
if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
|
||||
item.id = itemIdMap[item.id];
|
||||
item.title = await Folder.findUniqueFolderTitle(item.title);
|
||||
} else if (itemType === BaseModel.TYPE_RESOURCE) {
|
||||
if (!resourceIdMap[item.id]) resourceIdMap[item.id] = uuid.create();
|
||||
item.id = resourceIdMap[item.id];
|
||||
if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
|
||||
item.id = itemIdMap[item.id];
|
||||
createdResources[item.id] = item;
|
||||
} else if (itemType === BaseModel.TYPE_TAG) {
|
||||
const tag = await Tag.loadByTitle(item.title);
|
||||
if (tag) {
|
||||
tagIdMap[item.id] = tag.id;
|
||||
itemIdMap[item.id] = tag.id;
|
||||
continue;
|
||||
}
|
||||
|
||||
const tagId = uuid.create();
|
||||
tagIdMap[item.id] = tagId;
|
||||
itemIdMap[item.id] = tagId;
|
||||
item.id = tagId;
|
||||
} else if (itemType === BaseModel.TYPE_NOTE_TAG) {
|
||||
noteTagsToCreate.push(item);
|
||||
@ -123,8 +119,8 @@ class InteropService_Importer_Raw extends InteropService_Importer_Base {
|
||||
|
||||
for (let i = 0; i < noteTagsToCreate.length; i++) {
|
||||
const noteTag = noteTagsToCreate[i];
|
||||
const newNoteId = noteIdMap[noteTag.note_id];
|
||||
const newTagId = tagIdMap[noteTag.tag_id];
|
||||
const newNoteId = itemIdMap[noteTag.note_id];
|
||||
const newTagId = itemIdMap[noteTag.tag_id];
|
||||
|
||||
if (!newNoteId) {
|
||||
result.warnings.push(sprintf('Non-existent note %s referenced in tag %s', noteTag.note_id, noteTag.tag_id));
|
||||
@ -149,7 +145,7 @@ class InteropService_Importer_Raw extends InteropService_Importer_Base {
|
||||
for (let i = 0; i < resourceStats.length; i++) {
|
||||
const resourceFilePath = this.sourcePath_ + '/resources/' + resourceStats[i].path;
|
||||
const oldId = Resource.pathToId(resourceFilePath);
|
||||
const newId = resourceIdMap[oldId];
|
||||
const newId = itemIdMap[oldId];
|
||||
if (!newId) {
|
||||
result.warnings.push(sprintf('Resource file is not referenced in any note and so was not imported: %s', oldId));
|
||||
continue;
|
||||
|
@ -44,7 +44,7 @@ class ResourceService extends BaseService {
|
||||
|
||||
if (change.type === ItemChange.TYPE_CREATE || change.type === ItemChange.TYPE_UPDATE) {
|
||||
const note = noteById(change.item_id);
|
||||
const resourceIds = Note.linkedResourceIds(note.body);
|
||||
const resourceIds = await Note.linkedResourceIds(note.body);
|
||||
await NoteResource.setAssociatedResources(note.id, resourceIds);
|
||||
} else if (change.type === ItemChange.TYPE_DELETE) {
|
||||
await NoteResource.remove(change.item_id);
|
||||
|
Loading…
Reference in New Issue
Block a user