You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	This commit is contained in:
		
							
								
								
									
										455
									
								
								CliClient/tests/feature_NoteHistory.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										455
									
								
								CliClient/tests/feature_NoteHistory.js
									
									
									
									
									
										Normal 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); | ||||
| 	})); | ||||
|  | ||||
| }); | ||||
| @@ -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([]); | ||||
| 	})); | ||||
| }); | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -16,7 +16,6 @@ export interface NoteEditorProps { | ||||
| 	windowCommand: any; | ||||
| 	folders: any[]; | ||||
| 	notesParentType: string; | ||||
| 	historyNotes: any[]; | ||||
| 	selectedNoteTags: any[]; | ||||
| 	lastEditorScrollPercents: any; | ||||
| 	selectedNoteHash: string; | ||||
|   | ||||
| @@ -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_}`); | ||||
|   | ||||
| @@ -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, | ||||
| 	}; | ||||
| }; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user