mirror of https://github.com/laurent22/joplin.git
Merge branch 'master' of github.com:laurent22/joplin
commit
c7a9e5f656
|
@ -150,4 +150,31 @@ describe('models_Folder', function() {
|
|||
expect(foldersById[f4.id].note_count).toBe(0);
|
||||
}));
|
||||
|
||||
it('should not count completed to-dos', asyncTest(async () => {
|
||||
|
||||
let f1 = await Folder.save({ title: 'folder1' });
|
||||
let f2 = await Folder.save({ title: 'folder2', parent_id: f1.id });
|
||||
let f3 = await Folder.save({ title: 'folder3', parent_id: f2.id });
|
||||
let f4 = await Folder.save({ title: 'folder4' });
|
||||
|
||||
let n1 = await Note.save({ title: 'note1', parent_id: f3.id });
|
||||
let n2 = await Note.save({ title: 'note2', parent_id: f3.id });
|
||||
let n3 = await Note.save({ title: 'note3', parent_id: f1.id });
|
||||
let n4 = await Note.save({ title: 'note4', parent_id: f3.id, is_todo: true, todo_completed: 0 });
|
||||
let n5 = await Note.save({ title: 'note5', parent_id: f3.id, is_todo: true, todo_completed: 999 });
|
||||
let n6 = await Note.save({ title: 'note6', parent_id: f3.id, is_todo: true, todo_completed: 999 });
|
||||
|
||||
const folders = await Folder.all();
|
||||
await Folder.addNoteCounts(folders, false);
|
||||
|
||||
const foldersById = {};
|
||||
folders.forEach((f) => { foldersById[f.id] = f; });
|
||||
|
||||
expect(folders.length).toBe(4);
|
||||
expect(foldersById[f1.id].note_count).toBe(4);
|
||||
expect(foldersById[f2.id].note_count).toBe(3);
|
||||
expect(foldersById[f3.id].note_count).toBe(3);
|
||||
expect(foldersById[f4.id].note_count).toBe(0);
|
||||
}));
|
||||
|
||||
});
|
||||
|
|
|
@ -0,0 +1,350 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
|
||||
require('app-module-path').addPath(__dirname);
|
||||
const {setupDatabaseAndSynchronizer, switchClient, asyncTest } = require('test-utils.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const { reducer, defaultState, stateUtils} = require('lib/reducer.js');
|
||||
|
||||
async function createNTestFolders(n) {
|
||||
let folders = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
let folder = await Folder.save({ title: 'folder' });
|
||||
folders.push(folder);
|
||||
}
|
||||
return folders;
|
||||
}
|
||||
|
||||
async function createNTestNotes(n, folder) {
|
||||
let notes = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
let note = await Note.save({ title: 'note', parent_id: folder.id });
|
||||
notes.push(note);
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
async function createNTestTags(n) {
|
||||
let tags = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
let tag = await Tag.save({ title: 'tag' });
|
||||
tags.push(tag);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
function initTestState(folders, selectedFolderIndex, notes, selectedIndexes, tags=null, selectedTagIndex=null) {
|
||||
let state = defaultState;
|
||||
if (folders != null) {
|
||||
state = reducer(state, { type: 'FOLDER_UPDATE_ALL', items: folders });
|
||||
}
|
||||
if (selectedFolderIndex != null) {
|
||||
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[selectedFolderIndex].id });
|
||||
}
|
||||
if (notes != null) {
|
||||
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes, noteSource: 'test' });
|
||||
}
|
||||
if (selectedIndexes != null) {
|
||||
let selectedIds = [];
|
||||
for (let i = 0; i < selectedIndexes.length; i++) {
|
||||
selectedIds.push(notes[selectedIndexes[i]].id);
|
||||
}
|
||||
state = reducer(state, { type: 'NOTE_SELECT', ids: selectedIds });
|
||||
}
|
||||
if (tags != null) {
|
||||
state = reducer(state, { type: 'TAG_UPDATE_ALL', items: tags });
|
||||
}
|
||||
if (selectedTagIndex != null) {
|
||||
state = reducer(state, { type: 'TAG_SELECT', id: tags[selectedTagIndex].id });
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function createExpectedState(items, keepIndexes, selectedIndexes) {
|
||||
let expected = { items: [], selectedIds: []};
|
||||
|
||||
for (let i = 0; i < selectedIndexes.length; i++) {
|
||||
expected.selectedIds.push(items[selectedIndexes[i]].id);
|
||||
}
|
||||
for (let i = 0; i < keepIndexes.length; i++) {
|
||||
expected.items.push(items[keepIndexes[i]]);
|
||||
}
|
||||
return expected;
|
||||
}
|
||||
|
||||
function getIds(items, indexes=null) {
|
||||
let ids = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (indexes == null || i in indexes) {
|
||||
ids.push(items[i].id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
let insideBeforeEach = false;
|
||||
|
||||
describe('Reducer', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
insideBeforeEach = true;
|
||||
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
|
||||
done();
|
||||
|
||||
insideBeforeEach = false;
|
||||
});
|
||||
|
||||
// tests for NOTE_DELETE
|
||||
it('should delete selected note', asyncTest(async () => {
|
||||
// create 1 folder
|
||||
let folders = await createNTestFolders(1);
|
||||
// create 5 notes
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
// select the 1st folder and the 3rd note
|
||||
let state = initTestState(folders, 0, notes, [2]);
|
||||
|
||||
// test action
|
||||
// delete the third note
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[2].id});
|
||||
|
||||
// expect that the third note is missing, and the 4th note is now selected
|
||||
let expected = createExpectedState(notes, [0,1,3,4], [3]);
|
||||
|
||||
// check the ids of all the remaining notes
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
// check the ids of the selected notes
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete selected note at top', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[0].id});
|
||||
|
||||
let expected = createExpectedState(notes, [1,2,3,4], [1]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete last remaining note', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(1, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [0]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[0].id});
|
||||
|
||||
let expected = createExpectedState(notes, [], []);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete selected note at bottom', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[4].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2,3], [3]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete note when a note below is selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [3]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[1].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,2,3,4], [3]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete note when a note above is selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[3].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2,4], [1]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete selected notes', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1,2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[1].id});
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[2].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,3,4], [3]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete note when a notes below it are selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [3,4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[1].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,2,3,4], [3,4]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete note when a notes above it are selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1,2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[3].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2,4], [1,2]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete notes at end', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [3,4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[3].id});
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[4].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2], [2]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete notes when non-contiguous selection', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [0,2,4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[0].id});
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[2].id});
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[4].id});
|
||||
|
||||
let expected = createExpectedState(notes, [1,3], [1]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
// tests for FOLDER_DELETE
|
||||
it('should delete selected notebook', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(5);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 2, notes, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'FOLDER_DELETE', id: folders[2].id});
|
||||
|
||||
let expected = createExpectedState(folders, [0,1,3,4], [3]);
|
||||
|
||||
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
it('should delete notebook when a book above is selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(5);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 1, notes, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'FOLDER_DELETE', id: folders[2].id});
|
||||
|
||||
let expected = createExpectedState(folders, [0,1,3,4], [1]);
|
||||
|
||||
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
it('should delete notebook when a book below is selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(5);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 4, notes, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'FOLDER_DELETE', id: folders[2].id});
|
||||
|
||||
let expected = createExpectedState(folders, [0,1,3,4], [4]);
|
||||
|
||||
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
// tests for TAG_DELETE
|
||||
it('should delete selected tag', asyncTest(async () => {
|
||||
let tags = await createNTestTags(5);
|
||||
let state = initTestState(null, null, null, null, tags, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'TAG_DELETE', id: tags[2].id});
|
||||
|
||||
let expected = createExpectedState(tags, [0,1,3,4], [3]);
|
||||
|
||||
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
it('should delete tag when a tag above is selected', asyncTest(async () => {
|
||||
let tags = await createNTestTags(5);
|
||||
let state = initTestState(null, null, null, null, tags, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'TAG_DELETE', id: tags[4].id});
|
||||
|
||||
let expected = createExpectedState(tags, [0,1,2,3], [2]);
|
||||
|
||||
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
it('should delete tag when a tag below is selected', asyncTest(async () => {
|
||||
let tags = await createNTestTags(5);
|
||||
let state = initTestState(null, null, null, null, tags, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'TAG_DELETE', id: tags[0].id});
|
||||
|
||||
let expected = createExpectedState(tags, [1,2,3,4], [2]);
|
||||
|
||||
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,336 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const { asyncTest, setupDatabaseAndSynchronizer, switchClient } = require('test-utils.js');
|
||||
const InteropService_Exporter_Md = require('lib/services/InteropService_Exporter_Md.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
|
||||
|
||||
const exportDir = `${__dirname}/export`;
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
describe('services_InteropService_Exporter_Md', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
|
||||
await fs.remove(exportDir);
|
||||
await fs.mkdirp(exportDir);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should create resources directory', asyncTest(async () => {
|
||||
const service = new InteropService_Exporter_Md();
|
||||
await service.init(exportDir);
|
||||
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/_resources/`)).toBe(true);
|
||||
}));
|
||||
|
||||
it('should create note paths and add them to context', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note1 = await Note.load(note1.id);
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note1.body))[0]);
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2' });
|
||||
let note3 = await Note.save({ title: 'note3', parent_id: folder2.id });
|
||||
await shim.attachFileToNote(note3, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note3 = await Note.load(note3.id);
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note3);
|
||||
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note3.body))[0]);
|
||||
|
||||
expect(!exporter.context() && !(exporter.context().notePaths || Object.keys(exporter.context().notePaths).length)).toBe(false, 'Context should be empty before processing.');
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.processItem(Folder, folder2);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(3, 'There should be 3 note paths in the context.');
|
||||
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
|
||||
expect(exporter.context().notePaths[note2.id]).toBe('folder1/note2.md');
|
||||
expect(exporter.context().notePaths[note3.id]).toBe('folder2/note3.md');
|
||||
}));
|
||||
|
||||
it('should handle duplicate note names', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
let note1_2 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1_2);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(2, 'There should be 2 note paths in the context.');
|
||||
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
|
||||
expect(exporter.context().notePaths[note1_2.id]).toBe('folder1/note1 (1).md');
|
||||
}));
|
||||
|
||||
it('should not override existing files', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
// Create a file with the path of note1 before processing note1
|
||||
await shim.fsDriver().writeFile(`${exportDir}/folder1/note1.md`, 'Note content', 'utf-8');
|
||||
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(1, 'There should be 1 note paths in the context.');
|
||||
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1 (1).md');
|
||||
}));
|
||||
|
||||
it('should save resource files in _resource directory', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note1 = await Note.load(note1.id);
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note1.body))[0]);
|
||||
let resource1 = await Resource.load(itemsToExport[2].itemOrId);
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
|
||||
await shim.attachFileToNote(note2, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note2 = await Note.load(note2.id);
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note2.body))[0]);
|
||||
let resource2 = await Resource.load(itemsToExport[5].itemOrId);
|
||||
|
||||
await exporter.processResource(resource1, Resource.fullPath(resource1));
|
||||
await exporter.processResource(resource2, Resource.fullPath(resource2));
|
||||
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/_resources/${Resource.filename(resource1)}`)).toBe(true, 'Resource file should be copied to _resources directory.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/_resources/${Resource.filename(resource2)}`)).toBe(true, 'Resource file should be copied to _resources directory.');
|
||||
}));
|
||||
|
||||
it('should save notes in fs', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
|
||||
let folder3 = await Folder.save({ title: 'folder3' });
|
||||
let note3 = await Note.save({ title: 'note3', parent_id: folder3.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder3.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note3);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.processItem(Folder, folder2);
|
||||
await exporter.processItem(Folder, folder3);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
await exporter.processItem(Note, note1);
|
||||
await exporter.processItem(Note, note2);
|
||||
await exporter.processItem(Note, note3);
|
||||
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/${exporter.context().notePaths[note1.id]}`)).toBe(true, 'File should be saved in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/${exporter.context().notePaths[note2.id]}`)).toBe(true, 'File should be saved in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/${exporter.context().notePaths[note3.id]}`)).toBe(true, 'File should be saved in filesystem.');
|
||||
}));
|
||||
|
||||
it('should replace resource ids with relative paths', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note1 = await Note.load(note1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
let resource1 = await Resource.load((await Note.linkedResourceIds(note1.body))[0]);
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
|
||||
await shim.attachFileToNote(note2, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note2 = await Note.load(note2.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
let resource2 = await Resource.load((await Note.linkedResourceIds(note2.body))[0]);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.processItem(Folder, folder2);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
let context = {
|
||||
resourcePaths: {},
|
||||
};
|
||||
context.resourcePaths[resource1.id] = 'resource1.jpg';
|
||||
context.resourcePaths[resource2.id] = 'resource2.jpg';
|
||||
exporter.updateContext(context);
|
||||
await exporter.processItem(Note, note1);
|
||||
await exporter.processItem(Note, note2);
|
||||
|
||||
let note1_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note1.id]}`);
|
||||
let note2_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note2.id]}`);
|
||||
|
||||
expect(note1_body).toContain('](../_resources/resource1.jpg)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note2_body).toContain('](../../_resources/resource2.jpg)', 'Resource id should be replaced with a relative path.');
|
||||
}));
|
||||
|
||||
it('should replace note ids with relative paths', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
const changeNoteBodyAndReload = async (note, newBody) => {
|
||||
note.body = newBody;
|
||||
await Note.save(note);
|
||||
return await Note.load(note.id);
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
|
||||
|
||||
let folder3 = await Folder.save({ title: 'folder3' });
|
||||
let note3 = await Note.save({ title: 'note3', parent_id: folder3.id });
|
||||
|
||||
note1 = await changeNoteBodyAndReload(note1, `# Some text \n\n [A link to note3](:/${note3.id})`);
|
||||
note2 = await changeNoteBodyAndReload(note2, `# Some text \n\n [A link to note3](:/${note3.id}) some more text \n ## And some headers \n and [A link to note1](:/${note1.id}) more links`);
|
||||
note3 = await changeNoteBodyAndReload(note3, `[A link to note3](:/${note2.id})`);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note3);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.processItem(Folder, folder2);
|
||||
await exporter.processItem(Folder, folder3);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
await exporter.processItem(Note, note1);
|
||||
await exporter.processItem(Note, note2);
|
||||
await exporter.processItem(Note, note3);
|
||||
|
||||
let note1_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note1.id]}`);
|
||||
let note2_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note2.id]}`);
|
||||
let note3_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note3.id]}`);
|
||||
|
||||
expect(note1_body).toContain('](../folder3/note3.md)', 'Note id should be replaced with a relative path.');
|
||||
expect(note2_body).toContain('](../../folder3/note3.md)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note2_body).toContain('](../../folder1/note1.md)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note3_body).toContain('](../folder1/folder2/note2.md)', 'Resource id should be replaced with a relative path.');
|
||||
}));
|
||||
|
||||
it('should url encode relative note links', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder with space1' });
|
||||
let note1 = await Note.save({ title: 'note1 name with space', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder1.id, body: `[link](:/${note1.id})` });
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
await exporter.processItem(Note, note1);
|
||||
await exporter.processItem(Note, note2);
|
||||
|
||||
let note2_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note2.id]}`);
|
||||
expect(note2_body).toContain('[link](../folder%20with%20space1/note1%20name%20with%20space.md)', 'Whitespace in URL should be encoded');
|
||||
}));
|
||||
});
|
|
@ -462,6 +462,7 @@ class Application extends BaseApplication {
|
|||
this.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'exportPdf',
|
||||
noteId: null,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -26,6 +26,7 @@ class NotePropertiesDialog extends React.Component {
|
|||
id: _('ID'),
|
||||
user_created_time: _('Created'),
|
||||
user_updated_time: _('Updated'),
|
||||
todo_completed: _('Completed'),
|
||||
location: _('Location'),
|
||||
source_url: _('URL'),
|
||||
revisionsLink: _('Note History'),
|
||||
|
@ -71,6 +72,11 @@ class NotePropertiesDialog extends React.Component {
|
|||
|
||||
formNote.user_updated_time = time.formatMsToLocal(note.user_updated_time);
|
||||
formNote.user_created_time = time.formatMsToLocal(note.user_created_time);
|
||||
|
||||
if (note.todo_completed) {
|
||||
formNote.todo_completed = time.formatMsToLocal(note.todo_completed);
|
||||
}
|
||||
|
||||
formNote.source_url = note.source_url;
|
||||
|
||||
formNote.location = '';
|
||||
|
@ -89,6 +95,11 @@ class NotePropertiesDialog extends React.Component {
|
|||
const note = Object.assign({ id: formNote.id }, this.latLongFromLocation(formNote.location));
|
||||
note.user_created_time = time.formatLocalToMs(formNote.user_created_time);
|
||||
note.user_updated_time = time.formatLocalToMs(formNote.user_updated_time);
|
||||
|
||||
if (formNote.todo_completed) {
|
||||
note.todo_completed = time.formatMsToLocal(formNote.todo_completed);
|
||||
}
|
||||
|
||||
note.source_url = formNote.source_url;
|
||||
|
||||
return note;
|
||||
|
@ -338,7 +349,7 @@ class NotePropertiesDialog extends React.Component {
|
|||
return dms.format('DDMMss', { decimalPlaces: 0 });
|
||||
}
|
||||
|
||||
if (['user_updated_time', 'user_created_time'].indexOf(key) >= 0) {
|
||||
if (['user_updated_time', 'user_created_time', 'todo_completed'].indexOf(key) >= 0) {
|
||||
return time.formatMsToLocal(note[key]);
|
||||
}
|
||||
|
||||
|
|
|
@ -689,13 +689,21 @@ class NoteTextComponent extends React.Component {
|
|||
if (newTags.length !== oldTags.length) return true;
|
||||
|
||||
for (let i = 0; i < newTags.length; ++i) {
|
||||
let found = false;
|
||||
let currNewTag = newTags[i];
|
||||
for (let j = 0; j < oldTags.length; ++j) {
|
||||
let currOldTag = oldTags[j];
|
||||
if (currOldTag.id === currNewTag.id && currOldTag.updated_time !== currNewTag.updated_time) {
|
||||
return true;
|
||||
if (currOldTag.id === currNewTag.id) {
|
||||
found = true;
|
||||
if (currOldTag.updated_time !== currNewTag.updated_time) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -1103,9 +1111,11 @@ class NoteTextComponent extends React.Component {
|
|||
if (!command) return;
|
||||
|
||||
let fn = null;
|
||||
let args = null;
|
||||
|
||||
if (command.name === 'exportPdf') {
|
||||
fn = this.commandSavePdf;
|
||||
args = {noteId: command.noteId};
|
||||
} else if (command.name === 'print') {
|
||||
fn = this.commandPrint;
|
||||
}
|
||||
|
@ -1157,7 +1167,7 @@ class NoteTextComponent extends React.Component {
|
|||
|
||||
requestAnimationFrame(() => {
|
||||
fn = fn.bind(this);
|
||||
fn();
|
||||
fn(args);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1250,7 +1260,7 @@ class NoteTextComponent extends React.Component {
|
|||
setTimeout(async () => {
|
||||
if (target === 'pdf') {
|
||||
try {
|
||||
const pdfData = await InteropServiceHelper.exportNoteToPdf(this.state.note.id, {
|
||||
const pdfData = await InteropServiceHelper.exportNoteToPdf(options.noteId, {
|
||||
printBackground: true,
|
||||
pageSize: Setting.value('export.pdfPageSize'),
|
||||
landscape: Setting.value('export.pdfPageOrientation') === 'landscape',
|
||||
|
@ -1262,7 +1272,7 @@ class NoteTextComponent extends React.Component {
|
|||
}
|
||||
} else if (target === 'printer') {
|
||||
try {
|
||||
await InteropServiceHelper.printNote(this.state.note.id, {
|
||||
await InteropServiceHelper.printNote(options.noteId, {
|
||||
printBackground: true,
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -1276,18 +1286,20 @@ class NoteTextComponent extends React.Component {
|
|||
}, 100);
|
||||
}
|
||||
|
||||
async commandSavePdf() {
|
||||
async commandSavePdf(args) {
|
||||
try {
|
||||
if (!this.state.note) throw new Error(_('Only one note can be printed or exported to PDF at a time.'));
|
||||
if (!this.state.note && !args.noteId) throw new Error(_('Only one note can be exported to PDF at a time.'));
|
||||
|
||||
const note = (!args.noteId ? this.state.note : await Note.load(args.noteId));
|
||||
|
||||
const path = bridge().showSaveDialog({
|
||||
filters: [{ name: _('PDF File'), extensions: ['pdf'] }],
|
||||
defaultPath: safeFilename(this.state.note.title),
|
||||
defaultPath: safeFilename(note.title),
|
||||
});
|
||||
|
||||
if (!path) return;
|
||||
|
||||
await this.printTo_('pdf', { path: path });
|
||||
await this.printTo_('pdf', { path: path, noteId: args.noteId });
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
|
@ -1295,7 +1307,9 @@ class NoteTextComponent extends React.Component {
|
|||
|
||||
async commandPrint() {
|
||||
try {
|
||||
await this.printTo_('printer');
|
||||
if (!this.state.note) throw new Error(_('Only one note can be printed at a time.'));
|
||||
|
||||
await this.printTo_('printer', { noteId: this.state.note.id });
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
|
|
|
@ -171,6 +171,7 @@ class NoteListUtils {
|
|||
props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'exportPdf',
|
||||
noteId: noteIds[0],
|
||||
});
|
||||
},
|
||||
})
|
||||
|
|
|
@ -450,11 +450,14 @@ class BaseApplication {
|
|||
refreshFolders = true;
|
||||
}
|
||||
|
||||
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key.indexOf('folders.sortOrder') === 0) || action.type == 'SETTING_UPDATE_ALL')) {
|
||||
if (this.hasGui() && action.type == 'SETTING_UPDATE_ALL') {
|
||||
refreshFolders = 'now';
|
||||
}
|
||||
|
||||
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'showNoteCounts') || action.type == 'SETTING_UPDATE_ALL')) {
|
||||
if (this.hasGui() && action.type == 'SETTING_UPDATE_ONE' && (
|
||||
action.key.indexOf('folders.sortOrder') === 0 ||
|
||||
action.key == 'showNoteCounts' ||
|
||||
action.key == 'showCompletedTodos')) {
|
||||
refreshFolders = 'now';
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,8 @@ class FoldersScreenUtils {
|
|||
}
|
||||
|
||||
if (Setting.value('showNoteCounts')) {
|
||||
await Folder.addNoteCounts(folders);
|
||||
await Folder.addNoteCounts(folders,
|
||||
Setting.value('showCompletedTodos'));
|
||||
}
|
||||
|
||||
return folders;
|
||||
|
|
|
@ -21,7 +21,10 @@ class FsDriverBase {
|
|||
return output;
|
||||
}
|
||||
|
||||
async findUniqueFilename(name) {
|
||||
async findUniqueFilename(name, reservedNames = null) {
|
||||
if (reservedNames === null) {
|
||||
reservedNames = [];
|
||||
}
|
||||
let counter = 1;
|
||||
|
||||
let nameNoExt = filename(name, true);
|
||||
|
@ -29,7 +32,8 @@ class FsDriverBase {
|
|||
if (extension) extension = `.${extension}`;
|
||||
let nameToTry = nameNoExt + extension;
|
||||
while (true) {
|
||||
const exists = await this.exists(nameToTry);
|
||||
// Check if the filename does not exist in the filesystem and is not reserved
|
||||
const exists = await this.exists(nameToTry) || reservedNames.includes(nameToTry);
|
||||
if (!exists) return nameToTry;
|
||||
nameToTry = `${nameNoExt} (${counter})${extension}`;
|
||||
counter++;
|
||||
|
|
|
@ -107,15 +107,19 @@ class Folder extends BaseItem {
|
|||
|
||||
// Calculates note counts for all folders and adds the note_count attribute to each folder
|
||||
// Note: this only calculates the overall number of nodes for this folder and all its descendants
|
||||
static async addNoteCounts(folders) {
|
||||
static async addNoteCounts(folders, includeCompletedTodos = true) {
|
||||
const foldersById = {};
|
||||
folders.forEach((f) => {
|
||||
foldersById[f.id] = f;
|
||||
f.note_count = 0;
|
||||
});
|
||||
|
||||
const where = !includeCompletedTodos ? 'WHERE (notes.is_todo = 0 OR notes.todo_completed = 0)' : '';
|
||||
|
||||
const sql = `SELECT folders.id as folder_id, count(notes.parent_id) as note_count
|
||||
FROM folders LEFT JOIN notes ON notes.parent_id = folders.id
|
||||
GROUP BY folders.id`;
|
||||
FROM folders LEFT JOIN notes ON notes.parent_id = folders.id
|
||||
${where} GROUP BY folders.id`;
|
||||
|
||||
const noteCounts = await this.db().selectAll(sql);
|
||||
noteCounts.forEach((noteCount) => {
|
||||
let parentId = noteCount.folder_id;
|
||||
|
|
|
@ -143,6 +143,10 @@ class Note extends BaseItem {
|
|||
return this.linkedItemIdsByType(BaseModel.TYPE_RESOURCE, body);
|
||||
}
|
||||
|
||||
static async linkedNoteIds(body) {
|
||||
return this.linkedItemIdsByType(BaseModel.TYPE_NOTE, body);
|
||||
}
|
||||
|
||||
static async replaceResourceInternalToExternalLinks(body) {
|
||||
const resourceIds = await this.linkedResourceIds(body);
|
||||
const Resource = this.getClass('Resource');
|
||||
|
|
|
@ -136,38 +136,69 @@ function folderSetCollapsed(state, action) {
|
|||
// When deleting a note, tag or folder
|
||||
function handleItemDelete(state, action) {
|
||||
const map = {
|
||||
FOLDER_DELETE: ['folders', 'selectedFolderId'],
|
||||
NOTE_DELETE: ['notes', 'selectedNoteIds'],
|
||||
TAG_DELETE: ['tags', 'selectedTagId'],
|
||||
SEARCH_DELETE: ['searches', 'selectedSearchId'],
|
||||
FOLDER_DELETE: ['folders', 'selectedFolderId', true],
|
||||
NOTE_DELETE: ['notes', 'selectedNoteIds', false],
|
||||
TAG_DELETE: ['tags', 'selectedTagId', true],
|
||||
SEARCH_DELETE: ['searches', 'selectedSearchId', true],
|
||||
};
|
||||
|
||||
const listKey = map[action.type][0];
|
||||
const selectedItemKey = map[action.type][1];
|
||||
const isSingular = map[action.type][2];
|
||||
|
||||
const selectedItemKeys = isSingular ? [state[selectedItemKey]] : state[selectedItemKey];
|
||||
const isSelected = selectedItemKeys.includes(action.id);
|
||||
|
||||
let previousIndex = 0;
|
||||
let newItems = [];
|
||||
const items = state[listKey];
|
||||
let newItems = [];
|
||||
let newSelectedIndexes = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let item = items[i];
|
||||
if (isSelected) {
|
||||
// the selected item is deleted so select the following item
|
||||
// if multiple items are selected then just use the first one
|
||||
if (selectedItemKeys[0] == item.id) {
|
||||
newSelectedIndexes.push(newItems.length);
|
||||
}
|
||||
} else {
|
||||
// the selected item/s is not deleted so keep it selected
|
||||
if (selectedItemKeys.includes(item.id)) {
|
||||
newSelectedIndexes.push(newItems.length);
|
||||
}
|
||||
}
|
||||
if (item.id == action.id) {
|
||||
previousIndex = i;
|
||||
continue;
|
||||
}
|
||||
newItems.push(item);
|
||||
}
|
||||
|
||||
if (newItems.length == 0) {
|
||||
newSelectedIndexes = []; // no remaining items so no selection
|
||||
|
||||
} else if (newSelectedIndexes.length == 0) {
|
||||
newSelectedIndexes.push(0); // no selection exists so select the top
|
||||
|
||||
} else {
|
||||
// when the items at end of list are deleted then select the end
|
||||
for (let i = 0; i < newSelectedIndexes.length; i++) {
|
||||
if (newSelectedIndexes[i] >= newItems.length) {
|
||||
newSelectedIndexes = [newItems.length - 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let newState = Object.assign({}, state);
|
||||
newState[listKey] = newItems;
|
||||
|
||||
if (previousIndex >= newItems.length) {
|
||||
previousIndex = newItems.length - 1;
|
||||
const newIds = [];
|
||||
for (let i = 0; i < newSelectedIndexes.length; i++) {
|
||||
newIds.push(newItems[newSelectedIndexes[i]].id);
|
||||
}
|
||||
newState[selectedItemKey] = isSingular ? newIds[0] : newIds;
|
||||
|
||||
const newId = previousIndex >= 0 ? newItems[previousIndex].id : null;
|
||||
newState[selectedItemKey] = action.type === 'NOTE_DELETE' ? [newId] : newId;
|
||||
|
||||
if (!newId && newState.notesParentType !== 'Folder') {
|
||||
if ((newIds.length == 0) && newState.notesParentType !== 'Folder') {
|
||||
newState.notesParentType = 'Folder';
|
||||
}
|
||||
|
||||
|
|
|
@ -341,6 +341,8 @@ class InteropService {
|
|||
for (let typeOrderIndex = 0; typeOrderIndex < typeOrder.length; typeOrderIndex++) {
|
||||
const type = typeOrder[typeOrderIndex];
|
||||
|
||||
await exporter.prepareForProcessingItemType(type, itemsToExport);
|
||||
|
||||
for (let i = 0; i < itemsToExport.length; i++) {
|
||||
const itemType = itemsToExport[i].type;
|
||||
|
||||
|
|
|
@ -3,7 +3,12 @@
|
|||
const Setting = require('lib/models/Setting');
|
||||
|
||||
class InteropService_Exporter_Base {
|
||||
constructor() {
|
||||
this.context_ = {};
|
||||
}
|
||||
|
||||
async init(destDir) {}
|
||||
async prepareForProcessingItemType(type, itemsToExport) {}
|
||||
async processItem(ItemClass, item) {}
|
||||
async processResource(resource, filePath) {}
|
||||
async close() {}
|
||||
|
@ -17,7 +22,7 @@ class InteropService_Exporter_Base {
|
|||
}
|
||||
|
||||
updateContext(context) {
|
||||
this.context_ = context;
|
||||
this.context_ = Object.assign(this.context_, context);
|
||||
}
|
||||
|
||||
context() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const InteropService_Exporter_Base = require('lib/services/InteropService_Exporter_Base');
|
||||
const { basename, friendlySafeFilename, rtrimSlashes } = require('lib/path-utils.js');
|
||||
const { basename, friendlySafeFilename } = require('lib/path-utils.js');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const Note = require('lib/models/Note');
|
||||
|
@ -31,36 +31,92 @@ class InteropService_Exporter_Md extends InteropService_Exporter_Base {
|
|||
}
|
||||
}
|
||||
|
||||
async replaceResourceIdsByRelativePaths_(item) {
|
||||
const linkedResourceIds = await Note.linkedResourceIds(item.body);
|
||||
const relativePath = rtrimSlashes(await this.makeDirPath_(item, '..'));
|
||||
async relaceLinkedItemIdsByRelativePaths_(item) {
|
||||
const relativePathToRoot = await this.makeDirPath_(item, '..');
|
||||
|
||||
let newBody = await this.replaceResourceIdsByRelativePaths_(item.body, relativePathToRoot);
|
||||
return await this.replaceNoteIdsByRelativePaths_(newBody, relativePathToRoot);
|
||||
}
|
||||
|
||||
async replaceResourceIdsByRelativePaths_(noteBody, relativePathToRoot) {
|
||||
const linkedResourceIds = await Note.linkedResourceIds(noteBody);
|
||||
const resourcePaths = this.context() && this.context().resourcePaths ? this.context().resourcePaths : {};
|
||||
|
||||
let newBody = item.body;
|
||||
let createRelativePath = function(resourcePath) {
|
||||
return `${relativePathToRoot}_resources/${basename(resourcePath)}`;
|
||||
};
|
||||
return await this.replaceItemIdsByRelativePaths_(noteBody, linkedResourceIds, resourcePaths, createRelativePath);
|
||||
}
|
||||
|
||||
for (let i = 0; i < linkedResourceIds.length; i++) {
|
||||
const id = linkedResourceIds[i];
|
||||
const resourcePath = `${relativePath}/_resources/${basename(resourcePaths[id])}`;
|
||||
newBody = newBody.replace(new RegExp(`:/${id}`, 'g'), resourcePath);
|
||||
async replaceNoteIdsByRelativePaths_(noteBody, relativePathToRoot) {
|
||||
const linkedNoteIds = await Note.linkedNoteIds(noteBody);
|
||||
const notePaths = this.context() && this.context().notePaths ? this.context().notePaths : {};
|
||||
|
||||
let createRelativePath = function(notePath) {
|
||||
return encodeURI(`${relativePathToRoot}${notePath}`.trim());
|
||||
};
|
||||
return await this.replaceItemIdsByRelativePaths_(noteBody, linkedNoteIds, notePaths, createRelativePath);
|
||||
}
|
||||
|
||||
async replaceItemIdsByRelativePaths_(noteBody, linkedItemIds, paths, fn_createRelativePath) {
|
||||
let newBody = noteBody;
|
||||
|
||||
for (let i = 0; i < linkedItemIds.length; i++) {
|
||||
const id = linkedItemIds[i];
|
||||
let itemPath = fn_createRelativePath(paths[id]);
|
||||
newBody = newBody.replace(new RegExp(`:/${id}`, 'g'), itemPath);
|
||||
}
|
||||
|
||||
return newBody;
|
||||
}
|
||||
|
||||
async prepareForProcessingItemType(type, itemsToExport) {
|
||||
if (type === BaseModel.TYPE_NOTE) {
|
||||
// Create unique file path for the note
|
||||
const context = {
|
||||
notePaths: {},
|
||||
};
|
||||
for (let i = 0; i < itemsToExport.length; i++) {
|
||||
const itemType = itemsToExport[i].type;
|
||||
|
||||
if (itemType !== type) continue;
|
||||
|
||||
const itemOrId = itemsToExport[i].itemOrId;
|
||||
const note = typeof itemOrId === 'object' ? itemOrId : await Note.load(itemOrId);
|
||||
|
||||
if (!note) continue;
|
||||
|
||||
let notePath = `${await this.makeDirPath_(note)}${friendlySafeFilename(note.title, null, true)}.md`;
|
||||
notePath = await shim.fsDriver().findUniqueFilename(`${this.destDir_}/${notePath}`, Object.values(context.notePaths));
|
||||
context.notePaths[note.id] = notePath;
|
||||
}
|
||||
|
||||
// Strip the absolute path to export dir and keep only the relative paths
|
||||
const destDir = this.destDir_;
|
||||
Object.keys(context.notePaths).map(function(id) {
|
||||
context.notePaths[id] = context.notePaths[id].substr(destDir.length + 1);
|
||||
});
|
||||
|
||||
this.updateContext(context);
|
||||
}
|
||||
}
|
||||
|
||||
async processItem(ItemClass, item) {
|
||||
if ([BaseModel.TYPE_NOTE, BaseModel.TYPE_FOLDER].indexOf(item.type_) < 0) return;
|
||||
|
||||
const dirPath = `${this.destDir_}/${await this.makeDirPath_(item)}`;
|
||||
if (item.type_ === BaseModel.TYPE_FOLDER) {
|
||||
const dirPath = `${this.destDir_}/${await this.makeDirPath_(item)}`;
|
||||
|
||||
if (this.createdDirs_.indexOf(dirPath) < 0) {
|
||||
await shim.fsDriver().mkdir(dirPath);
|
||||
this.createdDirs_.push(dirPath);
|
||||
}
|
||||
if (this.createdDirs_.indexOf(dirPath) < 0) {
|
||||
await shim.fsDriver().mkdir(dirPath);
|
||||
this.createdDirs_.push(dirPath);
|
||||
}
|
||||
|
||||
if (item.type_ === BaseModel.TYPE_NOTE) {
|
||||
let noteFilePath = `${dirPath}/${friendlySafeFilename(item.title, null, true)}.md`;
|
||||
noteFilePath = await shim.fsDriver().findUniqueFilename(noteFilePath);
|
||||
const noteBody = await this.replaceResourceIdsByRelativePaths_(item);
|
||||
} else if (item.type_ === BaseModel.TYPE_NOTE) {
|
||||
const notePaths = this.context() && this.context().notePaths ? this.context().notePaths : {};
|
||||
let noteFilePath = `${this.destDir_}/${notePaths[item.id]}`;
|
||||
|
||||
let noteBody = await this.relaceLinkedItemIdsByRelativePaths_(item);
|
||||
const modNote = Object.assign({}, item, { body: noteBody });
|
||||
const noteContent = await Note.serializeForEdit(modNote);
|
||||
await shim.fsDriver().writeFile(noteFilePath, noteContent, 'utf-8');
|
||||
|
|
Loading…
Reference in New Issue