1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

Desktop: Resolves #2409: Add note history (back/forward buttons) (#2819)

This commit is contained in:
Naveen M V 2020-05-15 16:43:42 +05:30 committed by GitHub
parent ecc50790ed
commit b3f32ffc59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 907 additions and 60 deletions

View File

@ -0,0 +1,455 @@
require('app-module-path').addPath(__dirname);
const { asyncTest, id, ids, createNTestFolders, sortedIds, createNTestNotes, TestApp } = require('test-utils.js');
const BaseModel = require('lib/BaseModel.js');
const { uuid } = require('lib/uuid.js');
const Note = require('lib/models/Note.js');
const Folder = require('lib/models/Folder.js');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids.js');
let testApp = null;
const goBackWard = (state) => {
if (!state.backwardHistoryNotes.length) return;
testApp.dispatch({ type: 'HISTORY_BACKWARD' });
};
const goForward = (state) => {
if (!state.forwardHistoryNotes.length) return;
testApp.dispatch({ type: 'HISTORY_FORWARD' });
};
const goToNote = (testApp, note) => {
testApp.dispatch({ type: 'NOTE_SELECT', id: note.id });
};
describe('integration_ForwardBackwardNoteHistory', function() {
beforeEach(async (done) => {
testApp = new TestApp();
await testApp.start(['--no-welcome']);
done();
});
afterEach(async (done) => {
if (testApp !== null) await testApp.destroy();
testApp = null;
done();
});
it('should save history when navigating through notes', asyncTest(async () => {
// setup
const folders = await createNTestFolders(2);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);
// let notes1 = await createNTestNotes(5, folders[1]);
await testApp.wait();
testApp.dispatch({ type: 'FOLDER_SELECT', id: id(folders[0]) });
await testApp.wait();
testApp.dispatch({ type: 'NOTE_SELECT', id: notes0[3].id });
await testApp.wait();
testApp.dispatch({ type: 'NOTE_SELECT', id: notes0[2].id });
await testApp.wait();
testApp.dispatch({ type: 'NOTE_SELECT', id: notes0[1].id });
await testApp.wait();
testApp.dispatch({ type: 'NOTE_SELECT', id: notes0[0].id });
await testApp.wait();
let state = testApp.store().getState();
goBackWard(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[1].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
goBackWard(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[2].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
goForward(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[1].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
testApp.dispatch({ type: 'NOTE_SELECT', id: notes0[4].id });
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[4].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
}));
it('should save history when navigating through notebooks', asyncTest(async () => {
const folders = await createNTestFolders(2);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);
const notes1 = await createNTestNotes(5, folders[1]);
await testApp.wait();
testApp.dispatch({ type: 'FOLDER_SELECT', id: id(folders[0]) });
await testApp.wait();
testApp.dispatch({ type: 'FOLDER_SELECT', id: id(folders[1]) });
await testApp.wait();
let state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes1[4].id]);
expect(state.selectedFolderId).toEqual(folders[1].id);
testApp.dispatch({ type: 'FOLDER_SELECT', id: id(folders[0]) });
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[4].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
goBackWard(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes1[4].id]);
expect(state.selectedFolderId).toEqual(folders[1].id);
goForward(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[4].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
}));
it('should save history when searching for a note', asyncTest(async () => {
const folders = await createNTestFolders(2);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);
const notes1 = await createNTestNotes(5, folders[1]);
await testApp.wait();
testApp.dispatch({ type: 'FOLDER_SELECT', id: id(folders[0]) });
await testApp.wait();
testApp.dispatch({ type: 'FOLDER_SELECT', id: id(folders[1]) });
await testApp.wait();
let state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes1[4].id]); // notes1[4]
expect(state.selectedFolderId).toEqual(folders[1].id);
const searchId = uuid.create();
testApp.dispatch({
type: 'SEARCH_UPDATE',
search: {
id: searchId,
title: notes0[0].title,
query_pattern: notes0[0].title,
query_folder_id: null,
type: BaseModel.TYPE_SEARCH,
},
});
await testApp.wait();
testApp.dispatch({
type: 'SEARCH_SELECT',
id: searchId,
});
await testApp.wait();
state = testApp.store().getState();
expect(ids(state.backwardHistoryNotes)).toEqual(ids([notes0[4], notes1[4]]));
expect(ids(state.forwardHistoryNotes)).toEqual([]);
}));
it('should ensure no adjacent duplicates', asyncTest(async () => {
const folders = await createNTestFolders(2);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);
await testApp.wait();
testApp.dispatch({ type: 'FOLDER_SELECT', id: id(folders[0]) });
await testApp.wait();
testApp.dispatch({ type: 'NOTE_SELECT', id: notes0[0].id });
await testApp.wait();
goToNote(testApp, notes0[1]);
await testApp.wait();
goToNote(testApp, notes0[2]);
await testApp.wait();
goToNote(testApp, notes0[3]);
await testApp.wait();
goToNote(testApp, notes0[2]);
await testApp.wait();
goToNote(testApp, notes0[3]);
await testApp.wait();
goToNote(testApp, notes0[2]);
await testApp.wait();
goToNote(testApp, notes0[3]);
await testApp.wait();
goToNote(testApp, notes0[2]);
await testApp.wait();
goToNote(testApp, notes0[3]);
await testApp.wait();
goToNote(testApp, notes0[2]);
await testApp.wait();
goToNote(testApp, notes0[3]);
await testApp.wait();
let state = testApp.store().getState();
goBackWard(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[2].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
goBackWard(state);
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[3].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
goBackWard(state);
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[2].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
goBackWard(state);
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[3].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
testApp.dispatch({ type: 'NOTE_DELETE', id: notes0[2].id });
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[3].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
}));
it('should ensure history is not corrupted when notes get deleted.', asyncTest(async () => {
const folders = await createNTestFolders(2);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);
await testApp.wait();
testApp.dispatch({ type: 'FOLDER_SELECT', id: id(folders[0]) });
await testApp.wait();
testApp.dispatch({ type: 'NOTE_SELECT', id: notes0[0].id });
await testApp.wait();
goToNote(testApp, notes0[1]);
await testApp.wait();
goToNote(testApp, notes0[2]);
await testApp.wait();
testApp.dispatch({ type: 'NOTE_DELETE', id: notes0[1].id });
await testApp.wait();
let state = testApp.store().getState();
goBackWard(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[0].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
}));
it('should ensure history is not corrupted when notes get created.', asyncTest(async () => {
const folders = await createNTestFolders(2);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);
await testApp.wait();
testApp.dispatch({ type: 'FOLDER_SELECT', id: id(folders[0]) });
await testApp.wait();
goToNote(testApp, notes0[0]);
await testApp.wait();
goToNote(testApp, notes0[1]);
await testApp.wait();
const newNote = await Note.save({
parent_id: folders[0].id,
is_todo: 0,
body: 'test',
});
await testApp.wait();
goToNote(testApp, newNote);
await testApp.wait();
let state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([newNote.id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
goToNote(testApp, notes0[2]);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[2].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
goBackWard(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([newNote.id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
goBackWard(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[1].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
goForward(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([newNote.id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
goForward(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[2].id]);
expect(state.selectedFolderId).toEqual(folders[0].id);
}));
it('should ensure history works when traversing all notes', asyncTest(async () => {
const folders = await createNTestFolders(2);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);
const notes1 = await createNTestNotes(5, folders[1]);
await testApp.wait();
testApp.dispatch({ type: 'FOLDER_SELECT', id: id(folders[0]) });
await testApp.wait();
goToNote(testApp, notes0[0]);
await testApp.wait();
testApp.dispatch({ type: 'SMART_FILTER_SELECT', id: ALL_NOTES_FILTER_ID });
await testApp.wait();
let state = testApp.store().getState();
expect(sortedIds(state.notes)).toEqual(sortedIds(notes0.concat(notes1)));
expect(state.selectedNoteIds).toEqual(ids([notes0[0]]));
goToNote(testApp, notes0[2]);
await testApp.wait();
goToNote(testApp, notes0[4]);
await testApp.wait();
goToNote(testApp, notes1[2]);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes1[2].id]);
goBackWard(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[4].id]);
goBackWard(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[2].id]);
goBackWard(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[0].id]);
goForward(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[2].id]);
goForward(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedNoteIds).toEqual([notes0[4].id]);
}));
it('should ensure history works when traversing through conflict notes', asyncTest(async () => {
const folders = await createNTestFolders(1);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);
await testApp.wait();
// create two conflict notes with parent_id folder 1
const note1 = await Note.save({ title: 'note 1', parent_id: folders[0].id, is_conflict: 1 });
await testApp.wait();
const note2 = await Note.save({ title: 'note 2', parent_id: folders[0].id, is_conflict: 1 });
await testApp.wait();
// Testing history between conflict notes
testApp.dispatch({ type: 'FOLDER_SELECT', id: Folder.conflictFolderId() });
await testApp.wait();
goToNote(testApp, note1);
await testApp.wait();
goToNote(testApp, note2);
await testApp.wait();
let state = testApp.store().getState();
expect(state.selectedFolderId).toBe(Folder.conflictFolderId());
expect(state.selectedNoteIds[0]).toBe(note2.id);
goBackWard(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedFolderId).toBe(Folder.conflictFolderId());
expect(state.selectedNoteIds[0]).toBe(note1.id);
goForward(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedFolderId).toBe(Folder.conflictFolderId());
expect(state.selectedNoteIds[0]).toBe(note2.id);
// Testing history between conflict and non conflict notes.
testApp.dispatch({ type: 'FOLDER_SELECT', id: folders[0].id });
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedFolderId).toBe(folders[0].id);
expect(state.selectedNoteIds[0]).toBe(notes0[4].id);
goBackWard(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedFolderId).toBe(Folder.conflictFolderId());
expect(state.selectedNoteIds[0]).toBe(note2.id);
goForward(state);
await testApp.wait();
state = testApp.store().getState();
expect(state.selectedFolderId).toBe(folders[0].id);
expect(state.selectedNoteIds[0]).toBe(notes0[4].id);
}));
});

