diff --git a/package-lock.json b/package-lock.json index 14cc4892b..8d9b63fc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "trilium", - "version": "0.60.1-beta", + "version": "0.60.2-beta", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "trilium", - "version": "0.60.1-beta", + "version": "0.60.2-beta", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { diff --git a/package.json b/package.json index 3353f12b9..43b1241f2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "trilium", "productName": "Trilium Notes", "description": "Trilium Notes", - "version": "0.60.1-beta", + "version": "0.60.2-beta", "license": "AGPL-3.0-only", "main": "electron.js", "bin": { diff --git a/src/etapi/backup.js b/src/etapi/backup.js new file mode 100644 index 000000000..8dc7f8ed1 --- /dev/null +++ b/src/etapi/backup.js @@ -0,0 +1,14 @@ +const eu = require("./etapi_utils"); +const backupService = require("../services/backup"); + +function register(router) { + eu.route(router, 'put', '/etapi/backup/:backupName', async (req, res, next) => { + await backupService.backupNow(req.params.backupName); + + res.sendStatus(204); + }); +} + +module.exports = { + register +}; diff --git a/src/etapi/etapi.openapi.yaml b/src/etapi/etapi.openapi.yaml index 7c41693d1..754fb05b3 100644 --- a/src/etapi/etapi.openapi.yaml +++ b/src/etapi/etapi.openapi.yaml @@ -700,7 +700,26 @@ paths: application/json; charset=utf-8: schema: $ref: '#/components/schemas/Error' - + /backup/{backupName}: + parameters: + - name: backupName + in: path + required: true + description: If the backupName is e.g. "now", then the backup will be written to "backup-now.db" file + schema: + $ref: '#/components/schemas/StringId' + put: + description: Create a database backup under a given name + operationId: createBackup + responses: + '204': + description: backup has been created + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/Error' components: securitySchemes: EtapiTokenAuth: @@ -880,6 +899,10 @@ components: type: string pattern: '[a-zA-Z0-9_]{4,32}' example: evnnmvHTCgIn + StringId: + type: string + pattern: '[a-zA-Z0-9_]{1,32}' + example: my_ID EntityIdList: type: array items: diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js index 60f7b34a3..1d8f426f2 100644 --- a/src/public/app/widgets/note_tree.js +++ b/src/public/app/widgets/note_tree.js @@ -148,6 +148,9 @@ const TPL = ` const MAX_SEARCH_RESULTS_IN_TREE = 100; +// this has to be hanged on the actual elements to effectively intercept and stop click event +const cancelClickPropagation = e => e.stopPropagation(); + export default class NoteTreeWidget extends NoteContextAwareWidget { constructor() { super(); @@ -559,7 +562,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== 'root'; if (isHoistedNote) { - const $unhoistButton = $(''); + const $unhoistButton = $('') + .on("click", cancelClickPropagation); // unhoist button is prepended since compared to other buttons this is not just convenience // on the mobile interface - it's the only way to unhoist @@ -567,19 +571,22 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { } if (note.hasLabel('workspace') && !isHoistedNote) { - const $enterWorkspaceButton = $(''); + const $enterWorkspaceButton = $('') + .on("click", cancelClickPropagation); $span.append($enterWorkspaceButton); } if (note.type === 'search') { - const $refreshSearchButton = $(''); + const $refreshSearchButton = $('') + .on("click", cancelClickPropagation); $span.append($refreshSearchButton); } if (!['search', 'launcher'].includes(note.type) && !note.isOptions() && !note.isLaunchBarConfig()) { - const $createChildNoteButton = $(''); + const $createChildNoteButton = $('') + .on("click", cancelClickPropagation); $span.append($createChildNoteButton); } diff --git a/src/routes/api/recent_changes.js b/src/routes/api/recent_changes.js index 646898e9c..ca0a23c71 100644 --- a/src/routes/api/recent_changes.js +++ b/src/routes/api/recent_changes.js @@ -27,7 +27,8 @@ function getRecentChanges(req) { for (const noteRevisionRow of noteRevisionRows) { const note = becca.getNote(noteRevisionRow.noteId); - if (note?.hasAncestor(ancestorNoteId)) { + // for deleted notes, the becca note is null, and it's not possible to (easily) determine if it belongs to a subtree + if (ancestorNoteId === 'root' || note?.hasAncestor(ancestorNoteId)) { recentChanges.push(noteRevisionRow); } } @@ -43,8 +44,8 @@ function getRecentChanges(req) { notes.title AS current_title, notes.isProtected AS current_isProtected, notes.title, - notes.utcDateCreated AS utcDate, - notes.dateCreated AS date + notes.utcDateCreated AS utcDate, -- different from the second SELECT + notes.dateCreated AS date -- different from the second SELECT FROM notes UNION ALL SELECT @@ -54,15 +55,16 @@ function getRecentChanges(req) { notes.title AS current_title, notes.isProtected AS current_isProtected, notes.title, - notes.utcDateModified AS utcDate, - notes.dateModified AS date + notes.utcDateModified AS utcDate, -- different from the first SELECT + notes.dateModified AS date -- different from the first SELECT FROM notes WHERE notes.isDeleted = 1`); for (const noteRow of noteRows) { const note = becca.getNote(noteRow.noteId); - if (note?.hasAncestor(ancestorNoteId)) { + // for deleted notes, the becca note is null, and it's not possible to (easily) determine if it belongs to a subtree + if (ancestorNoteId === 'root' || note?.hasAncestor(ancestorNoteId)) { recentChanges.push(noteRow); } } diff --git a/src/routes/api/sql.js b/src/routes/api/sql.js index 09e14cc86..1c853f365 100644 --- a/src/routes/api/sql.js +++ b/src/routes/api/sql.js @@ -37,7 +37,7 @@ function execute(req) { continue; } - if (query.toLowerCase().startsWith('select')) { + if (query.toLowerCase().startsWith('select') || query.toLowerCase().startsWith('with')) { results.push(sql.getRows(query)); } else { diff --git a/src/routes/routes.js b/src/routes/routes.js index a52a067f5..2989208b5 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -65,6 +65,7 @@ const etapiBranchRoutes = require('../etapi/branches'); const etapiNoteRoutes = require('../etapi/notes'); const etapiSpecialNoteRoutes = require('../etapi/special_notes'); const etapiSpecRoute = require('../etapi/spec'); +const etapiBackupRoute = require('../etapi/backup'); const csrfMiddleware = csurf({ cookie: true, @@ -315,6 +316,7 @@ function register(app) { etapiNoteRoutes.register(router); etapiSpecialNoteRoutes.register(router); etapiSpecRoute.register(router); + etapiBackupRoute.register(router); app.use('', router); } diff --git a/src/services/build.js b/src/services/build.js index 88c7d6157..c539d9bd7 100644 --- a/src/services/build.js +++ b/src/services/build.js @@ -1 +1 @@ -module.exports = { buildDate:"2023-05-26T23:11:53+02:00", buildRevision: "82efc924136c5b215e39f2108f00dd2bf075271c" }; +module.exports = { buildDate:"2023-06-08T22:46:52+02:00", buildRevision: "6e69cafe5419e8efcc6f652647f9227dbcfa1e18" }; diff --git a/test-etapi/create-backup.http b/test-etapi/create-backup.http new file mode 100644 index 000000000..59ffbebc4 --- /dev/null +++ b/test-etapi/create-backup.http @@ -0,0 +1,4 @@ +PUT {{triliumHost}}/etapi/backup/etapi_test +Authorization: {{authToken}} + +> {% client.assert(response.status === 201); %}