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); %}