diff --git a/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml b/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml index 094828c17..00ea588d2 100644 --- a/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml +++ b/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml @@ -2,7 +2,7 @@ - 3.16.1 + 3.25.1 1 @@ -57,6 +57,7 @@ 1 apiTokenId + 1 @@ -111,529 +112,573 @@ INT|0s 1 - + 10 TEXT|0s + NULL + + + 11 + TEXT|0s 1 "" - - 11 + + 12 int|0s 0 - + 1 attributeId + 1 - + noteId + - + name value + - + value + - + attributeId 1 sqlite_autoindex_attributes_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s 1 - + 3 TEXT|0s 1 - + 4 INTEGER|0s 1 - + 5 TEXT|0s - + 6 INTEGER|0s 1 0 - + 7 INTEGER|0s 1 0 - + 8 TEXT|0s - 1 + NULL - + 9 TEXT|0s 1 - + 10 TEXT|0s 1 + + + 11 + TEXT|0s + 1 "" - + 1 branchId + 1 - + noteId parentNoteId + - + parentNoteId + - + branchId 1 sqlite_autoindex_branches_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s NULL - + 3 TEXT|0s 1 "" - + 4 TEXT|0s 1 - + 1 noteId + 1 - + noteId 1 sqlite_autoindex_note_contents_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s - + 3 TEXT|0s 1 '' - + 4 TEXT|0s 1 - + 1 noteRevisionId + 1 - + noteRevisionId 1 sqlite_autoindex_note_revision_contents_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s 1 - + 3 TEXT|0s - + 4 INT|0s 1 - + 5 INT|0s 1 0 - + 6 INT|0s 1 0 - + 7 TEXT|0s 1 - + 8 TEXT|0s 1 - + 9 TEXT|0s 1 - + 10 TEXT|0s 1 - + 11 TEXT|0s 1 - + 12 TEXT|0s 1 '' - + 13 TEXT|0s 1 '' - + 14 TEXT|0s 1 '' - + 1 noteRevisionId + 1 - + noteId + - + utcDateLastEdited + - + utcDateCreated + - + dateLastEdited + - + dateCreated + - + noteRevisionId 1 sqlite_autoindex_note_revisions_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s 1 "note" - + 3 INT|0s 1 - + 4 INT|0s 1 0 - + 5 TEXT|0s 1 'text' - + 6 TEXT|0s 1 'text/html' - + 7 TEXT|0s 1 "" - + 8 INT|0s 1 0 - + 9 + TEXT|0s + NULL + + + 10 INT|0s 1 0 - - 10 - TEXT|0s - 1 - - + 11 TEXT|0s 1 - + 12 TEXT|0s 1 - + 13 TEXT|0s 1 - + + 14 + TEXT|0s + 1 + + 1 noteId + 1 - + title + - + type + - + isDeleted + - + dateCreated + - + dateModified + - + utcDateCreated + - + utcDateModified + - + noteId 1 sqlite_autoindex_notes_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s - + 3 INTEGER|0s 1 0 - + 4 TEXT|0s 1 "" - + 5 TEXT|0s 1 - + 6 TEXT|0s 1 - + 1 name + 1 - + name 1 sqlite_autoindex_options_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s 1 - + 3 TEXT|0s 1 "" - + 4 TEXT|0s 1 - + 5 INT|0s - + 1 noteId + 1 - + noteId 1 sqlite_autoindex_recent_notes_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s 1 - + 1 sourceId + 1 - + utcDateCreated + - + sourceId 1 sqlite_autoindex_source_ids_1 - + 1 text|0s - + 2 text|0s - + 3 text|0s - + 4 - integer|0s + int|0s - + 5 text|0s - + 1 - + 2 - + 1 INTEGER|0s 1 1 - + 2 TEXT|0s 1 - + 3 TEXT|0s 1 - + 4 TEXT|0s 1 - + 5 TEXT|0s 1 - + entityName entityId + 1 - + utcSyncDate + - + id 1 diff --git a/src/public/javascripts/dialogs/recent_changes.js b/src/public/javascripts/dialogs/recent_changes.js index 2038d243e..9255479d4 100644 --- a/src/public/javascripts/dialogs/recent_changes.js +++ b/src/public/javascripts/dialogs/recent_changes.js @@ -41,9 +41,26 @@ export async function showDialog() { $noteLink = $("").text(change.current_title); if (change.canBeUndeleted) { + const $undeleteLink = $(``) + .text("undelete") + .on('click', async () => { + const confirmDialog = await import('../dialogs/confirm.js'); + const text = 'Do you want to undelete this note and its sub-notes?'; + + if (await confirmDialog.confirm(text)) { + await server.put(`notes/${change.noteId}/undelete`); + + $dialog.modal('hide'); + + await treeCache.reloadNotes([change.noteId]); + + treeService.activateNote(change.noteId); + } + }); + $noteLink .append(' (') - .append($(``).text("undelete")) + .append($undeleteLink) .append(')'); } } diff --git a/src/public/javascripts/services/branches.js b/src/public/javascripts/services/branches.js index adad473bd..1e2a2a7d9 100644 --- a/src/public/javascripts/services/branches.js +++ b/src/public/javascripts/services/branches.js @@ -265,6 +265,24 @@ ws.subscribeToMessages(async message => { } }); +ws.subscribeToMessages(async message => { + if (message.taskType !== 'undelete-notes') { + return; + } + + if (message.type === 'task-error') { + toastService.closePersistent(message.taskId); + toastService.showError(message.message); + } else if (message.type === 'task-progress-count') { + toastService.showPersistent(makeToast(message.taskId, "Undeleting notes in progress: " + message.progressCount)); + } else if (message.type === 'task-succeeded') { + const toast = makeToast(message.taskId, "Undeleting notes finished successfully."); + toast.closeAfter = 5000; + + toastService.showPersistent(toast); + } +}); + export default { moveBeforeNode, moveAfterNode, diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js index a8391cf65..256b7b58e 100644 --- a/src/routes/api/notes.js +++ b/src/routes/api/notes.js @@ -71,6 +71,16 @@ async function deleteNote(req) { } } +async function undeleteNote(req) { + const note = await repository.getNote(req.params.noteId); + + const taskContext = TaskContext.getInstance(utils.randomString(), 'undelete-notes'); + + await noteService.undeleteNote(note, note.deleteId, taskContext); + + await taskContext.taskSucceeded(); +} + async function sortNotes(req) { const noteId = req.params.noteId; @@ -172,6 +182,7 @@ module.exports = { getNote, updateNote, deleteNote, + undeleteNote, createNote, sortNotes, protectSubtree, diff --git a/src/routes/api/recent_changes.js b/src/routes/api/recent_changes.js index 80599e624..c4fda587c 100644 --- a/src/routes/api/recent_changes.js +++ b/src/routes/api/recent_changes.js @@ -2,6 +2,7 @@ const sql = require('../../services/sql'); const protectedSessionService = require('../../services/protected_session'); +const noteService = require('../../services/notes'); async function getRecentChanges() { const recentChanges = await sql.getRows( @@ -60,17 +61,10 @@ async function getRecentChanges() { else { const deleteId = change.current_deleteId; - const undeletedParentCount = await sql.getValue(` - SELECT COUNT(parentNote.noteId) - FROM branches - JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId - WHERE branches.noteId = ? - AND branches.isDeleted = 1 - AND branches.deleteId = ? - AND parentNote.isDeleted = 0`, [change.noteId, deleteId]); + const undeletedParentBranches = await noteService.getUndeletedParentBranches(change.noteId, deleteId); // note (and the subtree) can be undeleted if there's at least one undeleted parent (whose branch would be undeleted by this op) - change.canBeUndeleted = undeletedParentCount > 0; + change.canBeUndeleted = undeletedParentBranches.length > 0; } } } diff --git a/src/routes/routes.js b/src/routes/routes.js index 4d3108451..2d4e333fc 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -131,6 +131,7 @@ function register(app) { apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote); apiRoute(PUT, '/api/notes/:noteId', notesApiRoute.updateNote); apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote); + apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote); apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote); apiRoute(PUT, '/api/notes/:noteId/sort', notesApiRoute.sortNotes); apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectSubtree); diff --git a/src/services/notes.js b/src/services/notes.js index 778e9c770..8bd488450 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -445,6 +445,88 @@ async function deleteBranch(branch, deleteId, taskContext) { } } +/** + * @param {Note} note + * @param {string} deleteId + * @param {TaskContext} taskContext + */ +async function undeleteNote(note, deleteId, taskContext) { + const undeletedParentBranches = await getUndeletedParentBranches(note.noteId, deleteId); + + if (undeletedParentBranches.length === 0) { + // cannot undelete if there's no undeleted parent + return; + } + + for (const parentBranch of undeletedParentBranches) { + await undeleteBranch(parentBranch, deleteId, taskContext); + } +} + +/** + * @param {Branch} branch + * @param {string} deleteId + * @param {TaskContext} taskContext + */ +async function undeleteBranch(branch, deleteId, taskContext) { + if (!branch.isDeleted) { + return; + } + + const note = await branch.getNote(); + + if (note.isDeleted && note.deleteId !== deleteId) { + return; + } + + branch.isDeleted = false; + await branch.save(); + + taskContext.increaseProgressCount(); + + if (note.isDeleted && note.deleteId === deleteId) { + note.isDeleted = false; + await note.save(); + + const attrs = await repository.getEntities(` + SELECT * FROM attributes + WHERE isDeleted = 1 + AND deleteId = ? + AND (noteId = ? + OR (type = 'relation' AND value = ?))`, [deleteId, note.noteId, note.noteId]); + + for (const attr of attrs) { + attr.isDeleted = false; + await attr.save(); + } + + const childBranches = await repository.getEntities(` + SELECT branches.* + FROM branches + WHERE branches.isDeleted = 1 + AND branches.deleteId = ? + AND branches.parentNoteId = ?`, [deleteId, note.noteId]); + + for (const childBranch of childBranches) { + await deleteBranch(childBranch, deleteId, taskContext); + } + } +} + +/** + * @return return deleted branches of an undeleted parent note + */ +async function getUndeletedParentBranches(noteId, deleteId) { + return await repository.getEntities(` + SELECT branches.* + FROM branches + JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId + WHERE branches.noteId = ? + AND branches.isDeleted = 1 + AND branches.deleteId = ? + AND parentNote.isDeleted = 0`, [noteId, deleteId]); +} + async function scanForLinks(noteId) { const note = await repository.getNote(noteId); if (!note || !['text', 'relation-map'].includes(note.type)) { @@ -552,7 +634,9 @@ module.exports = { createNewNoteWithTarget, updateNote, deleteBranch, + undeleteNote, protectNoteRecursively, scanForLinks, - duplicateNote + duplicateNote, + getUndeletedParentBranches };