Merge branch 'stable'

This commit is contained in:
zadam 2023-06-27 23:15:20 +02:00
commit 05d2f4fe96
16 changed files with 124 additions and 64 deletions

View File

@ -0,0 +1 @@
UPDATE branches SET notePosition = notePosition - 999899999 WHERE parentNoteId = 'root' AND notePosition > 999999999;

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "trilium", "name": "trilium",
"version": "0.60.2-beta", "version": "0.60.4",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "trilium", "name": "trilium",
"version": "0.60.2-beta", "version": "0.60.4",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {

View File

@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.60.2-beta", "version": "0.60.4",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {

View File

@ -33,13 +33,7 @@ paths:
content: content:
application/json; charset=utf-8: application/json; charset=utf-8:
schema: schema:
properties: $ref: '#/components/schemas/NoteWithBranch'
note:
$ref: '#/components/schemas/Note'
description: Created note
branch:
$ref: '#/components/schemas/Branch'
description: Created branch
default: default:
description: unexpected error description: unexpected error
content: content:
@ -291,6 +285,29 @@ paths:
application/json; charset=utf-8: application/json; charset=utf-8:
schema: schema:
$ref: '#/components/schemas/Error' $ref: '#/components/schemas/Error'
/notes/{noteId}/import:
parameters:
- name: noteId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
post:
description: Imports ZIP file into a given note.
operationId: importZip
responses:
'201':
description: note created
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/NoteWithBranch'
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
/notes/{noteId}/note-revision: /notes/{noteId}/note-revision:
parameters: parameters:
- name: noteId - name: noteId
@ -852,6 +869,13 @@ components:
utcDateModified: utcDateModified:
$ref: '#/components/schemas/UtcDateTime' $ref: '#/components/schemas/UtcDateTime'
readOnly: true readOnly: true
NoteWithBranch:
type: object
properties:
note:
$ref: '#/components/schemas/Note'
branch:
$ref: '#/components/schemas/Branch'
Attribute: Attribute:
type: object type: object
description: Attribute (Label, Relation) is a key-value record attached to a note. description: Attribute (Label, Relation) is a key-value record attached to a note.

View File

@ -8,6 +8,7 @@ const v = require("./validators");
const searchService = require("../services/search/services/search"); const searchService = require("../services/search/services/search");
const SearchContext = require("../services/search/search_context"); const SearchContext = require("../services/search/search_context");
const zipExportService = require("../services/export/zip"); const zipExportService = require("../services/export/zip");
const zipImportService = require("../services/import/zip");
function register(router) { function register(router) {
eu.route(router, 'get', '/etapi/notes', (req, res, next) => { eu.route(router, 'get', '/etapi/notes', (req, res, next) => {
@ -141,11 +142,21 @@ function register(router) {
// (e.g. branchIds are not seen in UI), that we export "note export" instead. // (e.g. branchIds are not seen in UI), that we export "note export" instead.
const branch = note.getParentBranches()[0]; const branch = note.getParentBranches()[0];
console.log(note.getParentBranches());
zipExportService.exportToZip(taskContext, branch, format, res); zipExportService.exportToZip(taskContext, branch, format, res);
}); });
eu.route(router, 'post' ,'/etapi/notes/:noteId/import', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const taskContext = new TaskContext('no-progress-reporting');
zipImportService.importZip(taskContext, req.body, note).then(importedNote => {
res.status(201).json({
note: mappers.mapNoteToPojo(importedNote),
branch: mappers.mapBranchToPojo(importedNote.getBranches()[0]),
});
}); // we need better error handling here, async errors won't be properly processed.
});
eu.route(router, 'post' ,'/etapi/notes/:noteId/note-revision', (req, res, next) => { eu.route(router, 'post' ,'/etapi/notes/:noteId/note-revision', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId); const note = eu.getAndCheckNote(req.params.noteId);

View File

@ -230,6 +230,15 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
} }
} }
async runActiveNoteCommand(params) {
if (this.isNoteContext(params.ntxId)) {
// make sure that script is saved before running it #4028
await this.spacedUpdate.updateNowIfNecessary();
}
return await this.parent.triggerCommand('runActiveNote', params);
}
async printActiveNoteEvent() { async printActiveNoteEvent() {
if (!this.noteContext.isActive()) { if (!this.noteContext.isActive()) {
return; return;

View File

@ -93,11 +93,11 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
}); });
this.$iconCategory = this.$widget.find("select[name='icon-category']"); this.$iconCategory = this.$widget.find("select[name='icon-category']");
this.$iconCategory.on('change', () => this.renderFilteredDropdown()); this.$iconCategory.on('change', () => this.renderDropdown());
this.$iconCategory.on('click', e => e.stopPropagation()); this.$iconCategory.on('click', e => e.stopPropagation());
this.$iconSearch = this.$widget.find("input[name='icon-search']"); this.$iconSearch = this.$widget.find("input[name='icon-search']");
this.$iconSearch.on('input', () => this.renderFilteredDropdown()); this.$iconSearch.on('input', () => this.renderDropdown());
this.$notePathList = this.$widget.find(".note-path-list"); this.$notePathList = this.$widget.find(".note-path-list");
this.$widget.on('show.bs.dropdown', async () => { this.$widget.on('show.bs.dropdown', async () => {
@ -140,15 +140,9 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
} }
} }
renderFilteredDropdown() { async renderDropdown() {
const categoryId = parseInt(this.$iconCategory.find('option:selected').val()); const iconToCount = await this.getIconToCountMap();
const search = this.$iconSearch.val(); const {icons} = (await import('./icon_list.js')).default;
this.renderDropdown(categoryId, search);
}
async renderDropdown(categoryId, search) {
const iconToCountPromise = this.getIconToCountMap();
this.$iconList.empty(); this.$iconList.empty();
@ -164,9 +158,8 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
); );
} }
const {icons} = (await import('./icon_list.js')).default; const categoryId = parseInt(this.$iconCategory.find('option:selected').val());
const search = this.$iconSearch.val().trim().toLowerCase();
search = search?.trim()?.toLowerCase();
const filteredIcons = icons.filter(icon => { const filteredIcons = icons.filter(icon => {
if (categoryId && icon.category_id !== categoryId) { if (categoryId && icon.category_id !== categoryId) {
@ -182,8 +175,6 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
return true; return true;
}); });
const iconToCount = await iconToCountPromise;
filteredIcons.sort((a, b) => { filteredIcons.sort((a, b) => {
const countA = iconToCount[a.className] || 0; const countA = iconToCount[a.className] || 0;
const countB = iconToCount[b.className] || 0; const countB = iconToCount[b.className] || 0;
@ -199,9 +190,12 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
} }
async getIconToCountMap() { async getIconToCountMap() {
const {iconClassToCountMap} = await server.get('other/icon-usage'); if (!this.iconToCountCache) {
this.iconToCountCache = server.get('other/icon-usage');
setTimeout(() => this.iconToCountCache = null, 20000); // invalidate cache after 20 seconds
}
return iconClassToCountMap; return (await this.iconToCountCache).iconClassToCountMap;
} }
renderIcon(icon) { renderIcon(icon) {

View File

@ -285,6 +285,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
}) })
const content = { const content = {
type: "excalidraw",
version: 2,
_meta: "This note has type `canvas`. It uses excalidraw and stores an exported svg alongside.", _meta: "This note has type `canvas`. It uses excalidraw and stores an exported svg alongside.",
elements, // excalidraw elements, // excalidraw
appState, // excalidraw appState, // excalidraw

View File

@ -185,14 +185,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
async doRefresh(note) { async doRefresh(note) {
const noteComplement = await froca.getNoteComplement(note.noteId); const noteComplement = await froca.getNoteComplement(note.noteId);
await this.spacedUpdate.allowUpdateWithoutChange(() => { await this.spacedUpdate.allowUpdateWithoutChange(() =>
// https://github.com/zadam/trilium/issues/3914 this.watchdog.editor.setData(noteComplement.content || ""));
// todo: quite hacky, but it works. remove it if ckeditor has fixed it.
this.$editor.trigger('focus');
this.$editor.trigger('blur')
this.watchdog.editor.setData(noteComplement.content || "");
});
} }
getData() { getData() {

View File

@ -4,7 +4,7 @@ const build = require('./build');
const packageJson = require('../../package'); const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir'); const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 213; const APP_DB_VERSION = 214;
const SYNC_VERSION = 29; const SYNC_VERSION = 29;
const CLIPPER_PROTOCOL_VERSION = "1.0"; const CLIPPER_PROTOCOL_VERSION = "1.0";

View File

@ -1 +1 @@
module.exports = { buildDate:"2023-06-08T22:46:52+02:00", buildRevision: "6e69cafe5419e8efcc6f652647f9227dbcfa1e18" }; module.exports = { buildDate:"2023-06-19T23:26:50+02:00", buildRevision: "5905950c17791ce0eb278e010c2c8b3450fdb447" };

View File

@ -26,11 +26,13 @@ const fs = require("fs");
function getNewNotePosition(parentNote) { function getNewNotePosition(parentNote) {
if (parentNote.isLabelTruthy('newNotesOnTop')) { if (parentNote.isLabelTruthy('newNotesOnTop')) {
const minNotePos = parentNote.getChildBranches() const minNotePos = parentNote.getChildBranches()
.filter(branch => branch.noteId !== '_hidden') // has "always last" note position
.reduce((min, note) => Math.min(min, note.notePosition), 0); .reduce((min, note) => Math.min(min, note.notePosition), 0);
return minNotePos - 10; return minNotePos - 10;
} else { } else {
const maxNotePos = parentNote.getChildBranches() const maxNotePos = parentNote.getChildBranches()
.filter(branch => branch.noteId !== '_hidden') // has "always last" note position
.reduce((max, note) => Math.max(max, note.notePosition), 0); .reduce((max, note) => Math.max(max, note.notePosition), 0);
return maxNotePos + 10; return maxNotePos + 10;

View File

@ -19,20 +19,22 @@ class NoteFlatTextExp extends Expression {
/** /**
* @param {BNote} note * @param {BNote} note
* @param {string[]} tokens * @param {string[]} remainingTokens - tokens still needed to be found in the path towards root
* @param {string[]} path * @param {string[]} takenPath - path so far taken towards from candidate note towards the root.
* It contains the suffix fragment of the full note path.
*/ */
const searchDownThePath = (note, tokens, path) => { const searchPathTowardsRoot = (note, remainingTokens, takenPath) => {
if (tokens.length === 0) { if (remainingTokens.length === 0) {
const retPath = this.getNotePath(note, path); // we're done, just build the result
const resultPath = this.getNotePath(note, takenPath);
if (retPath) { if (resultPath) {
const noteId = retPath[retPath.length - 1]; const noteId = resultPath[resultPath.length - 1];
if (!resultNoteSet.hasNoteId(noteId)) { if (!resultNoteSet.hasNoteId(noteId)) {
// we could get here from multiple paths, the first one wins because the paths // we could get here from multiple paths, the first one wins because the paths
// are sorted by importance // are sorted by importance
executionContext.noteIdToNotePath[noteId] = retPath; executionContext.noteIdToNotePath[noteId] = resultPath;
resultNoteSet.add(becca.notes[noteId]); resultNoteSet.add(becca.notes[noteId]);
} }
@ -42,22 +44,23 @@ class NoteFlatTextExp extends Expression {
} }
if (note.parents.length === 0 || note.noteId === 'root') { if (note.parents.length === 0 || note.noteId === 'root') {
// we've reached root, but there are still remaining tokens -> this candidate note produced no result
return; return;
} }
const foundAttrTokens = []; const foundAttrTokens = [];
for (const token of tokens) { for (const token of remainingTokens) {
if (note.type.includes(token) || note.mime.includes(token)) { if (note.type.includes(token) || note.mime.includes(token)) {
foundAttrTokens.push(token); foundAttrTokens.push(token);
} }
} }
for (const attribute of note.ownedAttributes) { for (const attribute of note.getOwnedAttributes()) {
const normalizedName = utils.normalize(attribute.name); const normalizedName = utils.normalize(attribute.name);
const normalizedValue = utils.normalize(attribute.value); const normalizedValue = utils.normalize(attribute.value);
for (const token of tokens) { for (const token of remainingTokens) {
if (normalizedName.includes(token) || normalizedValue.includes(token)) { if (normalizedName.includes(token) || normalizedValue.includes(token)) {
foundAttrTokens.push(token); foundAttrTokens.push(token);
} }
@ -68,19 +71,19 @@ class NoteFlatTextExp extends Expression {
const title = utils.normalize(beccaService.getNoteTitle(note.noteId, parentNote.noteId)); const title = utils.normalize(beccaService.getNoteTitle(note.noteId, parentNote.noteId));
const foundTokens = foundAttrTokens.slice(); const foundTokens = foundAttrTokens.slice();
for (const token of tokens) { for (const token of remainingTokens) {
if (title.includes(token)) { if (title.includes(token)) {
foundTokens.push(token); foundTokens.push(token);
} }
} }
if (foundTokens.length > 0) { if (foundTokens.length > 0) {
const remainingTokens = tokens.filter(token => !foundTokens.includes(token)); const newRemainingTokens = remainingTokens.filter(token => !foundTokens.includes(token));
searchDownThePath(parentNote, remainingTokens, [...path, note.noteId]); searchPathTowardsRoot(parentNote, newRemainingTokens, [note.noteId, ...takenPath]);
} }
else { else {
searchDownThePath(parentNote, tokens, [...path, note.noteId]); searchPathTowardsRoot(parentNote, remainingTokens, [note.noteId, ...takenPath]);
} }
} }
} }
@ -90,7 +93,7 @@ class NoteFlatTextExp extends Expression {
for (const note of candidateNotes) { for (const note of candidateNotes) {
// autocomplete should be able to find notes by their noteIds as well (only leafs) // autocomplete should be able to find notes by their noteIds as well (only leafs)
if (this.tokens.length === 1 && note.noteId.toLowerCase() === this.tokens[0]) { if (this.tokens.length === 1 && note.noteId.toLowerCase() === this.tokens[0]) {
searchDownThePath(note, [], []); searchPathTowardsRoot(note, [], [note.noteId]);
continue; continue;
} }
@ -123,7 +126,7 @@ class NoteFlatTextExp extends Expression {
if (foundTokens.length > 0) { if (foundTokens.length > 0) {
const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token)); const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token));
searchDownThePath(parentNote, remainingTokens, [note.noteId]); searchPathTowardsRoot(parentNote, remainingTokens, [note.noteId]);
} }
} }
} }
@ -131,14 +134,22 @@ class NoteFlatTextExp extends Expression {
return resultNoteSet; return resultNoteSet;
} }
getNotePath(note, path) { /**
if (path.length === 0) { * @param {BNote} note
* @param {string[]} takenPath
* @returns {string[]}
*/
getNotePath(note, takenPath) {
if (takenPath.length === 0) {
throw new Error("Path is not expected to be empty.");
} else if (takenPath.length === 1 && takenPath[0] === note.noteId) {
return note.getBestNotePath(); return note.getBestNotePath();
} else { } else {
const closestNoteId = path[0]; // this note is the closest to root containing the last matching token(s), thus completing the requirements
const closestNoteBestNotePath = becca.getNote(closestNoteId).getBestNotePath(); // what's in this note's predecessors does not matter, thus we'll choose the best note path
const topMostMatchingTokenNotePath = becca.getNote(takenPath[0]).getBestNotePath();
return [...closestNoteBestNotePath, ...path.slice(1)]; return [...topMostMatchingTokenNotePath, ...takenPath.slice(1)];
} }
} }

View File

@ -10,7 +10,6 @@ const becca = require('../../../becca/becca');
const beccaService = require('../../../becca/becca_service'); const beccaService = require('../../../becca/becca_service');
const utils = require('../../utils'); const utils = require('../../utils');
const log = require('../../log'); const log = require('../../log');
const scriptService = require("../../script");
const hoistedNoteService = require("../../hoisted_note"); const hoistedNoteService = require("../../hoisted_note");
function searchFromNote(note) { function searchFromNote(note) {
@ -73,6 +72,7 @@ function searchFromRelation(note, relationName) {
return []; return [];
} }
const scriptService = require("../../script"); // to avoid circular dependency
const result = scriptService.executeNote(scriptNote, {originEntity: note}); const result = scriptService.executeNote(scriptNote, {originEntity: note});
if (!Array.isArray(result)) { if (!Array.isArray(result)) {

View File

@ -6,7 +6,7 @@ const ws = require('./ws');
const taskContexts = {}; const taskContexts = {};
class TaskContext { class TaskContext {
constructor(taskId, taskType = null, data = null) { constructor(taskId, taskType = null, data = {}) {
this.taskId = taskId; this.taskId = taskId;
this.taskType = taskType; this.taskType = taskType;
this.data = data; this.data = data;

View File

@ -0,0 +1,12 @@
POST {{triliumHost}}/etapi/notes/root/import
Authorization: {{authToken}}
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
< ../db/demo.zip
> {%
client.assert(response.status === 201);
client.assert(response.body.note.title == "Trilium Demo");
client.assert(response.body.branch.parentNoteId == "root");
%}