diff --git a/packages/lib/JoplinDatabase.ts b/packages/lib/JoplinDatabase.ts index b0ff9c08a18..d3d8a7193be 100644 --- a/packages/lib/JoplinDatabase.ts +++ b/packages/lib/JoplinDatabase.ts @@ -343,7 +343,7 @@ export default class JoplinDatabase extends Database { // must be set in the synchronizer too. // Note: v16 and v17 don't do anything. They were used to debug an issue. - const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38]; + const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39]; let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion); @@ -888,6 +888,10 @@ export default class JoplinDatabase extends Database { GROUP BY tags.id`); } + if (targetVersion == 39) { + queries.push('ALTER TABLE `notes` ADD COLUMN conflict_original_id TEXT NOT NULL DEFAULT ""'); + } + const updateVersionQuery = { sql: 'UPDATE version SET version = ?', params: [targetVersion] }; queries.push(updateVersionQuery); diff --git a/packages/lib/Synchronizer.ts b/packages/lib/Synchronizer.ts index b6317acf369..3c8e96407f9 100644 --- a/packages/lib/Synchronizer.ts +++ b/packages/lib/Synchronizer.ts @@ -610,10 +610,7 @@ export default class Synchronizer { // ------------------------------------------------------------------------------ if (mustHandleConflict) { - const conflictedNote = Object.assign({}, local); - delete conflictedNote.id; - conflictedNote.is_conflict = 1; - await Note.save(conflictedNote, { autoTimestamp: false, changeSource: ItemChange.SOURCE_SYNC }); + await Note.createConflictNote(local, ItemChange.SOURCE_SYNC); } } else if (action == 'resourceConflict') { // ------------------------------------------------------------------------------ diff --git a/packages/lib/models/Note.test.ts b/packages/lib/models/Note.test.ts index fe6867014d2..d27804b6a87 100644 --- a/packages/lib/models/Note.test.ts +++ b/packages/lib/models/Note.test.ts @@ -6,6 +6,7 @@ import { sortedIds, createNTestNotes, setupDatabaseAndSynchronizer, switchClient import Folder from './Folder'; import Note from './Note'; import Tag from './Tag'; +import ItemChange from './ItemChange'; const ArrayUtils = require('../ArrayUtils.js'); async function allItems() { @@ -344,4 +345,44 @@ describe('models_Note', function() { expect(sortedNotes3[4].id).toBe(note2.id); })); + it('should create a conflict note', async () => { + const folder = await Folder.save({ title: 'Source Folder' }); + const origNote = await Note.save({ title: 'note', parent_id: folder.id }); + const conflictedNote = await Note.createConflictNote(origNote, ItemChange.SOURCE_SYNC); + + expect(conflictedNote.is_conflict).toBe(1); + expect(conflictedNote.conflict_original_id).toBe(origNote.id); + expect(conflictedNote.parent_id).toBe(folder.id); + }); + + it('should copy conflicted note to target folder and cancel conflict', (async () => { + const srcfolder = await Folder.save({ title: 'Source Folder' }); + const targetfolder = await Folder.save({ title: 'Target Folder' }); + + const note1 = await Note.save({ title: 'note', parent_id: srcfolder.id }); + const conflictedNote = await Note.createConflictNote(note1, ItemChange.SOURCE_SYNC); + + const note2 = await Note.copyToFolder(conflictedNote.id, targetfolder.id); + + expect(note2.id === conflictedNote.id).toBe(false); + expect(note2.title).toBe(conflictedNote.title); + expect(note2.is_conflict).toBe(0); + expect(note2.conflict_original_id).toBe(''); + expect(note2.parent_id).toBe(targetfolder.id); + })); + + it('should move conflicted note to target folder and cancel conflict', (async () => { + const srcFolder = await Folder.save({ title: 'Source Folder' }); + const targetFolder = await Folder.save({ title: 'Target Folder' }); + const note1 = await Note.save({ title: 'note', parent_id: srcFolder.id }); + + const conflictedNote = await Note.createConflictNote(note1, ItemChange.SOURCE_SYNC); + + const movedNote = await Note.moveToFolder(conflictedNote.id, targetFolder.id); + + expect(movedNote.parent_id).toBe(targetFolder.id); + expect(movedNote.is_conflict).toBe(0); + expect(movedNote.conflict_original_id).toBe(''); + })); + }); diff --git a/packages/lib/models/Note.ts b/packages/lib/models/Note.ts index 0790fdea911..b845d52bd54 100644 --- a/packages/lib/models/Note.ts +++ b/packages/lib/models/Note.ts @@ -523,6 +523,7 @@ export default class Note extends BaseItem { changes: { parent_id: folderId, is_conflict: 0, // Also reset the conflict flag in case we're moving the note out of the conflict folder + conflict_original_id: '', // Reset parent id as well. }, }); } @@ -537,6 +538,7 @@ export default class Note extends BaseItem { id: noteId, parent_id: folderId, is_conflict: 0, + conflict_original_id: '', updated_time: time.unixMs(), }; @@ -911,4 +913,12 @@ export default class Note extends BaseItem { return new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); } + + static async createConflictNote(sourceNote: NoteEntity, changeSource: number): Promise { + const conflictNote = Object.assign({}, sourceNote); + delete conflictNote.id; + conflictNote.is_conflict = 1; + conflictNote.conflict_original_id = sourceNote.id; + return await Note.save(conflictNote, { autoTimestamp: false, changeSource: changeSource }); + } } diff --git a/packages/lib/services/database/types.ts b/packages/lib/services/database/types.ts index 7b80180c611..c99da9766e1 100644 --- a/packages/lib/services/database/types.ts +++ b/packages/lib/services/database/types.ts @@ -98,6 +98,7 @@ export interface NoteEntity { "created_time"?: number "updated_time"?: number "is_conflict"?: number + "conflict_original_id"?: string "latitude"?: number "longitude"?: number "altitude"?: number diff --git a/packages/lib/services/synchronizer/Synchronizer.conflicts.test.ts b/packages/lib/services/synchronizer/Synchronizer.conflicts.test.ts index 1e31f39e83c..0a1324bc20f 100644 --- a/packages/lib/services/synchronizer/Synchronizer.conflicts.test.ts +++ b/packages/lib/services/synchronizer/Synchronizer.conflicts.test.ts @@ -44,9 +44,10 @@ describe('Synchronizer.conflicts', function() { // the conflicted and original note must be the same in every way, to make sure no data has been lost. const conflictedNote = conflictedNotes[0]; expect(conflictedNote.id == note2conf.id).toBe(false); + expect(conflictedNote.conflict_original_id).toBe(note2conf.id); for (const n in conflictedNote) { if (!conflictedNote.hasOwnProperty(n)) continue; - if (n == 'id' || n == 'is_conflict') continue; + if (n == 'id' || n == 'is_conflict' || n == 'conflict_original_id') continue; expect(conflictedNote[n]).toBe(note2conf[n]); } diff --git a/packages/server/src/utils/testing/testUtils.ts b/packages/server/src/utils/testing/testUtils.ts index 1ef2e1b14a5..502e74e9eb8 100644 --- a/packages/server/src/utils/testing/testUtils.ts +++ b/packages/server/src/utils/testing/testUtils.ts @@ -477,6 +477,7 @@ encryption_applied: 0 markup_language: 1 is_shared: 1 share_id: ${note.share_id || ''} +conflict_original_id: type_: 1`; }