View File

@ -5,7 +5,7 @@ const { setupDatabaseAndSynchronizer, switchClient, asyncTest, createNTestNotes,
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');
const { reducer, defaultState, stateUtils, MAX_HISTORY } = require('lib/reducer.js');
function initTestState(folders, selectedFolderIndex, notes, selectedNoteIndexes, tags = null, selectedTagIndex = null) {
let state = defaultState;
@ -36,6 +36,33 @@ function initTestState(folders, selectedFolderIndex, notes, selectedNoteIndexes,
return state;
}
function goToNote(notes, selectedNoteIndexes, state) {
if (selectedNoteIndexes != null) {
const selectedIds = [];
for (let i = 0; i < selectedNoteIndexes.length; i++) {
selectedIds.push(notes[selectedNoteIndexes[i]].id);
}
state = reducer(state, { type: 'NOTE_SELECT', ids: selectedIds });
}
return state;
}
function goBackWard(state) {
if (!state.backwardHistoryNotes.length) return state;
state = reducer(state, {
type: 'HISTORY_BACKWARD',
});
return state;
}
function goForward(state) {
if (!state.forwardHistoryNotes.length) return state;
state = reducer(state, {
type: 'HISTORY_FORWARD',
});
return state;
}
function createExpectedState(items, keepIndexes, selectedIndexes) {
const expected = { items: [], selectedIds: [] };
@ -345,4 +372,213 @@ describe('Reducer', function() {
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should remove deleted note from history', asyncTest(async () => {
// create 1 folder
const folders = await createNTestFolders(1);
// create 5 notes
const notes = await createNTestNotes(5, folders[0]);
// select the 1st folder and the 1st note
let state = initTestState(folders, 0, notes, [0]);
// select second note
state = goToNote(notes, [1], state);
// select third note
state = goToNote(notes, [2], state);
// select fourth note
state = goToNote(notes, [3], state);
// expect history to contain first, second and third note
expect(state.backwardHistoryNotes.length).toEqual(3);
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 3)));
// delete third note
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
// expect history to not contain third note
expect(getIds(state.backwardHistoryNotes)).not.toContain(notes[2].id);
}));
it('should remove all notes of a deleted notebook from history', asyncTest(async () => {
const folders = await createNTestFolders(2);
const notes = [];
for (let i = 0; i < folders.length; i++) {
notes.push(...await createNTestNotes(3, folders[i]));
}
let state = initTestState(folders, 0, notes.slice(0,3), [0]);
state = goToNote(notes, [1], state);
state = goToNote(notes, [2], state);
// go to second folder
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[1].id });
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 3)));
// delete the first folder
state = reducer(state, { type: 'FOLDER_DELETE', id: folders[0].id });
expect(getIds(state.backwardHistoryNotes)).toEqual([]);
}));
it('should maintain history correctly when going backward and forward', asyncTest(async () => {
const folders = await createNTestFolders(2);
const notes = [];
for (let i = 0; i < folders.length; i++) {
notes.push(...await createNTestNotes(5, folders[i]));
}
let state = initTestState(folders, 0, notes.slice(0,5), [0]);
state = goToNote(notes, [1], state);
state = goToNote(notes, [2], state);
state = goToNote(notes, [3], state);
state = goToNote(notes, [4], state);
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 4)));
state = goBackWard(state);
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0,3)));
expect(getIds(state.forwardHistoryNotes)).toEqual(getIds(notes.slice(4, 5)));
state = goBackWard(state);
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0,2)));
// because we push the last seen note to stack.
expect(getIds(state.forwardHistoryNotes)).toEqual(getIds([notes[4], notes[3]]));
state = goForward(state);
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0,3)));
expect(getIds(state.forwardHistoryNotes)).toEqual(getIds([notes[4]]));
state = goForward(state);
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0,4)));
expect(getIds(state.forwardHistoryNotes)).toEqual([]);
}));
it('should remember the last seen note of a notebook', asyncTest(async () => {
const folders = await createNTestFolders(2);
const notes = [];
for (let i = 0; i < folders.length; i++) {
notes.push(...await createNTestNotes(5, folders[i]));
}
let state = initTestState(folders, 0, notes.slice(0,5), [0]);
state = goToNote(notes, [1], state);
state = goToNote(notes, [2], state);
state = goToNote(notes, [3], state);
state = goToNote(notes, [4], state); // last seen note is notes[4]
// go to second folder
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[1].id });
state = goToNote(notes, [5], state);
state = goToNote(notes, [6], state);
// return to first folder
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[0].id });
expect(state.lastSelectedNotesIds.Folder[folders[0].id]).toEqual([notes[4].id]);
// return to second folder
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[1].id });
expect(state.lastSelectedNotesIds.Folder[folders[1].id]).toEqual([notes[6].id]);
}));
it('should ensure that history is free of adjacent duplicates', asyncTest(async () => {
// create 1 folder
const folders = await createNTestFolders(1);
// create 5 notes
const notes = await createNTestNotes(5, folders[0]);
// select the 1st folder and the 1st note
let state = initTestState(folders, 0, notes, [0]);
// backward = 0 1 2 3 2 3 2 3 2 3 2
// forward =
// current = 3
state = goToNote(notes, [1], state);
state = goToNote(notes, [2], state);
state = goToNote(notes, [3], state);
state = goToNote(notes, [2], state);
state = goToNote(notes, [3], state);
state = goToNote(notes, [2], state);
state = goToNote(notes, [3], state);
state = goToNote(notes, [2], state);
state = goToNote(notes, [3], state);
state = goToNote(notes, [2], state);
state = goToNote(notes, [3], state);
// backward = 0 1 2 3 2 3 2 3 2 3
// forward = 3
// current = 2
state = goBackWard(state);
// backward = 0 1 2 3 2 3 2 3 2
// forward = 3 2
// current = 3
state = goBackWard(state);
// backward = 0 1 2 3 2 3 2 3
// forward = 3 2 3
// current = 2
state = goBackWard(state);
// backward = 0 1 2 3 2 3 2
// forward = 3 2 3 2
// current = 3
state = goBackWard(state);
expect(state.backwardHistoryNotes.map(n=>n.id)).toEqual([notes[0], notes[1], notes[2], notes[3], notes[2], notes[3], notes[2]].map(n=>n.id));
expect(state.forwardHistoryNotes.map(n=>n.id)).toEqual([notes[3], notes[2], notes[3], notes[2]].map(n=>n.id));
expect(state.selectedNoteIds).toEqual([notes[3].id]);
// delete third note
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
// if adjacent duplicates not removed
// backward = 0 1 3 3
// forward = 3 3
// current = 3
// if adjacent duplicates are removed
// backward = 0 1 3
// forward = 3
// current = 3
// Expected: adjacent duplicates are removed and latest history does not contain current note
// backward = 0 1
// forward =
// current = 3
expect(state.backwardHistoryNotes.map(x => x.id)).toEqual([notes[0].id, notes[1].id]);
expect(state.forwardHistoryNotes.map(x => x.id)).toEqual([]);
expect(state.selectedNoteIds).toEqual([notes[3].id]);
}));
it('should ensure history max limit is maintained', asyncTest(async () => {
const folders = await createNTestFolders(1);
// create 5 notes
const notes = await createNTestNotes(5, folders[0]);
// select the 1st folder and the 1st note
let state = initTestState(folders, 0, notes, [0]);
const idx = 0;
for (let i = 0; i < 2 * MAX_HISTORY; i++) {
state = goToNote(notes, [i % 5], state);
}
expect(state.backwardHistoryNotes.length).toEqual(MAX_HISTORY);
expect(state.forwardHistoryNotes.map(x => x.id)).toEqual([]);
for (let i = 0; i < 2 * MAX_HISTORY; i++) {
state = goBackWard(state);
}
expect(state.backwardHistoryNotes).toEqual([]);
expect(state.forwardHistoryNotes.length).toEqual(MAX_HISTORY);
for (let i = 0; i < 2 * MAX_HISTORY; i++) {
state = goForward(state);
}
expect(state.backwardHistoryNotes.length).toEqual(MAX_HISTORY);
expect(state.forwardHistoryNotes.map(x => x.id)).toEqual([]);
}));
});

View File

@ -554,7 +554,6 @@ const mapStateToProps = (state: any) => {
watchedNoteFiles: state.watchedNoteFiles,
windowCommand: state.windowCommand,
notesParentType: state.notesParentType,
historyNotes: state.historyNotes,
selectedNoteTags: state.selectedNoteTags,
lastEditorScrollPercents: state.lastEditorScrollPercents,
selectedNoteHash: state.selectedNoteHash,

View File

@ -16,7 +16,6 @@ export interface NoteEditorProps {
windowCommand: any;
folders: any[];
notesParentType: string;
historyNotes: any[];
selectedNoteTags: any[];
lastEditorScrollPercents: any;
selectedNoteHash: string;

View File

@ -68,10 +68,6 @@ export default function useMessageHandler(scrollWhenReady:any, setScrollWhenRead
folderId: item.parent_id,
noteId: item.id,
hash: resourceUrlInfo.hash,
historyNoteAction: {
id: formNote.id,
parent_id: formNote.parent_id,
},
});
} else {
throw new Error(`Unsupported item type: ${item.type_}`);

View File

@ -18,11 +18,12 @@ interface NoteToolbarProps {
selectedFolderId: string,
folders: any[],
watchedNoteFiles: string[],
backwardHistoryNotes: any[],
forwardHistoryNotes: any[],
notesParentType: string,
note: any,
dispatch: Function,
onButtonClick(event:ButtonClickEvent):void,
historyNotes: any[],
}
function styles_(props:NoteToolbarProps) {
@ -37,12 +38,37 @@ function styles_(props:NoteToolbarProps) {
}
function useToolbarItems(props:NoteToolbarProps) {
const { note, selectedFolderId, folders, watchedNoteFiles, notesParentType, dispatch, onButtonClick, historyNotes } = props;
const { note, selectedFolderId, folders, watchedNoteFiles, notesParentType, dispatch
, onButtonClick, backwardHistoryNotes, forwardHistoryNotes } = props;
const toolbarItems = [];
const folder = Folder.byId(folders, selectedFolderId);
toolbarItems.push({
tooltip: _('Back'),
iconName: 'fa-arrow-left',
enabled: (backwardHistoryNotes.length > 0),
onClick: () => {
if (!backwardHistoryNotes.length) return;
props.dispatch({
type: 'HISTORY_BACKWARD',
});
},
});
toolbarItems.push({
tooltip: _('Front'),
iconName: 'fa-arrow-right',
enabled: (forwardHistoryNotes.length > 0),
onClick: () => {
if (!forwardHistoryNotes.length) return;
props.dispatch({
type: 'HISTORY_FORWARD',
});
},
});
if (folder && ['Search', 'Tag', 'SmartFilter'].includes(notesParentType)) {
toolbarItems.push({
title: _('In: %s', substrWithEllipsis(folder.title, 0, 16)),
@ -58,25 +84,6 @@ function useToolbarItems(props:NoteToolbarProps) {
});
}
if (historyNotes.length) {
toolbarItems.push({
tooltip: _('Back'),
iconName: 'fa-arrow-left',
onClick: () => {
if (!historyNotes.length) return;
const lastItem = historyNotes[historyNotes.length - 1];
dispatch({
type: 'FOLDER_AND_NOTE_SELECT',
folderId: lastItem.parent_id,
noteId: lastItem.id,
historyNoteAction: 'pop',
});
},
});
}
if (watchedNoteFiles.indexOf(note.id) >= 0) {
toolbarItems.push({
tooltip: _('Click to stop external editing'),
@ -149,7 +156,8 @@ const mapStateToProps = (state:any) => {
selectedFolderId: state.selectedFolderId,
folders: state.folders,
watchedNoteFiles: state.watchedNoteFiles,
historyNotes: state.historyNotes,
backwardHistoryNotes: state.backwardHistoryNotes,
forwardHistoryNotes: state.forwardHistoryNotes,
notesParentType: state.notesParentType,
};
};

View File

@ -443,6 +443,11 @@ class BaseApplication {
refreshFolders = true;
}
if (action.type == 'HISTORY_BACKWARD' || action.type == 'HISTORY_FORWARD') {
refreshNotes = true;
refreshNotesUseSelectedNoteId = true;
}
if (action.type == 'FOLDER_SELECT' || action.type === 'FOLDER_DELETE' || action.type === 'FOLDER_AND_NOTE_SELECT' || (action.type === 'SEARCH_UPDATE' && newState.notesParentType === 'Folder')) {
Setting.setValue('activeFolderId', newState.selectedFolderId);
this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null;

View File

@ -53,12 +53,15 @@ const defaultState = {
resourceFetcher: {
toFetchCount: 0,
},
historyNotes: [],
backwardHistoryNotes: [],
forwardHistoryNotes: [],
plugins: {},
provisionalNoteIds: [],
editorNoteStatuses: {},
};
const MAX_HISTORY = 200;
const stateUtils = {};
const derivedStateCache_ = {};
@ -115,6 +118,27 @@ stateUtils.lastSelectedNoteIds = function(state) {
return output ? output : [];
};
stateUtils.getCurrentNote = function(state) {
const selectedNoteIds = state.selectedNoteIds;
const notes = state.notes;
if (selectedNoteIds != null && selectedNoteIds.length > 0) {
const currNote = notes.find(note => note.id === selectedNoteIds[0]);
if (currNote != null) {
return {
id: currNote.id,
parent_id: currNote.parent_id,
notesParentType: state.notesParentType,
selectedFolderId: state.selectedFolderId,
selectedTagId: state.selectedTagId,
selectedSearchId: state.selectedSearchId,
searches: state.searches,
selectedSmartFilterId: state.selectedSmartFilterId,
};
}
}
return null;
};
function arrayHasEncryptedItems(array) {
for (let i = 0; i < array.length; i++) {
if (array[i].encryption_applied) return true;
@ -146,6 +170,10 @@ function folderSetCollapsed(state, action) {
return newState;
}
function removeAdjacentDuplicates(items) {
return items.filter((item, idx) => (idx >= 1) ? items[idx - 1].id !== item.id : true);
}
// When deleting a note, tag or folder
function handleItemDelete(state, action) {
const map = {
@ -264,8 +292,6 @@ function defaultNotesParentType(state, exclusion) {
function changeSelectedFolder(state, action, options = null) {
if (!options) options = {};
if (!('clearNoteHistory' in options)) options.clearNoteHistory = true;
const newState = Object.assign({}, state);
newState.selectedFolderId = 'folderId' in action ? action.folderId : action.id;
if (!newState.selectedFolderId) {
@ -276,7 +302,6 @@ function changeSelectedFolder(state, action, options = null) {
if (newState.selectedFolderId === state.selectedFolderId && newState.notesParentType === state.notesParentType) return state;
if (options.clearNoteHistory) newState.historyNotes = [];
if (options.clearSelectedNoteIds) newState.selectedNoteIds = [];
return newState;
@ -296,7 +321,6 @@ function recordLastSelectedNoteIds(state, noteIds) {
function changeSelectedNotes(state, action, options = null) {
if (!options) options = {};
if (!('clearNoteHistory' in options)) options.clearNoteHistory = true;
let noteIds = [];
if (action.id) noteIds = [action.id];
@ -337,8 +361,6 @@ function changeSelectedNotes(state, action, options = null) {
newState = recordLastSelectedNoteIds(newState, newState.selectedNoteIds);
if (options.clearNoteHistory) newState.historyNotes = [];
return newState;
}
@ -353,20 +375,158 @@ function removeItemFromArray(array, property, value) {
return array;
}
const getContextFromHistory = (ctx) => {
const result = {};
result.notesParentType = ctx.notesParentType;
if (result.notesParentType === 'Folder') {
result.selectedFolderId = ctx.selectedFolderId;
} else if (result.notesParentType === 'Tag') {
result.selectedTagId = ctx.selectedTagId;
} else if (result.notesParentType === 'Search') {
result.selectedSearchId = ctx.selectedSearchId;
result.searches = ctx.searches;
} else if (result.notesParentType === 'SmartFilter') {
result.selectedSmartFilterId = ctx.selectedSmartFilterId;
}
return result;
};
function handleHistory(state, action) {
let newState = Object.assign({}, state);
let backwardHistoryNotes = newState.backwardHistoryNotes.slice();
let forwardHistoryNotes = newState.forwardHistoryNotes.slice();
const currentNote = stateUtils.getCurrentNote(state);
switch (action.type) {
case 'HISTORY_BACKWARD': {
const note = backwardHistoryNotes[backwardHistoryNotes.length - 1];
if (currentNote != null && (forwardHistoryNotes.length === 0 || currentNote.id != forwardHistoryNotes[forwardHistoryNotes.length - 1].id)) {
forwardHistoryNotes = forwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
}
newState = changeSelectedFolder(newState, Object.assign({}, action, { type: 'FOLDER_SELECT', folderId: note.parent_id }));
newState = changeSelectedNotes(newState, Object.assign({}, action, { type: 'NOTE_SELECT', noteId: note.id }));
const ctx = backwardHistoryNotes[backwardHistoryNotes.length - 1];
newState = Object.assign(newState, getContextFromHistory(ctx));
backwardHistoryNotes.pop();
break;
}
case 'HISTORY_FORWARD': {
const note = forwardHistoryNotes[forwardHistoryNotes.length - 1];
if (currentNote != null && (backwardHistoryNotes.length === 0 || currentNote.id != backwardHistoryNotes[backwardHistoryNotes.length - 1].id)) {
backwardHistoryNotes = backwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
}
newState = changeSelectedFolder(newState, Object.assign({}, action, { type: 'FOLDER_SELECT', folderId: note.parent_id }));
newState = changeSelectedNotes(newState, Object.assign({}, action, { type: 'NOTE_SELECT', noteId: note.id }));
const ctx = forwardHistoryNotes[forwardHistoryNotes.length - 1];
newState = Object.assign(newState, getContextFromHistory(ctx));
forwardHistoryNotes.pop();
break;
}
case 'NOTE_SELECT':
if (currentNote != null && action.id != currentNote.id) {
forwardHistoryNotes = [];
backwardHistoryNotes = backwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
}
// History should be free from duplicates.
if (backwardHistoryNotes != null && backwardHistoryNotes.length > 0 &&
action.id === backwardHistoryNotes[backwardHistoryNotes.length - 1].id) {
backwardHistoryNotes.pop();
}
break;
case 'TAG_SELECT':
case 'FOLDER_AND_NOTE_SELECT':
case 'FOLDER_SELECT':
if (currentNote != null) {
forwardHistoryNotes = [];
backwardHistoryNotes = backwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
}
break;
case 'NOTE_UPDATE_ONE': {
const modNote = action.note;
backwardHistoryNotes = backwardHistoryNotes.map(note => {
if (note.id === modNote.id) {
return Object.assign(note, { parent_id: modNote.parent_id, selectedFolderId: modNote.parent_id });
}
return note;
});
forwardHistoryNotes = forwardHistoryNotes.map(note => {
if (note.id === modNote.id) {
return Object.assign(note, { parent_id: modNote.parent_id, selectedFolderId: modNote.parent_id });
}
return note;
});
break;
}
case 'SEARCH_UPDATE':
if (currentNote != null && (backwardHistoryNotes.length === 0 ||
backwardHistoryNotes[backwardHistoryNotes.length - 1].id != currentNote.id)) {
forwardHistoryNotes = [];
backwardHistoryNotes = backwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
}
break;
case 'FOLDER_DELETE':
backwardHistoryNotes = backwardHistoryNotes.filter(note => note.parent_id != action.id);
forwardHistoryNotes = forwardHistoryNotes.filter(note => note.parent_id != action.id);
backwardHistoryNotes = removeAdjacentDuplicates(backwardHistoryNotes);
forwardHistoryNotes = removeAdjacentDuplicates(forwardHistoryNotes);
break;
case 'NOTE_DELETE': {
backwardHistoryNotes = backwardHistoryNotes.filter(note => note.id != action.id);
forwardHistoryNotes = forwardHistoryNotes.filter(note => note.id != action.id);
backwardHistoryNotes = removeAdjacentDuplicates(backwardHistoryNotes);
forwardHistoryNotes = removeAdjacentDuplicates(forwardHistoryNotes);
// Fix the case where after deletion the currently selected note is also the latest in history
const selectedNoteIds = newState.selectedNoteIds;
if (selectedNoteIds.length && backwardHistoryNotes.length && backwardHistoryNotes[backwardHistoryNotes.length - 1].id === selectedNoteIds[0]) {
backwardHistoryNotes = backwardHistoryNotes.slice(0, backwardHistoryNotes.length - 1);
}
if (selectedNoteIds.length && forwardHistoryNotes.length && forwardHistoryNotes[forwardHistoryNotes.length - 1].id === selectedNoteIds[0]) {
forwardHistoryNotes = forwardHistoryNotes.slice(0, forwardHistoryNotes.length - 1);
}
break;
}
default:
// console.log('Unknown action in history reducer.' ,action.type);
return state;
}
newState.backwardHistoryNotes = backwardHistoryNotes;
newState.forwardHistoryNotes = forwardHistoryNotes;
return newState;
}
const reducer = (state = defaultState, action) => {
// if (!['SIDE_MENU_OPEN_PERCENT'].includes(action.type)) console.info('Action', action.type);
let newState = state;
// NOTE_DELETE requires post processing
if (action.type !== 'NOTE_DELETE') {
newState = handleHistory(newState, action);
}
try {
switch (action.type) {
case 'NOTE_SELECT':
case 'NOTE_SELECT_ADD':
case 'NOTE_SELECT_REMOVE':
case 'NOTE_SELECT_TOGGLE':
newState = changeSelectedNotes(state, action);
newState = changeSelectedNotes(newState, action);
break;
case 'NOTE_SELECT_EXTEND':
{
newState = Object.assign({}, state);
@ -424,29 +584,14 @@ const reducer = (state = defaultState, action) => {
break;
case 'FOLDER_SELECT':
newState = changeSelectedFolder(state, action, { clearSelectedNoteIds: true });
newState = changeSelectedFolder(newState, action, { clearSelectedNoteIds: true });
break;
case 'FOLDER_AND_NOTE_SELECT':
{
newState = changeSelectedFolder(state, action, { clearNoteHistory: false });
newState = changeSelectedFolder(newState, action);
const noteSelectAction = Object.assign({}, action, { type: 'NOTE_SELECT' });
newState = changeSelectedNotes(newState, noteSelectAction, { clearNoteHistory: false });
if (action.historyNoteAction) {
const historyNotes = newState.historyNotes.slice();
if (typeof action.historyNoteAction === 'object') {
historyNotes.push(Object.assign({}, action.historyNoteAction));
} else if (action.historyNoteAction === 'pop') {
historyNotes.pop();
}
newState.historyNotes = historyNotes;
} else if (newState !== state) {
// Clear the note history if folder and selected note have actually been changed. For example
// they won't change if they are already selected. That way, the "Back" button to go to the
// previous note wll stay.
newState.historyNotes = [];
}
newState = changeSelectedNotes(newState, noteSelectAction);
}
break;
@ -543,6 +688,7 @@ const reducer = (state = defaultState, action) => {
newState.selectedNoteIds = newIndex >= 0 ? [newNotes[newIndex].id] : [];
}
if (action.provisional) {
newState.provisionalNoteIds.push(modNote.id);
} else {
@ -603,7 +749,6 @@ const reducer = (state = defaultState, action) => {
break;
case 'TAG_SELECT':
newState = Object.assign({}, state);
newState.selectedTagId = action.id;
if (!action.id) {
newState.notesParentType = defaultNotesParentType(state, 'Tag');
@ -654,7 +799,7 @@ const reducer = (state = defaultState, action) => {
break;
case 'FOLDER_DELETE':
newState = handleItemDelete(state, action);
newState = handleItemDelete(newState, action);
break;
case 'MASTERKEY_UPDATE_ALL':
@ -725,7 +870,7 @@ const reducer = (state = defaultState, action) => {
case 'SEARCH_UPDATE':
{
newState = Object.assign({}, state);
newState = handleHistory(state, action);
const searches = newState.searches.slice();
let found = false;
for (let i = 0; i < searches.length; i++) {
@ -846,7 +991,11 @@ const reducer = (state = defaultState, action) => {
newState.hasEncryptedItems = stateHasEncryptedItems(newState);
}
if (action.type === 'NOTE_DELETE') {
newState = handleHistory(newState, action);
}
return newState;
};
module.exports = { reducer, defaultState, stateUtils };
module.exports = { reducer, defaultState, stateUtils, MAX_HISTORY };